diff --git a/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.kt b/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.kt index 5750273..c36f261 100644 --- a/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.kt +++ b/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.kt @@ -1,254 +1,255 @@ -package com.fox2code.mmm.markdown; - -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.graphics.Color; -import android.os.Build; -import android.os.Bundle; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.Nullable; - -import com.fox2code.foxcompat.app.FoxActivity; -import com.fox2code.mmm.Constants; -import com.fox2code.mmm.MainApplication; -import com.fox2code.mmm.R; -import com.fox2code.mmm.XHooks; -import com.fox2code.mmm.utils.IntentHelper; -import com.fox2code.mmm.utils.io.net.Http; -import com.google.android.material.bottomnavigation.BottomNavigationView; -import com.google.android.material.chip.Chip; -import com.google.android.material.chip.ChipGroup; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.topjohnwu.superuser.internal.UiThreadHandler; - -import org.matomo.sdk.extra.TrackHelper; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Objects; - -import timber.log.Timber; - -public class MarkdownActivity extends FoxActivity { - private static final HashMap redirects = new HashMap<>(4); - private static final String[] variants = new String[]{"readme.md", "README.MD", ".github/README.md"}; - private TextView header; - private TextView footer; - - private static byte[] getRawMarkdown(String url) throws IOException { - String newUrl = redirects.get(url); - if (newUrl != null && !newUrl.equals(url)) { - return Http.doHttpGet(newUrl, true); - } - try { - return Http.doHttpGet(url, true); - } catch (IOException e) { - // Workaround GitHub README.md case sensitivity issue - if (url.startsWith("https://raw.githubusercontent.com/") && url.endsWith("/README.md")) { - String prefix = url.substring(0, url.length() - 9); - for (String suffix : variants) { - newUrl = prefix + suffix; - try { // Try with lowercase version - byte[] rawMarkdown = Http.doHttpGet(prefix + suffix, true); - redirects.put(url, newUrl); // Avoid retries - return rawMarkdown; - } catch (IOException ignored) { - } - } - } - throw e; - } - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker()); - this.setDisplayHomeAsUpEnabled(true); - Intent intent = this.getIntent(); +package com.fox2code.mmm.markdown + +import android.content.DialogInterface +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.graphics.Color +import android.os.Build +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.TextView +import android.widget.Toast +import com.fox2code.foxcompat.app.FoxActivity +import com.fox2code.mmm.Constants +import com.fox2code.mmm.MainApplication +import com.fox2code.mmm.R +import com.fox2code.mmm.XHooks +import com.fox2code.mmm.utils.IntentHelper +import com.fox2code.mmm.utils.io.net.Http +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.topjohnwu.superuser.internal.UiThreadHandler +import org.matomo.sdk.extra.TrackHelper +import timber.log.Timber +import java.io.IOException +import java.nio.charset.StandardCharsets + +class MarkdownActivity : FoxActivity() { + private var header: TextView? = null + private var footer: TextView? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().tracker) + setDisplayHomeAsUpEnabled(true) + val intent = this.intent if (!MainApplication.checkSecret(intent)) { - Timber.e("Impersonation detected!"); - this.forceBackPressed(); - return; + Timber.e("Impersonation detected!") + forceBackPressed() + return } - String url = Objects.requireNonNull(intent.getExtras()).getString(Constants.EXTRA_MARKDOWN_URL); - String title = intent.getExtras().getString(Constants.EXTRA_MARKDOWN_TITLE); - String config = intent.getExtras().getString(Constants.EXTRA_MARKDOWN_CONFIG); - boolean change_boot = intent.getExtras().getBoolean(Constants.EXTRA_MARKDOWN_CHANGE_BOOT); - boolean needs_ramdisk = intent.getExtras().getBoolean(Constants.EXTRA_MARKDOWN_NEEDS_RAMDISK); - int min_magisk = intent.getExtras().getInt(Constants.EXTRA_MARKDOWN_MIN_MAGISK); - int min_api = intent.getExtras().getInt(Constants.EXTRA_MARKDOWN_MIN_API); - int max_api = intent.getExtras().getInt(Constants.EXTRA_MARKDOWN_MAX_API); - if (title != null && !title.isEmpty()) { - this.setTitle(title); + val url = intent.extras?.getString(Constants.EXTRA_MARKDOWN_URL) + var title = intent.extras!!.getString(Constants.EXTRA_MARKDOWN_TITLE) + val config = intent.extras!!.getString(Constants.EXTRA_MARKDOWN_CONFIG) + val changeBoot = intent.extras!!.getBoolean(Constants.EXTRA_MARKDOWN_CHANGE_BOOT) + val needsRamdisk = intent.extras!!.getBoolean(Constants.EXTRA_MARKDOWN_NEEDS_RAMDISK) + val minMagisk = intent.extras!!.getInt(Constants.EXTRA_MARKDOWN_MIN_MAGISK) + val minApi = intent.extras!!.getInt(Constants.EXTRA_MARKDOWN_MIN_API) + val maxApi = intent.extras!!.getInt(Constants.EXTRA_MARKDOWN_MAX_API) + if (!title.isNullOrEmpty()) { + this.title = title + } else { + @Suppress("UNUSED_VALUE") + title = url } - setActionBarBackground(null); - this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, 0); - if (config != null && !config.isEmpty()) { - String configPkg = IntentHelper.getPackageOfConfig(config); + setActionBarBackground(null) + @Suppress("DEPRECATION") + this.window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, 0) + if (!config.isNullOrEmpty()) { + val configPkg = IntentHelper.getPackageOfConfig(config) try { - XHooks.checkConfigTargetExists(this, configPkg, config); - this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24, menu -> { - IntentHelper.openConfig(this, config); - return true; - }); - } catch (PackageManager.NameNotFoundException e) { - Timber.w("Config package \"" + configPkg + "\" missing for markdown view"); + XHooks.checkConfigTargetExists(this, configPkg, config) + this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24) { _: MenuItem? -> + IntentHelper.openConfig(this, config) + true + } + } catch (e: PackageManager.NameNotFoundException) { + Timber.w("Config package \"$configPkg\" missing for markdown view") } } // validate the url won't crash the app - if (url == null || url.isEmpty() || url.contains("..")) { - Timber.e("Invalid url %s", String.valueOf(url)); - this.forceBackPressed(); - return; + if (url.isNullOrEmpty() || url.contains("..")) { + Timber.e("Invalid url %s", url.toString()) + forceBackPressed() + return + } + Timber.i("Url for markdown %s", url.toString()) + setContentView(R.layout.markdown_view) + val markdownBackground = findViewById(R.id.markdownBackground) + val textView = findViewById(R.id.markdownView) + header = findViewById(R.id.markdownHeader) + footer = findViewById(R.id.markdownFooter) + updateBlurState() + UiThreadHandler.handler.post { // Fix header/footer height + this.updateScreenInsets(this.resources.configuration) } - //noinspection UnnecessaryCallToStringValueOf - Timber.i("Url for markdown %s", String.valueOf(url)); - setContentView(R.layout.markdown_view); - final ViewGroup markdownBackground = findViewById(R.id.markdownBackground); - final TextView textView = findViewById(R.id.markdownView); - this.header = findViewById(R.id.markdownHeader); - this.footer = findViewById(R.id.markdownFooter); - this.updateBlurState(); - UiThreadHandler.handler.post(() -> // Fix header/footer height - this.updateScreenInsets(this.getResources().getConfiguration())); // Really bad created (MSG by Der_Googler) // set "message" to null to disable dialog - if (change_boot) this.addChip(MarkdownChip.CHANGE_BOOT); - if (needs_ramdisk) this.addChip(MarkdownChip.NEED_RAMDISK); - if (min_magisk != 0) this.addChip(MarkdownChip.MIN_MAGISK, String.valueOf(min_magisk)); - if (min_api != 0) this.addChip(MarkdownChip.MIN_SDK, parseAndroidVersion(min_api)); - if (max_api != 0) this.addChip(MarkdownChip.MAX_SDK, parseAndroidVersion(max_api)); - - new Thread(() -> { + if (changeBoot) this.addChip(MarkdownChip.CHANGE_BOOT) + if (needsRamdisk) this.addChip(MarkdownChip.NEED_RAMDISK) + if (minMagisk != 0) this.addChip(MarkdownChip.MIN_MAGISK, minMagisk.toString()) + if (minApi != 0) this.addChip(MarkdownChip.MIN_SDK, parseAndroidVersion(minApi)) + if (maxApi != 0) this.addChip(MarkdownChip.MAX_SDK, parseAndroidVersion(maxApi)) + Thread({ try { - Timber.i("Downloading"); - byte[] rawMarkdown = getRawMarkdown(url); - Timber.i("Encoding"); - String markdown = new String(rawMarkdown, StandardCharsets.UTF_8); - Timber.i("Done!"); - runOnUiThread(() -> { - findViewById(R.id.markdownFooter).setMinimumHeight(this.getNavigationBarHeight()); - MainApplication.getINSTANCE().getMarkwon().setMarkdown(textView, MarkdownUrlLinker.urlLinkify(markdown)); + Timber.i("Downloading") + val rawMarkdown = getRawMarkdown(url) + Timber.i("Encoding") + val markdown = String(rawMarkdown, StandardCharsets.UTF_8) + Timber.i("Done!") + runOnUiThread { + footer?.minimumHeight = this.navigationBarHeight + MainApplication.getINSTANCE().markwon.setMarkdown( + textView, + MarkdownUrlLinker.urlLinkify(markdown) + ) if (markdownBackground != null) { - markdownBackground.setClickable(true); + markdownBackground.isClickable = true } - }); - } catch (Exception e) { - Timber.e(e); - runOnUiThread(() -> Toast.makeText(this, R.string.failed_download, Toast.LENGTH_SHORT).show()); + } + } catch (e: Exception) { + Timber.e(e) + runOnUiThread { + Toast.makeText(this, R.string.failed_download, Toast.LENGTH_SHORT).show() + } } - }, "Markdown load thread").start(); + }, "Markdown load thread").start() } - private void updateBlurState() { + private fun updateBlurState() { if (MainApplication.isBlurEnabled()) { // set bottom navigation bar color to transparent blur - BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation); + val bottomNavigationView = findViewById(R.id.bottom_navigation) if (bottomNavigationView != null) { - bottomNavigationView.setBackgroundColor(Color.TRANSPARENT); - bottomNavigationView.setAlpha(0.8F); + bottomNavigationView.setBackgroundColor(Color.TRANSPARENT) + bottomNavigationView.alpha = 0.8f } else { - Timber.w("Bottom navigation view not found"); + Timber.w("Bottom navigation view not found") } // set dialogs to have transparent blur - getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + window.addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND) } } - private void updateScreenInsets() { - this.runOnUiThread(() -> this.updateScreenInsets(this.getResources().getConfiguration())); + private fun updateScreenInsets() { + runOnUiThread { this.updateScreenInsets(this.resources.configuration) } } - private void updateScreenInsets(Configuration configuration) { - boolean landscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE; - int bottomInset = (landscape ? 0 : this.getNavigationBarHeight()); - int statusBarHeight = getStatusBarHeight(); - int actionBarHeight = getActionBarHeight(); - int combinedBarsHeight = statusBarHeight + actionBarHeight; - this.header.setMinHeight(combinedBarsHeight); - this.footer.setMinHeight(bottomInset); + private fun updateScreenInsets(configuration: Configuration) { + val landscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + val bottomInset = if (landscape) 0 else this.navigationBarHeight + val statusBarHeight = statusBarHeight + val actionBarHeight = actionBarHeight + val combinedBarsHeight = statusBarHeight + actionBarHeight + header!!.minHeight = combinedBarsHeight + footer!!.minHeight = bottomInset //this.actionBarBlur.invalidate(); } - @Override - public void refreshUI() { - super.refreshUI(); - this.updateScreenInsets(); - this.updateBlurState(); + override fun refreshUI() { + super.refreshUI() + this.updateScreenInsets() + updateBlurState() } - @Override - protected void onWindowUpdated() { - this.updateScreenInsets(); + override fun onWindowUpdated() { + this.updateScreenInsets() } - private void addChip(MarkdownChip markdownChip) { - this.makeChip(this.getString(markdownChip.title), markdownChip.desc == 0 ? null : this.getString(markdownChip.desc)); + private fun addChip(markdownChip: MarkdownChip) { + makeChip( + this.getString(markdownChip.title), + if (markdownChip.desc == 0) null else this.getString(markdownChip.desc) + ) } - private void addChip(MarkdownChip markdownChip, String extra) { - String title = this.getString(markdownChip.title); - if (title.contains("%s")) { - title = title.replace("%s", extra); + private fun addChip(markdownChip: MarkdownChip, extra: String) { + var title = this.getString(markdownChip.title) + title = if (title.contains("%s")) { + title.replace("%s", extra) } else { - title = title + " " + extra; + "$title $extra" } - this.makeChip(title, markdownChip.desc == 0 ? null : this.getString(markdownChip.desc)); + makeChip(title, if (markdownChip.desc == 0) null else this.getString(markdownChip.desc)) } - private void makeChip(String title, String message) { - final ChipGroup chip_group_holder = findViewById(R.id.chip_group_holder); - Chip chip = new Chip(this); - chip.setText(title); - chip.setVisibility(View.VISIBLE); + private fun makeChip(title: String, message: String?) { + val chipGroupHolder = findViewById(R.id.chip_group_holder) + val chip = Chip(this) + chip.text = title + chip.visibility = View.VISIBLE if (message != null) { - chip.setOnClickListener(_view -> { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); - - builder.setTitle(title).setMessage(message).setCancelable(true).setPositiveButton(R.string.ok, (x, y) -> x.dismiss()).show(); + chip.setOnClickListener { _: View? -> + val builder = MaterialAlertDialogBuilder(this) + builder.setTitle(title).setMessage(message).setCancelable(true) + .setPositiveButton(R.string.ok) { x: DialogInterface, _: Int -> x.dismiss() } + .show() + } + } + chipGroupHolder.addView(chip) + } - }); + private fun parseAndroidVersion(version: Int): String { + return when (version) { + Build.VERSION_CODES.JELLY_BEAN -> "4.1 JellyBean" + Build.VERSION_CODES.JELLY_BEAN_MR1 -> "4.2 JellyBean" + Build.VERSION_CODES.JELLY_BEAN_MR2 -> "4.3 JellyBean" + Build.VERSION_CODES.KITKAT -> "4.4 KitKat" + Build.VERSION_CODES.KITKAT_WATCH -> "4.4 KitKat Watch" + Build.VERSION_CODES.LOLLIPOP -> "5.0 Lollipop" + Build.VERSION_CODES.LOLLIPOP_MR1 -> "5.1 Lollipop" + Build.VERSION_CODES.M -> "6.0 Marshmallow" + Build.VERSION_CODES.N -> "7.0 Nougat" + Build.VERSION_CODES.N_MR1 -> "7.1 Nougat" + Build.VERSION_CODES.O -> "8.0 Oreo" + Build.VERSION_CODES.O_MR1 -> "8.1 Oreo" + Build.VERSION_CODES.P -> "9.0 Pie" + Build.VERSION_CODES.Q -> "10 (Q)" + Build.VERSION_CODES.R -> "11 (R)" + Build.VERSION_CODES.S -> "12 (S)" + Build.VERSION_CODES.S_V2 -> "12L" + Build.VERSION_CODES.TIRAMISU -> "13 Tiramisu" + else -> "Sdk: $version" } - chip_group_holder.addView(chip); } - private String parseAndroidVersion(int version) { - return switch (version) { - case Build.VERSION_CODES.JELLY_BEAN -> "4.1 JellyBean"; - case Build.VERSION_CODES.JELLY_BEAN_MR1 -> "4.2 JellyBean"; - case Build.VERSION_CODES.JELLY_BEAN_MR2 -> "4.3 JellyBean"; - case Build.VERSION_CODES.KITKAT -> "4.4 KitKat"; - case Build.VERSION_CODES.KITKAT_WATCH -> "4.4 KitKat Watch"; - case Build.VERSION_CODES.LOLLIPOP -> "5.0 Lollipop"; - case Build.VERSION_CODES.LOLLIPOP_MR1 -> "5.1 Lollipop"; - case Build.VERSION_CODES.M -> "6.0 Marshmallow"; - case Build.VERSION_CODES.N -> "7.0 Nougat"; - case Build.VERSION_CODES.N_MR1 -> "7.1 Nougat"; - case Build.VERSION_CODES.O -> "8.0 Oreo"; - case Build.VERSION_CODES.O_MR1 -> "8.1 Oreo"; - case Build.VERSION_CODES.P -> "9.0 Pie"; - case Build.VERSION_CODES.Q -> "10 (Q)"; - case Build.VERSION_CODES.R -> "11 (R)"; - case Build.VERSION_CODES.S -> "12 (S)"; - case Build.VERSION_CODES.S_V2 -> "12L"; - case Build.VERSION_CODES.TIRAMISU -> "13 Tiramisu"; - default -> "Sdk: " + version; - }; + override fun onResume() { + super.onResume() + val footer = findViewById(R.id.markdownFooter) + if (footer != null) footer.minimumHeight = this.navigationBarHeight } - @Override - protected void onResume() { - super.onResume(); - View footer = findViewById(R.id.markdownFooter); - if (footer != null) footer.setMinimumHeight(this.getNavigationBarHeight()); + companion object { + private val redirects = HashMap(4) + private val variants = arrayOf("readme.md", "README.MD", ".github/README.md") + @Throws(IOException::class) + private fun getRawMarkdown(url: String): ByteArray { + var newUrl = redirects[url] + return if (newUrl != null && newUrl != url) { + Http.doHttpGet(newUrl, true) + } else try { + Http.doHttpGet(url, true) + } catch (e: IOException) { + // Workaround GitHub README.md case sensitivity issue + if (url.startsWith("https://raw.githubusercontent.com/") && url.endsWith("/README.md")) { + val prefix = url.substring(0, url.length - 9) + for (suffix in variants) { + newUrl = prefix + suffix + try { // Try with lowercase version + val rawMarkdown = Http.doHttpGet(prefix + suffix, true) + redirects[url] = newUrl // Avoid retries + return rawMarkdown + } catch (ignored: IOException) { + } + } + } + throw e + } + } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/fox2code/mmm/markdown/MarkdownChip.kt b/app/src/main/java/com/fox2code/mmm/markdown/MarkdownChip.kt index 5665c8f..ca97e9a 100644 --- a/app/src/main/java/com/fox2code/mmm/markdown/MarkdownChip.kt +++ b/app/src/main/java/com/fox2code/mmm/markdown/MarkdownChip.kt @@ -1,19 +1,21 @@ -package com.fox2code.mmm.markdown; +package com.fox2code.mmm.markdown -import androidx.annotation.StringRes; +import androidx.annotation.StringRes +import com.fox2code.mmm.R -import com.fox2code.mmm.R; - -public enum MarkdownChip { - CHANGE_BOOT(R.string.module_can_change_boot, R.string.module_can_change_boot_desc), - NEED_RAMDISK(R.string.module_needs_ramdisk, R.string.module_needs_ramdisk_desc), - MIN_MAGISK(R.string.module_min_magisk_chip, 0), - MIN_SDK(R.string.module_min_sdk_chip, 0), - MAX_SDK(R.string.module_max_sdk_chip, 0); - @StringRes public final int title, desc; - - MarkdownChip(@StringRes int title,@StringRes int desc) { - this.title = title; - this.desc = desc; - } -} +enum class MarkdownChip( + @field:StringRes @param:StringRes val title: Int, + @field:StringRes @param:StringRes val desc: Int +) { + CHANGE_BOOT( + R.string.module_can_change_boot, + R.string.module_can_change_boot_desc + ), + NEED_RAMDISK( + R.string.module_needs_ramdisk, R.string.module_needs_ramdisk_desc + ), + MIN_MAGISK(R.string.module_min_magisk_chip, 0), MIN_SDK( + R.string.module_min_sdk_chip, 0 + ), + MAX_SDK(R.string.module_max_sdk_chip, 0) +} \ No newline at end of file diff --git a/app/src/main/java/com/fox2code/mmm/markdown/MarkdownUrlLinker.kt b/app/src/main/java/com/fox2code/mmm/markdown/MarkdownUrlLinker.kt index 1385cc7..95326fc 100644 --- a/app/src/main/java/com/fox2code/mmm/markdown/MarkdownUrlLinker.kt +++ b/app/src/main/java/com/fox2code/mmm/markdown/MarkdownUrlLinker.kt @@ -1,57 +1,51 @@ -package com.fox2code.mmm.markdown; +package com.fox2code.mmm.markdown -import java.util.ArrayList; +import timber.log.Timber -import timber.log.Timber; - -public enum MarkdownUrlLinker { +enum class MarkdownUrlLinker { ; - public static String urlLinkify(String url) { - int index = url.indexOf("https://"); - if (index == -1) - return url; - ArrayList linkifyTasks = new ArrayList<>(); - int extra = 0; - while (index != -1) { - int end = url.indexOf(' ', index); - end = end == -1 ? url.indexOf('\n', index) : Math.min(url.indexOf('\n', index), end); - if (end == -1) - end = url.length(); - if (index == 0 || '\n' == url.charAt(index - 1) || ' ' == url.charAt(index - 1)) { - int endDomain = url.indexOf('/', index + 9); - char endCh = url.charAt(end - 1); - if (endDomain != -1 && endDomain < end && endCh != '>' && endCh != ')' && endCh != ']') { - linkifyTasks.add(new LinkifyTask(index, end)); - extra += (end - index) + 4; - Timber.d("Linkify url: %s", url.substring(end)); - } - } - index = url.indexOf("https://", end); + private class LinkifyTask(val start: Int, val end: Int) { + companion object { + val NULL = LinkifyTask(0, 0) } - if (linkifyTasks.isEmpty()) - return url; - LinkifyTask prev = LinkifyTask.NULL; - StringBuilder stringBuilder = new StringBuilder(url.length() + extra); - for (LinkifyTask linkifyTask : linkifyTasks) { - stringBuilder.append(url, prev.end, linkifyTask.start).append('[').append(url, linkifyTask.start, linkifyTask.end).append("](").append(url, linkifyTask.start, linkifyTask.end).append(')'); - prev = linkifyTask; - } - if (prev.end != url.length()) - stringBuilder.append(url, prev.end, url.length()); - Timber.i("Added Markdown link to " + linkifyTasks.size() + " urls"); - return stringBuilder.toString(); } - private static class LinkifyTask { - static final LinkifyTask NULL = new LinkifyTask(0, 0); - - private final int start; - private final int end; - - private LinkifyTask(int start, int end) { - this.start = start; - this.end = end; + companion object { + @JvmStatic + fun urlLinkify(url: String): String { + var index = url.indexOf("https://") + if (index == -1) return url + val linkifyTasks = ArrayList() + var extra = 0 + while (index != -1) { + var end = url.indexOf(' ', index) + end = if (end == -1) url.indexOf('\n', index) else url.indexOf('\n', index) + .coerceAtMost(end) + if (end == -1) end = url.length + if (index == 0 || '\n' == url[index - 1] || ' ' == url[index - 1]) { + val endDomain = url.indexOf('/', index + 9) + val endCh = url[end - 1] + if (endDomain != -1 && endDomain < end && endCh != '>' && endCh != ')' && endCh != ']') { + linkifyTasks.add(LinkifyTask(index, end)) + extra += end - index + 4 + Timber.d("Linkify url: %s", url.substring(end)) + } + } + index = url.indexOf("https://", end) + } + if (linkifyTasks.isEmpty()) return url + var prev = LinkifyTask.NULL + val stringBuilder = StringBuilder(url.length + extra) + for (linkifyTask in linkifyTasks) { + stringBuilder.append(url, prev.end, linkifyTask.start).append('[') + .append(url, linkifyTask.start, linkifyTask.end).append("](") + .append(url, linkifyTask.start, linkifyTask.end).append(')') + prev = linkifyTask + } + if (prev.end != url.length) stringBuilder.append(url, prev.end, url.length) + Timber.i("Added Markdown link to " + linkifyTasks.size + " urls") + return stringBuilder.toString() } } -} +} \ No newline at end of file