Manage group links behind feature flag.

This commit is contained in:
Alan Evans
2020-08-26 15:59:34 -03:00
parent 860f06ec9e
commit bfed03b7b5
51 changed files with 2177 additions and 80 deletions

View File

@@ -0,0 +1,100 @@
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ShareCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import java.util.Objects;
public final class GroupLinkBottomSheetDialogFragment extends BottomSheetDialogFragment {
public static final String ARG_GROUP_ID = "group_id";
public static void show(@NonNull FragmentManager manager, @NonNull GroupId.V2 groupId) {
GroupLinkBottomSheetDialogFragment fragment = new GroupLinkBottomSheetDialogFragment();
Bundle args = new Bundle();
args.putString(ARG_GROUP_ID, groupId.toString());
fragment.setArguments(args);
fragment.show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
setStyle(DialogFragment.STYLE_NORMAL,
ThemeUtil.isDarkTheme(requireContext()) ? R.style.Theme_Signal_RoundedBottomSheet
: R.style.Theme_Signal_RoundedBottomSheet_Light);
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.group_link_share_bottom_sheet, container, false);
View shareViaSignalButton = view.findViewById(R.id.group_link_bottom_sheet_share_via_signal_button);
View copyButton = view.findViewById(R.id.group_link_bottom_sheet_copy_button);
View viewQrButton = view.findViewById(R.id.group_link_bottom_sheet_qr_code_button);
View shareBySystemButton = view.findViewById(R.id.group_link_bottom_sheet_share_via_system_button);
GroupId.V2 groupId = GroupId.parseOrThrow(Objects.requireNonNull(requireArguments().getString(ARG_GROUP_ID))).requireV2();
LiveGroup liveGroup = new LiveGroup(groupId);
liveGroup.getGroupLink().observe(getViewLifecycleOwner(), groupLink -> {
if (!groupLink.isEnabled()) {
Toast.makeText(requireContext(), R.string.GroupLinkBottomSheet_the_link_is_not_currently_active, Toast.LENGTH_SHORT).show();
dismiss();
return;
}
shareViaSignalButton.setOnClickListener(v -> dismiss()); // Todo [Alan] GV2 Add share within signal
shareViaSignalButton.setVisibility(View.GONE);
copyButton.setOnClickListener(v -> {
Context context = requireContext();
Util.copyToClipboard(context, groupLink.getUrl());
Toast.makeText(context, R.string.GroupLinkBottomSheet_copied_to_clipboard, Toast.LENGTH_SHORT).show();
dismiss();
});
viewQrButton.setOnClickListener(v -> dismiss()); // Todo [Alan] GV2 Add share QR within signal
viewQrButton.setVisibility(View.GONE);
shareBySystemButton.setOnClickListener(v -> {
ShareCompat.IntentBuilder.from(requireActivity())
.setType("text/plain")
.setText(groupLink.getUrl())
.startChooser();
dismiss();
});
});
return view;
}
@Override
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
BottomSheetUtil.show(manager, tag, this);
}
}

View File

@@ -0,0 +1,112 @@
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProviders;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
public final class ShareableGroupLinkDialogFragment extends DialogFragment {
private static final String ARG_GROUP_ID = "group_id";
private ShareableGroupLinkViewModel viewModel;
private GroupId.V2 groupId;
private SimpleProgressDialog.DismissibleDialog dialog;
public static DialogFragment create(@NonNull GroupId.V2 groupId) {
DialogFragment fragment = new ShareableGroupLinkDialogFragment();
Bundle args = new Bundle();
args.putString(ARG_GROUP_ID, groupId.toString());
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NO_FRAME, ThemeUtil.isDarkTheme(requireActivity()) ? R.style.TextSecure_DarkTheme
: R.style.TextSecure_LightTheme);
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.shareable_group_link_dialog_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
initializeViewModel();
initializeViews(view);
}
private void initializeViewModel() {
//noinspection ConstantConditions
groupId = GroupId.parseOrThrow(requireArguments().getString(ARG_GROUP_ID)).requireV2();
ShareableGroupLinkRepository repository = new ShareableGroupLinkRepository(requireContext(), groupId);
ShareableGroupLinkViewModel.Factory factory = new ShareableGroupLinkViewModel.Factory(groupId, repository);
viewModel = ViewModelProviders.of(this, factory).get(ShareableGroupLinkViewModel.class);
}
private void initializeViews(@NonNull View view) {
SwitchCompat shareableGroupLinkSwitch = view.findViewById(R.id.shareable_group_link_enable_switch);
TextView shareableGroupLinkDisplay = view.findViewById(R.id.shareable_group_link_display);
SwitchCompat approveNewMembersSwitch = view.findViewById(R.id.shareable_group_link_approve_new_members_switch);
View shareableGroupLinkRow = view.findViewById(R.id.shareable_group_link_row);
View shareRow = view.findViewById(R.id.shareable_group_link_share_row);
View resetLinkRow = view.findViewById(R.id.shareable_group_link_reset_link_row);
View approveNewMembersRow = view.findViewById(R.id.shareable_group_link_approve_new_members_row);
Toolbar toolbar = view.findViewById(R.id.shareable_group_link_toolbar);
toolbar.setNavigationOnClickListener(v -> dismissAllowingStateLoss());
viewModel.getGroupLink().observe(getViewLifecycleOwner(), groupLink -> {
shareableGroupLinkSwitch.setChecked(groupLink.isEnabled());
approveNewMembersSwitch.setChecked(groupLink.isRequiresApproval());
shareableGroupLinkDisplay.setText(groupLink.getUrl());
});
shareRow.setOnClickListener(v -> GroupLinkBottomSheetDialogFragment.show(requireFragmentManager(), groupId));
shareableGroupLinkRow.setOnClickListener(v -> viewModel.onToggleGroupLink(requireContext()));
approveNewMembersRow.setOnClickListener(v -> viewModel.onToggleApproveMembers(requireContext()));
resetLinkRow.setOnClickListener(v -> viewModel.onResetLink(requireContext()));
viewModel.getToasts().observe(getViewLifecycleOwner(), t -> Toast.makeText(requireContext(), t, Toast.LENGTH_SHORT).show());
viewModel.getBusy().observe(getViewLifecycleOwner(), busy -> {
if (busy) {
if (dialog == null) {
dialog = SimpleProgressDialog.showDelayed(requireContext());
}
} else {
if (dialog != null) {
dialog.dismiss();
dialog = null;
}
}
});
}
}

View File

@@ -0,0 +1,115 @@
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.signal.storageservice.protos.groups.AccessControl;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.io.IOException;
final class ShareableGroupLinkRepository {
private final Context context;
private final GroupId.V2 groupId;
ShareableGroupLinkRepository(@NonNull Context context, @NonNull GroupId.V2 groupId) {
this.context = context;
this.groupId = groupId;
}
void cycleGroupLinkPassword(@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback) {
SignalExecutors.UNBOUNDED.execute(() -> {
try {
GroupManager.cycleGroupLinkPassword(context, groupId);
callback.onComplete(null);
} catch (GroupNotAMemberException | GroupChangeFailedException | GroupInsufficientRightsException | IOException | GroupChangeBusyException e) {
callback.onError(GroupChangeFailureReason.fromException(e));
}
});
}
void toggleGroupLinkEnabled(@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback) {
setGroupLinkEnabledState(toggleGroupLinkState(true, false), callback);
}
void toggleGroupLinkApprovalRequired(@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback) {
setGroupLinkEnabledState(toggleGroupLinkState(false, true), callback);
}
private void setGroupLinkEnabledState(@NonNull GroupManager.GroupLinkState state,
@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback)
{
SignalExecutors.UNBOUNDED.execute(() -> {
try {
GroupManager.setGroupLinkEnabledState(context, groupId, state);
callback.onComplete(null);
} catch (GroupNotAMemberException | GroupChangeFailedException | GroupInsufficientRightsException | IOException | GroupChangeBusyException e) {
callback.onError(GroupChangeFailureReason.fromException(e));
}
});
}
@WorkerThread
private GroupManager.GroupLinkState toggleGroupLinkState(boolean toggleEnabled, boolean toggleApprovalNeeded) {
AccessControl.AccessRequired currentState = DatabaseFactory.getGroupDatabase(context)
.getGroup(groupId)
.get()
.requireV2GroupProperties()
.getDecryptedGroup()
.getAccessControl()
.getAddFromInviteLink();
boolean enabled;
boolean approvalNeeded;
switch (currentState) {
case UNKNOWN:
case UNSATISFIABLE:
case UNRECOGNIZED:
case MEMBER:
enabled = false;
approvalNeeded = false;
break;
case ANY:
enabled = true;
approvalNeeded = false;
break;
case ADMINISTRATOR:
enabled = true;
approvalNeeded = true;
break;
default: throw new AssertionError();
}
if (toggleApprovalNeeded) {
approvalNeeded = !approvalNeeded;
}
if (toggleEnabled) {
enabled = !enabled;
if (enabled) approvalNeeded = true;
}
if (approvalNeeded && enabled) {
return GroupManager.GroupLinkState.ENABLED_WITH_APPROVAL;
} else {
if (enabled) {
return GroupManager.GroupLinkState.ENABLED;
}
}
return GroupManager.GroupLinkState.DISABLED;
}
}

View File

@@ -0,0 +1,111 @@
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
import org.thoughtcrime.securesms.groups.v2.GroupLinkUrlAndStatus;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
final class ShareableGroupLinkViewModel extends ViewModel {
private final ShareableGroupLinkRepository repository;
private final LiveData<GroupLinkUrlAndStatus> groupLink;
private final SingleLiveEvent<String> toasts;
private final SingleLiveEvent<Boolean> busy;
private ShareableGroupLinkViewModel(@NonNull GroupId.V2 groupId, @NonNull ShareableGroupLinkRepository repository) {
this.repository = repository;
this.groupLink = new LiveGroup(groupId).getGroupLink();
this.toasts = new SingleLiveEvent<>();
this.busy = new SingleLiveEvent<>();
}
LiveData<GroupLinkUrlAndStatus> getGroupLink() {
return groupLink;
}
LiveData<String> getToasts() {
return toasts;
}
LiveData<Boolean> getBusy() {
return busy;
}
void onToggleGroupLink(@NonNull Context context) {
busy.setValue(true);
repository.toggleGroupLinkEnabled(new AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason>() {
@Override
public void onComplete(@Nullable Void result) {
busy.postValue(false);
}
@Override
public void onError(@Nullable GroupChangeFailureReason error) {
busy.postValue(false);
toasts.postValue(context.getString(GroupErrors.getUserDisplayMessage(error)));
}
});
}
void onToggleApproveMembers(@NonNull Context context) {
busy.setValue(true);
repository.toggleGroupLinkApprovalRequired(new AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason>() {
@Override
public void onComplete(@Nullable Void result) {
busy.postValue(false);
}
@Override
public void onError(@Nullable GroupChangeFailureReason error) {
busy.postValue(false);
toasts.postValue(context.getString(GroupErrors.getUserDisplayMessage(error)));
}
});
}
void onResetLink(@NonNull Context context) {
busy.setValue(true);
repository.cycleGroupLinkPassword(new AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason>() {
@Override
public void onComplete(@Nullable Void result) {
busy.postValue(false);
toasts.postValue(context.getString(R.string.ShareableGroupLinkDialogFragment__group_link_reset));
}
@Override
public void onError(@Nullable GroupChangeFailureReason error) {
busy.postValue(false);
toasts.postValue(context.getString(GroupErrors.getUserDisplayMessage(error)));
}
});
}
public static final class Factory implements ViewModelProvider.Factory {
private final GroupId.V2 groupId;
private final ShareableGroupLinkRepository repository;
public Factory(@NonNull GroupId.V2 groupId, @NonNull ShareableGroupLinkRepository repository) {
this.groupId = groupId;
this.repository = repository;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ShareableGroupLinkViewModel(groupId, repository));
}
}
}