That fuck shit the fascists are using
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}