Skip to content

Commit b795fc3

Browse files
refactor: Move web search logic to simple passthru api (#21)
1 parent 85fdd86 commit b795fc3

4 files changed

Lines changed: 99 additions & 27 deletions

File tree

app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import kotlinx.datetime.TimeZone
2323
import kotlinx.datetime.toLocalDateTime
2424

2525
private const val MORPHE_MANAGER_REPO_URL = "https://github.com/MorpheApp/morphe-manager"
26-
private const val MORPHE_API_URL = "https://api.morphe.software"
26+
internal const val MORPHE_API_URL = "https://api.morphe.software"
2727

2828
class ReVancedAPI(
2929
private val client: HttpService,
@@ -168,8 +168,9 @@ class ReVancedAPI(
168168
return asset.takeIf { it.version.removePrefix("v") != BuildConfig.VERSION_NAME }
169169
}
170170

171-
suspend fun getPatchesUpdate(): APIResponse<ReVancedAsset> =
172-
apiRequest("patches?prerelease=${prefs.usePatchesPrereleases.get()}")
171+
suspend fun getPatchesUpdate(): APIResponse<ReVancedAsset> = apiRequest(
172+
if (prefs.usePatchesPrereleases.get()) "patches/prerelease" else "patches"
173+
)
173174

174175
suspend fun getContributors(): APIResponse<List<ReVancedGitRepository>> {
175176
val config = repoConfig()

app/src/main/java/app/revanced/manager/ui/component/morphe/home/HomeDialogs.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ import kotlinx.coroutines.launch
4141
*/
4242
@Composable
4343
fun HomeDialogs(
44-
state: HomeStates,
45-
usingMountInstall: Boolean
44+
state: HomeStates
4645
) {
4746
val uriHandler = LocalUriHandler.current
4847

@@ -65,6 +64,7 @@ fun HomeDialogs(
6564
// User needs APK - show download instructions
6665
state.showApkAvailabilityDialog = false
6766
state.showDownloadInstructionsDialog = true
67+
state.resolveDownloadRedirect()
6868
}
6969
)
7070
}
@@ -74,7 +74,7 @@ fun HomeDialogs(
7474
DownloadInstructionsDialog(
7575
appName = state.pendingAppName!!,
7676
recommendedVersion = state.pendingRecommendedVersion,
77-
usingMountInstall = usingMountInstall,
77+
usingMountInstall = state.usingMountInstall,
7878
onDismiss = {
7979
state.showDownloadInstructionsDialog = false
8080
state.cleanupPendingData()

app/src/main/java/app/revanced/manager/ui/component/morphe/home/HomeStates.kt

Lines changed: 91 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import app.morphe.manager.R
1515
import app.revanced.manager.domain.bundles.PatchBundleSource
1616
import app.revanced.manager.domain.repository.PatchBundleRepository.Companion.DEFAULT_SOURCE_UID
1717
import app.revanced.manager.domain.repository.PatchOptionsRepository
18+
import app.revanced.manager.network.api.MORPHE_API_URL
1819
import app.revanced.manager.patcher.patch.PatchBundleInfo
1920
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
2021
import app.revanced.manager.ui.model.SelectedApp
@@ -30,6 +31,9 @@ import kotlinx.coroutines.launch
3031
import kotlinx.coroutines.withContext
3132
import org.koin.compose.koinInject
3233
import java.io.File
34+
import java.net.HttpURLConnection
35+
import java.net.SocketTimeoutException
36+
import java.net.URL
3337
import java.net.URLEncoder.encode
3438

3539
private const val PACKAGE_YOUTUBE = "com.google.android.youtube"
@@ -95,6 +99,7 @@ class HomeStates(
9599
var pendingAppName by mutableStateOf<String?>(null)
96100
var pendingRecommendedVersion by mutableStateOf<String?>(null)
97101
var pendingSelectedApp by mutableStateOf<SelectedApp?>(null)
102+
var resolvedDownloadUrl by mutableStateOf<String?>(null)
98103

99104
// Bundle update snackbar state
100105
var showBundleUpdateSnackbar by mutableStateOf(false)
@@ -255,38 +260,104 @@ class HomeStates(
255260
onStartQuickPatch(params)
256261
}
257262

258-
/**
259-
* Handle download instructions dialog continue action
260-
* Opens browser to APKMirror search and shows file picker prompt
261-
*/
262-
fun handleDownloadInstructionsContinue(uriHandler: UriHandler) {
263-
val baseQuery = if (pendingPackageName == PACKAGE_YOUTUBE) {
264-
pendingPackageName
265-
} else {
266-
// Some versions of YT Music don't show when the package name is used, use the app name instead
267-
"YouTube Music"
263+
// TODO: Move this logic somewhere more appropriate.
264+
fun resolveDownloadRedirect() {
265+
fun resolveUrlRedirect(url: String): String {
266+
return try {
267+
val originalUrl = URL(url)
268+
val connection = originalUrl.openConnection() as HttpURLConnection
269+
connection.instanceFollowRedirects = false
270+
connection.requestMethod = "HEAD"
271+
connection.connectTimeout = 5_000
272+
connection.readTimeout = 5_000
273+
274+
val responseCode = connection.responseCode
275+
if (responseCode in 300..399) {
276+
val location = connection.getHeaderField("Location")
277+
278+
if (location.isNullOrBlank()) {
279+
Log.d(tag, "Location tag is blank: ${connection.responseMessage}")
280+
getApiOfflineWebSearchUrl()
281+
} else {
282+
val resolved =
283+
if (location.startsWith("http://") || location.startsWith("https://")) {
284+
location
285+
} else {
286+
val prefix = "${originalUrl.protocol}://${originalUrl.host}"
287+
if (location.startsWith("/")) "$prefix$location" else "$prefix/$location"
288+
}
289+
Log.d(tag, "Result: $resolved")
290+
resolved
291+
}
292+
} else {
293+
Log.d(tag, "Unexpected response code: $responseCode")
294+
getApiOfflineWebSearchUrl()
295+
}
296+
} catch (ex: SocketTimeoutException) {
297+
Log.d(tag, "Timeout while resolving search redirect: $ex")
298+
// Timeout may be because the network is very slow.
299+
// Still use web-search api call in external browser.
300+
url
301+
} catch (ex: Exception) {
302+
Log.d(tag, "Exception while resolving search redirect: $ex")
303+
getApiOfflineWebSearchUrl()
304+
}
268305
}
269306

307+
// Must not escape colon search term separator, but recommended version must be escaped
308+
// because Android version string can be almost anything.
309+
val escapedVersion = encode(pendingRecommendedVersion, "UTF-8")
310+
val searchQuery = "$pendingPackageName:$escapedVersion:${Build.SUPPORTED_ABIS.first()}"
311+
// To test client fallback logic in getApiOfflineWebSearchUrl(), change this an invalid url.
312+
val searchUrl = "$MORPHE_API_URL/v1/web-search/$searchQuery"
313+
Log.d(tag, "Using search url: $searchUrl")
314+
315+
// Use API web-search if user clicks thru faster than redirect resolving can occur.
316+
resolvedDownloadUrl = searchUrl
317+
318+
scope.launch(Dispatchers.IO) {
319+
var resolved = resolveUrlRedirect(searchUrl)
320+
321+
// If redirect stays on api.morphe.software, try resolving again
322+
if (resolved.startsWith(MORPHE_API_URL)) {
323+
Log.d(tag, "Redirect still on API host, resolving again")
324+
resolved = resolveUrlRedirect(resolved)
325+
}
326+
327+
withContext(Dispatchers.Main) {
328+
resolvedDownloadUrl = resolved
329+
}
330+
}
331+
}
332+
333+
fun getApiOfflineWebSearchUrl(): String {
270334
val architecture = if (pendingPackageName == PACKAGE_YOUTUBE_MUSIC) {
271335
// YT Music requires architecture. This logic could be improved
272336
" (${Build.SUPPORTED_ABIS.first()})"
273337
} else {
274-
""
338+
"nodpi"
275339
}
276340

277-
val version = pendingRecommendedVersion ?: ""
278-
// Backslash search parameter opens the first search result
279-
// Use quotes to ensure it's an exact match of all search terms
280-
val searchQuery = "\\$baseQuery $version $architecture (nodpi) site:apkmirror.com".replace(" ", " ")
281-
val searchUrl = "https://duckduckgo.com/?q=${encode(searchQuery, "UTF-8")}"
341+
val searchQuery = "\"$pendingPackageName\" \"$pendingRecommendedVersion\" \"$architecture\" site:APKMirror.com"
342+
343+
val searchUrl = "https://google.com/search?q=${encode(searchQuery, "UTF-8")}"
282344
Log.d(tag, "Using search query: $searchQuery")
345+
return searchUrl
346+
}
347+
348+
/**
349+
* Handle download instructions dialog continue action
350+
* Opens browser to APKMirror search and shows file picker prompt.
351+
*/
352+
fun handleDownloadInstructionsContinue(uriHandler: UriHandler) {
353+
val urlToOpen = resolvedDownloadUrl!!
283354

284355
try {
285-
uriHandler.openUri(searchUrl)
286-
// After opening browser, show file picker prompt
356+
uriHandler.openUri(urlToOpen)
287357
showDownloadInstructionsDialog = false
288358
showFilePickerPromptDialog = true
289-
} catch (_: Exception) {
359+
} catch (ex: Exception) {
360+
Log.d(tag, "Failed to open URL: $ex")
290361
context.toast(context.getString(R.string.morphe_home_failed_to_open_url))
291362
showDownloadInstructionsDialog = false
292363
cleanupPendingData()
@@ -311,6 +382,7 @@ class HomeStates(
311382
pendingPackageName = null
312383
pendingAppName = null
313384
pendingRecommendedVersion = null
385+
resolvedDownloadUrl = null
314386
if (!keepSelectedApp) {
315387
// Delete temporary file if exists
316388
pendingSelectedApp?.let { app ->

app/src/main/java/app/revanced/manager/ui/screen/MorpheHomeScreen.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,7 @@ fun MorpheHomeScreen(
189189

190190
// All dialogs
191191
HomeDialogs(
192-
state = homeState,
193-
usingMountInstall = usingMountInstall
192+
state = homeState
194193
)
195194

196195
// Main scaffold

0 commit comments

Comments
 (0)