diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index 1bb12bfff..aad00c6d9 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -100,6 +100,17 @@
/>
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/res/menu/groups_list_actions.xml b/briar-android/res/menu/groups_list_actions.xml
index 511224ff1..16eeba462 100644
--- a/briar-android/res/menu/groups_list_actions.xml
+++ b/briar-android/res/menu/groups_list_actions.xml
@@ -6,7 +6,7 @@
\ No newline at end of file
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 004c00129..872800c9f 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -155,8 +155,11 @@
This group is empty
This group is dissolved
Remove
- Add Private Group
+ Create Private Group
This group is empty.\n\nYou can use the pen icon at the top to compose the first message.
+ Create Group
+ Send Invitation
+ Add a name for your private group
Compose Message
Message sent
Message received
diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java
index 4e66c87ab..1179905b6 100644
--- a/briar-android/src/org/briarproject/android/ActivityComponent.java
+++ b/briar-android/src/org/briarproject/android/ActivityComponent.java
@@ -28,7 +28,10 @@ import org.briarproject.android.keyagreement.KeyAgreementActivity;
import org.briarproject.android.keyagreement.ShowQrCodeFragment;
import org.briarproject.android.panic.PanicPreferencesActivity;
import org.briarproject.android.panic.PanicResponderActivity;
+import org.briarproject.android.privategroup.creation.CreateGroupActivity;
+import org.briarproject.android.privategroup.creation.CreateGroupFragment;
import org.briarproject.android.privategroup.conversation.GroupActivity;
+import org.briarproject.android.privategroup.creation.CreateGroupMessageFragment;
import org.briarproject.android.privategroup.list.GroupListFragment;
import org.briarproject.android.sharing.ContactSelectorFragment;
import org.briarproject.android.sharing.InvitationsBlogActivity;
@@ -73,6 +76,8 @@ public interface ActivityComponent {
void inject(InvitationsBlogActivity activity);
+ void inject(CreateGroupActivity activity);
+
void inject(GroupActivity activity);
void inject(CreateForumActivity activity);
@@ -118,6 +123,8 @@ public interface ActivityComponent {
// Fragments
void inject(ContactListFragment fragment);
+ void inject(CreateGroupFragment fragment);
+ void inject(CreateGroupMessageFragment fragment);
void inject(GroupListFragment fragment);
void inject(ForumListFragment fragment);
void inject(FeedFragment fragment);
diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java
index 07cace566..69505d32f 100644
--- a/briar-android/src/org/briarproject/android/ActivityModule.java
+++ b/briar-android/src/org/briarproject/android/ActivityModule.java
@@ -23,6 +23,8 @@ import org.briarproject.android.forum.ForumController;
import org.briarproject.android.forum.ForumControllerImpl;
import org.briarproject.android.privategroup.conversation.GroupController;
import org.briarproject.android.privategroup.conversation.GroupControllerImpl;
+import org.briarproject.android.privategroup.creation.CreateGroupController;
+import org.briarproject.android.privategroup.creation.CreateGroupControllerImpl;
import org.briarproject.android.privategroup.list.GroupListController;
import org.briarproject.android.privategroup.list.GroupListControllerImpl;
@@ -101,6 +103,13 @@ public class ActivityModule {
return groupListController;
}
+ @ActivityScope
+ @Provides
+ protected CreateGroupController provideCreateGroupController(
+ CreateGroupControllerImpl createGroupController) {
+ return createGroupController;
+ }
+
@ActivityScope
@Provides
protected GroupController provideGroupController(
diff --git a/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupActivity.java b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupActivity.java
new file mode 100644
index 000000000..991cdf526
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupActivity.java
@@ -0,0 +1,158 @@
+package org.briarproject.android.privategroup.creation;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.ActivityOptionsCompat;
+import android.widget.Toast;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.android.controller.handler.UiResultExceptionHandler;
+import org.briarproject.android.privategroup.conversation.GroupActivity;
+import org.briarproject.android.sharing.BaseMessageFragment.MessageFragmentListener;
+import org.briarproject.android.sharing.ContactSelectorActivity;
+import org.briarproject.android.sharing.ContactSelectorFragment;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+
+import javax.inject.Inject;
+
+import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
+import static android.widget.Toast.LENGTH_SHORT;
+
+public class CreateGroupActivity extends ContactSelectorActivity implements
+ CreateGroupListener, MessageFragmentListener {
+
+ @Inject
+ protected CreateGroupController controller;
+
+ @Override
+ public void injectActivity(ActivityComponent component) {
+ component.inject(this);
+ }
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ setContentView(R.layout.activity_fragment_container);
+
+ if (bundle == null) {
+ CreateGroupFragment fragment = new CreateGroupFragment();
+ getSupportFragmentManager().beginTransaction()
+ .add(R.id.fragmentContainer, fragment)
+ .commit();
+ } else {
+ byte[] groupBytes = bundle.getByteArray(GROUP_ID);
+ if (groupBytes != null) groupId = new GroupId(groupBytes);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
+ // At this point, the group had been created already,
+ // so don't allow to create it again.
+ openNewGroup();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (groupId != null) {
+ outState.putByteArray(GROUP_ID, groupId.getBytes());
+ }
+ }
+
+ @Override
+ public void onGroupNameChosen(String name) {
+ controller.createGroup(name,
+ new UiResultExceptionHandler(this) {
+ @Override
+ public void onResultUi(GroupId g) {
+ switchToContactSelectorFragment(g);
+ }
+
+ @Override
+ public void onExceptionUi(DbException exception) {
+ // TODO proper error handling
+ finish();
+ }
+ });
+ }
+
+ private void switchToContactSelectorFragment(GroupId g) {
+ ContactSelectorFragment fragment =
+ ContactSelectorFragment.newInstance(g);
+ getSupportFragmentManager().beginTransaction()
+ .setCustomAnimations(android.R.anim.fade_in,
+ android.R.anim.fade_out,
+ android.R.anim.slide_in_left,
+ android.R.anim.slide_out_right)
+ .replace(R.id.fragmentContainer, fragment)
+ .addToBackStack(fragment.getUniqueTag())
+ .commit();
+ }
+
+ @Override
+ public boolean isDisabled(GroupId groupId, Contact c) throws DbException {
+ return false;
+ }
+
+ @Override
+ public void contactsSelected(GroupId groupId,
+ Collection contacts) {
+ super.contactsSelected(groupId, contacts);
+
+ CreateGroupMessageFragment fragment = new CreateGroupMessageFragment();
+ getSupportFragmentManager().beginTransaction()
+ .setCustomAnimations(android.R.anim.fade_in,
+ android.R.anim.fade_out,
+ android.R.anim.slide_in_left,
+ android.R.anim.slide_out_right)
+ .replace(R.id.fragmentContainer, fragment)
+ .addToBackStack(fragment.getUniqueTag())
+ .commit();
+ }
+
+ @Override
+ public boolean onButtonClick(String message) {
+ controller.sendInvitation(groupId, contacts, message,
+ new UiResultExceptionHandler(this) {
+ @Override
+ public void onResultUi(Void result) {
+ Toast.makeText(CreateGroupActivity.this,
+ "Inviting members is not yet implemented",
+ LENGTH_SHORT).show();
+ openNewGroup();
+ }
+
+ @Override
+ public void onExceptionUi(DbException exception) {
+ // TODO proper error handling
+ finish();
+ }
+ });
+ return true;
+ }
+
+ private void openNewGroup() {
+ Intent i = new Intent(this, GroupActivity.class);
+ i.putExtra(GROUP_ID, groupId.getBytes());
+ ActivityOptionsCompat options =
+ makeCustomAnimation(this, android.R.anim.fade_in,
+ android.R.anim.fade_out);
+ ActivityCompat.startActivity(this, i, options.toBundle());
+ // finish this activity, so we can't come back to it
+ finish();
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupController.java b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupController.java
new file mode 100644
index 000000000..da1a00a0d
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupController.java
@@ -0,0 +1,19 @@
+package org.briarproject.android.privategroup.creation;
+
+import org.briarproject.android.controller.DbController;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+
+public interface CreateGroupController extends DbController {
+
+ void createGroup(String name,
+ ResultExceptionHandler result);
+
+ void sendInvitation(GroupId groupId, Collection contacts,
+ String message, ResultExceptionHandler result);
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java
new file mode 100644
index 000000000..a35c2ac20
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java
@@ -0,0 +1,68 @@
+package org.briarproject.android.privategroup.creation;
+
+import org.briarproject.android.controller.DbControllerImpl;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.db.DatabaseExecutor;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.privategroup.PrivateGroupManager;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+
+public class CreateGroupControllerImpl extends DbControllerImpl
+ implements CreateGroupController {
+
+ private static final Logger LOG =
+ Logger.getLogger(CreateGroupControllerImpl.class.getName());
+
+ private final PrivateGroupManager groupManager;
+
+ @Inject
+ CreateGroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
+ LifecycleManager lifecycleManager,
+ PrivateGroupManager groupManager) {
+ super(dbExecutor, lifecycleManager);
+ this.groupManager = groupManager;
+ }
+
+ @Override
+ public void createGroup(final String name,
+ final ResultExceptionHandler handler) {
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ LOG.info("Adding group to database...");
+ try {
+ handler.onResult(groupManager.addPrivateGroup(name));
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ handler.onException(e);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void sendInvitation(final GroupId groupId,
+ final Collection contacts, final String message,
+ final ResultExceptionHandler result) {
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ // TODO actually send invitation
+ //noinspection ConstantConditions
+ result.onResult(null);
+ }
+ });
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupFragment.java b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupFragment.java
new file mode 100644
index 000000000..7f21508e3
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupFragment.java
@@ -0,0 +1,93 @@
+package org.briarproject.android.privategroup.creation;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.android.fragment.BaseFragment;
+
+import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH;
+
+public class CreateGroupFragment extends BaseFragment {
+
+ public final static String TAG = CreateGroupFragment.class.getName();
+
+ private CreateGroupListener listener;
+ private EditText name;
+ private Button button;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ listener = (CreateGroupListener) context;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+
+ // inflate view
+ View v = inflater.inflate(R.layout.fragment_create_group, container,
+ false);
+ name = (EditText) v.findViewById(R.id.name);
+ name.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before,
+ int count) {
+ validateName();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+ button = (Button) v.findViewById(R.id.button);
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ listener.hideSoftKeyboard(name);
+ listener.onGroupNameChosen(name.getText().toString());
+ }
+ });
+
+ return v;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ listener.showSoftKeyboard(name);
+ }
+
+ @Override
+ public void injectFragment(ActivityComponent component) {
+ component.inject(this);
+ }
+
+ @Override
+ public String getUniqueTag() {
+ return TAG;
+ }
+
+ private void validateName() {
+ String name = this.name.getText().toString();
+ if (name.length() < 1 || name.length() > MAX_GROUP_NAME_LENGTH)
+ button.setEnabled(false);
+ else if(!button.isEnabled())
+ button.setEnabled(true);
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupListener.java b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupListener.java
new file mode 100644
index 000000000..3347be158
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupListener.java
@@ -0,0 +1,15 @@
+package org.briarproject.android.privategroup.creation;
+
+import android.view.View;
+
+import org.briarproject.android.fragment.BaseFragment.BaseFragmentListener;
+
+interface CreateGroupListener extends BaseFragmentListener {
+
+ void onGroupNameChosen(String name);
+
+ void showSoftKeyboard(View view);
+
+ void hideSoftKeyboard(View view);
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupMessageFragment.java b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupMessageFragment.java
new file mode 100644
index 000000000..d2a6600d4
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupMessageFragment.java
@@ -0,0 +1,33 @@
+package org.briarproject.android.privategroup.creation;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.android.sharing.BaseMessageFragment;
+
+
+public class CreateGroupMessageFragment extends BaseMessageFragment {
+
+ private final static String TAG =
+ CreateGroupMessageFragment.class.getName();
+
+ @Override
+ protected int getButtonText() {
+ return R.string.groups_create_group_invitation_button;
+ }
+
+ @Override
+ protected int getHintText() {
+ return R.string.forum_share_message;
+ }
+
+ @Override
+ public String getUniqueTag() {
+ return TAG;
+ }
+
+ @Override
+ public void injectFragment(ActivityComponent component) {
+ component.inject(this);
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/list/GroupListFragment.java b/briar-android/src/org/briarproject/android/privategroup/list/GroupListFragment.java
index 4489dd6bf..9e77003ac 100644
--- a/briar-android/src/org/briarproject/android/privategroup/list/GroupListFragment.java
+++ b/briar-android/src/org/briarproject/android/privategroup/list/GroupListFragment.java
@@ -1,8 +1,10 @@
package org.briarproject.android.privategroup.list;
+import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
+import android.support.v4.app.ActivityOptionsCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -15,6 +17,7 @@ import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.android.fragment.BaseFragment;
+import org.briarproject.android.privategroup.creation.CreateGroupActivity;
import org.briarproject.android.privategroup.list.GroupListController.GroupListListener;
import org.briarproject.android.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener;
import org.briarproject.android.view.BriarRecyclerView;
@@ -27,6 +30,8 @@ import java.util.logging.Logger;
import javax.inject.Inject;
+import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
+
public class GroupListFragment extends BaseFragment implements
GroupListListener, OnGroupRemoveClickListener {
@@ -48,8 +53,6 @@ public class GroupListFragment extends BaseFragment implements
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- setHasOptionsMenu(true);
-
View v = inflater.inflate(R.layout.list, container, false);
adapter = new GroupListAdapter(getContext(), this);
@@ -94,7 +97,12 @@ public class GroupListFragment extends BaseFragment implements
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_add_group:
- // TODO
+ Intent i = new Intent(getContext(), CreateGroupActivity.class);
+ ActivityOptionsCompat options =
+ makeCustomAnimation(getActivity(),
+ android.R.anim.slide_in_left,
+ android.R.anim.slide_out_right);
+ startActivity(i, options.toBundle());
return true;
default:
return super.onOptionsItemSelected(item);
diff --git a/briar-android/src/org/briarproject/android/sharing/BaseMessageFragment.java b/briar-android/src/org/briarproject/android/sharing/BaseMessageFragment.java
index 2613fd230..251846f64 100644
--- a/briar-android/src/org/briarproject/android/sharing/BaseMessageFragment.java
+++ b/briar-android/src/org/briarproject/android/sharing/BaseMessageFragment.java
@@ -4,7 +4,6 @@ import android.content.Context;
import android.os.Bundle;
import android.support.annotation.StringRes;
import android.view.LayoutInflater;
-import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
@@ -16,7 +15,7 @@ import org.briarproject.android.view.TextInputView.TextInputListener;
import static org.briarproject.api.sharing.SharingConstants.MAX_INVITATION_MESSAGE_LENGTH;
import static org.briarproject.util.StringUtils.truncateUtf8;
-abstract class BaseMessageFragment extends BaseFragment
+public abstract class BaseMessageFragment extends BaseFragment
implements TextInputListener {
protected LargeTextInputView message;