That fuck shit the fascists are using
at master 546 lines 23 kB view raw
1package org.tm.archive.util; 2 3import android.Manifest; 4import android.app.Activity; 5import android.content.ActivityNotFoundException; 6import android.content.Context; 7import android.content.Intent; 8import android.net.Uri; 9import android.os.AsyncTask; 10import android.os.Bundle; 11import android.os.Handler; 12import android.os.Looper; 13import android.os.ResultReceiver; 14import android.text.TextUtils; 15import android.widget.Toast; 16 17import androidx.annotation.NonNull; 18import androidx.annotation.Nullable; 19import androidx.core.app.TaskStackBuilder; 20import androidx.fragment.app.Fragment; 21import androidx.fragment.app.FragmentActivity; 22 23import com.google.android.material.dialog.MaterialAlertDialogBuilder; 24 25import org.signal.core.util.concurrent.RxExtensions; 26import org.signal.core.util.concurrent.SignalExecutors; 27import org.signal.core.util.concurrent.SimpleTask; 28import org.signal.core.util.logging.Log; 29import org.signal.libsignal.usernames.BaseUsernameException; 30import org.signal.libsignal.usernames.Username; 31import org.signal.ringrtc.CallLinkRootKey; 32import org.tm.archive.R; 33import org.tm.archive.WebRtcCallActivity; 34import org.tm.archive.calls.links.CallLinks; 35import org.tm.archive.contacts.sync.ContactDiscovery; 36import org.tm.archive.conversation.ConversationIntents; 37import org.tm.archive.database.CallLinkTable; 38import org.tm.archive.database.SignalDatabase; 39import org.tm.archive.database.model.GroupRecord; 40import org.tm.archive.dependencies.ApplicationDependencies; 41import org.tm.archive.groups.GroupId; 42import org.tm.archive.groups.ui.invitesandrequests.joining.GroupJoinBottomSheetDialogFragment; 43import org.tm.archive.groups.ui.invitesandrequests.joining.GroupJoinUpdateRequiredBottomSheetDialogFragment; 44import org.tm.archive.groups.v2.GroupInviteLinkUrl; 45import org.tm.archive.permissions.Permissions; 46import org.tm.archive.profiles.manage.UsernameRepository; 47import org.tm.archive.profiles.manage.UsernameRepository.UsernameAciFetchResult; 48import org.tm.archive.profiles.manage.UsernameRepository.UsernameLinkConversionResult; 49import org.tm.archive.proxy.ProxyBottomSheetFragment; 50import org.tm.archive.recipients.Recipient; 51import org.tm.archive.service.webrtc.links.CallLinkRoomId; 52import org.tm.archive.sms.MessageSender; 53import org.tm.archive.util.views.SimpleProgressDialog; 54import org.whispersystems.signalservice.api.push.ServiceId; 55import org.whispersystems.signalservice.api.push.UsernameLinkComponents; 56import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; 57 58import java.io.IOException; 59import java.util.Objects; 60import java.util.Optional; 61import java.util.concurrent.TimeUnit; 62 63public class CommunicationActions { 64 65 private static final String TAG = Log.tag(CommunicationActions.class); 66 67 /** 68 * Start a voice call. Assumes that permission request results will be routed to a handler on the Fragment. 69 */ 70 public static void startVoiceCall(@NonNull Fragment fragment, @NonNull Recipient recipient) { 71 startVoiceCall(new FragmentCallContext(fragment), recipient); 72 } 73 74 /** 75 * Start a voice call. Assumes that permission request results will be routed to a handler on the Activity. 76 */ 77 public static void startVoiceCall(@NonNull Activity activity, @NonNull Recipient recipient) { 78 startVoiceCall(new ActivityCallContext(activity), recipient); 79 } 80 81 private static void startVoiceCall(@NonNull CallContext callContext, @NonNull Recipient recipient) { 82 if (TelephonyUtil.isAnyPstnLineBusy(callContext.getContext())) { 83 Toast.makeText(callContext.getContext(), 84 R.string.CommunicationActions_a_cellular_call_is_already_in_progress, 85 Toast.LENGTH_SHORT) 86 .show(); 87 return; 88 } 89 90 if (recipient.isRegistered()) { 91 ApplicationDependencies.getSignalCallManager().isCallActive(new ResultReceiver(new Handler(Looper.getMainLooper())) { 92 @Override 93 protected void onReceiveResult(int resultCode, Bundle resultData) { 94 if (resultCode == 1) { 95 startCallInternal(callContext, recipient, false, false); 96 } else { 97 new MaterialAlertDialogBuilder(callContext.getContext()) 98 .setMessage(R.string.CommunicationActions_start_voice_call) 99 .setPositiveButton(R.string.CommunicationActions_call, (d, w) -> startCallInternal(callContext, recipient, false, false)) 100 .setNegativeButton(R.string.CommunicationActions_cancel, (d, w) -> d.dismiss()) 101 .setCancelable(true) 102 .show(); 103 } 104 } 105 }); 106 } else { 107 startInsecureCall(callContext, recipient); 108 } 109 } 110 111 /** 112 * Start a video call. Assumes that permission request results will be routed to a handler on the Fragment. 113 */ 114 public static void startVideoCall(@NonNull Fragment fragment, @NonNull Recipient recipient) { 115 startVideoCall(new FragmentCallContext(fragment), recipient, false); 116 } 117 118 /** 119 * Start a video call. Assumes that permission request results will be routed to a handler on the Activity. 120 */ 121 public static void startVideoCall(@NonNull Activity activity, @NonNull Recipient recipient) { 122 startVideoCall(new ActivityCallContext(activity), recipient, false); 123 } 124 125 private static void startVideoCall(@NonNull CallContext callContext, @NonNull Recipient recipient, boolean fromCallLink) { 126 if (TelephonyUtil.isAnyPstnLineBusy(callContext.getContext())) { 127 Toast.makeText(callContext.getContext(), 128 R.string.CommunicationActions_a_cellular_call_is_already_in_progress, 129 Toast.LENGTH_SHORT) 130 .show(); 131 return; 132 } 133 134 ApplicationDependencies.getSignalCallManager().isCallActive(new ResultReceiver(new Handler(Looper.getMainLooper())) { 135 @Override 136 protected void onReceiveResult(int resultCode, Bundle resultData) { 137 startCallInternal(callContext, recipient, resultCode != 1, fromCallLink); 138 } 139 }); 140 } 141 142 public static void startConversation(@NonNull Context context, @NonNull Recipient recipient, @Nullable String text) { 143 startConversation(context, recipient, text, null); 144 } 145 146 public static void startConversation(@NonNull Context context, 147 @NonNull Recipient recipient, 148 @Nullable String text, 149 @Nullable TaskStackBuilder backStack) 150 { 151 new AsyncTask<Void, Void, Long>() { 152 @Override 153 protected Long doInBackground(Void... voids) { 154 return SignalDatabase.threads().getOrCreateThreadIdFor(recipient); 155 } 156 157 @Override 158 protected void onPostExecute(@NonNull Long threadId) { 159 ConversationIntents.Builder builder = ConversationIntents.createBuilderSync(context, recipient.getId(), Objects.requireNonNull(threadId)); 160 if (!TextUtils.isEmpty(text)) { 161 builder.withDraftText(text); 162 } 163 164 Intent intent = builder.build(); 165 if (backStack != null) { 166 backStack.addNextIntent(intent); 167 backStack.startActivities(); 168 } else { 169 context.startActivity(intent); 170 } 171 } 172 }.execute(); 173 } 174 175 public static void startInsecureCall(@NonNull Activity activity, @NonNull Recipient recipient) { 176 startInsecureCall(new ActivityCallContext(activity), recipient); 177 } 178 179 public static void startInsecureCall(@NonNull Fragment fragment, @NonNull Recipient recipient) { 180 startInsecureCall(new FragmentCallContext(fragment), recipient); 181 } 182 183 public static void startInsecureCall(@NonNull CallContext callContext, @NonNull Recipient recipient) { 184 new MaterialAlertDialogBuilder(callContext.getContext()) 185 .setTitle(R.string.CommunicationActions_insecure_call) 186 .setMessage(R.string.CommunicationActions_carrier_charges_may_apply) 187 .setPositiveButton(R.string.CommunicationActions_call, (d, w) -> { 188 d.dismiss(); 189 startInsecureCallInternal(callContext, recipient); 190 }) 191 .setNegativeButton(R.string.CommunicationActions_cancel, (d, w) -> d.dismiss()) 192 .show(); 193 } 194 195 public static @NonNull Intent createIntentToShareTextViaShareSheet(@NonNull String text) { 196 Intent intent = new Intent(Intent.ACTION_SEND); 197 intent.setType("text/plain"); 198 intent.putExtra(Intent.EXTRA_TEXT, text); 199 200 return intent; 201 } 202 203 public static @NonNull Intent createIntentToComposeSmsThroughDefaultApp(@NonNull Recipient recipient, @Nullable String text) { 204 Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("smsto:" + recipient.requireSmsAddress())); 205 if (text != null) { 206 intent.putExtra("sms_body", text); 207 } 208 209 return intent; 210 } 211 212 public static void composeSmsThroughDefaultApp(@NonNull Context context, @NonNull Recipient recipient, @Nullable String text) { 213 Intent intent = createIntentToComposeSmsThroughDefaultApp(recipient, text); 214 context.startActivity(intent); 215 } 216 217 public static void openBrowserLink(@NonNull Context context, @NonNull String link) { 218 try { 219 Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link)); 220 context.startActivity(intent); 221 } catch (ActivityNotFoundException e) { 222 Toast.makeText(context, R.string.CommunicationActions_no_browser_found, Toast.LENGTH_SHORT).show(); 223 } 224 } 225 226 public static void openEmail(@NonNull Context context, @NonNull String address, @Nullable String subject, @Nullable String body) { 227 Intent intent = new Intent(Intent.ACTION_SENDTO); 228 intent.setData(Uri.parse("mailto:")); 229 intent.putExtra(Intent.EXTRA_EMAIL, new String[]{ address }); 230 intent.putExtra(Intent.EXTRA_SUBJECT, Util.emptyIfNull(subject)); 231 intent.putExtra(Intent.EXTRA_TEXT, Util.emptyIfNull(body)); 232 233 context.startActivity(Intent.createChooser(intent, context.getString(R.string.CommunicationActions_send_email))); 234 } 235 236 /** 237 * If the url is a group link it will handle it. 238 * If the url is a malformed group link, it will assume Signal needs to update. 239 * Otherwise returns false, indicating was not a group link. 240 */ 241 public static boolean handlePotentialGroupLinkUrl(@NonNull FragmentActivity activity, @NonNull String potentialGroupLinkUrl) { 242 try { 243 GroupInviteLinkUrl groupInviteLinkUrl = GroupInviteLinkUrl.fromUri(potentialGroupLinkUrl); 244 245 if (groupInviteLinkUrl == null) { 246 return false; 247 } 248 249 handleGroupLinkUrl(activity, groupInviteLinkUrl); 250 return true; 251 } catch (GroupInviteLinkUrl.InvalidGroupLinkException e) { 252 Log.w(TAG, "Could not parse group URL", e); 253 Toast.makeText(activity, R.string.GroupJoinUpdateRequiredBottomSheetDialogFragment_group_link_is_not_valid, Toast.LENGTH_SHORT).show(); 254 return true; 255 } catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) { 256 Log.w(TAG, "Group link is for an advanced version", e); 257 GroupJoinUpdateRequiredBottomSheetDialogFragment.show(activity.getSupportFragmentManager()); 258 return true; 259 } 260 } 261 262 public static void handleGroupLinkUrl(@NonNull FragmentActivity activity, 263 @NonNull GroupInviteLinkUrl groupInviteLinkUrl) 264 { 265 GroupId.V2 groupId = GroupId.v2(groupInviteLinkUrl.getGroupMasterKey()); 266 267 SimpleTask.run(SignalExecutors.BOUNDED, () -> { 268 GroupRecord group = SignalDatabase.groups().getGroup(groupId).orElse(null); 269 270 return group != null && group.isActive() ? Recipient.resolved(group.getRecipientId()) 271 : null; 272 }, 273 recipient -> { 274 if (recipient != null) { 275 CommunicationActions.startConversation(activity, recipient, null); 276 Toast.makeText(activity, R.string.GroupJoinBottomSheetDialogFragment_you_are_already_a_member, Toast.LENGTH_SHORT).show(); 277 } else { 278 GroupJoinBottomSheetDialogFragment.show(activity.getSupportFragmentManager(), groupInviteLinkUrl); 279 } 280 }); 281 } 282 283 /** 284 * If the url is a proxy link it will handle it. 285 * Otherwise returns false, indicating was not a proxy link. 286 */ 287 public static boolean handlePotentialProxyLinkUrl(@NonNull FragmentActivity activity, @NonNull String potentialProxyLinkUrl) { 288 String proxy = SignalProxyUtil.parseHostFromProxyDeepLink(potentialProxyLinkUrl); 289 290 if (proxy != null) { 291 ProxyBottomSheetFragment.showForProxy(activity.getSupportFragmentManager(), proxy); 292 return true; 293 } else { 294 return false; 295 } 296 } 297 298 /** 299 * If the url is a signal.me link it will handle it. 300 */ 301 public static void handlePotentialSignalMeUrl(@NonNull FragmentActivity activity, @NonNull String potentialUrl) { 302 String e164 = SignalMeUtil.parseE164FromLink(activity, potentialUrl); 303 UsernameLinkComponents username = UsernameRepository.parseLink(potentialUrl); 304 305 if (e164 != null) { 306 handleE164Link(activity, e164); 307 } else if (username != null) { 308 handleUsernameLink(activity, potentialUrl); 309 } 310 } 311 312 public static void handlePotentialCallLinkUrl(@NonNull FragmentActivity activity, @NonNull String potentialUrl) { 313 if (!CallLinks.isCallLink(potentialUrl)) { 314 return; 315 } 316 317 if (!FeatureFlags.adHocCalling()) { 318 Toast.makeText(activity, R.string.CommunicationActions_cant_join_call, Toast.LENGTH_SHORT).show(); 319 return; 320 } 321 322 CallLinkRootKey rootKey = CallLinks.parseUrl(potentialUrl); 323 if (rootKey == null) { 324 Log.w(TAG, "Failed to parse root key from call link"); 325 new MaterialAlertDialogBuilder(activity) 326 .setTitle(R.string.CommunicationActions_invalid_link) 327 .setMessage(R.string.CommunicationActions_this_is_not_a_valid_call_link) 328 .setPositiveButton(android.R.string.ok, null) 329 .show(); 330 return; 331 } 332 333 startVideoCall(new ActivityCallContext(activity), rootKey); 334 } 335 336 /** 337 * Attempts to start a video call for the given call link via root key. This will insert a call link into 338 * the user's database if one does not already exist. 339 * 340 * @param fragment The fragment, which will be used for context and permissions routing. 341 */ 342 public static void startVideoCall(@NonNull Fragment fragment, @NonNull CallLinkRootKey rootKey) { 343 startVideoCall(new FragmentCallContext(fragment), rootKey); 344 } 345 346 private static void startVideoCall(@NonNull CallContext callContext, @NonNull CallLinkRootKey rootKey) { 347 if (!FeatureFlags.adHocCalling()) { 348 Toast.makeText(callContext.getContext(), R.string.CommunicationActions_cant_join_call, Toast.LENGTH_SHORT).show(); 349 return; 350 } 351 352 SimpleTask.run(() -> { 353 CallLinkRoomId roomId = CallLinkRoomId.fromBytes(rootKey.deriveRoomId()); 354 CallLinkTable.CallLink callLink = SignalDatabase.callLinks().getOrCreateCallLinkByRootKey(rootKey); 355 356 if (callLink.getState().hasBeenRevoked()) { 357 return Optional.<Recipient>empty(); 358 } 359 360 return SignalDatabase.recipients().getByCallLinkRoomId(roomId).map(Recipient::resolved); 361 }, callLinkRecipient -> { 362 if (callLinkRecipient.isEmpty()) { 363 new MaterialAlertDialogBuilder(callContext.getContext()) 364 .setTitle(R.string.CommunicationActions_cant_join_call) 365 .setMessage(R.string.CommunicationActions_this_call_link_is_no_longer_valid) 366 .setPositiveButton(android.R.string.ok, null) 367 .show(); 368 } else { 369 startVideoCall(callContext, callLinkRecipient.get(), true); 370 } 371 }); 372 } 373 374 private static void startInsecureCallInternal(@NonNull CallContext callContext, @NonNull Recipient recipient) { 375 try { 376 Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + recipient.requireSmsAddress())); 377 callContext.startActivity(dialIntent); 378 } catch (ActivityNotFoundException anfe) { 379 Log.w(TAG, anfe); 380 Dialogs.showAlertDialog(callContext.getContext(), 381 callContext.getContext().getString(R.string.ConversationActivity_calls_not_supported), 382 callContext.getContext().getString(R.string.ConversationActivity_this_device_does_not_appear_to_support_dial_actions)); 383 } 384 } 385 386 private static void startCallInternal(@NonNull CallContext callContext, @NonNull Recipient recipient, boolean isVideo, boolean fromCallLink) { 387 if (isVideo) startVideoCallInternal(callContext, recipient, fromCallLink); 388 else startAudioCallInternal(callContext, recipient); 389 } 390 391 private static void startAudioCallInternal(@NonNull CallContext callContext, @NonNull Recipient recipient) { 392 callContext.getPermissionsBuilder() 393 .request(Manifest.permission.RECORD_AUDIO) 394 .ifNecessary() 395 .withRationaleDialog(callContext.getContext().getString(R.string.ConversationActivity__to_call_s_signal_needs_access_to_your_microphone, recipient.getDisplayName(callContext.getContext())), 396 R.drawable.ic_mic_solid_24) 397 .withPermanentDenialDialog(callContext.getContext().getString(R.string.ConversationActivity__to_call_s_signal_needs_access_to_your_microphone, recipient.getDisplayName(callContext.getContext()))) 398 .onAllGranted(() -> { 399 ApplicationDependencies.getSignalCallManager().startOutgoingAudioCall(recipient); 400 401 MessageSender.onMessageSent(); 402 403 Intent activityIntent = new Intent(callContext.getContext(), WebRtcCallActivity.class); 404 405 activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 406 407 callContext.startActivity(activityIntent); 408 }) 409 .execute(); 410 } 411 412 private static void startVideoCallInternal(@NonNull CallContext callContext, @NonNull Recipient recipient, boolean fromCallLink) { 413 callContext.getPermissionsBuilder() 414 .request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) 415 .ifNecessary() 416 .withRationaleDialog(callContext.getContext().getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.getDisplayName(callContext.getContext())), 417 R.drawable.ic_mic_solid_24, 418 R.drawable.ic_video_solid_24_tinted) 419 .withPermanentDenialDialog(callContext.getContext().getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.getDisplayName(callContext.getContext()))) 420 .onAllGranted(() -> { 421 ApplicationDependencies.getSignalCallManager().startPreJoinCall(recipient); 422 423 Intent activityIntent = new Intent(callContext.getContext(), WebRtcCallActivity.class); 424 425 activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 426 .putExtra(WebRtcCallActivity.EXTRA_ENABLE_VIDEO_IF_AVAILABLE, true) 427 .putExtra(WebRtcCallActivity.EXTRA_STARTED_FROM_CALL_LINK, fromCallLink); 428 429 callContext.startActivity(activityIntent); 430 }) 431 .execute(); 432 } 433 434 private static void handleE164Link(Activity activity, String e164) { 435 SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(activity, 500, 500); 436 437 SimpleTask.run(() -> { 438 Recipient recipient = Recipient.external(activity, e164); 439 440 if (!recipient.isRegistered() || !recipient.hasServiceId()) { 441 try { 442 ContactDiscovery.refresh(activity, recipient, false, TimeUnit.SECONDS.toMillis(10)); 443 recipient = Recipient.resolved(recipient.getId()); 444 } catch (IOException e) { 445 Log.w(TAG, "[handlePotentialSignalMeUrl] Failed to refresh directory for new contact."); 446 } 447 } 448 449 return recipient; 450 }, recipient -> { 451 dialog.dismiss(); 452 453 if (recipient.isRegistered() && recipient.hasServiceId()) { 454 startConversation(activity, recipient, null); 455 } else { 456 new MaterialAlertDialogBuilder(activity) 457 .setMessage(activity.getString(R.string.NewConversationActivity__s_is_not_a_signal_user, e164)) 458 .setPositiveButton(android.R.string.ok, null) 459 .show(); 460 } 461 }); 462 } 463 464 private static void handleUsernameLink(Activity activity, String link) { 465 SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(activity, 500, 500); 466 467 SimpleTask.run(() -> { 468 try { 469 UsernameLinkConversionResult result = RxExtensions.safeBlockingGet(UsernameRepository.fetchUsernameAndAciFromLink(link)); 470 471 // TODO we could be better here and report different types of errors to the UI 472 if (result instanceof UsernameLinkConversionResult.Success success) { 473 return Recipient.externalUsername(success.getAci(), success.getUsername().getUsername()); 474 } else { 475 return null; 476 } 477 } catch (InterruptedException e) { 478 Log.w(TAG, "Interrupted?", e); 479 return null; 480 } 481 }, recipient -> { 482 dialog.dismiss(); 483 484 if (recipient != null && recipient.isRegistered() && recipient.hasServiceId()) { 485 startConversation(activity, recipient, null); 486 } else { 487 new MaterialAlertDialogBuilder(activity) 488 .setMessage(activity.getString(R.string.UsernameLinkSettings_qr_result_not_found_no_username)) 489 .setPositiveButton(android.R.string.ok, null) 490 .show(); 491 } 492 }); 493 } 494 495 private interface CallContext { 496 @NonNull Permissions.PermissionsBuilder getPermissionsBuilder(); 497 void startActivity(@NonNull Intent intent); 498 @NonNull Context getContext(); 499 } 500 501 private static class ActivityCallContext implements CallContext { 502 private final Activity activity; 503 504 private ActivityCallContext(Activity activity) { 505 this.activity = activity; 506 } 507 508 @Override 509 public @NonNull Permissions.PermissionsBuilder getPermissionsBuilder() { 510 return Permissions.with(activity); 511 } 512 513 @Override 514 public void startActivity(@NonNull Intent intent) { 515 activity.startActivity(intent); 516 } 517 518 @Override 519 public @NonNull Context getContext() { 520 return activity; 521 } 522 } 523 524 private static class FragmentCallContext implements CallContext { 525 private final Fragment fragment; 526 527 private FragmentCallContext(Fragment fragment) { 528 this.fragment = fragment; 529 } 530 531 @Override 532 public @NonNull Permissions.PermissionsBuilder getPermissionsBuilder() { 533 return Permissions.with(fragment); 534 } 535 536 @Override 537 public void startActivity(@NonNull Intent intent) { 538 fragment.startActivity(intent); 539 } 540 541 @Override 542 public @NonNull Context getContext() { 543 return fragment.requireContext(); 544 } 545 } 546}