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.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}