├── AniyomiProvider ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ ├── drawable │ │ │ ├── baseline_get_app_24.xml │ │ │ ├── baseline_delete_outline_24.xml │ │ │ ├── baseline_open_in_browser_24.xml │ │ │ ├── baseline_install_mobile_24.xml │ │ │ ├── baseline_settings_24.xml │ │ │ └── ic_github_logo.xml │ │ └── layout │ │ │ ├── fragment_extension.xml │ │ │ ├── extension_item.xml │ │ │ └── bottom_sheet_layout.xml │ │ ├── java │ │ └── com │ │ │ └── example │ │ │ ├── ExtensionFragment.kt │ │ │ ├── ExtensionAdapter.kt │ │ │ └── BottomSheet.kt │ │ └── kotlin │ │ └── recloudstream │ │ └── AniyomiPlugin.kt └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── repo.json ├── settings.gradle.kts ├── gradle.properties ├── .github └── workflows │ └── build.yml ├── README.md ├── gradlew.bat └── gradlew /AniyomiProvider/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CranberrySoup/AniyomiCompatExtension/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | **/build 8 | /captures 9 | .externalNativeBuild 10 | .cxx 11 | local.properties 12 | .vscode -------------------------------------------------------------------------------- /repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Aniyomi Compat", 3 | "description": "Use Aniyomi Extensions in CloudStream!", 4 | "manifestVersion": 1, 5 | "pluginLists": [ 6 | "https://raw.githubusercontent.com/CranberrySoup/AniyomiCompatExtension/builds/plugins.json" 7 | ] 8 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Feb 20 16:26:11 CET 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME -------------------------------------------------------------------------------- /AniyomiProvider/src/main/res/drawable/baseline_get_app_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AniyomiProvider/src/main/res/drawable/baseline_delete_outline_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AniyomiProvider/src/main/res/drawable/baseline_open_in_browser_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AniyomiProvider/src/main/res/drawable/baseline_install_mobile_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /AniyomiProvider/src/main/res/layout/fragment_extension.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "CloudstreamPlugins" 2 | 3 | // This file sets what projects are included. All new projects should get automatically included unless specified in "disabled" variable. 4 | 5 | val disabled = listOf() 6 | 7 | File(rootDir, ".").eachDir { dir -> 8 | if (!disabled.contains(dir.name) && File(dir, "build.gradle.kts").exists()) { 9 | include(dir.name) 10 | } 11 | } 12 | 13 | fun File.eachDir(block: (File) -> Unit) { 14 | listFiles()?.filter { it.isDirectory }?.forEach { block(it) } 15 | } 16 | 17 | 18 | // To only include a single project, comment out the previous lines (except the first one), and include your plugin like so: 19 | // include("PluginName") 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx8G -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | -------------------------------------------------------------------------------- /AniyomiProvider/src/main/res/drawable/baseline_settings_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AniyomiProvider/src/main/res/layout/extension_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 16 | 17 | 18 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /AniyomiProvider/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.android") 3 | } 4 | // use an integer for version numbers 5 | version = 7 6 | 7 | cloudstream { 8 | // All of these properties are optional, you can safely remove them 9 | description = "Use Aniyomi Extensions in CloudStream!\nNot guaranteed to work perfectly." 10 | authors = listOf("CranberrySoup") 11 | 12 | /** 13 | * Status int as the following: 14 | * 0: Down 15 | * 1: Ok 16 | * 2: Slow 17 | * 3: Beta only 18 | * */ 19 | status = 1 // will be 3 if unspecified 20 | requiresResources = true 21 | 22 | // List of video source types. Users are able to filter for extensions in a given category. 23 | // You can find a list of avaliable types here: 24 | // https://recloudstream.github.io/cloudstream/html/app/com.lagradost.cloudstream3/-tv-type/index.html 25 | tvTypes = listOf("Others") 26 | iconUrl = "https://www.google.com/s2/favicons?domain=aniyomi.org&sz=%size%" 27 | } 28 | 29 | dependencies { 30 | implementation("androidx.preference:preference-ktx:1.2.1") 31 | implementation("androidx.legacy:legacy-support-v4:1.0.0") 32 | implementation("com.google.android.material:material:1.9.0") 33 | implementation("androidx.recyclerview:recyclerview:1.3.1") 34 | implementation("androidx.navigation:navigation-fragment-ktx:2.7.5") 35 | implementation("androidx.preference:preference:1.2.1") 36 | } 37 | 38 | android { 39 | buildFeatures { 40 | viewBinding = true 41 | buildConfig = true 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | # https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#concurrency 4 | concurrency: 5 | group: "build" 6 | cancel-in-progress: true 7 | 8 | on: 9 | push: 10 | branches: 11 | # choose your default branch 12 | - master 13 | - main 14 | paths-ignore: 15 | - '*.md' 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@master 23 | with: 24 | path: "src" 25 | 26 | - name: Checkout builds 27 | uses: actions/checkout@master 28 | with: 29 | ref: "builds" 30 | path: "builds" 31 | 32 | - name: Clean old builds 33 | run: rm $GITHUB_WORKSPACE/builds/*.cs3 || true 34 | 35 | - name: Setup JDK 17 36 | uses: actions/setup-java@v1 37 | with: 38 | java-version: 17 39 | 40 | - name: Setup Android SDK 41 | uses: android-actions/setup-android@v2 42 | 43 | - name: Build Plugins 44 | run: | 45 | cd $GITHUB_WORKSPACE/src 46 | chmod +x gradlew 47 | ./gradlew make makePluginsJson 48 | cp **/build/*.cs3 $GITHUB_WORKSPACE/builds 49 | cp build/plugins.json $GITHUB_WORKSPACE/builds 50 | 51 | - name: Push builds 52 | run: | 53 | cd $GITHUB_WORKSPACE/builds 54 | git config --local user.email "actions@github.com" 55 | git config --local user.name "GitHub Actions" 56 | git add . 57 | git commit --amend -m "Build $GITHUB_SHA" || exit 0 # do not error if nothing to commit 58 | git push --force 59 | -------------------------------------------------------------------------------- /AniyomiProvider/src/main/res/drawable/ic_github_logo.xml: -------------------------------------------------------------------------------- 1 | 8 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Use your Aniyomi Extensions in CloudStream! 2 | Not guaranteed to work perfectly. Please make an issue if any functional extension does not work with this system. 3 | 4 | Add this repository using this shortcode: [anicompat](https://raw.githubusercontent.com/CranberrySoup/AniyomiCompatExtension/master/repo.json) 5 | 6 | ~Click~ Pet the gorilla to install it on your phone: 7 | 8 | [alt_text](https://self-similarity.github.io/http-protocol-redirector?r=cloudstreamrepo://raw.githubusercontent.com/CranberrySoup/AniyomiCompatExtension/master/repo.json) 9 | 10 | --- 11 | 12 | ### Installation 13 | 14 | Install the extension in CloudStream. The extension should then download the internal compat APK automatically. Once the installation is complete your Aniyomi extensions should appear in CloudStream. 15 | 16 | Installing this plugin does __not__ automatically download all Aniyomi extensions, you still need to get those from Aniyomi. 17 | 18 | --- 19 | 20 | ### Troubleshooting 21 | 22 | **No Aniyomi extensions appear** 23 | 24 | 1. Go to plugin settings _(Settings -> Extensions -> Aniyomi Compat -> Click the extension settings button next to the trashcan)_ 25 | 2. Check that the compat apk is installed correctly by checking what it says under the **Currently Using:** tab. It should show some long path to an APK file. If it says none click **Force download APK**. 26 | 3. If step 2 does not work, grab the APK and install it yourself [here](https://github.com/CranberrySoup/AniyomiCompat/raw/builds/app-debug.apk) 27 | 4. Make sure that you actually have Aniyomi Extensions installed. It should show something other than 0 after **Number of extensions** 28 | 29 | **Aniyomi extensions do not work** 30 | 31 | 1. Make sure the extension is functioning in Aniyomi. 32 | 2. Try downloading the compat APK instead of using it internally. Download [here](https://github.com/CranberrySoup/AniyomiCompat/raw/builds/app-debug.apk) or click Install APK externally in plugin settings. 33 | 3. Restart CloudStream 34 | 4. Make an issue here if it still does not work 35 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega -------------------------------------------------------------------------------- /AniyomiProvider/src/main/java/com/example/ExtensionFragment.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Dialog 5 | import android.graphics.drawable.Drawable 6 | import android.os.Bundle 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import androidx.core.content.res.ResourcesCompat 11 | import androidx.recyclerview.widget.LinearLayoutManager 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.google.android.material.bottomsheet.BottomSheetBehavior 14 | import com.google.android.material.bottomsheet.BottomSheetDialog 15 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 16 | import com.lagradost.cloudstream3.APIHolder.apis 17 | import com.lagradost.cloudstream3.plugins.Plugin 18 | import recloudstream.AniyomiPlugin 19 | 20 | data class AniyomiExtension(val pkgName: String, val name: String, val icon: Drawable) 21 | 22 | class ExtensionFragment(private val plugin: Plugin) : BottomSheetDialogFragment() { 23 | override fun onCreateView( 24 | inflater: LayoutInflater, 25 | container: ViewGroup?, 26 | savedInstanceState: Bundle? 27 | ): View? { 28 | val id = plugin.resources!!.getIdentifier("fragment_extension", "layout", BuildConfig.LIBRARY_PACKAGE_NAME) 29 | val layout = plugin.resources!!.getLayout(id) 30 | return inflater.inflate(layout, container, false) 31 | } 32 | 33 | private fun View.findView(name: String): T { 34 | val id = plugin.resources!!.getIdentifier(name, "id", BuildConfig.LIBRARY_PACKAGE_NAME) 35 | return this.findViewById(id) 36 | } 37 | 38 | private fun getDrawable(name: String): Drawable? { 39 | val id = 40 | plugin.resources!!.getIdentifier(name, "drawable", BuildConfig.LIBRARY_PACKAGE_NAME) 41 | return ResourcesCompat.getDrawable(plugin.resources!!, id, null) 42 | } 43 | 44 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 45 | val dialog = super.onCreateDialog(savedInstanceState) 46 | (dialog as? BottomSheetDialog)?.behavior?.state = BottomSheetBehavior.STATE_EXPANDED 47 | return dialog 48 | } 49 | 50 | @SuppressLint("SetTextI18n") 51 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 52 | super.onViewCreated(view, savedInstanceState) 53 | val recyclerView = view.findView("extension_recycler_view") 54 | recyclerView.layoutManager = LinearLayoutManager(view.context) 55 | 56 | val extensions = AniyomiPlugin.listExtensions(view.context) 57 | 58 | val aniyomiApis = apis.filter { 59 | // Good enough 60 | it.name.endsWith("⦁") 61 | }.map { api -> 62 | val canShow = runCatching { 63 | val method = api.javaClass.getDeclaredMethod("canShowPreferenceScreen") 64 | (method.invoke(api) as? Boolean) ?: false 65 | }.getOrDefault(false) 66 | 67 | val pkgName = runCatching { 68 | val method = api.javaClass.getDeclaredMethod("getPkgName") 69 | (method.invoke(api) as? String) 70 | }.getOrNull() 71 | 72 | Triple(api, canShow, pkgName) 73 | } 74 | 75 | val combined = extensions.associateWith { extension -> 76 | aniyomiApis.firstOrNull { (_, _, pkgName) -> 77 | extension.pkgName == pkgName 78 | } 79 | } 80 | 81 | 82 | recyclerView.adapter = ExtensionAdapter(plugin, extensions, combined) 83 | } 84 | } -------------------------------------------------------------------------------- /AniyomiProvider/src/main/java/com/example/ExtensionAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.content.DialogInterface 6 | import android.graphics.drawable.Drawable 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.View.OnClickListener 10 | import android.view.ViewGroup 11 | import android.widget.ImageView 12 | import android.widget.TextView 13 | import androidx.appcompat.app.AppCompatActivity 14 | import androidx.core.content.res.ResourcesCompat 15 | import androidx.core.view.isVisible 16 | import androidx.preference.PreferenceScreen 17 | import androidx.recyclerview.widget.RecyclerView 18 | import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity 19 | import com.lagradost.cloudstream3.MainAPI 20 | import com.lagradost.cloudstream3.mvvm.logError 21 | import com.lagradost.cloudstream3.plugins.Plugin 22 | import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar 23 | import com.lagradost.cloudstream3.ui.settings.SettingsGeneral 24 | 25 | class ExtensionAdapter( 26 | private val plugin: Plugin, 27 | private val values: List, 28 | private val extensionSettings: Map?> 29 | ) : RecyclerView.Adapter() { 30 | 31 | private fun View.findView(name: String): T { 32 | val id = plugin.resources!!.getIdentifier(name, "id", BuildConfig.LIBRARY_PACKAGE_NAME) 33 | return this.findViewById(id) 34 | } 35 | 36 | private fun getDrawable(name: String): Drawable? { 37 | val id = 38 | plugin.resources!!.getIdentifier(name, "drawable", BuildConfig.LIBRARY_PACKAGE_NAME) 39 | return ResourcesCompat.getDrawable(plugin.resources!!, id, null) 40 | } 41 | 42 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 43 | val id = plugin.resources!!.getIdentifier("extension_item", "layout", BuildConfig.LIBRARY_PACKAGE_NAME) 44 | val layout = plugin.resources!!.getLayout(id) 45 | val inflater = LayoutInflater.from(parent.context) 46 | 47 | return ViewHolder( 48 | inflater.inflate(layout, parent, false) 49 | ) 50 | } 51 | 52 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 53 | val item = values[position] 54 | val extensionSettings = extensionSettings[item] 55 | 56 | val nameView = holder.itemView.findView("extension_name") 57 | val iconView = holder.itemView.findView("extension_icon") 58 | val settingsBtt = holder.itemView.findView("extension_settings_btt") 59 | 60 | nameView.text = item.name 61 | iconView.setImageDrawable(item.icon) 62 | 63 | settingsBtt.isVisible = extensionSettings?.second == true 64 | val settingsIcon = getDrawable("baseline_settings_24") 65 | settingsBtt.setImageDrawable(settingsIcon) 66 | 67 | settingsBtt.setOnClickListener(object : OnClickListener { 68 | override fun onClick(p0: View?) { 69 | openSettings(extensionSettings?.first, item, holder.itemView.context) 70 | } 71 | }) 72 | } 73 | 74 | fun openSettings(api: MainAPI?, extension: AniyomiExtension, context: Context) { 75 | if (api == null) return 76 | val activity = (context.getActivity() as? AppCompatActivity) ?: return 77 | val manager = activity.supportFragmentManager 78 | 79 | // Easiest way to get preference fragment 80 | val fragment = SettingsGeneral() 81 | 82 | try { 83 | manager 84 | .beginTransaction() 85 | .add(fragment, "AniyomiExtensionPreferences") 86 | .commitNow() 87 | 88 | fragment.view?.setPadding(0, 0, 0, 0) 89 | fragment.setUpToolbar(extension.name) 90 | fragment.preferenceScreen.removeAll() 91 | val method = api.javaClass.getDeclaredMethod( 92 | "showPreferenceScreen", 93 | PreferenceScreen::class.java 94 | ) 95 | method.invoke(api, fragment.preferenceScreen) 96 | 97 | Dialog(context, android.R.style.Theme_NoTitleBar_Fullscreen).apply { 98 | this.window?.attributes?.windowAnimations = android.R.style.Animation_Dialog 99 | this.setContentView(fragment.requireView()) 100 | this.setOnDismissListener(object : DialogInterface.OnDismissListener { 101 | override fun onDismiss(p0: DialogInterface?) { 102 | manager.beginTransaction() 103 | .remove(fragment) 104 | .commit() 105 | } 106 | }) 107 | }.show() 108 | 109 | } catch (t: Throwable) { 110 | logError(t) 111 | } 112 | } 113 | 114 | override fun getItemCount(): Int = values.size 115 | inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) 116 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" -------------------------------------------------------------------------------- /AniyomiProvider/src/main/java/com/example/BottomSheet.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Dialog 5 | import android.content.res.ColorStateList 6 | import android.graphics.drawable.Drawable 7 | import android.os.Build 8 | import android.os.Bundle 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.View.INVISIBLE 12 | import android.view.View.OnClickListener 13 | import android.view.View.VISIBLE 14 | import android.view.ViewGroup 15 | import android.widget.ImageView 16 | import android.widget.RadioButton 17 | import android.widget.RadioGroup 18 | import android.widget.TextView 19 | import android.widget.Toast 20 | import androidx.appcompat.app.AppCompatActivity 21 | import androidx.core.content.res.ResourcesCompat 22 | import androidx.core.view.isVisible 23 | import com.google.android.material.bottomsheet.BottomSheetBehavior 24 | import com.google.android.material.bottomsheet.BottomSheetDialog 25 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 26 | import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity 27 | import com.lagradost.cloudstream3.CommonActivity.showToast 28 | import com.lagradost.cloudstream3.mvvm.safe 29 | import com.lagradost.cloudstream3.plugins.Plugin 30 | import com.lagradost.cloudstream3.utils.Coroutines.ioSafe 31 | import com.lagradost.cloudstream3.utils.Coroutines.main 32 | import recloudstream.AniyomiPlugin 33 | import recloudstream.EpisodeSortMethods 34 | 35 | class BottomFragment(private val plugin: Plugin) : BottomSheetDialogFragment() { 36 | override fun onCreateView( 37 | inflater: LayoutInflater, 38 | container: ViewGroup?, 39 | savedInstanceState: Bundle? 40 | ): View? { 41 | val id = plugin.resources!!.getIdentifier("bottom_sheet_layout", "layout", BuildConfig.LIBRARY_PACKAGE_NAME) 42 | val layout = plugin.resources!!.getLayout(id) 43 | return inflater.inflate(layout, container, false) 44 | } 45 | 46 | private fun View.findView(name: String): T { 47 | val id = plugin.resources!!.getIdentifier(name, "id", BuildConfig.LIBRARY_PACKAGE_NAME) 48 | return this.findViewById(id) 49 | } 50 | 51 | private fun getDrawable(name: String): Drawable? { 52 | val id = 53 | plugin.resources!!.getIdentifier(name, "drawable", BuildConfig.LIBRARY_PACKAGE_NAME) 54 | return ResourcesCompat.getDrawable(plugin.resources!!, id, null) 55 | } 56 | 57 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 58 | val dialog = super.onCreateDialog(savedInstanceState) 59 | (dialog as? BottomSheetDialog)?.behavior?.state = BottomSheetBehavior.STATE_EXPANDED 60 | return dialog 61 | } 62 | 63 | @SuppressLint("SetTextI18n") 64 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 65 | super.onViewCreated(view, savedInstanceState) 66 | val cloudStreamVersion = view.findView("cloudstream_version") 67 | val apkVersion = view.findView("apk_version") 68 | val apkVersionHolder = view.findView("apk_version_holder") 69 | val apkOutdated = view.findView("apk_outdated") 70 | val currentlyUsing = view.findView("currently_using") 71 | val internallyInstalled = view.findView("internally_installed") 72 | val numberOfExtensions = view.findView("number_of_extensions") 73 | val forceInstallButton = view.findView("force_install_button") 74 | val deleteLocalRoot = view.findView("delete_local_root") 75 | val deleteLocalButton = view.findView("delete_local_button") 76 | val externalApkButton = view.findView("external_apk_button") 77 | val externalApkRoot = view.findView("external_apk_root") 78 | // val goToExtensionGithubButton = view.findView("go_to_apk_github") 79 | 80 | val sortingGroup = view.findView("sorting_group") 81 | val radioNone = view.findView("radio_button_none") 82 | val radioReverse = view.findView("radio_button_reverse") 83 | val radioAscending = view.findView("radio_button_ascending") 84 | val episodeSortNotice = view.findView("episode_sort_notice") 85 | 86 | val extensionSettingsButton = view.findView("extension_settings") 87 | 88 | runCatching { 89 | val context = view.context 90 | val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) 91 | val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 92 | packageInfo.longVersionCode 93 | } else { 94 | packageInfo.versionCode.toLong() 95 | } 96 | cloudStreamVersion.text = packageInfo.versionName + " • " + versionCode 97 | } 98 | 99 | apkOutdated.isVisible = false 100 | try { 101 | val cls = 102 | Class.forName("com.lagradost.aniyomicompat.BuildConfig") 103 | val instance = cls.newInstance() 104 | val code = cls.getDeclaredField("VERSION_CODE").getInt(instance) 105 | val name = cls.getDeclaredField("VERSION_NAME").get(instance) as? String 106 | apkVersion.text = "$name • $code" 107 | apkVersionHolder.isVisible = true 108 | 109 | ioSafe { 110 | val element = 111 | AniyomiPlugin.getApkMetadata()?.elements?.firstOrNull { it.versionCode != null } 112 | ?: return@ioSafe 113 | val onlineVersionCode = element.versionCode ?: return@ioSafe 114 | main { 115 | apkOutdated.isVisible = onlineVersionCode > code 116 | episodeSortNotice.isVisible = code < 6 117 | } 118 | } 119 | 120 | extensionSettingsButton.setOnClickListener(object : OnClickListener { 121 | override fun onClick(p0: View?) { 122 | if (code < 7) { 123 | Toast.makeText( 124 | context, 125 | "Update Aniyomi Compat to access settings!", 126 | Toast.LENGTH_LONG 127 | ).show() 128 | } else { 129 | val manager = (context?.getActivity() as? AppCompatActivity)?.supportFragmentManager 130 | ExtensionFragment(plugin).show(manager ?: return, "AniyomiExtensionFragment") 131 | } 132 | } 133 | }) 134 | 135 | } catch (_: Throwable) { 136 | apkVersionHolder.isVisible = false 137 | } 138 | 139 | currentlyUsing.text = 140 | (AniyomiPlugin.currentLoadedFile?.absolutePath ?: "None") 141 | internallyInstalled.text = 142 | AniyomiPlugin.getIsLocallyInstalled(view.context).toString() 143 | numberOfExtensions.text = 144 | AniyomiPlugin.listExtensions(view.context).size.toString() 145 | 146 | val textColor = currentlyUsing.currentTextColor 147 | 148 | extensionSettingsButton.imageTintList = ColorStateList.valueOf(textColor) 149 | extensionSettingsButton.setImageDrawable(getDrawable("baseline_settings_24")) 150 | 151 | forceInstallButton.imageTintList = ColorStateList.valueOf(textColor) 152 | forceInstallButton.setImageDrawable(getDrawable("baseline_get_app_24")) 153 | forceInstallButton.setOnClickListener(object : OnClickListener { 154 | override fun onClick(p0: View?) { 155 | showToast(view.context.getActivity(), "Downloading APK", Toast.LENGTH_LONG) 156 | ioSafe { 157 | AniyomiPlugin.downloadApk(view.context) 158 | this@BottomFragment.dismiss() 159 | } 160 | } 161 | }) 162 | 163 | externalApkRoot.visibility = 164 | if (AniyomiPlugin.getIsLocallyInstalled(view.context)) VISIBLE else INVISIBLE 165 | externalApkButton.imageTintList = ColorStateList.valueOf(textColor) 166 | externalApkButton.setImageDrawable(getDrawable("baseline_install_mobile_24")) 167 | externalApkButton.setOnClickListener(object : OnClickListener { 168 | override fun onClick(p0: View?) { 169 | showToast(view.context.getActivity(), "Installing APK", Toast.LENGTH_LONG) 170 | AniyomiPlugin.installApk(view.context) 171 | this@BottomFragment.dismiss() 172 | } 173 | }) 174 | 175 | 176 | deleteLocalRoot.visibility = 177 | if (AniyomiPlugin.getLocalFile(view.context).exists()) VISIBLE else INVISIBLE 178 | 179 | deleteLocalButton.imageTintList = ColorStateList.valueOf(textColor) 180 | deleteLocalButton.setImageDrawable(getDrawable("baseline_delete_outline_24")) 181 | deleteLocalButton.setOnClickListener(object : OnClickListener { 182 | override fun onClick(p0: View?) { 183 | showToast(view.context.getActivity(), "Deleting local file", Toast.LENGTH_LONG) 184 | safe { 185 | AniyomiPlugin.getLocalFile(view.context).delete() 186 | } 187 | this@BottomFragment.dismiss() 188 | } 189 | }) 190 | 191 | // goToExtensionGithubButton.imageTintList = ColorStateList.valueOf(textColor) 192 | // goToExtensionGithubButton.setImageDrawable(getDrawable("ic_github_logo")) 193 | // goToExtensionGithubButton.setOnClickListener(object : OnClickListener { 194 | // override fun onClick(p0: View?) { 195 | // runCatching { 196 | // val intent = Intent(Intent.ACTION_VIEW).apply { 197 | // data = Uri.parse("https://github.com/CranberrySoup/AniyomiCompatExtension") 198 | // } 199 | // activity?.startActivity(intent) 200 | // } 201 | // } 202 | // }) 203 | 204 | val sortingMap = mapOf( 205 | EpisodeSortMethods.None.num to radioNone, 206 | EpisodeSortMethods.Ascending.num to radioAscending, 207 | EpisodeSortMethods.Reverse.num to radioReverse 208 | ) 209 | sortingMap.forEach { (i, radioButton) -> 210 | radioButton.setOnClickListener(object : OnClickListener { 211 | override fun onClick(p0: View?) { 212 | AniyomiPlugin.aniyomiSortingMethod = i 213 | sortingGroup.check(radioButton.id) 214 | } 215 | }) 216 | } 217 | sortingMap[AniyomiPlugin.aniyomiSortingMethod]?.id?.let { selectedItem -> 218 | sortingGroup.check(selectedItem) 219 | } 220 | } 221 | } -------------------------------------------------------------------------------- /AniyomiProvider/src/main/kotlin/recloudstream/AniyomiPlugin.kt: -------------------------------------------------------------------------------- 1 | package recloudstream 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.pm.ApplicationInfo 7 | import android.content.pm.PackageManager 8 | import android.content.res.AssetManager 9 | import android.net.Uri 10 | import android.os.Build 11 | import android.widget.Toast 12 | import androidx.appcompat.app.AppCompatActivity 13 | import androidx.core.content.FileProvider 14 | import com.example.AniyomiExtension 15 | import com.example.BottomFragment 16 | import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity 17 | import com.lagradost.cloudstream3.AcraApplication.Companion.getKey 18 | import com.lagradost.cloudstream3.AcraApplication.Companion.setKey 19 | import com.lagradost.cloudstream3.CommonActivity.showToast 20 | import com.lagradost.cloudstream3.app 21 | import com.lagradost.cloudstream3.mvvm.logError 22 | import com.lagradost.cloudstream3.mvvm.safe 23 | import com.lagradost.cloudstream3.plugins.CloudstreamPlugin 24 | import com.lagradost.cloudstream3.plugins.Plugin 25 | import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe 26 | import com.lagradost.cloudstream3.utils.Coroutines.main 27 | import com.lagradost.cloudstream3.utils.txt 28 | import dalvik.system.BaseDexClassLoader 29 | import kotlinx.coroutines.runBlocking 30 | import java.io.File 31 | 32 | /** 33 | * Guarantee that the APK file is installed without interruptions. 34 | */ 35 | const val ANIYOMI_PLUGIN_SUCCESS_KEY = "Aniyomi_Plugin_Successful_Install" 36 | 37 | enum class EpisodeSortMethods(val num: Int) { 38 | None(0), 39 | Ascending(1), 40 | Reverse(2), 41 | } 42 | 43 | @CloudstreamPlugin 44 | class AniyomiPlugin : Plugin() { 45 | companion object { 46 | var currentLoadedFile: File? = null 47 | private var cachedApkMetadata: OutputMetadata? = null 48 | private const val packageName = "com.lagradost.aniyomicompat" 49 | private const val pluginClassName = "$packageName.AniyomiPlugin" 50 | private const val apkUrl = 51 | "https://github.com/CranberrySoup/AniyomiCompat/raw/builds/app-debug.apk" 52 | private const val apkMetadataUrl = 53 | "https://raw.githubusercontent.com/CranberrySoup/AniyomiCompat/builds/output-metadata.json" 54 | private const val apkDir = "AniyomiCompat" 55 | private const val apkName = "AniyomiCompat.apk" 56 | 57 | private const val sortingMethodKey = "ANIYOMI_SORTING_METHOD" 58 | var aniyomiSortingMethod: Int 59 | get() = getKey(sortingMethodKey) ?: EpisodeSortMethods.Ascending.num 60 | set(value) { 61 | setKey(sortingMethodKey, value) 62 | } 63 | 64 | data class Element( 65 | val versionCode: Long?, 66 | val versionName: String?, 67 | val outputFile: String? 68 | ) 69 | 70 | data class OutputMetadata( 71 | val elements: List? 72 | ) 73 | 74 | /** 75 | * From Aliucord: https://github.com/Aliucord/Aliucord/blob/2cf5ce8d74c9da6965f6c57454f9583545e9cd24/Injector/src/main/java/com/aliucord/injector/Injector.kt#L162-L175 76 | */ 77 | @SuppressLint("DiscouragedPrivateApi") // this private api seems to be stable, thanks to facebook who use it in the facebook app 78 | @Throws(Throwable::class) 79 | private fun addDexToClasspath(dex: File, classLoader: ClassLoader) { 80 | // https://android.googlesource.com/platform/libcore/+/58b4e5dbb06579bec9a8fc892012093b6f4fbe20/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java#59 81 | val pathListField = BaseDexClassLoader::class.java.getDeclaredField("pathList") 82 | .apply { isAccessible = true } 83 | val pathList = pathListField[classLoader]!! 84 | val addDexPath = 85 | pathList.javaClass.getDeclaredMethod( 86 | "addDexPath", 87 | String::class.java, 88 | File::class.java 89 | ) 90 | .apply { isAccessible = true } 91 | addDexPath.invoke(pathList, dex.absolutePath, null) 92 | } 93 | 94 | suspend fun getApkMetadata(): OutputMetadata? { 95 | return cachedApkMetadata ?: app.get(apkMetadataUrl) 96 | .parsedSafe()?.also { 97 | cachedApkMetadata = it 98 | } 99 | } 100 | 101 | fun listExtensions(context: Context): List { 102 | val extensionFeature = "tachiyomi.animeextension" 103 | val pkgManager = context.packageManager 104 | 105 | val flags = PackageManager.GET_CONFIGURATIONS 106 | 107 | @Suppress("DEPRECATION") 108 | val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 109 | pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(flags.toLong())) 110 | } else { 111 | pkgManager.getInstalledPackages(flags) 112 | } 113 | 114 | return installedPkgs.filter { pkg -> 115 | pkg.reqFeatures.orEmpty().any { it.name == extensionFeature } 116 | }.mapNotNull { 117 | val appInfo = it.applicationInfo ?: return@mapNotNull null 118 | val name = 119 | pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Aniyomi: ") 120 | val icon = pkgManager.getApplicationIcon(appInfo) 121 | AniyomiExtension(it.packageName, name, icon) 122 | } 123 | } 124 | 125 | fun getInstalledApplication(context: Context): ApplicationInfo? { 126 | return try { 127 | val pkgManager = context.packageManager 128 | 129 | pkgManager.getApplicationInfo( 130 | packageName, 131 | PackageManager.GET_META_DATA 132 | ) 133 | } catch (_: Throwable) { 134 | null 135 | } 136 | } 137 | 138 | fun getLocalFile(context: Context): File { 139 | return File(context.filesDir, "$apkDir/$apkName") 140 | } 141 | 142 | fun getIsLocallyInstalled(context: Context): Boolean { 143 | return getLocalFile(context).exists() && getKey(ANIYOMI_PLUGIN_SUCCESS_KEY) == true 144 | } 145 | 146 | fun loadAssets(file: File) { 147 | // println("Loading resources for aniyomi") 148 | // based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk 149 | val assets = AssetManager::class.java.getDeclaredConstructor().newInstance() 150 | val addAssetPath = 151 | AssetManager::class.java.getMethod("addAssetPath", String::class.java) 152 | addAssetPath.invoke(assets, file.absolutePath) 153 | } 154 | 155 | fun loadAniyomi(context: Context, file: File) { 156 | safe { 157 | println("Loading Aniyomi Compat at: ${file.absolutePath}") 158 | file.setReadOnly() 159 | val classLoader = context.classLoader 160 | addDexToClasspath(file, classLoader) 161 | val aniyomiPlugin = classLoader.loadClass(pluginClassName).newInstance() as Plugin 162 | aniyomiPlugin.load(context) 163 | println("Successful load of Aniyomi Compat") 164 | currentLoadedFile = file 165 | } 166 | } 167 | 168 | suspend fun downloadApk(context: Context): Boolean { 169 | return ioWorkSafe { 170 | val finalFile = getLocalFile(context) 171 | safe { 172 | finalFile.setWritable(true) 173 | } 174 | val tmpFile = File.createTempFile("AniyomiCompat", null) 175 | 176 | val request = app.get(apkUrl) 177 | if (!request.isSuccessful) return@ioWorkSafe false 178 | 179 | request.body.byteStream().use { 180 | tmpFile.writeBytes(it.readBytes()) 181 | } 182 | setKey(ANIYOMI_PLUGIN_SUCCESS_KEY, false) 183 | tmpFile.copyTo(finalFile, true) 184 | setKey(ANIYOMI_PLUGIN_SUCCESS_KEY, true) 185 | tmpFile.delete() 186 | true 187 | } == true 188 | } 189 | 190 | fun installApk(context: Context): Boolean { 191 | if (!getIsLocallyInstalled(context)) return false 192 | val file = getLocalFile(context) 193 | openApk(context, Uri.fromFile(file)) 194 | return true 195 | } 196 | 197 | private fun openApk(context: Context, uri: Uri) { 198 | try { 199 | uri.path?.let { 200 | val contentUri = FileProvider.getUriForFile( 201 | context, 202 | context.packageName + ".provider", 203 | File(it) 204 | ) 205 | val installIntent = Intent(Intent.ACTION_VIEW).apply { 206 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 207 | addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 208 | putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true) 209 | data = contentUri 210 | } 211 | context.startActivity(installIntent) 212 | } 213 | } catch (e: Exception) { 214 | logError(e) 215 | } 216 | } 217 | } 218 | 219 | override fun load(context: Context) { 220 | this.openSettings = openSettings@{ ctx -> 221 | val manager = (ctx.getActivity() as? AppCompatActivity)?.supportFragmentManager 222 | ?: return@openSettings 223 | BottomFragment(this).show(manager, "AniyomiCompat") 224 | } 225 | runBlocking { 226 | // Prefer app to make debugging easier 227 | val file = getInstalledApplication(context)?.let { File(it.sourceDir) } 228 | ?: getIsLocallyInstalled(context).takeIf { it }?.let { 229 | getLocalFile(context) 230 | } 231 | 232 | if (file == null) { 233 | if (downloadApk(context) && getIsLocallyInstalled(context)) { 234 | loadAniyomi(context, getLocalFile(context)) 235 | main { 236 | showToast( 237 | context.getActivity(), 238 | txt("Successfully installed Aniyomi Compat."), 239 | Toast.LENGTH_LONG 240 | ) 241 | } 242 | } else { 243 | main { 244 | showToast( 245 | context.getActivity(), 246 | txt("Unable to download Aniyomi Compat APK!"), 247 | Toast.LENGTH_LONG 248 | ) 249 | } 250 | } 251 | } else { 252 | loadAniyomi(context, file) 253 | } 254 | } 255 | } 256 | } -------------------------------------------------------------------------------- /AniyomiProvider/src/main/res/layout/bottom_sheet_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 27 | 28 | 29 | 32 | 33 | 40 | 41 | 48 | 49 | 50 | 51 | 55 | 56 | 63 | 64 | 71 | 72 | 81 | 82 | 83 | 86 | 87 | 94 | 95 | 102 | 103 | 104 | 105 | 108 | 109 | 116 | 117 | 124 | 125 | 126 | 131 | 132 | 139 | 140 | 150 | 151 | 155 | 156 | 161 | 162 | 167 | 168 | 173 | 174 | 175 | 176 | 177 | 178 | 182 | 183 | 189 | 190 | 197 | 198 | 205 | 206 | 207 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 245 | 246 | 254 | 255 | 262 | 263 | 264 | 269 | 270 | 276 | 277 | 283 | 284 | 292 | 293 | 294 | 302 | 303 | 304 | 309 | 310 | 318 | 319 | 326 | 327 | 328 | 329 | --------------------------------------------------------------------------------