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 == null) return true;
164
165        if ("sgnl".equals(uri.getScheme())) {
166            stickers = Uri.parse("https://stickers.cheogram.com/signal/" + uri.getQueryParameter("pack_id") + "," + uri.getQueryParameter("pack_key"));
167            if (hasStoragePermission(1)) downloadStickers();
168            return false;
169        }
170
171        if ("https".equals(uri.getScheme()) && "signal.art".equals(uri.getHost())) {
172            android.net.UrlQuerySanitizer q = new android.net.UrlQuerySanitizer();
173            q.setAllowUnregisteredParamaters(true);
174            q.parseQuery(uri.getFragment());
175            stickers = Uri.parse("https://stickers.cheogram.com/signal/" + q.getValue("pack_id") + "," + q.getValue("pack_key"));
176            if (hasStoragePermission(1)) downloadStickers();
177            return false;
178        }
179
180        if (SignupUtils.isSupportTokenRegistry() && xmppUri.isValidJid()) {
181            final String preAuth = xmppUri.getParameter(XmppUri.PARAMETER_PRE_AUTH);
182            final Jid jid = xmppUri.getJid();
183            if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
184                if (jid.getEscapedLocal() != null && accounts.contains(jid.asBareJid())) {
185                    showError(R.string.account_already_exists);
186                    return false;
187                }
188                intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth);
189                startActivity(intent);
190                return true;
191            }
192            if (accounts.size() == 0
193                    && xmppUri.isAction(XmppUri.ACTION_ROSTER)
194                    && "y"
195                            .equalsIgnoreCase(
196                                    Strings.nullToEmpty(xmppUri.getParameter(XmppUri.PARAMETER_IBR))
197                                            .trim())) {
198                intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth);
199                intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
200                startActivity(intent);
201                return true;
202            }
203        } else if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
204            showError(R.string.account_registrations_are_not_supported);
205            return false;
206        }
207
208        if (accounts.size() == 0) {
209            if (xmppUri.isValidJid()) {
210                intent = SignupUtils.getSignUpIntent(this);
211                intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
212                startActivity(intent);
213                return true;
214            } else {
215                showError(R.string.invalid_jid);
216                return false;
217            }
218        }
219
220        if (xmppUri.isAction(XmppUri.ACTION_MESSAGE) || xmppUri.isAction("command")) {
221            final Jid jid = xmppUri.getJid();
222            final String body = xmppUri.getBody();
223
224            if (jid != null) {
225                final Class<?> clazz = findShareViaAccountClass();
226                if (clazz != null) {
227                    intent = new Intent(this, clazz);
228                    intent.setData(uri);
229                } else {
230                    intent = new Intent(this, StartConversationActivity.class);
231                    intent.setAction(Intent.ACTION_VIEW);
232                    intent.setData(uri);
233                    intent.putExtra("account", accounts.get(0).toEscapedString());
234                }
235            } else {
236                intent = new Intent(this, ShareWithActivity.class);
237                intent.setAction(Intent.ACTION_SEND);
238                intent.setType("text/plain");
239                intent.putExtra(Intent.EXTRA_TEXT, body);
240            }
241        } else if (accounts.contains(xmppUri.getJid())) {
242            intent = new Intent(getApplicationContext(), EditAccountActivity.class);
243            intent.setAction(Intent.ACTION_VIEW);
244            intent.putExtra("jid", xmppUri.getJid().asBareJid().toString());
245            intent.setData(uri);
246            intent.putExtra("scanned", scanned);
247        } else if (xmppUri.isValidJid()) {
248            intent = new Intent(getApplicationContext(), StartConversationActivity.class);
249            intent.setAction(Intent.ACTION_VIEW);
250            intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
251            intent.putExtra("scanned", scanned);
252            intent.setData(uri);
253        } else {
254            showError(R.string.invalid_jid);
255            return false;
256        }
257        startActivity(intent);
258        return true;
259    }
260
261    private void checkForLinkHeader(final HttpUrl url) {
262        Log.d(Config.LOGTAG, "checking for link header on " + url);
263        this.call =
264                HttpConnectionManager.OK_HTTP_CLIENT.newCall(
265                        new Request.Builder().url(url).head().build());
266        this.call.enqueue(
267                new Callback() {
268                    @Override
269                    public void onFailure(@NotNull Call call, @NotNull IOException e) {
270                        Log.d(Config.LOGTAG, "unable to check HTTP url", e);
271                        showError(R.string.no_xmpp_adddress_found);
272                    }
273
274                    @Override
275                    public void onResponse(@NotNull Call call, @NotNull Response response) {
276                        if (response.isSuccessful()) {
277                            final String linkHeader = response.header("Link");
278                            if (linkHeader != null && processLinkHeader(linkHeader)) {
279                                return;
280                            }
281                        }
282                        showError(R.string.no_xmpp_adddress_found);
283                    }
284                });
285    }
286
287    private boolean processLinkHeader(final String header) {
288        final Matcher matcher = LINK_HEADER_PATTERN.matcher(header);
289        if (matcher.find()) {
290            final String group = matcher.group();
291            final String link = group.substring(1, group.length() - 1);
292            if (handleUri(Uri.parse(link))) {
293                finish();
294                return true;
295            }
296        }
297        return false;
298    }
299
300    private void showError(@StringRes int error) {
301        this.binding.progress.setVisibility(View.INVISIBLE);
302        this.binding.error.setText(error);
303        this.binding.error.setVisibility(View.VISIBLE);
304    }
305
306    private static Class<?> findShareViaAccountClass() {
307        try {
308            return Class.forName("eu.siacs.conversations.ui.ShareViaAccountActivity");
309        } catch (final ClassNotFoundException e) {
310            return null;
311        }
312    }
313
314    private void handleIntent(final Intent data) {
315        final String action = data == null ? null : data.getAction();
316        if (action == null) {
317            return;
318        }
319        switch (action) {
320            case Intent.ACTION_MAIN:
321                binding.progress.setVisibility(
322                        call != null && !call.isCanceled() ? View.VISIBLE : View.INVISIBLE);
323                break;
324            case Intent.ACTION_VIEW:
325            case Intent.ACTION_SENDTO:
326                if (handleUri(data.getData())) {
327                    finish();
328                }
329                break;
330            case ACTION_SCAN_QR_CODE:
331                Log.d(Config.LOGTAG, "scan. allow=" + allowProvisioning());
332                setIntent(createMainIntent());
333                startActivityForResult(new Intent(this, ScanActivity.class), REQUEST_SCAN_QR_CODE);
334                break;
335        }
336    }
337
338    private Intent createMainIntent() {
339        final Intent intent = new Intent(Intent.ACTION_MAIN);
340        intent.putExtra(EXTRA_ALLOW_PROVISIONING, allowProvisioning());
341        return intent;
342    }
343
344    private boolean allowProvisioning() {
345        final Intent launchIntent = getIntent();
346        return launchIntent != null
347                && launchIntent.getBooleanExtra(EXTRA_ALLOW_PROVISIONING, false);
348    }
349
350    @Override
351    public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) {
352        super.onActivityResult(requestCode, requestCode, intent);
353        if (requestCode == REQUEST_SCAN_QR_CODE && resultCode == RESULT_OK) {
354            final boolean allowProvisioning = allowProvisioning();
355            final String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT);
356            if (Strings.isNullOrEmpty(result)) {
357                finish();
358                return;
359            }
360            if (result.startsWith("BEGIN:VCARD\n")) {
361                final Matcher matcher = V_CARD_XMPP_PATTERN.matcher(result);
362                if (matcher.find()) {
363                    if (handleUri(Uri.parse(matcher.group(2)), true)) {
364                        finish();
365                    }
366                } else {
367                    showError(R.string.no_xmpp_adddress_found);
368                }
369                return;
370            } else if (QuickConversationsService.isConversations()
371                    && looksLikeJsonObject(result)
372                    && allowProvisioning) {
373                ProvisioningUtils.provision(this, result);
374                finish();
375                return;
376            }
377            final Uri uri = Uri.parse(result.trim());
378            if (allowProvisioning
379                    && "https".equalsIgnoreCase(uri.getScheme())
380                    && !XmppUri.INVITE_DOMAIN.equalsIgnoreCase(uri.getHost())) {
381                final HttpUrl httpUrl = HttpUrl.parse(uri.toString());
382                if (httpUrl != null) {
383                    checkForLinkHeader(httpUrl);
384                } else {
385                    finish();
386                }
387            } else if (handleUri(uri, true)) {
388                finish();
389            } else {
390                setIntent(new Intent(Intent.ACTION_VIEW, uri));
391            }
392        } else {
393            finish();
394        }
395    }
396
397    private static boolean looksLikeJsonObject(final String input) {
398        final String trimmed = Strings.nullToEmpty(input).trim();
399        return trimmed.charAt(0) == '{' && trimmed.charAt(trimmed.length() - 1) == '}';
400    }
401
402    protected boolean hasStoragePermission(int requestCode) {
403        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
404            if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
405                requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
406                return false;
407            } else {
408                return true;
409            }
410        } else {
411            return true;
412        }
413    }
414}