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