UriHandlerActivity.java

  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}