package net.rdrei.android.scdl2; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.support.annotation.Nullable; import com.google.inject.Inject; import com.gu.option.Option; import net.rdrei.android.scdl2.api.APIException; import net.rdrei.android.scdl2.api.MediaDownloadType; import net.rdrei.android.scdl2.api.PendingDownload; import net.rdrei.android.scdl2.api.ServiceManager; import net.rdrei.android.scdl2.api.entity.ResolveEntity; import net.rdrei.android.scdl2.api.service.ResolveService; import java.net.HttpURLConnection; import java.util.regex.Matcher; import java.util.regex.Pattern; public class ShareIntentResolver { @Inject private Activity mActivity; @Inject private ServiceManager mServiceManager; public static final Pattern URL_ID_PATTERN = Pattern.compile( "^https?://api.soundcloud.com/tracks/(\\d+)\\.json"); public static final Pattern URL_PLAYLIST_PATTERN = Pattern.compile( "^https?://api.soundcloud.com/playlists/(\\d+)\\.json"); public static final String SOUNDCLOUD_URI_SCHEME = "soundcloud"; public static final String SOUNDCLOUD_URI_HOST = "tracks"; public static final String SOUNDCLOUD_URI_PATH_RE = "^/\\d+$"; private static final String[] ALLOWED_HOSTS = {"soundcloud.com", "snd.sc", "m.soundcloud.com"}; public static class ShareIntentResolverException extends APIException { private static final long serialVersionUID = 1L; public ShareIntentResolverException(final String detailMessage) { super(detailMessage, -1); } public ShareIntentResolverException(final String detailMessage, final Throwable throwable) { super(detailMessage, throwable, -1); } } public static class UnsupportedUrlException extends ShareIntentResolverException { public UnsupportedUrlException(final String detailMessage) { super(detailMessage); } private static final long serialVersionUID = 1L; } public static class TrackNotFoundException extends ShareIntentResolverException { public TrackNotFoundException(final String detailMessage, final Throwable throwable) { super(detailMessage, throwable); } private static final long serialVersionUID = 1L; } public static class UnsupportedPlaylistUrlException extends ShareIntentResolverException { public UnsupportedPlaylistUrlException(String detailMessage) { super(detailMessage); } } /** * Resolves the Intent to a canonical URL for the track or raise a {@link * ShareIntentResolverException}. * <p/> * <b>This is blocking the current thread!</b> * * @return Canonical Track URL * @throws ShareIntentResolverException */ public String resolve() throws ShareIntentResolverException { final Intent intent = mActivity.getIntent(); // Check if we have a URL as extra. Uri uri = null; final String url = intent.getStringExtra(Intent.EXTRA_TEXT); if (url != null) { uri = Uri.parse(url); } if (uri == null) { uri = intent.getData(); } if (isDataUri(uri)) { final Option<String> maybeDataUrl = convertDataToWebUri(uri); if (maybeDataUrl.isDefined()) { return maybeDataUrl.get(); } } if (isValidUri(uri)) { try { return resolveUri(uri); } catch (final APIException e) { final int code = e.getCode(); if (code == HttpURLConnection.HTTP_NOT_FOUND) { throw new TrackNotFoundException( String.format("The given track could not be resolved for URL %s", uri.toString()), e); } throw new ShareIntentResolverException( String.format("Could not resolve URL: %s", uri.toString()), e); } } throw new UnsupportedUrlException( String.format("Given URL '%s' is not a valid soundcloud URL.", (uri == null) ? "unknown" : uri.toString())); } private Option<String> convertDataToWebUri(Uri uri) { try { final Long id = Long.parseLong(uri.getLastPathSegment()); return Option.some(String.format("https://api.soundcloud.com/tracks/%d.json", id)); } catch (NumberFormatException err) { return Option.none(); } } /** * Check whether a given URI looks like an internal URI. * * @param uri The URI to parse * @return True if the given URI is a SoundCloud app link. */ private boolean isDataUri(@Nullable Uri uri) { if (uri != null && SOUNDCLOUD_URI_SCHEME.equals(uri.getScheme()) && SOUNDCLOUD_URI_HOST.equals(uri.getHost())) { final String path = uri.getPath(); return path != null && path.matches(SOUNDCLOUD_URI_PATH_RE); } return false; } /** * Resolves the intent to a PendingDownload describing the ID and type of the download. * * @return PendingDownload containing the resolved track or playlist ID together with its type. * @throws ShareIntentResolverException */ public PendingDownload resolvePendingDownload() throws ShareIntentResolverException { final String id; final String url = resolve(); final Matcher idMatcher = URL_ID_PATTERN.matcher(url); final Matcher playlistMatcher = URL_PLAYLIST_PATTERN.matcher(url); if (!Config.Features.PLAYLIST_DOWNLOADS && playlistMatcher.find()) { throw new UnsupportedPlaylistUrlException( mActivity.getString(R.string.track_error_unsupported_playlist)); } if (idMatcher.find()) { id = idMatcher.group(1); } else { throw new ShareIntentResolverException( String.format("Could not parse ID from URL '%s'.", url)); } return new PendingDownload(id, MediaDownloadType.TRACK); } protected boolean isValidUri(final Uri uri) { if (uri == null) { return false; } final String scheme = uri.getScheme(); final String host = uri.getHost(); // Safeguard if we have some seriously weird uri as in BS/33260701. if (scheme == null || host == null) { return false; } if (!scheme.equals("http") && !scheme.equals("https")) { return false; } for (final String allowedHost : ALLOWED_HOSTS) { if (host.equals(allowedHost)) { return true; } } return false; } protected String resolveUri(final Uri uri) throws APIException { final ResolveService service = mServiceManager.resolveService(); final ResolveEntity entity = service.resolve(uri.toString()); return entity.getLocation(); } }