├── 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 | [
](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 |
--------------------------------------------------------------------------------