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