mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-16 20:59:54 +01:00
Changed the root package from net.sf.briar to org.briarproject.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
148
briar-android/src/org/briarproject/android/AndroidModule.java
Normal file
148
briar-android/src/org/briarproject/android/AndroidModule.java
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
143
briar-android/src/org/briarproject/android/BriarService.java
Normal file
143
briar-android/src/org/briarproject/android/BriarService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
225
briar-android/src/org/briarproject/android/SetupActivity.java
Normal file
225
briar-android/src/org/briarproject/android/SetupActivity.java
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.briarproject.android.invitation;
|
||||
|
||||
interface CodeEntryListener {
|
||||
|
||||
void codeEntered(int remoteCode);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
package org.briarproject.plugins.droidtooth;
|
||||
|
||||
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
|
||||
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
|
||||
import static android.bluetooth.BluetoothAdapter.STATE_OFF;
|
||||
import static android.bluetooth.BluetoothAdapter.STATE_ON;
|
||||
import static android.bluetooth.BluetoothDevice.EXTRA_DEVICE;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.briarproject.api.ContactId;
|
||||
import org.briarproject.api.TransportId;
|
||||
import org.briarproject.api.TransportProperties;
|
||||
import org.briarproject.api.android.AndroidExecutor;
|
||||
import org.briarproject.api.crypto.PseudoRandom;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPlugin;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import org.briarproject.util.LatchedReference;
|
||||
import org.briarproject.util.StringUtils;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothServerSocket;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
|
||||
class DroidtoothPlugin implements DuplexPlugin {
|
||||
|
||||
// Share an ID with the J2SE Bluetooth plugin
|
||||
static final byte[] TRANSPORT_ID =
|
||||
StringUtils.fromHexString("d99c9313c04417dcf22fc60d12a187ea"
|
||||
+ "00a539fd260f08a13a0d8a900cde5e49"
|
||||
+ "1b4df2ffd42e40c408f2db7868f518aa");
|
||||
static final TransportId ID = new TransportId(TRANSPORT_ID);
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(DroidtoothPlugin.class.getName());
|
||||
private static final int UUID_BYTES = 16;
|
||||
private static final String FOUND = "android.bluetooth.device.action.FOUND";
|
||||
private static final String DISCOVERY_FINISHED =
|
||||
"android.bluetooth.adapter.action.DISCOVERY_FINISHED";
|
||||
|
||||
private final Executor pluginExecutor;
|
||||
private final AndroidExecutor androidExecutor;
|
||||
private final Context appContext;
|
||||
private final SecureRandom secureRandom;
|
||||
private final Clock clock;
|
||||
private final DuplexPluginCallback callback;
|
||||
private final int maxFrameLength;
|
||||
private final long maxLatency, pollingInterval;
|
||||
|
||||
private volatile boolean running = false;
|
||||
private volatile boolean wasEnabled = false, isEnabled = false;
|
||||
|
||||
// Non-null if running has ever been true
|
||||
private volatile BluetoothAdapter adapter = null;
|
||||
|
||||
DroidtoothPlugin(Executor pluginExecutor, AndroidExecutor androidExecutor,
|
||||
Context appContext, SecureRandom secureRandom, Clock clock,
|
||||
DuplexPluginCallback callback, int maxFrameLength, long maxLatency,
|
||||
long pollingInterval) {
|
||||
this.pluginExecutor = pluginExecutor;
|
||||
this.androidExecutor = androidExecutor;
|
||||
this.appContext = appContext;
|
||||
this.secureRandom = secureRandom;
|
||||
this.clock = clock;
|
||||
this.callback = callback;
|
||||
this.maxFrameLength = maxFrameLength;
|
||||
this.maxLatency = maxLatency;
|
||||
this.pollingInterval = pollingInterval;
|
||||
}
|
||||
|
||||
public TransportId getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
// Share a name with the J2SE Bluetooth plugin
|
||||
return "BLUETOOTH_PLUGIN_NAME";
|
||||
}
|
||||
|
||||
public int getMaxFrameLength() {
|
||||
return maxFrameLength;
|
||||
}
|
||||
|
||||
public long getMaxLatency() {
|
||||
return maxLatency;
|
||||
}
|
||||
|
||||
public boolean start() throws IOException {
|
||||
// BluetoothAdapter.getDefaultAdapter() must be called on a thread
|
||||
// with a message queue, so submit it to the AndroidExecutor
|
||||
try {
|
||||
adapter = androidExecutor.call(new Callable<BluetoothAdapter>() {
|
||||
public BluetoothAdapter call() throws Exception {
|
||||
return BluetoothAdapter.getDefaultAdapter();
|
||||
}
|
||||
});
|
||||
} catch(InterruptedException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(ExecutionException e) {
|
||||
throw new IOException(e.toString());
|
||||
}
|
||||
if(adapter == null) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Bluetooth is not supported");
|
||||
return false;
|
||||
}
|
||||
running = true;
|
||||
wasEnabled = isEnabled = adapter.isEnabled();
|
||||
pluginExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
bind();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private void bind() {
|
||||
if(!running) return;
|
||||
if(!enableBluetooth()) return;
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Local address " + adapter.getAddress());
|
||||
// Advertise the Bluetooth address to contacts
|
||||
TransportProperties p = new TransportProperties();
|
||||
p.put("address", adapter.getAddress());
|
||||
callback.mergeLocalProperties(p);
|
||||
// Bind a server socket to accept connections from contacts
|
||||
BluetoothServerSocket ss = null;
|
||||
try {
|
||||
ss = InsecureBluetooth.listen(adapter, "RFCOMM", getUuid());
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
tryToClose(ss);
|
||||
return;
|
||||
}
|
||||
if(!running) {
|
||||
tryToClose(ss);
|
||||
return;
|
||||
}
|
||||
acceptContactConnections(ss);
|
||||
}
|
||||
|
||||
private boolean enableBluetooth() {
|
||||
isEnabled = adapter.isEnabled();
|
||||
if(isEnabled) return true;
|
||||
// Try to enable the adapter and wait for the result
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Enabling Bluetooth");
|
||||
IntentFilter filter = new IntentFilter(ACTION_STATE_CHANGED);
|
||||
BluetoothStateReceiver receiver = new BluetoothStateReceiver();
|
||||
appContext.registerReceiver(receiver, filter);
|
||||
try {
|
||||
if(adapter.enable()) {
|
||||
isEnabled = receiver.waitForStateChange();
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Enabled: " + isEnabled);
|
||||
return isEnabled;
|
||||
} else {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Could not enable adapter");
|
||||
return false;
|
||||
}
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Interrupted while enabling Bluetooth");
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private UUID getUuid() {
|
||||
String uuid = callback.getLocalProperties().get("uuid");
|
||||
if(uuid == null) {
|
||||
byte[] random = new byte[UUID_BYTES];
|
||||
secureRandom.nextBytes(random);
|
||||
uuid = UUID.nameUUIDFromBytes(random).toString();
|
||||
TransportProperties p = new TransportProperties();
|
||||
p.put("uuid", uuid);
|
||||
callback.mergeLocalProperties(p);
|
||||
}
|
||||
return UUID.fromString(uuid);
|
||||
}
|
||||
|
||||
private void tryToClose(BluetoothServerSocket ss) {
|
||||
try {
|
||||
if(ss != null) ss.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptContactConnections(BluetoothServerSocket ss) {
|
||||
while(true) {
|
||||
BluetoothSocket s;
|
||||
try {
|
||||
s = ss.accept();
|
||||
} catch(IOException e) {
|
||||
// This is expected when the socket is closed
|
||||
if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
|
||||
tryToClose(ss);
|
||||
return;
|
||||
}
|
||||
callback.incomingConnectionCreated(wrapSocket(s));
|
||||
if(!running) return;
|
||||
}
|
||||
}
|
||||
|
||||
private DuplexTransportConnection wrapSocket(BluetoothSocket s) {
|
||||
return new DroidtoothTransportConnection(this, s);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
running = false;
|
||||
// Disable Bluetooth if we enabled it at startup
|
||||
if(isEnabled && !wasEnabled) disableBluetooth();
|
||||
}
|
||||
|
||||
private void disableBluetooth() {
|
||||
isEnabled = adapter.isEnabled();
|
||||
if(!isEnabled) return;
|
||||
// Try to disable the adapter and wait for the result
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Disabling Bluetooth");
|
||||
IntentFilter filter = new IntentFilter(ACTION_STATE_CHANGED);
|
||||
BluetoothStateReceiver receiver = new BluetoothStateReceiver();
|
||||
appContext.registerReceiver(receiver, filter);
|
||||
try {
|
||||
if(adapter.disable()) {
|
||||
isEnabled = receiver.waitForStateChange();
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Enabled: " + isEnabled);
|
||||
} else {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Could not disable adapter");
|
||||
}
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Interrupted while disabling Bluetooth");
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldPoll() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public long getPollingInterval() {
|
||||
return pollingInterval;
|
||||
}
|
||||
|
||||
public void poll(Collection<ContactId> connected) {
|
||||
if(!running) return;
|
||||
if(!enableBluetooth()) return;
|
||||
// Try to connect to known devices in parallel
|
||||
Map<ContactId, TransportProperties> remote =
|
||||
callback.getRemoteProperties();
|
||||
for(Entry<ContactId, TransportProperties> e : remote.entrySet()) {
|
||||
final ContactId c = e.getKey();
|
||||
if(connected.contains(c)) continue;
|
||||
final String address = e.getValue().get("address");
|
||||
if(StringUtils.isNullOrEmpty(address)) continue;
|
||||
final String uuid = e.getValue().get("uuid");
|
||||
if(StringUtils.isNullOrEmpty(uuid)) continue;
|
||||
pluginExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
if(!running) return;
|
||||
BluetoothSocket s = connect(address, uuid);
|
||||
if(s != null)
|
||||
callback.outgoingConnectionCreated(c, wrapSocket(s));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private BluetoothSocket connect(String address, String uuid) {
|
||||
// Validate the address
|
||||
if(!BluetoothAdapter.checkBluetoothAddress(address)) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Invalid address " + address);
|
||||
return null;
|
||||
}
|
||||
// Validate the UUID
|
||||
UUID u;
|
||||
try {
|
||||
u = UUID.fromString(uuid);
|
||||
} catch(IllegalArgumentException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.warning("Invalid UUID " + uuid);
|
||||
return null;
|
||||
}
|
||||
// Try to connect
|
||||
BluetoothDevice d = adapter.getRemoteDevice(address);
|
||||
BluetoothSocket s = null;
|
||||
try {
|
||||
s = InsecureBluetooth.createSocket(d, u);
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Connecting to " + address);
|
||||
s.connect();
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Connected to " + address);
|
||||
return s;
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
tryToClose(s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void tryToClose(BluetoothSocket s) {
|
||||
try {
|
||||
if(s != null) s.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public DuplexTransportConnection createConnection(ContactId c) {
|
||||
if(!running) return null;
|
||||
TransportProperties p = callback.getRemoteProperties().get(c);
|
||||
if(p == null) return null;
|
||||
String address = p.get("address");
|
||||
if(StringUtils.isNullOrEmpty(address)) return null;
|
||||
String uuid = p.get("uuid");
|
||||
if(StringUtils.isNullOrEmpty(uuid)) return null;
|
||||
BluetoothSocket s = connect(address, uuid);
|
||||
if(s == null) return null;
|
||||
return new DroidtoothTransportConnection(this, s);
|
||||
}
|
||||
|
||||
public boolean supportsInvitations() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
|
||||
long timeout) {
|
||||
if(!running) return null;
|
||||
// Use the invitation codes to generate the UUID
|
||||
byte[] b = r.nextBytes(UUID_BYTES);
|
||||
UUID uuid = UUID.nameUUIDFromBytes(b);
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Invitation UUID " + uuid);
|
||||
// Bind a server socket for receiving invitation connections
|
||||
BluetoothServerSocket ss = null;
|
||||
try {
|
||||
ss = InsecureBluetooth.listen(adapter, "RFCOMM", uuid);
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
tryToClose(ss);
|
||||
return null;
|
||||
}
|
||||
// Start the background threads
|
||||
LatchedReference<BluetoothSocket> socketLatch =
|
||||
new LatchedReference<BluetoothSocket>();
|
||||
new DiscoveryThread(socketLatch, uuid.toString(), timeout).start();
|
||||
new BluetoothListenerThread(socketLatch, ss).start();
|
||||
// Wait for an incoming or outgoing connection
|
||||
try {
|
||||
BluetoothSocket s = socketLatch.waitForReference(timeout);
|
||||
if(s != null) return new DroidtoothTransportConnection(this, s);
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Interrupted while exchanging invitations");
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
// Closing the socket will terminate the listener thread
|
||||
tryToClose(ss);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class BluetoothStateReceiver extends BroadcastReceiver {
|
||||
|
||||
private final CountDownLatch finished = new CountDownLatch(1);
|
||||
|
||||
private volatile boolean enabled = false;
|
||||
|
||||
public void onReceive(Context ctx, Intent intent) {
|
||||
int state = intent.getIntExtra(EXTRA_STATE, 0);
|
||||
if(state == STATE_ON) {
|
||||
enabled = true;
|
||||
ctx.unregisterReceiver(this);
|
||||
finished.countDown();
|
||||
} else if(state == STATE_OFF) {
|
||||
ctx.unregisterReceiver(this);
|
||||
finished.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
boolean waitForStateChange() throws InterruptedException {
|
||||
finished.await();
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
|
||||
private class DiscoveryThread extends Thread {
|
||||
|
||||
private final LatchedReference<BluetoothSocket> socketLatch;
|
||||
private final String uuid;
|
||||
private final long timeout;
|
||||
|
||||
private DiscoveryThread(LatchedReference<BluetoothSocket> socketLatch,
|
||||
String uuid, long timeout) {
|
||||
this.socketLatch = socketLatch;
|
||||
this.uuid = uuid;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
long now = clock.currentTimeMillis();
|
||||
long end = now + timeout;
|
||||
while(now < end && running && !socketLatch.isSet()) {
|
||||
// Discover nearby devices
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Discovering nearby devices");
|
||||
List<String> addresses;
|
||||
try {
|
||||
addresses = discoverDevices(end - now);
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Interrupted while discovering devices");
|
||||
return;
|
||||
}
|
||||
// Connect to any device with the right UUID
|
||||
for(String address : addresses) {
|
||||
now = clock.currentTimeMillis();
|
||||
if(now < end && running && !socketLatch.isSet()) {
|
||||
BluetoothSocket s = connect(address, uuid);
|
||||
if(s == null) continue;
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Outgoing connection");
|
||||
if(!socketLatch.set(s)) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Closing redundant connection");
|
||||
tryToClose(s);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> discoverDevices(long timeout)
|
||||
throws InterruptedException {
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(FOUND);
|
||||
filter.addAction(DISCOVERY_FINISHED);
|
||||
DiscoveryReceiver disco = new DiscoveryReceiver();
|
||||
appContext.registerReceiver(disco, filter);
|
||||
adapter.startDiscovery();
|
||||
return disco.waitForAddresses(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DiscoveryReceiver extends BroadcastReceiver {
|
||||
|
||||
private final CountDownLatch finished = new CountDownLatch(1);
|
||||
private final List<String> addresses = new ArrayList<String>();
|
||||
|
||||
@Override
|
||||
public void onReceive(Context ctx, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if(action.equals(DISCOVERY_FINISHED)) {
|
||||
ctx.unregisterReceiver(this);
|
||||
finished.countDown();
|
||||
} else if(action.equals(FOUND)) {
|
||||
BluetoothDevice d = intent.getParcelableExtra(EXTRA_DEVICE);
|
||||
addresses.add(d.getAddress());
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> waitForAddresses(long timeout)
|
||||
throws InterruptedException {
|
||||
finished.await(timeout, MILLISECONDS);
|
||||
return Collections.unmodifiableList(addresses);
|
||||
}
|
||||
}
|
||||
|
||||
private static class BluetoothListenerThread extends Thread {
|
||||
|
||||
private final LatchedReference<BluetoothSocket> socketLatch;
|
||||
private final BluetoothServerSocket serverSocket;
|
||||
|
||||
private BluetoothListenerThread(
|
||||
LatchedReference<BluetoothSocket> socketLatch,
|
||||
BluetoothServerSocket serverSocket) {
|
||||
this.socketLatch = socketLatch;
|
||||
this.serverSocket = serverSocket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
BluetoothSocket s = serverSocket.accept();
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Incoming connection");
|
||||
if(!socketLatch.set(s)) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Closing redundant connection");
|
||||
s.close();
|
||||
}
|
||||
} catch(IOException e) {
|
||||
// This is expected when the socket is closed
|
||||
if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.briarproject.plugins.droidtooth;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.briarproject.api.TransportId;
|
||||
import org.briarproject.api.android.AndroidExecutor;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPlugin;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import org.briarproject.api.system.SystemClock;
|
||||
import android.content.Context;
|
||||
|
||||
public class DroidtoothPluginFactory implements DuplexPluginFactory {
|
||||
|
||||
private static final int MAX_FRAME_LENGTH = 1024;
|
||||
private static final long MAX_LATENCY = 60 * 1000; // 1 minute
|
||||
private static final long POLLING_INTERVAL = 3 * 60 * 1000; // 3 minutes
|
||||
|
||||
private final Executor pluginExecutor;
|
||||
private final AndroidExecutor androidExecutor;
|
||||
private final Context appContext;
|
||||
private final SecureRandom secureRandom;
|
||||
private final Clock clock;
|
||||
|
||||
public DroidtoothPluginFactory(Executor pluginExecutor,
|
||||
AndroidExecutor androidExecutor, Context appContext,
|
||||
SecureRandom secureRandom) {
|
||||
this.pluginExecutor = pluginExecutor;
|
||||
this.androidExecutor = androidExecutor;
|
||||
this.appContext = appContext;
|
||||
this.secureRandom = secureRandom;
|
||||
clock = new SystemClock();
|
||||
}
|
||||
|
||||
public TransportId getId() {
|
||||
return DroidtoothPlugin.ID;
|
||||
}
|
||||
|
||||
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
|
||||
return new DroidtoothPlugin(pluginExecutor, androidExecutor, appContext,
|
||||
secureRandom, clock, callback, MAX_FRAME_LENGTH, MAX_LATENCY,
|
||||
POLLING_INTERVAL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.briarproject.plugins.droidtooth;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.briarproject.api.plugins.Plugin;
|
||||
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
|
||||
class DroidtoothTransportConnection implements DuplexTransportConnection {
|
||||
|
||||
private final Plugin plugin;
|
||||
private final BluetoothSocket socket;
|
||||
|
||||
DroidtoothTransportConnection(Plugin plugin, BluetoothSocket socket) {
|
||||
this.plugin = plugin;
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
public int getMaxFrameLength() {
|
||||
return plugin.getMaxFrameLength();
|
||||
}
|
||||
|
||||
public long getMaxLatency() {
|
||||
return plugin.getMaxLatency();
|
||||
}
|
||||
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return socket.getInputStream();
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
return socket.getOutputStream();
|
||||
}
|
||||
|
||||
public boolean shouldFlush() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void dispose(boolean exception, boolean recognised)
|
||||
throws IOException {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package org.briarproject.plugins.droidtooth;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothServerSocket;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.ParcelUuid;
|
||||
|
||||
// Based on http://stanford.edu/~tpurtell/InsecureBluetooth.java by T.J. Purtell
|
||||
class InsecureBluetooth {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(InsecureBluetooth.class.getName());
|
||||
|
||||
private static final int TYPE_RFCOMM = 1;
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
static BluetoothServerSocket listen(BluetoothAdapter adapter, String name,
|
||||
UUID uuid) throws IOException {
|
||||
if(Build.VERSION.SDK_INT >= 10) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Listening with new API");
|
||||
return adapter.listenUsingInsecureRfcommWithServiceRecord(name,
|
||||
uuid);
|
||||
}
|
||||
try {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Listening via reflection");
|
||||
// Find an available channel
|
||||
String className = BluetoothAdapter.class.getCanonicalName()
|
||||
+ ".RfcommChannelPicker";
|
||||
Class<?> channelPickerClass = null;
|
||||
Class<?>[] children = BluetoothAdapter.class.getDeclaredClasses();
|
||||
for(Class<?> c : children) {
|
||||
if(c.getCanonicalName().equals(className)) {
|
||||
channelPickerClass = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(channelPickerClass == null)
|
||||
throw new IOException("Can't find channel picker class");
|
||||
Constructor<?> constructor =
|
||||
channelPickerClass.getDeclaredConstructor(UUID.class);
|
||||
constructor.setAccessible(true);
|
||||
Object channelPicker = constructor.newInstance(uuid);
|
||||
Method nextChannel =
|
||||
channelPickerClass.getDeclaredMethod("nextChannel");
|
||||
nextChannel.setAccessible(true);
|
||||
int channel = (Integer) nextChannel.invoke(channelPicker);
|
||||
if(channel == -1) throw new IOException("No available channels");
|
||||
// Listen on the channel
|
||||
BluetoothServerSocket socket = listen(channel);
|
||||
// Add a service record
|
||||
Field f = BluetoothAdapter.class.getDeclaredField("mService");
|
||||
f.setAccessible(true);
|
||||
Object mService = f.get(adapter);
|
||||
Method addRfcommServiceRecord =
|
||||
mService.getClass().getDeclaredMethod(
|
||||
"addRfcommServiceRecord", String.class,
|
||||
ParcelUuid.class, int.class, IBinder.class);
|
||||
addRfcommServiceRecord.setAccessible(true);
|
||||
int handle = (Integer) addRfcommServiceRecord.invoke(mService, name,
|
||||
new ParcelUuid(uuid), channel, new Binder());
|
||||
if(handle == -1) {
|
||||
socket.close();
|
||||
throw new IOException("Can't register SDP record for " + name);
|
||||
}
|
||||
Field f1 = BluetoothAdapter.class.getDeclaredField("mHandler");
|
||||
f1.setAccessible(true);
|
||||
Object mHandler = f1.get(adapter);
|
||||
Method setCloseHandler = socket.getClass().getDeclaredMethod(
|
||||
"setCloseHandler", Handler.class, int.class);
|
||||
setCloseHandler.setAccessible(true);
|
||||
setCloseHandler.invoke(socket, mHandler, handle);
|
||||
return socket;
|
||||
} catch(NoSuchMethodException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(NoSuchFieldException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(IllegalAccessException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(InstantiationException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(InvocationTargetException e) {
|
||||
if(e.getCause() instanceof IOException) {
|
||||
throw (IOException) e.getCause();
|
||||
} else {
|
||||
throw new IOException(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BluetoothServerSocket listen(int port) throws IOException {
|
||||
try {
|
||||
Constructor<BluetoothServerSocket> constructor =
|
||||
BluetoothServerSocket.class.getDeclaredConstructor(
|
||||
int.class, boolean.class, boolean.class, int.class);
|
||||
constructor.setAccessible(true);
|
||||
BluetoothServerSocket socket = constructor.newInstance(TYPE_RFCOMM,
|
||||
false, false, port);
|
||||
Field f = BluetoothServerSocket.class.getDeclaredField("mSocket");
|
||||
f.setAccessible(true);
|
||||
Object mSocket = f.get(socket);
|
||||
Method bindListen =
|
||||
mSocket.getClass().getDeclaredMethod("bindListen");
|
||||
bindListen.setAccessible(true);
|
||||
int errno = (Integer) bindListen.invoke(mSocket);
|
||||
if(errno != 0) {
|
||||
socket.close();
|
||||
throw new IOException("Can't bind: errno " + errno);
|
||||
}
|
||||
return socket;
|
||||
} catch(NoSuchMethodException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(NoSuchFieldException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(IllegalAccessException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(InstantiationException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(InvocationTargetException e) {
|
||||
if(e.getCause() instanceof IOException) {
|
||||
throw (IOException) e.getCause();
|
||||
} else {
|
||||
throw new IOException(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
static BluetoothSocket createSocket(BluetoothDevice device, UUID uuid)
|
||||
throws IOException {
|
||||
if(Build.VERSION.SDK_INT >= 10) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Creating socket with new API");
|
||||
return device.createInsecureRfcommSocketToServiceRecord(uuid);
|
||||
}
|
||||
try {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Creating socket via reflection");
|
||||
Constructor<BluetoothSocket> constructor =
|
||||
BluetoothSocket.class.getDeclaredConstructor(int.class,
|
||||
int.class, boolean.class, boolean.class,
|
||||
BluetoothDevice.class, int.class, ParcelUuid.class);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(TYPE_RFCOMM, -1, false, true, device,
|
||||
-1, new ParcelUuid(uuid));
|
||||
} catch(NoSuchMethodException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(IllegalAccessException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(InstantiationException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(InvocationTargetException e) {
|
||||
if(e.getCause() instanceof IOException) {
|
||||
throw (IOException) e.getCause();
|
||||
} else {
|
||||
throw new IOException(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.briarproject.plugins.tcp;
|
||||
|
||||
import static android.content.Context.WIFI_SERVICE;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.briarproject.api.crypto.PseudoRandom;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import android.content.Context;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.net.wifi.WifiManager.MulticastLock;
|
||||
|
||||
class DroidLanTcpPlugin extends LanTcpPlugin {
|
||||
|
||||
private final Context appContext;
|
||||
|
||||
DroidLanTcpPlugin(Executor pluginExecutor, Context appContext, Clock clock,
|
||||
DuplexPluginCallback callback, int maxFrameLength, long maxLatency,
|
||||
long pollingInterval) {
|
||||
super(pluginExecutor, clock, callback, maxFrameLength, maxLatency,
|
||||
pollingInterval);
|
||||
this.appContext = appContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
|
||||
long timeout) {
|
||||
Object o = appContext.getSystemService(WIFI_SERVICE);
|
||||
WifiManager wifi = (WifiManager) o;
|
||||
if(wifi == null || !wifi.isWifiEnabled()) return null;
|
||||
MulticastLock lock = wifi.createMulticastLock("invitation");
|
||||
lock.acquire();
|
||||
try {
|
||||
return super.createInvitationConnection(r, timeout);
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.briarproject.plugins.tcp;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.briarproject.api.TransportId;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPlugin;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import org.briarproject.api.system.SystemClock;
|
||||
import android.content.Context;
|
||||
|
||||
public class DroidLanTcpPluginFactory implements DuplexPluginFactory {
|
||||
|
||||
private static final int MAX_FRAME_LENGTH = 1024;
|
||||
private static final long MAX_LATENCY = 60 * 1000; // 1 minute
|
||||
private static final long POLLING_INTERVAL = 60 * 1000; // 1 minute
|
||||
|
||||
private final Executor pluginExecutor;
|
||||
private final Context appContext;
|
||||
private final Clock clock;
|
||||
|
||||
public DroidLanTcpPluginFactory(Executor pluginExecutor,
|
||||
Context appContext) {
|
||||
this.pluginExecutor = pluginExecutor;
|
||||
this.appContext = appContext;
|
||||
clock = new SystemClock();
|
||||
}
|
||||
|
||||
public TransportId getId() {
|
||||
return LanTcpPlugin.ID;
|
||||
}
|
||||
|
||||
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
|
||||
return new DroidLanTcpPlugin(pluginExecutor, appContext, clock,
|
||||
callback, MAX_FRAME_LENGTH, MAX_LATENCY, POLLING_INTERVAL);
|
||||
}
|
||||
}
|
||||
606
briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
Normal file
606
briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
Normal file
@@ -0,0 +1,606 @@
|
||||
package org.briarproject.plugins.tor;
|
||||
|
||||
import static android.content.Context.MODE_PRIVATE;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import net.freehaven.tor.control.EventHandler;
|
||||
import net.freehaven.tor.control.TorControlConnection;
|
||||
import org.briarproject.api.ContactId;
|
||||
import org.briarproject.api.TransportConfig;
|
||||
import org.briarproject.api.TransportId;
|
||||
import org.briarproject.api.TransportProperties;
|
||||
import org.briarproject.api.crypto.PseudoRandom;
|
||||
import org.briarproject.api.lifecycle.ShutdownManager;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPlugin;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.util.StringUtils;
|
||||
import socks.Socks5Proxy;
|
||||
import socks.SocksSocket;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.FileObserver;
|
||||
|
||||
class TorPlugin implements DuplexPlugin, EventHandler {
|
||||
|
||||
static final byte[] TRANSPORT_ID =
|
||||
StringUtils.fromHexString("fa866296495c73a52e6a82fd12db6f15"
|
||||
+ "47753b5e636bb8b24975780d7d2e3fc2"
|
||||
+ "d32a4c480c74de2dc6e3157a632a0287");
|
||||
static final TransportId ID = new TransportId(TRANSPORT_ID);
|
||||
|
||||
private static final int SOCKS_PORT = 59050, CONTROL_PORT = 59051;
|
||||
private static final int COOKIE_TIMEOUT = 3000; // Milliseconds
|
||||
private static final int HOSTNAME_TIMEOUT = 30 * 1000; // Milliseconds
|
||||
private static final Pattern ONION =
|
||||
Pattern.compile("[a-z2-7]{16}\\.onion");
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(TorPlugin.class.getName());
|
||||
|
||||
private final Executor pluginExecutor;
|
||||
private final Context appContext;
|
||||
private final ShutdownManager shutdownManager;
|
||||
private final DuplexPluginCallback callback;
|
||||
private final int maxFrameLength;
|
||||
private final long maxLatency, pollingInterval;
|
||||
private final File torDirectory, torFile, geoIpFile, configFile, doneFile;
|
||||
private final File cookieFile, pidFile, hostnameFile;
|
||||
|
||||
private volatile boolean running = false;
|
||||
private volatile Process tor = null;
|
||||
private volatile int pid = -1;
|
||||
private volatile ServerSocket socket = null;
|
||||
private volatile Socket controlSocket = null;
|
||||
private volatile TorControlConnection controlConnection = null;
|
||||
|
||||
TorPlugin(Executor pluginExecutor, Context appContext,
|
||||
ShutdownManager shutdownManager, DuplexPluginCallback callback,
|
||||
int maxFrameLength, long maxLatency, long pollingInterval) {
|
||||
this.pluginExecutor = pluginExecutor;
|
||||
this.appContext = appContext;
|
||||
this.shutdownManager = shutdownManager;
|
||||
this.callback = callback;
|
||||
this.maxFrameLength = maxFrameLength;
|
||||
this.maxLatency = maxLatency;
|
||||
this.pollingInterval = pollingInterval;
|
||||
torDirectory = appContext.getDir("tor", MODE_PRIVATE);
|
||||
torFile = new File(torDirectory, "tor");
|
||||
geoIpFile = new File(torDirectory, "geoip");
|
||||
configFile = new File(torDirectory, "torrc");
|
||||
doneFile = new File(torDirectory, "done");
|
||||
cookieFile = new File(torDirectory, ".tor/control_auth_cookie");
|
||||
pidFile = new File(torDirectory, ".tor/pid");
|
||||
hostnameFile = new File(torDirectory, "hostname");
|
||||
}
|
||||
|
||||
public TransportId getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return "TOR_PLUGIN_NAME";
|
||||
}
|
||||
|
||||
public int getMaxFrameLength() {
|
||||
return maxFrameLength;
|
||||
}
|
||||
|
||||
public long getMaxLatency() {
|
||||
return maxLatency;
|
||||
}
|
||||
|
||||
public boolean start() throws IOException {
|
||||
// Try to connect to an existing Tor process if there is one
|
||||
boolean startProcess = false;
|
||||
try {
|
||||
controlSocket = new Socket("127.0.0.1", CONTROL_PORT);
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Tor is already running");
|
||||
if(readPidFile() == -1) {
|
||||
controlSocket.close();
|
||||
killZombieProcess();
|
||||
startProcess = true;
|
||||
}
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Tor is not running");
|
||||
startProcess = true;
|
||||
}
|
||||
if(startProcess) {
|
||||
// Install the binary, GeoIP database and config file if necessary
|
||||
if(!isInstalled() && !install()) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Could not install Tor");
|
||||
return false;
|
||||
}
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Starting Tor");
|
||||
// Watch for the auth cookie file being created/updated
|
||||
cookieFile.getParentFile().mkdirs();
|
||||
cookieFile.createNewFile();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
FileObserver obs = new WriteObserver(cookieFile, latch);
|
||||
obs.startWatching();
|
||||
// Start a new Tor process
|
||||
String torPath = torFile.getAbsolutePath();
|
||||
String configPath = configFile.getAbsolutePath();
|
||||
String[] cmd = { torPath, "-f", configPath };
|
||||
String[] env = { "HOME=" + torDirectory.getAbsolutePath() };
|
||||
try {
|
||||
tor = Runtime.getRuntime().exec(cmd, env, torDirectory);
|
||||
} catch(SecurityException e1) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e1.toString(), e1);
|
||||
return false;
|
||||
}
|
||||
// Log the process's standard output until it detaches
|
||||
if(LOG.isLoggable(INFO)) {
|
||||
Scanner stdout = new Scanner(tor.getInputStream());
|
||||
while(stdout.hasNextLine()) LOG.info(stdout.nextLine());
|
||||
stdout.close();
|
||||
}
|
||||
try {
|
||||
// Wait for the process to detach or exit
|
||||
int exit = tor.waitFor();
|
||||
if(exit != 0) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Tor exited with value " + exit);
|
||||
return false;
|
||||
}
|
||||
// Wait for the auth cookie file to be created/updated
|
||||
if(!latch.await(COOKIE_TIMEOUT, MILLISECONDS)) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Auth cookie not created");
|
||||
listFiles(torDirectory);
|
||||
return false;
|
||||
}
|
||||
} catch(InterruptedException e1) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Interrupted while starting Tor");
|
||||
return false;
|
||||
}
|
||||
// Now we should be able to connect to the new process
|
||||
controlSocket = new Socket("127.0.0.1", CONTROL_PORT);
|
||||
}
|
||||
// Read the PID of the Tor process so we can kill it if necessary
|
||||
pid = readPidFile();
|
||||
// Create a shutdown hook to ensure the Tor process is killed
|
||||
shutdownManager.addShutdownHook(new Runnable() {
|
||||
public void run() {
|
||||
if(tor != null) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Killing Tor via destroy()");
|
||||
tor.destroy();
|
||||
}
|
||||
if(pid != -1) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Killing Tor via killProcess(" + pid + ")");
|
||||
android.os.Process.killProcess(pid);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Open a control connection and authenticate using the cookie file
|
||||
controlConnection = new TorControlConnection(controlSocket);
|
||||
controlConnection.authenticate(read(cookieFile));
|
||||
// Register to receive events from the Tor process
|
||||
controlConnection.setEventHandler(this);
|
||||
controlConnection.setEvents(Arrays.asList("NOTICE", "WARN", "ERR"));
|
||||
running = true;
|
||||
// Bind a server socket to receive incoming hidden service connections
|
||||
pluginExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
bind();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isInstalled() {
|
||||
return doneFile.exists();
|
||||
}
|
||||
|
||||
private boolean install() {
|
||||
InputStream in = null;
|
||||
OutputStream out = null;
|
||||
try {
|
||||
// Unzip the Tor binary to the filesystem
|
||||
in = getTorInputStream();
|
||||
out = new FileOutputStream(torFile);
|
||||
copy(in, out);
|
||||
// Unzip the GeoIP database to the filesystem
|
||||
in = getGeoIpInputStream();
|
||||
out = new FileOutputStream(geoIpFile);
|
||||
copy(in, out);
|
||||
// Copy the config file to the filesystem
|
||||
in = getConfigInputStream();
|
||||
out = new FileOutputStream(configFile);
|
||||
copy(in, out);
|
||||
// Make the Tor binary executable
|
||||
if(!setExecutable(torFile)) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Could not make Tor executable");
|
||||
return false;
|
||||
}
|
||||
// Create a file to indicate that installation succeeded
|
||||
doneFile.createNewFile();
|
||||
return true;
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
tryToClose(in);
|
||||
tryToClose(out);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream getTorInputStream() throws IOException {
|
||||
InputStream in = appContext.getResources().getAssets().open("tor");
|
||||
ZipInputStream zin = new ZipInputStream(in);
|
||||
if(zin.getNextEntry() == null) throw new IOException();
|
||||
return zin;
|
||||
}
|
||||
|
||||
private InputStream getGeoIpInputStream() throws IOException {
|
||||
InputStream in = appContext.getResources().getAssets().open("geoip");
|
||||
ZipInputStream zin = new ZipInputStream(in);
|
||||
if(zin.getNextEntry() == null) throw new IOException();
|
||||
return zin;
|
||||
}
|
||||
|
||||
private InputStream getConfigInputStream() throws IOException {
|
||||
return appContext.getResources().getAssets().open("torrc");
|
||||
}
|
||||
|
||||
private void copy(InputStream in, OutputStream out) throws IOException {
|
||||
byte[] buf = new byte[4096];
|
||||
while(true) {
|
||||
int read = in.read(buf);
|
||||
if(read == -1) break;
|
||||
out.write(buf, 0, read);
|
||||
}
|
||||
in.close();
|
||||
out.close();
|
||||
}
|
||||
|
||||
private boolean setExecutable(File f) {
|
||||
if(Build.VERSION.SDK_INT >= 9) {
|
||||
return f.setExecutable(true, true);
|
||||
} else {
|
||||
String[] command = { "chmod", "700", f.getAbsolutePath() };
|
||||
try {
|
||||
return Runtime.getRuntime().exec(command).waitFor() == 0;
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Interrupted while executing chmod");
|
||||
Thread.currentThread().interrupt();
|
||||
} catch(SecurityException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void tryToClose(InputStream in) {
|
||||
try {
|
||||
if(in != null) in.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void tryToClose(OutputStream out) {
|
||||
try {
|
||||
if(out != null) out.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void listFiles(File f) {
|
||||
if(f.isDirectory()) for(File f1 : f.listFiles()) listFiles(f1);
|
||||
else if(LOG.isLoggable(INFO)) LOG.info(f.getAbsolutePath());
|
||||
}
|
||||
|
||||
private byte[] read(File f) throws IOException {
|
||||
byte[] b = new byte[(int) f.length()];
|
||||
FileInputStream in = new FileInputStream(f);
|
||||
try {
|
||||
int offset = 0;
|
||||
while(offset < b.length) {
|
||||
int read = in.read(b, offset, b.length - offset);
|
||||
if(read == -1) throw new EOFException();
|
||||
offset += read;
|
||||
}
|
||||
return b;
|
||||
} finally {
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
|
||||
private int readPidFile() {
|
||||
// Read the PID of the Tor process so we can kill it if necessary
|
||||
try {
|
||||
return Integer.parseInt(new String(read(pidFile), "UTF-8").trim());
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.warning("Could not read PID file");
|
||||
} catch(NumberFormatException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.warning("Could not parse PID file");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the app crashes, leaving a Tor process running, and the user clears
|
||||
* the app's data, removing the PID file and auth cookie file, it's no
|
||||
* longer possible to communicate with the zombie process and it must be
|
||||
* killed. ActivityManager.killBackgroundProcesses() doesn't seem to work
|
||||
* in this case, so we must parse the output of ps to get the PID.
|
||||
* <p>
|
||||
* On all tested devices, the output consists of a header line followed by
|
||||
* one line per process. The second column is the PID and the last column
|
||||
* is the process name, which includes the app's package name.
|
||||
*/
|
||||
private void killZombieProcess() {
|
||||
String packageName = "/" + appContext.getPackageName() + "/";
|
||||
try {
|
||||
// Parse the output of ps
|
||||
Process ps = Runtime.getRuntime().exec("ps");
|
||||
Scanner scanner = new Scanner(ps.getInputStream());
|
||||
// Discard the header line
|
||||
if(scanner.hasNextLine()) scanner.nextLine();
|
||||
// Look for a Tor process with our package name
|
||||
while(scanner.hasNextLine()) {
|
||||
String[] columns = scanner.nextLine().split("\\s+");
|
||||
if(columns.length < 3) break;
|
||||
int pid = Integer.parseInt(columns[1]);
|
||||
String name = columns[columns.length - 1];
|
||||
if(name.contains(packageName) && name.endsWith("/tor")) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Killing zombie process " + pid);
|
||||
android.os.Process.killProcess(pid);
|
||||
}
|
||||
}
|
||||
scanner.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Could not parse ps output");
|
||||
} catch(SecurityException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.warning("Could not execute ps");
|
||||
}
|
||||
}
|
||||
|
||||
private void bind() {
|
||||
// If there's already a port number stored in config, reuse it
|
||||
String portString = callback.getConfig().get("port");
|
||||
int port;
|
||||
if(StringUtils.isNullOrEmpty(portString)) port = 0;
|
||||
else port = Integer.parseInt(portString);
|
||||
// Bind a server socket to receive connections from the Tor process
|
||||
ServerSocket ss = null;
|
||||
try {
|
||||
ss = new ServerSocket();
|
||||
ss.bind(new InetSocketAddress("127.0.0.1", port));
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
tryToClose(ss);
|
||||
}
|
||||
if(!running) {
|
||||
tryToClose(ss);
|
||||
return;
|
||||
}
|
||||
socket = ss;
|
||||
// Store the port number
|
||||
final String localPort = String.valueOf(ss.getLocalPort());
|
||||
TransportConfig c = new TransportConfig();
|
||||
c.put("port", localPort);
|
||||
callback.mergeConfig(c);
|
||||
// Create a hidden service if necessary
|
||||
pluginExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
publishHiddenService(localPort);
|
||||
}
|
||||
});
|
||||
// Accept incoming hidden service connections from the Tor process
|
||||
acceptContactConnections(ss);
|
||||
}
|
||||
|
||||
private void tryToClose(ServerSocket ss) {
|
||||
try {
|
||||
if(ss != null) ss.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void publishHiddenService(final String port) {
|
||||
if(!running) return;
|
||||
if(!hostnameFile.exists()) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Creating hidden service");
|
||||
try {
|
||||
// Watch for the hostname file being created/updated
|
||||
hostnameFile.getParentFile().mkdirs();
|
||||
hostnameFile.createNewFile();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
FileObserver obs = new WriteObserver(hostnameFile, latch);
|
||||
obs.startWatching();
|
||||
// Use the control connection to update the Tor config
|
||||
List<String> config = Arrays.asList(
|
||||
"HiddenServiceDir " + torDirectory.getAbsolutePath(),
|
||||
"HiddenServicePort 80 127.0.0.1:" + port);
|
||||
controlConnection.setConf(config);
|
||||
controlConnection.saveConf();
|
||||
// Wait for the hostname file to be created/updated
|
||||
if(!latch.await(HOSTNAME_TIMEOUT, MILLISECONDS)) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Hidden service not created");
|
||||
listFiles(torDirectory);
|
||||
return;
|
||||
}
|
||||
if(!running) return;
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Interrupted while creating hidden service");
|
||||
}
|
||||
}
|
||||
// Publish the hidden service's onion hostname in transport properties
|
||||
try {
|
||||
String hostname = new String(read(hostnameFile), "UTF-8").trim();
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Hidden service " + hostname);
|
||||
TransportProperties p = new TransportProperties();
|
||||
p.put("onion", hostname);
|
||||
callback.mergeLocalProperties(p);
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptContactConnections(ServerSocket ss) {
|
||||
while(true) {
|
||||
Socket s;
|
||||
try {
|
||||
s = ss.accept();
|
||||
} catch(IOException e) {
|
||||
// This is expected when the socket is closed
|
||||
if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
|
||||
tryToClose(ss);
|
||||
return;
|
||||
}
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Connection received");
|
||||
TorTransportConnection conn = new TorTransportConnection(this, s);
|
||||
callback.incomingConnectionCreated(conn);
|
||||
if(!running) return;
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() throws IOException {
|
||||
running = false;
|
||||
if(socket != null) tryToClose(socket);
|
||||
try {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Stopping Tor");
|
||||
if(controlSocket == null)
|
||||
controlSocket = new Socket("127.0.0.1", CONTROL_PORT);
|
||||
if(controlConnection == null) {
|
||||
controlConnection = new TorControlConnection(controlSocket);
|
||||
controlConnection.authenticate(read(cookieFile));
|
||||
}
|
||||
controlConnection.shutdownTor("TERM");
|
||||
controlSocket.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Killing Tor");
|
||||
if(tor != null) tor.destroy();
|
||||
if(pid != -1) android.os.Process.killProcess(pid);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldPoll() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public long getPollingInterval() {
|
||||
return pollingInterval;
|
||||
}
|
||||
|
||||
public void poll(Collection<ContactId> connected) {
|
||||
if(!running) return;
|
||||
Map<ContactId, TransportProperties> remote =
|
||||
callback.getRemoteProperties();
|
||||
for(final ContactId c : remote.keySet()) {
|
||||
if(connected.contains(c)) continue;
|
||||
pluginExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
connectAndCallBack(c);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void connectAndCallBack(ContactId c) {
|
||||
DuplexTransportConnection d = createConnection(c);
|
||||
if(d != null) callback.outgoingConnectionCreated(c, d);
|
||||
}
|
||||
|
||||
public DuplexTransportConnection createConnection(ContactId c) {
|
||||
if(!running) return null;
|
||||
TransportProperties p = callback.getRemoteProperties().get(c);
|
||||
if(p == null) return null;
|
||||
String onion = p.get("onion");
|
||||
if(StringUtils.isNullOrEmpty(onion)) return null;
|
||||
if(!ONION.matcher(onion).matches()) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Invalid hostname: " + onion);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Connecting to " + onion);
|
||||
Socks5Proxy proxy = new Socks5Proxy("127.0.0.1", SOCKS_PORT);
|
||||
proxy.resolveAddrLocally(false);
|
||||
Socket s = new SocksSocket(proxy, onion, 80);
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Connected to " + onion);
|
||||
return new TorTransportConnection(this, s);
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean supportsInvitations() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
|
||||
long timeout) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public void circuitStatus(String status, String circID, String path) {}
|
||||
|
||||
public void streamStatus(String status, String streamID, String target) {}
|
||||
|
||||
public void orConnStatus(String status, String orName) {}
|
||||
|
||||
public void bandwidthUsed(long read, long written) {}
|
||||
|
||||
public void newDescriptors(List<String> orList) {}
|
||||
|
||||
public void message(String severity, String msg) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info(severity + " " + msg);
|
||||
}
|
||||
|
||||
public void unrecognized(String type, String msg) {}
|
||||
|
||||
private static class WriteObserver extends FileObserver {
|
||||
|
||||
private final CountDownLatch latch;
|
||||
|
||||
private WriteObserver(File file, CountDownLatch latch) {
|
||||
super(file.getAbsolutePath(), CLOSE_WRITE);
|
||||
this.latch = latch;
|
||||
}
|
||||
|
||||
public void onEvent(int event, String path) {
|
||||
stopWatching();
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.briarproject.plugins.tor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.briarproject.api.TransportId;
|
||||
import org.briarproject.api.lifecycle.ShutdownManager;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPlugin;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
public class TorPluginFactory implements DuplexPluginFactory {
|
||||
|
||||
private static final int MAX_FRAME_LENGTH = 1024;
|
||||
private static final long MAX_LATENCY = 60 * 1000; // 1 minute
|
||||
private static final long POLLING_INTERVAL = 3 * 60 * 1000; // 3 minutes
|
||||
|
||||
private final Executor pluginExecutor;
|
||||
private final Context appContext;
|
||||
private final ShutdownManager shutdownManager;
|
||||
|
||||
public TorPluginFactory(Executor pluginExecutor, Context appContext,
|
||||
ShutdownManager shutdownManager) {
|
||||
this.pluginExecutor = pluginExecutor;
|
||||
this.appContext = appContext;
|
||||
this.shutdownManager = shutdownManager;
|
||||
}
|
||||
|
||||
public TransportId getId() {
|
||||
return TorPlugin.ID;
|
||||
}
|
||||
|
||||
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
|
||||
// Check that we have a Tor binary for this architecture
|
||||
if(!Build.CPU_ABI.startsWith("armeabi")) return null;
|
||||
return new TorPlugin(pluginExecutor,appContext, shutdownManager,
|
||||
callback, MAX_FRAME_LENGTH, MAX_LATENCY, POLLING_INTERVAL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.briarproject.plugins.tor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
import org.briarproject.api.plugins.Plugin;
|
||||
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
|
||||
|
||||
class TorTransportConnection implements DuplexTransportConnection {
|
||||
|
||||
private final Plugin plugin;
|
||||
private final Socket socket;
|
||||
|
||||
TorTransportConnection(Plugin plugin, Socket socket) {
|
||||
this.plugin = plugin;
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
public int getMaxFrameLength() {
|
||||
return plugin.getMaxFrameLength();
|
||||
}
|
||||
|
||||
public long getMaxLatency() {
|
||||
return plugin.getMaxLatency();
|
||||
}
|
||||
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return socket.getInputStream();
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
return socket.getOutputStream();
|
||||
}
|
||||
|
||||
public boolean shouldFlush() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void dispose(boolean exception, boolean recognised)
|
||||
throws IOException {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user