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