From 1a812f13272578317da6c9db3a9501f5a45a1800 Mon Sep 17 00:00:00 2001 From: Torsten Grote <t@grobox.de> Date: Thu, 13 Oct 2016 12:28:04 -0300 Subject: [PATCH] UI for creating private groups --- briar-android/AndroidManifest.xml | 11 ++ .../res/layout/fragment_create_group.xml | 23 +++ .../res/menu/groups_list_actions.xml | 2 +- briar-android/res/values/strings.xml | 5 +- .../android/ActivityComponent.java | 7 + .../briarproject/android/ActivityModule.java | 9 + .../creation/CreateGroupActivity.java | 158 ++++++++++++++++++ .../creation/CreateGroupController.java | 19 +++ .../creation/CreateGroupControllerImpl.java | 68 ++++++++ .../creation/CreateGroupFragment.java | 93 +++++++++++ .../creation/CreateGroupListener.java | 15 ++ .../creation/CreateGroupMessageFragment.java | 33 ++++ .../privategroup/list/GroupListFragment.java | 14 +- .../android/sharing/BaseMessageFragment.java | 3 +- 14 files changed, 453 insertions(+), 7 deletions(-) create mode 100644 briar-android/res/layout/fragment_create_group.xml create mode 100644 briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupActivity.java create mode 100644 briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupController.java create mode 100644 briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java create mode 100644 briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupFragment.java create mode 100644 briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupListener.java create mode 100644 briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupMessageFragment.java diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index 1bb12bfff4..aad00c6d9e 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -100,6 +100,17 @@ /> </activity> + <activity + android:name=".android.privategroup.creation.CreateGroupActivity" + android:label="@string/groups_create_group_title" + android:parentActivityName=".android.NavDrawerActivity" + android:windowSoftInputMode="adjustResize"> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value=".android.NavDrawerActivity" + /> + </activity> + <activity android:name=".android.privategroup.conversation.GroupActivity" android:label="@string/app_name" diff --git a/briar-android/res/layout/fragment_create_group.xml b/briar-android/res/layout/fragment_create_group.xml new file mode 100644 index 0000000000..a9601e2abb --- /dev/null +++ b/briar-android/res/layout/fragment_create_group.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="@dimen/margin_medium"> + + <EditText + android:id="@+id/name" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:gravity="bottom" + android:hint="@string/groups_create_group_hint"/> + + <Button + android:id="@+id/button" + style="@style/BriarButton" + android:enabled="false" + android:text="@string/groups_create_group_button"/> + +</LinearLayout> diff --git a/briar-android/res/menu/groups_list_actions.xml b/briar-android/res/menu/groups_list_actions.xml index 511224ff16..16eeba4625 100644 --- a/briar-android/res/menu/groups_list_actions.xml +++ b/briar-android/res/menu/groups_list_actions.xml @@ -6,7 +6,7 @@ <item android:id="@+id/action_add_group" android:icon="@drawable/ic_add_white" - android:title="@string/groups_add_group_title" + android:title="@string/groups_create_group_title" app:showAsAction="ifRoom"/> </menu> \ No newline at end of file diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index 004c001290..872800c9f0 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -155,8 +155,11 @@ <string name="groups_group_is_empty">This group is empty</string> <string name="groups_group_is_dissolved">This group is dissolved</string> <string name="groups_remove">Remove</string> - <string name="groups_add_group_title">Add Private Group</string> + <string name="groups_create_group_title">Create Private Group</string> <string name="groups_no_messages">This group is empty.\n\nYou can use the pen icon at the top to compose the first message.</string> + <string name="groups_create_group_button">Create Group</string> + <string name="groups_create_group_invitation_button">Send Invitation</string> + <string name="groups_create_group_hint">Add a name for your private group</string> <string name="groups_compose_message">Compose Message</string> <string name="groups_message_sent">Message sent</string> <string name="groups_message_received">Message received</string> diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java index 4e66c87aba..1179905b69 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 07cace5662..69505d32f3 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 0000000000..991cdf5265 --- /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<GroupId, DbException>(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<ContactId> 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<Void, DbException>(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 0000000000..da1a00a0d1 --- /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<GroupId, DbException> result); + + void sendInvitation(GroupId groupId, Collection<ContactId> contacts, + String message, ResultExceptionHandler<Void, DbException> 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 0000000000..a35c2ac204 --- /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<GroupId, DbException> 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<ContactId> contacts, final String message, + final ResultExceptionHandler<Void, DbException> 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 0000000000..7f21508e38 --- /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 0000000000..3347be1586 --- /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 0000000000..d2a6600d49 --- /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 4489dd6bf0..9e77003ace 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 2613fd230b..251846f644 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; -- GitLab