Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Feature Activity test #5597

Open
adamstyrc opened this issue Apr 24, 2020 · 23 comments
Open

Dynamic Feature Activity test #5597

adamstyrc opened this issue Apr 24, 2020 · 23 comments
Assignees

Comments

@adamstyrc
Copy link

Description

I've added new module to my Android project which is not a library:
apply plugin: 'com.android.library'
but a dynamic feature that I want to uninstall at some time from my App Bundle
apply plugin: 'com.android.dynamic-feature'

After work on UI done I wanted to write a Robolectric/AndroidX test to check my Activity behavior.

When I tried to run Activity with **launchActivity() ** I get

java.lang.RuntimeException: Unable to resolve activity for Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=org.robolectric.default/com.goincharge.app.onboarding.ui.AppOnboardingActivity } -- see #4736 for details

at org.robolectric.android.fakes.RoboMonitoringInstrumentation.startActivitySyncInternal(RoboMonitoringInstrumentation.java:48)
at org.robolectric.android.internal.LocalActivityInvoker.startActivity(LocalActivityInvoker.java:34)
at androidx.test.core.app.ActivityScenario.launchInternal(ActivityScenario.java:231)
at androidx.test.core.app.ActivityScenario.launch(ActivityScenario.java:190)
at com.goincharge.app.onboarding.ui.AppOnboardingActivityTest.aa(AppOnboardingActivityTest.kt:43)

and when I tried with old:
Robolectric.setupActivity(AppOnboardingActivity::class.java)

I got
android.content.res.Resources$NotFoundException: Resource ID #0x7f08005f

at org.robolectric.shadows.ShadowLegacyAssetManager.getResName(ShadowLegacyAssetManager.java:1283)
at org.robolectric.shadows.ShadowLegacyAssetManager.resolveResourceValue(ShadowLegacyAssetManager.java:1066)
at org.robolectric.shadows.ShadowLegacyAssetManager.resolve(ShadowLegacyAssetManager.java:1026)
at org.robolectric.shadows.ShadowLegacyAssetManager.getAndResolve(ShadowLegacyAssetManager.java:1020)
at org.robolectric.shadows.ShadowLegacyAssetManager.getResourceValue(ShadowLegacyAssetManager.java:319)
at android.content.res.AssetManager.getResourceValue(AssetManager.java)
at android.content.res.Resources.getValue(Resources.java:1009)
at androidx.appcompat.widget.ResourceManagerInternal.loadDrawableFromDelegates(ResourceManagerInternal.java:252)
at androidx.appcompat.widget.ResourceManagerInternal.getDrawable(ResourceManagerInternal.java:139)
at androidx.appcompat.widget.ResourceManagerInternal.getDrawable(ResourceManagerInternal.java:132)
at androidx.appcompat.widget.ResourceManagerInternal.checkVectorDrawableSetup(ResourceManagerInternal.java:504)
at androidx.appcompat.widget.ResourceManagerInternal.getDrawable(ResourceManagerInternal.java:137)
at androidx.appcompat.widget.AppCompatDrawableManager.getDrawable(AppCompatDrawableManager.java:411)
at androidx.appcompat.widget.TintTypedArray.getDrawableIfKnown(TintTypedArray.java:86)
at androidx.appcompat.app.AppCompatDelegateImpl.attachToWindow(AppCompatDelegateImpl.java:647)
at androidx.appcompat.app.AppCompatDelegateImpl.ensureWindow(AppCompatDelegateImpl.java:623)
at androidx.appcompat.app.AppCompatDelegateImpl.onCreate(AppCompatDelegateImpl.java:350)
at androidx.appcompat.app.AppCompatActivity.onCreate(AppCompatActivity.java:105)

Steps to Reproduce

Create Activity in the new Dynamic Feature module and just setup a test. My AndroidManifest.xml

<dist:module
    dist:instant="false"
    dist:title="@string/feature_app_onboarding">
    <dist:delivery>
        <dist:install-time />
    </dist:delivery>

    <dist:fusing dist:include="true" />
</dist:module>

<application>
    <activity
        android:name=".ui.AppOnboardingActivity"
        android:screenOrientation="portrait">

...

Robolectric & Android Version

Robolectric 4.3.1

Link to a public git repo demonstrating the problem:

Project not public

@hoisie
Copy link
Contributor

hoisie commented Sep 3, 2020

Switch to binary resources and try again. Add this to app/build.gradle inside the android block:

    testOptions {
        unitTests {
            includeAndroidResources = true
        }
    }

@ganadist
Copy link
Contributor

ganadist commented Jul 8, 2021

When set includeAndroidResources = true, robolectric shows another error.

Tested with AGP 4.2, Robolectric 4.6.1, Java 11, and targetSdkVersion is 29 or 30

    W/PackageParser: Failed to parse feature1/build/intermediates/apk_for_local_test/debugUnitTest/apk-for-local-test.ap_
     java.lang.NullPointerException
    	at org.robolectric.res.android.LoadedArsc$LoadedPackage.Load(LoadedArsc.java:728)
    	at org.robolectric.res.android.LoadedArsc.LoadTable(LoadedArsc.java:911)
    	at org.robolectric.res.android.LoadedArsc.Load(LoadedArsc.java:957)
    	at org.robolectric.res.android.CppApkAssets.LoadImpl_measured(CppApkAssets.java:266)
    	at org.robolectric.res.android.CppApkAssets.lambda$LoadImpl$0(CppApkAssets.java:205)
    	at org.robolectric.util.PerfStatsCollector.measure(PerfStatsCollector.java:53)
    	at org.robolectric.res.android.CppApkAssets.LoadImpl(CppApkAssets.java:202)
    	at org.robolectric.res.android.CppApkAssets.Load(CppApkAssets.java:117)
    	at org.robolectric.shadows.ShadowArscApkAssets9.nativeLoad(ShadowArscApkAssets9.java:264)
    	at org.robolectric.shadows.ShadowArscApkAssets9.nativeLoad(ShadowArscApkAssets9.java:285)
    	at android.content.res.ApkAssets.nativeLoad(ApkAssets.java)
    	at android.content.res.ApkAssets.__constructor__(ApkAssets.java:291)
    	at android.content.res.ApkAssets.<init>(ApkAssets.java)
    	at android.content.res.ApkAssets.loadFromPath(ApkAssets.java:140)
    	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    	at org.robolectric.util.ReflectionHelpers.callStaticMethod(ReflectionHelpers.java:343)
    	at org.robolectric.internal.bytecode.ShadowImpl.directlyOn(ShadowImpl.java:56)
    	at org.robolectric.shadow.api.Shadow.directlyOn(Shadow.java:61)
    	at org.robolectric.shadows.ShadowArscApkAssets9.lambda$loadFromPath$3(ShadowArscApkAssets9.java:225)
    	at org.robolectric.shadows.ShadowArscApkAssets9.getFromCacheOrLoad(ShadowArscApkAssets9.java:152)
    	at org.robolectric.shadows.ShadowArscApkAssets9.loadFromPath(ShadowArscApkAssets9.java:222)
    	at android.content.res.ApkAssets.loadFromPath(ApkAssets.java)
    	at android.content.res.ApkAssets.loadFromPath(ApkAssets.java:127)
    	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    	at org.robolectric.util.ReflectionHelpers.callStaticMethod(ReflectionHelpers.java:343)
    	at org.robolectric.internal.bytecode.ShadowImpl.directlyOn(ShadowImpl.java:56)
    	at org.robolectric.shadow.api.Shadow.directlyOn(Shadow.java:61)
    	at org.robolectric.shadows.ShadowArscApkAssets9.lambda$loadFromPath$0(ShadowArscApkAssets9.java:164)
    	at org.robolectric.shadows.ShadowArscApkAssets9.getFromCacheOrLoad(ShadowArscApkAssets9.java:152)
    	at org.robolectric.shadows.ShadowArscApkAssets9.loadFromPath(ShadowArscApkAssets9.java:162)
    	at android.content.res.ApkAssets.loadFromPath(ApkAssets.java)
    	at android.content.pm.PackageParser.parseApkLiteInner(PackageParser.java:1442)
    	at android.content.pm.PackageParser.parseApkLite(PackageParser.java:1415)
    	at android.content.pm.PackageParser.parseMonolithicPackageLite(PackageParser.java:932)
    	at android.content.pm.PackageParser.parseMonolithicPackage(PackageParser.java:1140)
    	at android.content.pm.PackageParser.parsePackage(PackageParser.java:1054)
    	at android.content.pm.PackageParser.parsePackage(PackageParser.java:1063)
    	at org.robolectric.shadows.ShadowPackageParser.callParsePackage(ShadowPackageParser.java:47)
    	at org.robolectric.android.internal.AndroidTestEnvironment.loadAppPackage_measured(AndroidTestEnvironment.java:326)
    	at org.robolectric.android.internal.AndroidTestEnvironment.lambda$loadAppPackage$1(AndroidTestEnvironment.java:296)
    	at org.robolectric.util.PerfStatsCollector.measure(PerfStatsCollector.java:53)
    	at org.robolectric.android.internal.AndroidTestEnvironment.loadAppPackage(AndroidTestEnvironment.java:294)
    	at org.robolectric.android.internal.AndroidTestEnvironment.installAndCreateApplication(AndroidTestEnvironment.java:195)
    	at org.robolectric.android.internal.AndroidTestEnvironment.setUpApplicationState(AndroidTestEnvironment.java:171)
    	at org.robolectric.RobolectricTestRunner.beforeTest(RobolectricTestRunner.java:319)
    	at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:269)
    	at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:89)
    	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    	at java.base/java.lang.Thread.run(Thread.java:829)

@ganadist
Copy link
Contributor

ganadist commented Jul 9, 2021

Also, when set includeAndroidResources = false + targetSdk 28, robolectric will shows this error since 4.6.x (It was okay until 4.5.x)

Caused by: java.lang.NullPointerException: (No message provided)
        at org.robolectric.shadows.ShadowLegacyAssetManager.getAndResolve(ShadowLegacyAssetManager.java:1019)	
	at org.robolectric.shadows.ShadowLegacyAssetManager.getResourceValue(ShadowLegacyAssetManager.java:319)	
	at android.content.res.AssetManager.getResourceValue(AssetManager.java)
	at android.content.res.ResourcesImpl.getValue(ResourcesImpl.java:212)	
	at android.content.res.Resources.getBoolean(Resources.java:1068)	
	at android.database.sqlite.SQLiteGlobal.isCompatibilityWalSupported(SQLiteGlobal.java:89)	
	at android.database.sqlite.SQLiteDatabase.__constructor__(SQLiteDatabase.java:298)	
	at android.database.sqlite.SQLiteDatabase.<init>(SQLiteDatabase.java)	
	at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:738)	
	at android.database.sqlite.SQLiteDatabase.createInMemory(SQLiteDatabase.java:920)	
	at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:350)	
	at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:298)	
	at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:145)	
	at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:106)	
	at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:622)	
	at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.java:399)	
	at androidx.room.RoomDatabase.query(RoomDatabase.java:442)	
	at androidx.room.util.DBUtil.query(DBUtil.java:83)	

@utzcoz
Copy link
Member

utzcoz commented Jul 10, 2021

Hi @ganadist , could you help to provide a sample app that can reproduce it? Thanks.

@ganadist
Copy link
Contributor

ganadist commented Jul 10, 2021

Here is a sample app for test

$ git clone https://github.com/ganadist/VersionCodeDemo

$ cd VersionCodeDemo


#### robolectric 4.5.1 or 4.6.1 / includeAndroidResources = true / sdk is 30
$ git checkout origin/dynamic_feature_robolectric
$ ./gradlew :dynamicfeature1:testDevelopDebugUnitTest

// got error at org.robolectric.res.android.LoadedArsc$LoadedPackage.Load(LoadedArsc.java:728)

#### robolectric 4.5.1 or 4.6.1 / includeAndroidResources = false / sdk is 28
$ git checkout HEAD~1
$ ./gradlew :dynamicfeature1:testDevelopDebugUnitTest

// passed


#### robolectric 4.6.1 / includeAndroidResources = false / sdk is 28
$ git checkout origin/dynamic_feature_robolectric_461
$ ./gradlew :dynamicfeature1:testDevelopDebugUnitTest

// got error at  org.robolectric.shadows.ShadowLegacyAssetManager.getAndResolve(ShadowLegacyAssetManager.java:1019)	

#### robolectric 4.5.1 / includeAndroidResources = false / sdk is 28
$ git checkout HEAD~1
$ ./gradlew :dynamicfeature1:testDevelopDebugUnitTest

// passed

@utzcoz
Copy link
Member

utzcoz commented Jul 10, 2021

After simple debug, I found that entry_begin is null at

ResTable_lib_entry entry_begin =
child_chunk.asResTable_lib_entry();
. I will take a look to check the reason. And again @ganadist , thanks your sample app, it helps a lot.

@hoisie
Copy link
Contributor

hoisie commented Jul 10, 2021

Awesome, thanks for the example. Seems like this is an edge case in the ARSC unpacking logic. You can get the ARSC file by unzipping the ap_ file generated by the Gradle build. The file format is not well documented (I think it's mainly documented in the package manager source code), but it is relatively simple. I can look for more docs about the ARSC format.

@utzcoz
Copy link
Member

utzcoz commented Jul 11, 2021

The asResTable_lib_entry returns null because header size is less than ResTable_lib_entry.SIZEOF at

public ResTable_lib_entry asResTable_lib_entry() {
if (header_size() >= ResTable_lib_entry.SIZEOF) {
return new ResTable_lib_entry(device_chunk_.myBuf(), device_chunk_.myOffset());
} else {
return null;
}
}

The log says header size is 12, and it is less than ResTable_lib_entry.SIZEOF. I also added log before https://cs.android.com/android/platform/superproject/+/master:frameworks/base/libs/androidfw/LoadedArsc.cpp;l=612?q=LoadedArsc.cpp, to check the real header size that AOSP read from above mentioned sample app, it is also 12.

Added log:

        if (child_chunk.data_size() / sizeof(ResTable_lib_entry) < dtohl(lib->count)) {
          LOG(ERROR) << "RES_TABLE_LIBRARY_TYPE too small to hold entries.";
          return {};
        }
        LOG(ERROR) << "header " << child_chunk.header_size();

        loaded_package->dynamic_package_map_.reserve(dtohl(lib->count));

Printed log:

07-12 00:06:21.306   540   586 E system_server: header 12

Look like Robolectric's Chunk#asResTable_lib_entry and its related buffer parsing are not correctly.

@utzcoz
Copy link
Member

utzcoz commented Jul 11, 2021

I have modified Chunk#asResTable_lib_entry with following dirty code:

    if (header_size() > 0) {
      ResTable_lib_entry lib_entry = new ResTable_lib_entry(device_chunk_.myBuf(), device_chunk_.myOffset() + header_size());
      return lib_entry;
    }

And make sure LoadedArsc#Load's foreach for RES_TABLE_LIBRARY_TYPE with correct increment step:

entry_iter.myOffset() != entry_begin.myOffset() + dtohl(lib.count) * ResTable_lib_entry.SIZEOF

Also need to add try-catch for new iterator entry (Actually it needs a checking for ByteBuffer's end position to ensure the new offset doesn't exceed allowed position):

              try {
                entry_iter = new ResTable_lib_entry(entry_iter.myBuf(), entry_iter.myOffset() + ResTable_lib_entry.SIZEOF);
              } catch (Exception e) {
                System.err.println("failed to create new entry " + e.getMessage());
                break;
              }

With above patches, the NPE disappears with includeAndroidResources = true and sdk 30. But PackageParser throws the below error:

Caused by: android.content.pm.PackageParser$PackageParserException: ~/VersionCodeDemo/dynamicfeature1/build/intermediates/apk_for_local_test/developDebugUnitTest/apk-for-local-test.ap_ (at Binary XML file line #2): Expected base APK, but found split dynamicfeature1
	at android.content.pm.PackageParser.parseBaseApk(PackageParser.java:1191)
	at android.content.pm.PackageParser.parseMonolithicPackage(PackageParser.java:1150)
	at android.content.pm.PackageParser.parsePackage(PackageParser.java:1054)
	at android.content.pm.PackageParser.parsePackage(PackageParser.java:1063)
	at org.robolectric.shadows.ShadowPackageParser.callParsePackage(ShadowPackageParser.java:47)

In a sense, the above modification is reasonable for me, so I will look at the new PackageParser exception before imporving modification for Robolectric's ARSC parser.

@ganadist
Copy link
Contributor

ganadist commented Jul 12, 2021

It seems ShadowPackageParser should parse with parseClusterPackage ( https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/PackageParser.java;l=1037-1064?q=parsePackage&ss=android) (directory must contain base apk and split(dynamic feature) apk ),

But base apk is located on app/build/intermediates/apk_for_local_test/developDebugUnitTest/apk-for-local-test.ap_ and can be built with :app:assembleDevelopDebugUnitTest task

@ganadist
Copy link
Contributor

ganadist commented Jul 12, 2021

I tried to modify robolectric as #5597 (comment) , also modify to use package directory instead file.

ganadist@85c7eb3#diff-16f874a8ce1fd6aa0c4faba55475bad44949ee2087391001f2a2d209a8dab559R47-R56

Also, I copied base.apk and split.apk, manually

cp -f app/build/intermediates/apk_for_local_test/developDebugUnitTest/apk-for-local-test.ap_ \ 
  dynamicfeature1/build/intermediates/apk_for_local_test/developDebugUnitTest/base.apk

cp -f dynamicfeature1/build/intermediates/apk_for_local_test/developDebugUnitTest/apk-for-local-test.ap_ \ 
  dynamicfeature1/build/intermediates/apk_for_local_test/developDebugUnitTest/split.apk

Then, it seems PackageParser loaded apks properly, but I got another error while load resources.

https://scans.gradle.com/s/awandqybwpftw/tests/:dynamicfeature1:testDevelopDebugUnitTest/com.example.dynamicfeature1.ActivityTest/test_Activity?expanded-stacktrace=WyIwLTEiXQ&top-execution=1

ganadist added a commit to ganadist/robolectric that referenced this issue Jul 12, 2021
@utzcoz
Copy link
Member

utzcoz commented Jul 13, 2021

That's cool. I need more time to learn what AGP and frameworks do for dynamic feature apks.

Update 1:

a. AGP builds base apk and split apk.
b. AGP uses ddmlib SplitApkInstaller with adb install-create/install-write/install-commit to install multiple apks together. For example, two VersionCodeDemo apks are installed in /data/app/vmdl245829075.tmp/0_app-beta-debug and /data/app/vmdl245829075.tmp/1_dynamicfeature_-beta-debug on emulator.
c. PackageInstallerSession#validateApkInstallLocked renames two apks to base.apk and split_dynamicfeature1.apk. This API looks like called by PackageManagerShellCommand through DataLoaderService by binder. It is one step of apk installing.
d. The ApkLiteParseUtils#parsePackageSplitNames uses split tag in AndroidManifest#manifest to check whether this apk is a split apk in PackageInstallerSession#validateApkInstallLocked. For example, VersionCodeDemo's dynamic feature apk's AndroidManifest#manfiest has split="dynamicfeature1"(We can use Android Studio's Build/Analyze Apk to get generated final AndroidManifest.xml to check this value).

To sum up, AGP builds base apk and split apk with flavor and normal name, and install them together into Android device. The system will renames apks with base.apk and split_customDynamicFeatureName.apk, and uses apk's manifest split tag value to check whether this apk is base.apk or split apk. The Robolectric needs know whether current test target is a split apk or apk needs dynamic feature. If the result is yes, the Robolectric should find split apk's base.apk or base.apk needed split apk, renames those apks with specific names and parses those apks with parseClusterPackage at AndroidTestEnvironment#loadAppPackage_measured. If we can do it correctly, maybe we can fix apks loading with dynamic feature.

I'm sorry to response too late, because it's too busy on work recently.

Update 2: The error #5597 (comment) shows is caused by incorrect LoadedApk.getResDir(). This is normal test apk's res dir:

package test.package, res dir ~/test-app/app/build/intermediates/apk_for_local_test/debugUnitTest/apk-for-local-test.ap_

The res dir is test apk, but this comment's res dir is its parent directory(Caused by: java.io.IOException: Failed to load asset path /home/ganadist/src/build/AndroidStudioProjects/vvv/dynamicfeature1/build/intermediates/apk_for_local_test/developDebugUnitTest). I will check the real res dir of app with dynamic feature on emulator. cc @ganadist.

@utzcoz
Copy link
Member

utzcoz commented Apr 23, 2022

No folks take this issue at GSoC 2022, so I will take this issue again to continue related work at next few weekends. cc @ganadist ..

@utzcoz
Copy link
Member

utzcoz commented May 28, 2022

FYI @ganadist, I cherry-picked your previous commit and pushed it to #7336 for next continuous work to support dynamic feature.

@zhaomin-android
Copy link

Is there a solution to this problem?
i also meet this problem
only use robolectric 4.5.1 and includeAndroidResources = false can work
but because includeAndroidResources = false, setcontentview in activity can not work

utzcoz pushed a commit to utzcoz/robolectric that referenced this issue Oct 1, 2022
@sbeach
Copy link

sbeach commented Dec 29, 2022

My team is not using dynamic modules, but wanted to comment we are experiencing a similar test failure with ShadowPackageParser. Since we use static modules, I added my details to a different issue: #4278 (comment).
I hope the extra information is helpful in debugging both issues.

@saran-epifi
Copy link

@utzcoz Hey thanks for your attention to this issue. I see that you have a PR with the fix. Do you have any timeline in mind by when we will be able to see this fix get released.

@utzcoz
Copy link
Member

utzcoz commented Feb 2, 2023

@saran-epifi Sorry for pending/delaying. I think I will come back to this issue at next month after I resolved some pending PRs about Kotlin conversion for some modules' test code.

@clickrapha
Copy link

Hi @utzcoz, any update about this issue?

@utzcoz
Copy link
Member

utzcoz commented Feb 29, 2024

@clickrapha I am working on other stuffs recent years, and I think I still spend about some months on other stuffs. There is only one patch to fix some building errors, but it doesn't support full dynamic feature support now. And I need time to analyze the real dynamic feature supporting in AOSP and implement it in Robolectric. Also this task is listed in Robolecric GSoC 2024's project/idea list, and we can push the progress with students if someone wants to take this task.

@ksachin7
Copy link

@utzcoz @hoisie I am interested in working on this Issue as my GSOC Project, should I propose my initial solutions for this here in this discussion or through mail ?

@Pramath02
Copy link

@utzcoz I am interested in working on this issue.

@utzcoz
Copy link
Member

utzcoz commented Apr 2, 2024

Anyone who wants to take this project in GSoC 2024, please check and comment at #8865.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants