/* * Copyright (C) 2015 Open Whisper Systems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.thoughtcrime.securesms; import android.content.Intent; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.ViewGroup; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import org.signal.core.util.DimensionUnit; import org.signal.core.util.concurrent.SimpleTask; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.components.menu.ActionItem; import org.thoughtcrime.securesms.components.menu.SignalContextMenu; import org.thoughtcrime.securesms.contacts.ContactSelectionListItem; import org.thoughtcrime.securesms.contacts.management.ContactsManagementRepository; import org.thoughtcrime.securesms.contacts.management.ContactsManagementViewModel; import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery; import org.thoughtcrime.securesms.conversation.ConversationIntents; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.LifecycleDisposable; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Activity container for starting a new conversation. * * @author Moxie Marlinspike */ public class NewConversationActivity extends ContactSelectionActivity implements ContactSelectionListFragment.ListCallback, ContactSelectionListFragment.OnItemLongClickListener { @SuppressWarnings("unused") private static final String TAG = Log.tag(NewConversationActivity.class); private ContactsManagementViewModel viewModel; private ActivityResultLauncher contactLauncher; private final LifecycleDisposable disposables = new LifecycleDisposable(); @Override public void onCreate(Bundle bundle, boolean ready) { super.onCreate(bundle, ready); assert getSupportActionBar() != null; getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setTitle(R.string.NewConversationActivity__new_message); disposables.bindTo(this); ContactsManagementRepository repository = new ContactsManagementRepository(this); ContactsManagementViewModel.Factory factory = new ContactsManagementViewModel.Factory(repository); contactLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), activityResult -> { if (activityResult.getResultCode() == RESULT_OK) { handleManualRefresh(); } }); viewModel = new ViewModelProvider(this, factory).get(ContactsManagementViewModel.class); } @Override public void onBeforeContactSelected(@NonNull Optional recipientId, String number, @NonNull Consumer callback) { boolean smsSupported = SignalStore.misc().getSmsExportPhase().allowSmsFeatures(); if (recipientId.isPresent()) { launch(Recipient.resolved(recipientId.get())); } else { Log.i(TAG, "[onContactSelected] Maybe creating a new recipient."); if (SignalStore.account().isRegistered()) { Log.i(TAG, "[onContactSelected] Doing contact refresh."); AlertDialog progress = SimpleProgressDialog.show(this); SimpleTask.run(getLifecycle(), () -> { Recipient resolved = Recipient.external(this, number); if (!resolved.isRegistered() || !resolved.hasServiceId()) { Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh."); try { ContactDiscovery.refresh(this, resolved, false); resolved = Recipient.resolved(resolved.getId()); } catch (IOException e) { Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact."); return null; } } return resolved; }, resolved -> { progress.dismiss(); if (resolved != null) { if (smsSupported || resolved.isRegistered() && resolved.hasServiceId()) { launch(resolved); } else { new MaterialAlertDialogBuilder(this) .setMessage(getString(R.string.NewConversationActivity__s_is_not_a_signal_user, resolved.getDisplayName(this))) .setPositiveButton(android.R.string.ok, null) .show(); } } else { new MaterialAlertDialogBuilder(this) .setMessage(R.string.NetworkFailure__network_error_check_your_connection_and_try_again) .setPositiveButton(android.R.string.ok, null) .show(); } }); } else if (smsSupported) { launch(Recipient.external(this, number)); } } callback.accept(true); } @Override public void onSelectionChanged() { } private void launch(Recipient recipient) { long existingThread = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId()); Intent intent = ConversationIntents.createBuilder(this, recipient.getId(), existingThread) .withDraftText(getIntent().getStringExtra(Intent.EXTRA_TEXT)) .withDataUri(getIntent().getData()) .withDataType(getIntent().getType()) .build(); startActivity(intent); finish(); } @Override public boolean onOptionsItemSelected(MenuItem item) { super.onOptionsItemSelected(item); switch (item.getItemId()) { case android.R.id.home: super.onBackPressed(); return true; case R.id.menu_refresh: handleManualRefresh(); return true; case R.id.menu_new_group: handleCreateGroup(); return true; case R.id.menu_invite: handleInvite(); return true; } return false; } private void handleManualRefresh() { contactsFragment.setRefreshing(true); onRefresh(); } private void handleCreateGroup() { startActivity(CreateGroupActivity.newIntent(this)); } private void handleInvite() { startActivity(new Intent(this, InviteActivity.class)); } @Override public boolean onCreateOptionsMenu(Menu menu) { menu.clear(); getMenuInflater().inflate(R.menu.new_conversation_activity, menu); super.onCreateOptionsMenu(menu); return true; } @Override public void onInvite() { handleInvite(); finish(); } @Override public void onNewGroup(boolean forceV1) { handleCreateGroup(); finish(); } @Override public boolean onLongClick(ContactSelectionListItem contactSelectionListItem, RecyclerView recyclerView) { RecipientId recipientId = contactSelectionListItem.getRecipientId().orElse(null); if (recipientId == null) { return false; } List actions = generateContextualActionsForRecipient(recipientId); if (actions.isEmpty()) { return false; } new SignalContextMenu.Builder(contactSelectionListItem, (ViewGroup) contactSelectionListItem.getRootView()) .preferredVerticalPosition(SignalContextMenu.VerticalPosition.BELOW) .preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.START) .offsetX((int) DimensionUnit.DP.toPixels(12)) .offsetY((int) DimensionUnit.DP.toPixels(12)) .onDismiss(() -> recyclerView.suppressLayout(false)) .show(actions); recyclerView.suppressLayout(true); return true; } private @NonNull List generateContextualActionsForRecipient(@NonNull RecipientId recipientId) { Recipient recipient = Recipient.resolved(recipientId); return Stream.of( createMessageActionItem(recipient), createAudioCallActionItem(recipient), createVideoCallActionItem(recipient), createRemoveActionItem(recipient), createBlockActionItem(recipient) ).filter(Objects::nonNull).collect(Collectors.toList()); } private @NonNull ActionItem createMessageActionItem(@NonNull Recipient recipient) { return new ActionItem( R.drawable.ic_chat_message_24, getString(R.string.NewConversationActivity__message), R.color.signal_colorOnSurface, () -> startActivity(ConversationIntents.createBuilder(this, recipient.getId(), -1L).build()) ); } private @Nullable ActionItem createAudioCallActionItem(@NonNull Recipient recipient) { if (recipient.isSelf() || recipient.isGroup()) { return null; } if (recipient.isRegistered() || (SignalStore.misc().getSmsExportPhase().allowSmsFeatures())) { return new ActionItem( R.drawable.ic_phone_right_24, getString(R.string.NewConversationActivity__audio_call), R.color.signal_colorOnSurface, () -> CommunicationActions.startVoiceCall(this, recipient) ); } else { return null; } } private @Nullable ActionItem createVideoCallActionItem(@NonNull Recipient recipient) { if (recipient.isSelf() || recipient.isMmsGroup() || !recipient.isRegistered()) { return null; } return new ActionItem( R.drawable.ic_video_call_24, getString(R.string.NewConversationActivity__video_call), R.color.signal_colorOnSurface, () -> CommunicationActions.startVideoCall(this, recipient) ); } private @Nullable ActionItem createRemoveActionItem(@NonNull Recipient recipient) { if (!FeatureFlags.hideContacts() || recipient.isSelf() || recipient.isGroup()) { return null; } return new ActionItem( R.drawable.ic_minus_circle_20, // TODO [alex] -- correct asset getString(R.string.NewConversationActivity__remove), R.color.signal_colorOnSurface, () -> { if (recipient.isSystemContact()) { displayIsInSystemContactsDialog(recipient); } else { displayRemovalDialog(recipient); } } ); } @SuppressWarnings("CodeBlock2Expr") private @Nullable ActionItem createBlockActionItem(@NonNull Recipient recipient) { if (recipient.isSelf()) { return null; } return new ActionItem( R.drawable.ic_block_tinted_24, getString(R.string.NewConversationActivity__block), R.color.signal_colorError, () -> BlockUnblockDialog.showBlockFor(this, this.getLifecycle(), recipient, () -> { disposables.add(viewModel.blockContact(recipient).subscribe(() -> { displaySnackbar(R.string.NewConversationActivity__s_has_been_blocked, recipient.getDisplayName(this)); contactsFragment.reset(); })); }) ); } private void displayIsInSystemContactsDialog(@NonNull Recipient recipient) { new MaterialAlertDialogBuilder(this) .setTitle(getString(R.string.NewConversationActivity__unable_to_remove_s, recipient.getShortDisplayName(this))) .setMessage(R.string.NewConversationActivity__this_person_is_saved_to_your) .setPositiveButton(R.string.NewConversationActivity__view_contact, (dialog, which) -> contactLauncher.launch(new Intent(Intent.ACTION_VIEW, recipient.getContactUri())) ) .setNegativeButton(android.R.string.cancel, null) .show(); } private void displayRemovalDialog(@NonNull Recipient recipient) { new MaterialAlertDialogBuilder(this) .setTitle(getString(R.string.NewConversationActivity__remove_s, recipient.getShortDisplayName(this))) .setMessage(R.string.NewConversationActivity__you_wont_see_this_person) .setPositiveButton(R.string.NewConversationActivity__remove, (dialog, which) -> { disposables.add(viewModel.hideContact(recipient).subscribe(() -> { displaySnackbar(R.string.NewConversationActivity__s_has_been_removed, recipient.getDisplayName(this)); })); } ) .setNegativeButton(android.R.string.cancel, null) .show(); } private void displaySnackbar(@StringRes int message, Object ... formatArgs) { Snackbar.make(findViewById(android.R.id.content), getString(message, formatArgs), Snackbar.LENGTH_SHORT).show(); } }