diff --git a/src/all/genkanio/AndroidManifest.xml b/src/all/genkanio/AndroidManifest.xml
new file mode 100644
index 000000000..be2ea6bbd
--- /dev/null
+++ b/src/all/genkanio/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="eu.kanade.tachiyomi.extension">
+
+    <application>
+        <activity
+            android:name=".all.genkanio.GenkanIOUrlActivity"
+            android:excludeFromRecents="true"
+            android:theme="@android:style/Theme.NoDisplay">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data
+                    android:host="genkan.io"
+                    android:pathPattern="/manga/..*"
+                    android:scheme="https" />
+            </intent-filter>
+
+        </activity>
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/src/all/genkanio/build.gradle b/src/all/genkanio/build.gradle
new file mode 100644
index 000000000..eb0a37920
--- /dev/null
+++ b/src/all/genkanio/build.gradle
@@ -0,0 +1,12 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+    extName = 'Genkan.io'
+    pkgNameSuffix = "all.genkanio"
+    extClass = '.GenkanIO'
+    extVersionCode = 1
+    libVersion = '1.2'
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/all/genkanio/res/mipmap-hdpi/ic_launcher.png b/src/all/genkanio/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..0a2c55aa7
Binary files /dev/null and b/src/all/genkanio/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/all/genkanio/res/mipmap-mdpi/ic_launcher.png b/src/all/genkanio/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..e6f0260ff
Binary files /dev/null and b/src/all/genkanio/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/all/genkanio/res/mipmap-xhdpi/ic_launcher.png b/src/all/genkanio/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..ca210a848
Binary files /dev/null and b/src/all/genkanio/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/all/genkanio/res/mipmap-xxhdpi/ic_launcher.png b/src/all/genkanio/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..872736ea2
Binary files /dev/null and b/src/all/genkanio/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/all/genkanio/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/genkanio/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..e3e31f8c0
Binary files /dev/null and b/src/all/genkanio/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/all/genkanio/res/web_hi_res_512.png b/src/all/genkanio/res/web_hi_res_512.png
new file mode 100644
index 000000000..3c424c17c
Binary files /dev/null and b/src/all/genkanio/res/web_hi_res_512.png differ
diff --git a/src/all/genkanio/src/eu/kanade/tachiyomi/extension/all/genkanio/GenkanIO.kt b/src/all/genkanio/src/eu/kanade/tachiyomi/extension/all/genkanio/GenkanIO.kt
new file mode 100644
index 000000000..704f4d331
--- /dev/null
+++ b/src/all/genkanio/src/eu/kanade/tachiyomi/extension/all/genkanio/GenkanIO.kt
@@ -0,0 +1,182 @@
+package eu.kanade.tachiyomi.extension.all.genkanio
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.ParsedHttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import rx.Observable
+import java.util.Calendar
+
+open class GenkanIO() : ParsedHttpSource() {
+    override val lang = "all"
+    final override val name = "Genkan.io"
+    final override val baseUrl = "https://genkan.io"
+    final override val supportsLatest = false
+
+    // genkan.io defaults to listing manga alphabetically, and provides no configuration
+    fun alphabeticalMangaRequest(page: Int): Request = GET("$baseUrl/manga?page=$page", headers)
+
+    fun alphabeticalMangaFromElement(element: Element): SManga {
+        val manga = SManga.create()
+
+        element.select("a").let {
+            manga.url = it.attr("href").substringAfter(baseUrl)
+            manga.title = it.text()
+        }
+        manga.thumbnail_url = element.select("img").attr("src")
+        return manga
+    }
+
+    fun alphabeticalMangaSelector() = "ul[role=list] > li"
+    fun alphabeticalMangaNextPageSelector() = "a[rel=next]"
+
+    // popular
+    override fun popularMangaRequest(page: Int) = alphabeticalMangaRequest(page)
+    override fun popularMangaFromElement(element: Element) = alphabeticalMangaFromElement(element)
+    override fun popularMangaSelector() = alphabeticalMangaSelector()
+    override fun popularMangaNextPageSelector() = alphabeticalMangaNextPageSelector()
+
+    // latest
+
+    override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException("Not used")
+    override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException("Not used")
+    override fun latestUpdatesSelector() = throw UnsupportedOperationException("Not used")
+    override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException("Not used")
+
+    // search
+    override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
+        return client.newCall(searchMangaRequest(page, query, filters))
+            .asObservableSuccess()
+            .map { response ->
+                // Genkan.io redirects if the search query contains characters it deems "illegal" (e.g: '-')
+                // Return no responses if any redirects occurred
+                if (response.priorResponse != null)
+                    MangasPage(emptyList(), false)
+                else
+                    searchMangaParse(response)
+            }
+    }
+
+    override fun searchMangaRequest(page: Int, query: String, @Suppress("UNUSED_PARAMETER") filters: FilterList): Request {
+        val url = "$baseUrl/manga".toHttpUrlOrNull()!!.newBuilder()
+            .addQueryParameter("page", "$page")
+            .addQueryParameter("search", query)
+        return GET("$url")
+    }
+
+    override fun searchMangaFromElement(element: Element) = alphabeticalMangaFromElement(element)
+    override fun searchMangaSelector() = alphabeticalMangaSelector()
+    override fun searchMangaNextPageSelector() = alphabeticalMangaNextPageSelector()
+
+    // chapter list (is paginated),
+    override fun chapterListParse(response: Response): List<SChapter> = throw UnsupportedOperationException("Not used")
+    override fun chapterListRequest(manga: SManga): Request = throw UnsupportedOperationException("Not used")
+    data class ChapterPage(val chapters: List<SChapter>, val hasnext: Boolean)
+
+    override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
+        return if (manga.status != SManga.LICENSED) {
+            // Returns an observable which emits the list of chapters found on a page,
+            // for every page starting from specified page
+            fun getAllPagesFrom(page: Int): Observable<List<SChapter>> =
+                client.newCall(chapterListRequest(manga, page))
+                    .asObservableSuccess()
+                    .concatMap { response ->
+                        val cp = chapterPageParse(response)
+                        if (cp.hasnext)
+                            Observable.just(cp.chapters).concatWith(getAllPagesFrom(page + 1))
+                        else
+                            Observable.just(cp.chapters)
+                    }
+            getAllPagesFrom(1).reduce(List<SChapter>::plus)
+        } else {
+            Observable.error(Exception("Licensed - No chapters to show"))
+        }
+    }
+
+    private fun chapterPageParse(response: Response): ChapterPage {
+        val document = response.asJsoup()
+
+        val mangas = document.select(chapterListSelector()).map { element ->
+            chapterFromElement(element)
+        }
+
+        val hasNextPage = chapterListNextPageSelector()?.let { selector ->
+            document.select(selector).first()
+        } != null
+
+        return ChapterPage(mangas, hasNextPage)
+    }
+
+    private fun chapterListRequest(manga: SManga, page: Int): Request {
+        val url = "$baseUrl${manga.url}".toHttpUrlOrNull()!!.newBuilder()
+            .addQueryParameter("page", "$page")
+        return GET("$url", headers)
+    }
+
+    override fun chapterFromElement(element: Element): SChapter = element.children().let { tablerow ->
+        val isTitleBlank: (String) -> Boolean = { s: String -> s == "-" || s.isBlank() }
+        val (numElem, nameElem, languageElem, groupElem, viewsElem) = tablerow
+        val (releasedElem, urlElem) = Pair(tablerow[5], tablerow[6])
+        SChapter.create().apply {
+            name = if (isTitleBlank(nameElem.text())) "Chapter ${numElem.text()}" else "Ch. ${numElem.text()}: ${nameElem.text()}"
+            url = urlElem.select("a").attr("href").substringAfter(baseUrl)
+            date_upload = parseRelativeDate(releasedElem.text()) ?: 0
+            scanlator = "${groupElem.text()} - ${languageElem.text()}"
+            chapter_number = numElem.text().toFloat()
+        }
+    }
+
+    override fun chapterListSelector() = "tbody > tr"
+    fun chapterListNextPageSelector() = "a[rel=next]"
+
+    // manga
+
+    override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
+        thumbnail_url = document.selectFirst("section > div > img").attr("src")
+        status = SManga.UNKNOWN // unreported
+        artist = null // unreported
+        author = null // unreported
+        description = document.selectFirst("h2").nextElementSibling().text()
+            .plus("\n\n\n")
+            // Add additional details from info table
+            .plus(
+                document.select("ul.mt-1").joinToString("\n") {
+                    "${it.previousElementSibling().text()}: ${it.text()}"
+                }
+            )
+    }
+
+    private fun parseRelativeDate(date: String): Long? {
+        val trimmedDate = date.substringBefore(" ago").removeSuffix("s").split(" ")
+
+        val calendar = Calendar.getInstance()
+        when (trimmedDate[1]) {
+            "year" -> calendar.apply { add(Calendar.YEAR, -trimmedDate[0].toInt()) }
+            "month" -> calendar.apply { add(Calendar.MONTH, -trimmedDate[0].toInt()) }
+            "week" -> calendar.apply { add(Calendar.WEEK_OF_MONTH, -trimmedDate[0].toInt()) }
+            "day" -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }
+            "hour" -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }
+            "minute" -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }
+            "second" -> calendar.apply { add(Calendar.SECOND, 0) }
+        }
+
+        return calendar.timeInMillis
+    }
+
+    // Pages
+    override fun pageListParse(document: Document): List<Page> = document.select("main > div > img").mapIndexed { index, img ->
+        Page(index, "", img.attr("src"))
+    }
+
+    override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
+}
diff --git a/src/all/genkanio/src/eu/kanade/tachiyomi/extension/all/genkanio/GenkanIOUrlActivity.kt b/src/all/genkanio/src/eu/kanade/tachiyomi/extension/all/genkanio/GenkanIOUrlActivity.kt
new file mode 100644
index 000000000..4f8705fb7
--- /dev/null
+++ b/src/all/genkanio/src/eu/kanade/tachiyomi/extension/all/genkanio/GenkanIOUrlActivity.kt
@@ -0,0 +1,44 @@
+package eu.kanade.tachiyomi.extension.all.genkanio
+
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import kotlin.system.exitProcess
+
+class GenkanIOUrlActivity : Activity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        val pathSegments = intent?.data?.pathSegments
+        if (pathSegments != null && pathSegments.size >= 2) {
+            // url scheme is of the form "/manga/ID-MANGANAME"
+            val (_, titleComponent) = pathSegments
+
+            // This is essentially substringBefore(titleComponent, '-'), don't have access to stdlib
+            var titleId = ""
+            for (i in 0 until titleComponent.length) {
+                if (titleComponent[i] == '-') break
+                titleId = titleId.plus(titleComponent[i])
+            }
+
+            val mainIntent = Intent().apply {
+                action = "eu.kanade.tachiyomi.SEARCH"
+                putExtra("query", titleId)
+                putExtra("filter", packageName)
+            }
+
+            try {
+                startActivity(mainIntent)
+            } catch (e: ActivityNotFoundException) {
+                Log.e("GenkanIOUrlActivity", e.toString())
+            }
+        } else {
+            Log.e("GenkanIOUrlActivity", "could not parse uri from intent $intent")
+        }
+
+        finish()
+        exitProcess(0)
+    }
+}