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.StringRes;
 16import androidx.appcompat.app.AppCompatActivity;
 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 org.jetbrains.annotations.NotNull;
 49
 50import java.io.IOException;
 51import java.util.List;
 52import java.util.regex.Matcher;
 53import java.util.regex.Pattern;
 54
 55public class UriHandlerActivity extends AppCompatActivity {
 56
 57    public static final String ACTION_SCAN_QR_CODE = "scan_qr_code";
 58    private static final String EXTRA_ALLOW_PROVISIONING = "extra_allow_provisioning";
 59    private static final int REQUEST_SCAN_QR_CODE = 0x1234;
 60    private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN = 0x6789;
 61    private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION = 0x6790;
 62    private static final Pattern V_CARD_XMPP_PATTERN = Pattern.compile("\nIMPP([^:]*):(xmpp:.+)\n");
 63    private static final Pattern LINK_HEADER_PATTERN = Pattern.compile("<(.*?)>");
 64    private ActivityUriHandlerBinding binding;
 65    private Call call;
 66    private Uri stickers;
 67
 68    public static void scan(final Activity activity) {
 69        scan(activity, false);
 70    }
 71
 72    public static void scan(final Activity activity, final boolean provisioning) {
 73        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
 74                || ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
 75                        == PackageManager.PERMISSION_GRANTED) {
 76            final Intent intent = new Intent(activity, UriHandlerActivity.class);
 77            intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE);
 78            if (provisioning) {
 79                intent.putExtra(EXTRA_ALLOW_PROVISIONING, true);
 80            }
 81            intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
 82            activity.startActivity(intent);
 83        } else {
 84            activity.requestPermissions(
 85                    new String[] {Manifest.permission.CAMERA},
 86                    provisioning
 87                            ? REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION
 88                            : REQUEST_CAMERA_PERMISSIONS_TO_SCAN);
 89        }
 90    }
 91
 92    public static void onRequestPermissionResult(
 93            Activity activity, int requestCode, int[] grantResults) {
 94        if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN
 95                && requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) {
 96            return;
 97        }
 98        if (grantResults.length > 0) {
 99            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
100                if (requestCode == REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) {
101                    scan(activity, true);
102                } else {
103                    scan(activity);
104                }
105            } else {
106                Toast.makeText(
107                                activity,
108                                R.string.qr_code_scanner_needs_access_to_camera,
109                                Toast.LENGTH_SHORT)
110                        .show();
111            }
112        }
113    }
114
115    @Override
116    protected void onCreate(Bundle savedInstanceState) {
117        super.onCreate(savedInstanceState);
118        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_uri_handler);
119    }
120
121    @Override
122    public void onStart() {
123        super.onStart();
124        handleIntent(getIntent());
125    }
126
127    @Override
128    public void onNewIntent(final Intent intent) {
129        super.onNewIntent(intent);
130        handleIntent(intent);
131    }
132
133
134    @Override
135    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
136        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
137        if (grantResults.length > 0) {
138            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
139                downloadStickers();
140            }
141        }
142        finish();
143    }
144
145    private void downloadStickers() {
146        Intent intent = new Intent(this, DownloadDefaultStickers.class);
147        intent.setData(stickers);
148        intent.putExtra("tor", PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getBoolean("use_tor", getResources().getBoolean(R.bool.use_tor)));
149        ContextCompat.startForegroundService(this, intent);
150        Toast.makeText(this, "Sticker download started", Toast.LENGTH_SHORT).show();
151        finish();
152    }
153
154    private boolean handleUri(final Uri uri) {
155        return handleUri(uri, false);
156    }
157
158    private boolean handleUri(final Uri uri, final boolean scanned) {
159        final Intent intent;
160        final XmppUri xmppUri = new XmppUri(uri);
161        final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids(false);
162
163        if (uri.getScheme().equals("sgnl")) {
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 (uri.getScheme().equals("https") && uri.getHost().equals("signal.art")) {
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(@NotNull Call call, @NotNull 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(@NotNull Call call, @NotNull 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}