1package eu.siacs.conversations.ui;
2
3import android.Manifest;
4import android.app.Activity;
5import android.content.Intent;
6import android.content.pm.PackageManager;
7import android.net.Uri;
8import android.os.Bundle;
9import android.util.Log;
10import android.view.View;
11import android.widget.Toast;
12
13import androidx.annotation.NonNull;
14import androidx.annotation.StringRes;
15import androidx.core.content.ContextCompat;
16import androidx.databinding.DataBindingUtil;
17
18import com.google.common.base.Strings;
19
20import eu.siacs.conversations.Config;
21import eu.siacs.conversations.R;
22import eu.siacs.conversations.databinding.ActivityUriHandlerBinding;
23import eu.siacs.conversations.http.HttpConnectionManager;
24import eu.siacs.conversations.persistance.DatabaseBackend;
25import eu.siacs.conversations.services.QuickConversationsService;
26import eu.siacs.conversations.utils.ProvisioningUtils;
27import eu.siacs.conversations.utils.SignupUtils;
28import eu.siacs.conversations.utils.XmppUri;
29import eu.siacs.conversations.xmpp.Jid;
30
31import okhttp3.Call;
32import okhttp3.Callback;
33import okhttp3.HttpUrl;
34import okhttp3.Request;
35import okhttp3.Response;
36
37import java.io.IOException;
38import java.util.List;
39import java.util.regex.Matcher;
40import java.util.regex.Pattern;
41
42public class UriHandlerActivity extends BaseActivity {
43
44 public static final String ACTION_SCAN_QR_CODE = "scan_qr_code";
45 private static final String EXTRA_ALLOW_PROVISIONING = "extra_allow_provisioning";
46 private static final int REQUEST_SCAN_QR_CODE = 0x1234;
47 private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN = 0x6789;
48 private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION = 0x6790;
49 private static final Pattern V_CARD_XMPP_PATTERN = Pattern.compile("\nIMPP([^:]*):(xmpp:.+)\n");
50 private static final Pattern LINK_HEADER_PATTERN = Pattern.compile("<(.*?)>");
51 private ActivityUriHandlerBinding binding;
52 private Call call;
53
54 public static void scan(final Activity activity) {
55 scan(activity, false);
56 }
57
58 public static void scan(final Activity activity, final boolean provisioning) {
59 if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
60 final Intent intent = new Intent(activity, UriHandlerActivity.class);
61 intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE);
62 if (provisioning) {
63 intent.putExtra(EXTRA_ALLOW_PROVISIONING, true);
64 }
65 intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
66 activity.startActivity(intent);
67 } else {
68 activity.requestPermissions(
69 new String[] {Manifest.permission.CAMERA},
70 provisioning
71 ? REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION
72 : REQUEST_CAMERA_PERMISSIONS_TO_SCAN);
73 }
74 }
75
76 public static void onRequestPermissionResult(
77 Activity activity, int requestCode, int[] grantResults) {
78 if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN
79 && requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) {
80 return;
81 }
82 if (grantResults.length > 0) {
83 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
84 if (requestCode == REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) {
85 scan(activity, true);
86 } else {
87 scan(activity);
88 }
89 } else {
90 Toast.makeText(
91 activity,
92 R.string.qr_code_scanner_needs_access_to_camera,
93 Toast.LENGTH_SHORT)
94 .show();
95 }
96 }
97 }
98
99 @Override
100 protected void onCreate(Bundle savedInstanceState) {
101 super.onCreate(savedInstanceState);
102 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_uri_handler);
103 Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
104 }
105
106 @Override
107 public void onStart() {
108 super.onStart();
109 handleIntent(getIntent());
110 }
111
112 @Override
113 public void onNewIntent(final Intent intent) {
114 super.onNewIntent(intent);
115 handleIntent(intent);
116 }
117
118 private boolean handleUri(final Uri uri) {
119 return handleUri(uri, false);
120 }
121
122 private boolean handleUri(final Uri uri, final boolean scanned) {
123 final Intent intent;
124 final XmppUri xmppUri = new XmppUri(uri);
125 final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids(false);
126
127 if (SignupUtils.isSupportTokenRegistry() && xmppUri.isValidJid()) {
128 final String preAuth = xmppUri.getParameter(XmppUri.PARAMETER_PRE_AUTH);
129 final Jid jid = xmppUri.getJid();
130 if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
131 if (jid.getEscapedLocal() != null && accounts.contains(jid.asBareJid())) {
132 showError(R.string.account_already_exists);
133 return false;
134 }
135 intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth);
136 startActivity(intent);
137 return true;
138 }
139 if (accounts.isEmpty()
140 && xmppUri.isAction(XmppUri.ACTION_ROSTER)
141 && "y"
142 .equalsIgnoreCase(
143 Strings.nullToEmpty(xmppUri.getParameter(XmppUri.PARAMETER_IBR))
144 .trim())) {
145 intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth);
146 intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
147 startActivity(intent);
148 return true;
149 }
150 } else if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
151 showError(R.string.account_registrations_are_not_supported);
152 return false;
153 }
154
155 if (accounts.isEmpty()) {
156 if (xmppUri.isValidJid()) {
157 intent = SignupUtils.getSignUpIntent(this);
158 intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
159 startActivity(intent);
160 return true;
161 } else {
162 showError(R.string.invalid_jid);
163 return false;
164 }
165 }
166
167 if (xmppUri.isAction(XmppUri.ACTION_MESSAGE)) {
168 final Jid jid = xmppUri.getJid();
169 final String body = xmppUri.getBody();
170
171 if (jid != null) {
172 final Class<?> clazz = findShareViaAccountClass();
173 if (clazz != null) {
174 intent = new Intent(this, clazz);
175 intent.putExtra("contact", jid.toEscapedString());
176 intent.putExtra("body", body);
177 } else {
178 intent = new Intent(this, StartConversationActivity.class);
179 intent.setAction(Intent.ACTION_VIEW);
180 intent.setData(uri);
181 intent.putExtra("account", accounts.get(0).toEscapedString());
182 }
183 } else {
184 intent = new Intent(this, ShareWithActivity.class);
185 intent.setAction(Intent.ACTION_SEND);
186 intent.setType("text/plain");
187 intent.putExtra(Intent.EXTRA_TEXT, body);
188 }
189 } else if (accounts.contains(xmppUri.getJid())) {
190 intent = new Intent(getApplicationContext(), EditAccountActivity.class);
191 intent.setAction(Intent.ACTION_VIEW);
192 intent.putExtra("jid", xmppUri.getJid().asBareJid().toString());
193 intent.setData(uri);
194 intent.putExtra("scanned", scanned);
195 } else if (xmppUri.isValidJid()) {
196 intent = new Intent(getApplicationContext(), StartConversationActivity.class);
197 intent.setAction(Intent.ACTION_VIEW);
198 intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
199 intent.putExtra("scanned", scanned);
200 intent.setData(uri);
201 } else {
202 showError(R.string.invalid_jid);
203 return false;
204 }
205 startActivity(intent);
206 return true;
207 }
208
209 private void checkForLinkHeader(final HttpUrl url) {
210 Log.d(Config.LOGTAG, "checking for link header on " + url);
211 this.call =
212 HttpConnectionManager.okHttpClient(this).newCall(
213 new Request.Builder().url(url).head().build());
214 this.call.enqueue(
215 new Callback() {
216 @Override
217 public void onFailure(@NonNull Call call, @NonNull IOException e) {
218 Log.d(Config.LOGTAG, "unable to check HTTP url", e);
219 showErrorOnUiThread(R.string.no_xmpp_adddress_found);
220 }
221
222 @Override
223 public void onResponse(@NonNull Call call, @NonNull Response response) {
224 if (response.isSuccessful()) {
225 final String linkHeader = response.header("Link");
226 if (linkHeader != null && processLinkHeader(linkHeader)) {
227 return;
228 }
229 }
230 showErrorOnUiThread(R.string.no_xmpp_adddress_found);
231 }
232 });
233 }
234
235 private boolean processLinkHeader(final String header) {
236 final Matcher matcher = LINK_HEADER_PATTERN.matcher(header);
237 if (matcher.find()) {
238 final String group = matcher.group();
239 final String link = group.substring(1, group.length() - 1);
240 if (handleUri(Uri.parse(link))) {
241 finish();
242 return true;
243 }
244 }
245 return false;
246 }
247
248 private void showError(@StringRes int error) {
249 this.binding.progress.setVisibility(View.INVISIBLE);
250 this.binding.error.setText(error);
251 this.binding.error.setVisibility(View.VISIBLE);
252 }
253
254 private void showErrorOnUiThread(@StringRes int error) {
255 runOnUiThread(()-> showError(error));
256 }
257
258 private static Class<?> findShareViaAccountClass() {
259 try {
260 return Class.forName("eu.siacs.conversations.ui.ShareViaAccountActivity");
261 } catch (final ClassNotFoundException e) {
262 return null;
263 }
264 }
265
266 private void handleIntent(final Intent data) {
267 final String action = data == null ? null : data.getAction();
268 if (action == null) {
269 return;
270 }
271 switch (action) {
272 case Intent.ACTION_MAIN:
273 binding.progress.setVisibility(
274 call != null && !call.isCanceled() ? View.VISIBLE : View.INVISIBLE);
275 break;
276 case Intent.ACTION_VIEW:
277 case Intent.ACTION_SENDTO:
278 if (handleUri(data.getData())) {
279 finish();
280 }
281 break;
282 case ACTION_SCAN_QR_CODE:
283 Log.d(Config.LOGTAG, "scan. allow=" + allowProvisioning());
284 setIntent(createMainIntent());
285 startActivityForResult(new Intent(this, ScanActivity.class), REQUEST_SCAN_QR_CODE);
286 break;
287 }
288 }
289
290 private Intent createMainIntent() {
291 final Intent intent = new Intent(Intent.ACTION_MAIN);
292 intent.putExtra(EXTRA_ALLOW_PROVISIONING, allowProvisioning());
293 return intent;
294 }
295
296 private boolean allowProvisioning() {
297 final Intent launchIntent = getIntent();
298 return launchIntent != null
299 && launchIntent.getBooleanExtra(EXTRA_ALLOW_PROVISIONING, false);
300 }
301
302 @Override
303 public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) {
304 super.onActivityResult(requestCode, requestCode, intent);
305 if (requestCode == REQUEST_SCAN_QR_CODE && resultCode == RESULT_OK) {
306 final boolean allowProvisioning = allowProvisioning();
307 final String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT);
308 if (Strings.isNullOrEmpty(result)) {
309 finish();
310 return;
311 }
312 if (result.startsWith("BEGIN:VCARD\n")) {
313 final Matcher matcher = V_CARD_XMPP_PATTERN.matcher(result);
314 if (matcher.find()) {
315 if (handleUri(Uri.parse(matcher.group(2)), true)) {
316 finish();
317 }
318 } else {
319 showError(R.string.no_xmpp_adddress_found);
320 }
321 return;
322 } else if (QuickConversationsService.isConversations()
323 && looksLikeJsonObject(result)
324 && allowProvisioning) {
325 ProvisioningUtils.provision(this, result);
326 finish();
327 return;
328 }
329 final Uri uri = Uri.parse(result.trim());
330 if (allowProvisioning
331 && "https".equalsIgnoreCase(uri.getScheme())
332 && !XmppUri.INVITE_DOMAIN.equalsIgnoreCase(uri.getHost())) {
333 final HttpUrl httpUrl = HttpUrl.parse(uri.toString());
334 if (httpUrl != null) {
335 checkForLinkHeader(httpUrl);
336 } else {
337 finish();
338 }
339 } else if (handleUri(uri, true)) {
340 finish();
341 } else {
342 setIntent(new Intent(Intent.ACTION_VIEW, uri));
343 }
344 } else {
345 finish();
346 }
347 }
348
349 private static boolean looksLikeJsonObject(final String input) {
350 final String trimmed = Strings.nullToEmpty(input).trim();
351 return trimmed.charAt(0) == '{' && trimmed.charAt(trimmed.length() - 1) == '}';
352 }
353}