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