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.Build;
9import android.os.Bundle;
10import android.util.Log;
11import android.view.View;
12import android.widget.Toast;
13
14import androidx.annotation.StringRes;
15import androidx.appcompat.app.AppCompatActivity;
16import androidx.core.content.ContextCompat;
17import androidx.databinding.DataBindingUtil;
18
19import com.google.common.base.Strings;
20
21import eu.siacs.conversations.Config;
22import eu.siacs.conversations.R;
23import eu.siacs.conversations.databinding.ActivityUriHandlerBinding;
24import eu.siacs.conversations.http.HttpConnectionManager;
25import eu.siacs.conversations.persistance.DatabaseBackend;
26import eu.siacs.conversations.services.QuickConversationsService;
27import eu.siacs.conversations.utils.ProvisioningUtils;
28import eu.siacs.conversations.utils.SignupUtils;
29import eu.siacs.conversations.utils.XmppUri;
30import eu.siacs.conversations.xmpp.Jid;
31
32import okhttp3.Call;
33import okhttp3.Callback;
34import okhttp3.HttpUrl;
35import okhttp3.Request;
36import okhttp3.Response;
37
38import org.jetbrains.annotations.NotNull;
39
40import java.io.IOException;
41import java.util.List;
42import java.util.regex.Matcher;
43import java.util.regex.Pattern;
44
45public class UriHandlerActivity extends AppCompatActivity {
46
47 public static final String ACTION_SCAN_QR_CODE = "scan_qr_code";
48 private static final String EXTRA_ALLOW_PROVISIONING = "extra_allow_provisioning";
49 private static final int REQUEST_SCAN_QR_CODE = 0x1234;
50 private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN = 0x6789;
51 private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION = 0x6790;
52 private static final Pattern V_CARD_XMPP_PATTERN = Pattern.compile("\nIMPP([^:]*):(xmpp:.+)\n");
53 private static final Pattern LINK_HEADER_PATTERN = Pattern.compile("<(.*?)>");
54 private ActivityUriHandlerBinding binding;
55 private Call call;
56
57 public static void scan(final Activity activity) {
58 scan(activity, false);
59 }
60
61 public static void scan(final Activity activity, final boolean provisioning) {
62 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
63 || ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
64 == PackageManager.PERMISSION_GRANTED) {
65 final Intent intent = new Intent(activity, UriHandlerActivity.class);
66 intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE);
67 if (provisioning) {
68 intent.putExtra(EXTRA_ALLOW_PROVISIONING, true);
69 }
70 intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
71 activity.startActivity(intent);
72 } else {
73 activity.requestPermissions(
74 new String[] {Manifest.permission.CAMERA},
75 provisioning
76 ? REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION
77 : REQUEST_CAMERA_PERMISSIONS_TO_SCAN);
78 }
79 }
80
81 public static void onRequestPermissionResult(
82 Activity activity, int requestCode, int[] grantResults) {
83 if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN
84 && requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) {
85 return;
86 }
87 if (grantResults.length > 0) {
88 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
89 if (requestCode == REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) {
90 scan(activity, true);
91 } else {
92 scan(activity);
93 }
94 } else {
95 Toast.makeText(
96 activity,
97 R.string.qr_code_scanner_needs_access_to_camera,
98 Toast.LENGTH_SHORT)
99 .show();
100 }
101 }
102 }
103
104 @Override
105 protected void onCreate(Bundle savedInstanceState) {
106 super.onCreate(savedInstanceState);
107 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_uri_handler);
108 }
109
110 @Override
111 public void onStart() {
112 super.onStart();
113 handleIntent(getIntent());
114 }
115
116 @Override
117 public void onNewIntent(final Intent intent) {
118 super.onNewIntent(intent);
119 handleIntent(intent);
120 }
121
122 private boolean handleUri(final Uri uri) {
123 return handleUri(uri, false);
124 }
125
126 private boolean handleUri(final Uri uri, final boolean scanned) {
127 final Intent intent;
128 final XmppUri xmppUri = new XmppUri(uri);
129 final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids(false);
130
131 if (SignupUtils.isSupportTokenRegistry() && xmppUri.isValidJid()) {
132 final String preAuth = xmppUri.getParameter(XmppUri.PARAMETER_PRE_AUTH);
133 final Jid jid = xmppUri.getJid();
134 if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
135 if (jid.getEscapedLocal() != null && accounts.contains(jid.asBareJid())) {
136 showError(R.string.account_already_exists);
137 return false;
138 }
139 intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth);
140 startActivity(intent);
141 return true;
142 }
143 if (accounts.size() == 0
144 && xmppUri.isAction(XmppUri.ACTION_ROSTER)
145 && "y"
146 .equalsIgnoreCase(
147 Strings.nullToEmpty(xmppUri.getParameter(XmppUri.PARAMETER_IBR))
148 .trim())) {
149 intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth);
150 intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
151 startActivity(intent);
152 return true;
153 }
154 } else if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
155 showError(R.string.account_registrations_are_not_supported);
156 return false;
157 }
158
159 if (accounts.size() == 0) {
160 if (xmppUri.isValidJid()) {
161 intent = SignupUtils.getSignUpIntent(this);
162 intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
163 startActivity(intent);
164 return true;
165 } else {
166 showError(R.string.invalid_jid);
167 return false;
168 }
169 }
170
171 if (xmppUri.isAction(XmppUri.ACTION_MESSAGE)) {
172 final Jid jid = xmppUri.getJid();
173 final String body = xmppUri.getBody();
174
175 if (jid != null) {
176 final Class<?> clazz = findShareViaAccountClass();
177 if (clazz != null) {
178 intent = new Intent(this, clazz);
179 intent.putExtra("contact", jid.toEscapedString());
180 intent.putExtra("body", body);
181 } else {
182 intent = new Intent(this, StartConversationActivity.class);
183 intent.setAction(Intent.ACTION_VIEW);
184 intent.setData(uri);
185 intent.putExtra("account", accounts.get(0).toEscapedString());
186 }
187 } else {
188 intent = new Intent(this, ShareWithActivity.class);
189 intent.setAction(Intent.ACTION_SEND);
190 intent.setType("text/plain");
191 intent.putExtra(Intent.EXTRA_TEXT, body);
192 }
193 } else if (accounts.contains(xmppUri.getJid())) {
194 intent = new Intent(getApplicationContext(), EditAccountActivity.class);
195 intent.setAction(Intent.ACTION_VIEW);
196 intent.putExtra("jid", xmppUri.getJid().asBareJid().toString());
197 intent.setData(uri);
198 intent.putExtra("scanned", scanned);
199 } else if (xmppUri.isValidJid()) {
200 intent = new Intent(getApplicationContext(), StartConversationActivity.class);
201 intent.setAction(Intent.ACTION_VIEW);
202 intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
203 intent.putExtra("scanned", scanned);
204 intent.setData(uri);
205 } else {
206 showError(R.string.invalid_jid);
207 return false;
208 }
209 startActivity(intent);
210 return true;
211 }
212
213 private void checkForLinkHeader(final HttpUrl url) {
214 Log.d(Config.LOGTAG, "checking for link header on " + url);
215 this.call =
216 HttpConnectionManager.OK_HTTP_CLIENT.newCall(
217 new Request.Builder().url(url).head().build());
218 this.call.enqueue(
219 new Callback() {
220 @Override
221 public void onFailure(@NotNull Call call, @NotNull IOException e) {
222 Log.d(Config.LOGTAG, "unable to check HTTP url", e);
223 showError(R.string.no_xmpp_adddress_found);
224 }
225
226 @Override
227 public void onResponse(@NotNull Call call, @NotNull Response response) {
228 if (response.isSuccessful()) {
229 final String linkHeader = response.header("Link");
230 if (linkHeader != null && processLinkHeader(linkHeader)) {
231 return;
232 }
233 }
234 showError(R.string.no_xmpp_adddress_found);
235 }
236 });
237 }
238
239 private boolean processLinkHeader(final String header) {
240 final Matcher matcher = LINK_HEADER_PATTERN.matcher(header);
241 if (matcher.find()) {
242 final String group = matcher.group();
243 final String link = group.substring(1, group.length() - 1);
244 if (handleUri(Uri.parse(link))) {
245 finish();
246 return true;
247 }
248 }
249 return false;
250 }
251
252 private void showError(@StringRes int error) {
253 this.binding.progress.setVisibility(View.INVISIBLE);
254 this.binding.error.setText(error);
255 this.binding.error.setVisibility(View.VISIBLE);
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}