That fuck shit the fascists are using
at master 399 lines 15 kB view raw
1/* 2 * Copyright (C) 2015 Open Whisper Systems 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17package org.tm.archive; 18 19import android.content.Intent; 20import android.os.Bundle; 21import android.view.Menu; 22import android.view.MenuItem; 23import android.view.View; 24import android.view.ViewGroup; 25 26import androidx.activity.result.ActivityResultLauncher; 27import androidx.activity.result.contract.ActivityResultContracts; 28import androidx.annotation.NonNull; 29import androidx.annotation.Nullable; 30import androidx.annotation.StringRes; 31import androidx.appcompat.app.AlertDialog; 32import androidx.lifecycle.ViewModelProvider; 33import androidx.recyclerview.widget.RecyclerView; 34 35import com.google.android.material.dialog.MaterialAlertDialogBuilder; 36import com.google.android.material.snackbar.Snackbar; 37 38import org.signal.core.util.DimensionUnit; 39import org.signal.core.util.concurrent.LifecycleDisposable; 40import org.signal.core.util.concurrent.SimpleTask; 41import org.signal.core.util.logging.Log; 42import org.tm.archive.components.menu.ActionItem; 43import org.tm.archive.components.menu.SignalContextMenu; 44import org.tm.archive.contacts.management.ContactsManagementRepository; 45import org.tm.archive.contacts.management.ContactsManagementViewModel; 46import org.tm.archive.contacts.paged.ContactSearchKey; 47import org.tm.archive.contacts.sync.ContactDiscovery; 48import org.tm.archive.conversation.ConversationIntents; 49import org.tm.archive.groups.ui.creategroup.CreateGroupActivity; 50import org.tm.archive.keyvalue.SignalStore; 51import org.tm.archive.recipients.Recipient; 52import org.tm.archive.recipients.RecipientId; 53import org.tm.archive.recipients.RecipientRepository; 54import org.tm.archive.recipients.ui.findby.FindByActivity; 55import org.tm.archive.recipients.ui.findby.FindByMode; 56import org.tm.archive.util.CommunicationActions; 57import org.tm.archive.util.views.SimpleProgressDialog; 58 59import java.io.IOException; 60import java.util.List; 61import java.util.Objects; 62import java.util.Optional; 63import java.util.concurrent.TimeUnit; 64import java.util.function.Consumer; 65import java.util.stream.Collectors; 66import java.util.stream.Stream; 67 68import io.reactivex.rxjava3.disposables.Disposable; 69 70/** 71 * Activity container for starting a new conversation. 72 * 73 * @author Moxie Marlinspike 74 */ 75public class NewConversationActivity extends ContactSelectionActivity 76 implements ContactSelectionListFragment.NewConversationCallback, ContactSelectionListFragment.OnItemLongClickListener, ContactSelectionListFragment.FindByCallback 77{ 78 79 @SuppressWarnings("unused") 80 private static final String TAG = Log.tag(NewConversationActivity.class); 81 82 private ContactsManagementViewModel viewModel; 83 private ActivityResultLauncher<Intent> contactLauncher; 84 private ActivityResultLauncher<Intent> createGroupLauncher; 85 private ActivityResultLauncher<FindByMode> findByLauncher; 86 87 private final LifecycleDisposable disposables = new LifecycleDisposable(); 88 89 @Override 90 public void onCreate(Bundle bundle, boolean ready) { 91 super.onCreate(bundle, ready); 92 assert getSupportActionBar() != null; 93 getSupportActionBar().setDisplayHomeAsUpEnabled(true); 94 getSupportActionBar().setTitle(R.string.NewConversationActivity__new_message); 95 96 disposables.bindTo(this); 97 98 ContactsManagementRepository repository = new ContactsManagementRepository(this); 99 ContactsManagementViewModel.Factory factory = new ContactsManagementViewModel.Factory(repository); 100 101 contactLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), activityResult -> { 102 if (activityResult.getResultCode() != RESULT_CANCELED) { 103 handleManualRefresh(); 104 } 105 }); 106 107 findByLauncher = registerForActivityResult(new FindByActivity.Contract(), result -> { 108 if (result != null) { 109 launch(result); 110 } 111 }); 112 113 createGroupLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { 114 if (result.getResultCode() == RESULT_OK) { 115 finish(); 116 } 117 }); 118 119 viewModel = new ViewModelProvider(this, factory).get(ContactsManagementViewModel.class); 120 } 121 122 @Override 123 public void onBeforeContactSelected(boolean isFromUnknownSearchKey, @NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) { 124 if (recipientId.isPresent()) { 125 launch(Recipient.resolved(recipientId.get())); 126 } else { 127 Log.i(TAG, "[onContactSelected] Maybe creating a new recipient."); 128 129 if (SignalStore.account().isRegistered()) { 130 Log.i(TAG, "[onContactSelected] Doing contact refresh."); 131 132 AlertDialog progress = SimpleProgressDialog.show(this); 133 134 SimpleTask.run(getLifecycle(), () -> RecipientRepository.lookupNewE164(this, number), result -> { 135 progress.dismiss(); 136 137 if (result instanceof RecipientRepository.LookupResult.Success) { 138 Recipient resolved = Recipient.resolved(((RecipientRepository.LookupResult.Success) result).getRecipientId()); 139 if (resolved.isRegistered() && resolved.hasServiceId()) { 140 launch(resolved); 141 } 142 } else if (result instanceof RecipientRepository.LookupResult.NotFound || result instanceof RecipientRepository.LookupResult.InvalidEntry) { 143 new MaterialAlertDialogBuilder(this) 144 .setMessage(getString(R.string.NewConversationActivity__s_is_not_a_signal_user, number)) 145 .setPositiveButton(android.R.string.ok, null) 146 .show(); 147 } else { 148 new MaterialAlertDialogBuilder(this) 149 .setMessage(R.string.NetworkFailure__network_error_check_your_connection_and_try_again) 150 .setPositiveButton(android.R.string.ok, null) 151 .show(); 152 } 153 }); 154 } 155 } 156 157 callback.accept(true); 158 } 159 160 @Override 161 public void onSelectionChanged() { 162 } 163 164 private void launch(Recipient recipient) { 165 launch(recipient.getId()); 166 } 167 168 169 private void launch(RecipientId recipientId) { 170 Disposable disposable = ConversationIntents.createBuilder(this, recipientId, -1L) 171 .map(builder -> builder 172 .withDraftText(getIntent().getStringExtra(Intent.EXTRA_TEXT)) 173 .withDataUri(getIntent().getData()) 174 .withDataType(getIntent().getType()) 175 .build()) 176 .subscribe(intent -> { 177 startActivity(intent); 178 finish(); 179 }); 180 181 disposables.add(disposable); 182 } 183 184 @Override 185 public boolean onOptionsItemSelected(MenuItem item) { 186 super.onOptionsItemSelected(item); 187 188 int itemId = item.getItemId(); 189 190 if (itemId == android.R.id.home) { 191 super.onBackPressed(); 192 return true; 193 } else if (itemId == R.id.menu_refresh) { 194 handleManualRefresh(); 195 return true; 196 } else if (itemId == R.id.menu_new_group) { 197 handleCreateGroup(); 198 return true; 199 } else if (itemId == R.id.menu_invite) { 200 handleInvite(); 201 return true; 202 } else { 203 return false; 204 } 205 } 206 207 private void handleManualRefresh() { 208 contactsFragment.setRefreshing(true); 209 onRefresh(); 210 } 211 212 private void handleCreateGroup() { 213 createGroupLauncher.launch(CreateGroupActivity.newIntent(this)); 214 } 215 216 private void handleInvite() { 217 startActivity(new Intent(this, InviteActivity.class)); 218 } 219 220 @Override 221 public boolean onCreateOptionsMenu(Menu menu) { 222 menu.clear(); 223 getMenuInflater().inflate(R.menu.new_conversation_activity, menu); 224 225 super.onCreateOptionsMenu(menu); 226 return true; 227 } 228 229 @Override 230 public void onInvite() { 231 handleInvite(); 232 finish(); 233 } 234 235 @Override 236 public void onNewGroup(boolean forceV1) { 237 handleCreateGroup(); 238// finish(); 239 } 240 241 @Override 242 public void onFindByUsername() { 243 findByLauncher.launch(FindByMode.USERNAME); 244 } 245 246 @Override 247 public void onFindByPhoneNumber() { 248 findByLauncher.launch(FindByMode.PHONE_NUMBER); 249 } 250 251 @Override 252 public boolean onLongClick(View anchorView, ContactSearchKey contactSearchKey, RecyclerView recyclerView) { 253 RecipientId recipientId = contactSearchKey.requireRecipientSearchKey().getRecipientId(); 254 List<ActionItem> actions = generateContextualActionsForRecipient(recipientId); 255 if (actions.isEmpty()) { 256 return false; 257 } 258 259 new SignalContextMenu.Builder(anchorView, (ViewGroup) anchorView.getRootView()) 260 .preferredVerticalPosition(SignalContextMenu.VerticalPosition.BELOW) 261 .preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.START) 262 .offsetX((int) DimensionUnit.DP.toPixels(12)) 263 .offsetY((int) DimensionUnit.DP.toPixels(12)) 264 .onDismiss(() -> recyclerView.suppressLayout(false)) 265 .show(actions); 266 267 recyclerView.suppressLayout(true); 268 269 return true; 270 } 271 272 private @NonNull List<ActionItem> generateContextualActionsForRecipient(@NonNull RecipientId recipientId) { 273 Recipient recipient = Recipient.resolved(recipientId); 274 275 return Stream.of( 276 createMessageActionItem(recipient), 277 createAudioCallActionItem(recipient), 278 createVideoCallActionItem(recipient), 279 createRemoveActionItem(recipient), 280 createBlockActionItem(recipient) 281 ).filter(Objects::nonNull).collect(Collectors.toList()); 282 } 283 284 private @NonNull ActionItem createMessageActionItem(@NonNull Recipient recipient) { 285 return new ActionItem( 286 R.drawable.ic_chat_message_24, 287 getString(R.string.NewConversationActivity__message), 288 R.color.signal_colorOnSurface, 289 () -> { 290 Disposable disposable = ConversationIntents.createBuilder(this, recipient.getId(), -1L) 291 .subscribe(builder -> startActivity(builder.build())); 292 293 disposables.add(disposable); 294 } 295 ); 296 } 297 298 private @Nullable ActionItem createAudioCallActionItem(@NonNull Recipient recipient) { 299 if (recipient.isSelf() || recipient.isGroup()) { 300 return null; 301 } 302 303 if (recipient.isRegistered()) { 304 return new ActionItem( 305 R.drawable.ic_phone_right_24, 306 getString(R.string.NewConversationActivity__audio_call), 307 R.color.signal_colorOnSurface, 308 () -> CommunicationActions.startVoiceCall(this, recipient) 309 ); 310 } else { 311 return null; 312 } 313 } 314 315 private @Nullable ActionItem createVideoCallActionItem(@NonNull Recipient recipient) { 316 if (recipient.isSelf() || recipient.isMmsGroup() || !recipient.isRegistered()) { 317 return null; 318 } 319 320 return new ActionItem( 321 R.drawable.ic_video_call_24, 322 getString(R.string.NewConversationActivity__video_call), 323 R.color.signal_colorOnSurface, 324 () -> CommunicationActions.startVideoCall(this, recipient) 325 ); 326 } 327 328 private @Nullable ActionItem createRemoveActionItem(@NonNull Recipient recipient) { 329 if (recipient.isSelf() || recipient.isGroup()) { 330 return null; 331 } 332 333 return new ActionItem( 334 R.drawable.ic_minus_circle_20, // TODO [alex] -- correct asset 335 getString(R.string.NewConversationActivity__remove), 336 R.color.signal_colorOnSurface, 337 () -> { 338 if (recipient.isSystemContact()) { 339 displayIsInSystemContactsDialog(recipient); 340 } else { 341 displayRemovalDialog(recipient); 342 } 343 } 344 ); 345 } 346 347 @SuppressWarnings("CodeBlock2Expr") 348 private @Nullable ActionItem createBlockActionItem(@NonNull Recipient recipient) { 349 if (recipient.isSelf()) { 350 return null; 351 } 352 353 return new ActionItem( 354 R.drawable.ic_block_tinted_24, 355 getString(R.string.NewConversationActivity__block), 356 R.color.signal_colorError, 357 () -> BlockUnblockDialog.showBlockFor(this, 358 this.getLifecycle(), 359 recipient, 360 () -> { 361 disposables.add(viewModel.blockContact(recipient).subscribe(() -> { 362 displaySnackbar(R.string.NewConversationActivity__s_has_been_blocked, recipient.getDisplayName(this)); 363 contactsFragment.reset(); 364 })); 365 }) 366 ); 367 } 368 369 private void displayIsInSystemContactsDialog(@NonNull Recipient recipient) { 370 new MaterialAlertDialogBuilder(this) 371 .setTitle(getString(R.string.NewConversationActivity__unable_to_remove_s, recipient.getShortDisplayName(this))) 372 .setMessage(R.string.NewConversationActivity__this_person_is_saved_to_your) 373 .setPositiveButton(R.string.NewConversationActivity__view_contact, 374 (dialog, which) -> contactLauncher.launch(new Intent(Intent.ACTION_VIEW, recipient.getContactUri())) 375 ) 376 .setNegativeButton(android.R.string.cancel, null) 377 .show(); 378 } 379 380 private void displayRemovalDialog(@NonNull Recipient recipient) { 381 new MaterialAlertDialogBuilder(this) 382 .setTitle(getString(R.string.NewConversationActivity__remove_s, recipient.getShortDisplayName(this))) 383 .setMessage(R.string.NewConversationActivity__you_wont_see_this_person) 384 .setPositiveButton(R.string.NewConversationActivity__remove, 385 (dialog, which) -> { 386 disposables.add(viewModel.hideContact(recipient).subscribe(() -> { 387 onRefresh(); 388 displaySnackbar(R.string.NewConversationActivity__s_has_been_removed, recipient.getDisplayName(this)); 389 })); 390 } 391 ) 392 .setNegativeButton(android.R.string.cancel, null) 393 .show(); 394 } 395 396 private void displaySnackbar(@StringRes int message, Object... formatArgs) { 397 Snackbar.make(findViewById(android.R.id.content), getString(message, formatArgs), Snackbar.LENGTH_SHORT).show(); 398 } 399}