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