beeware / briefcase-android-gradle-template

A template for generating Android Gradle projects with Briefcase
MIT License
21 stars 22 forks source link

Chaquopy #52

Closed mhsmith closed 2 years ago

mhsmith commented 2 years ago

Chaquopy work in progress.

Initial proof of concept app (with matplotlib added to requires in pyproject.toml):

[2023-01-05: Switched from NamedTemporaryFile to BytesIO so it works on Windows: this requires Toga 0.3.0dev39 or later.]

import toga
from toga.style import Pack
from toga.style.pack import COLUMN, ROW

class Hello(toga.App):
    def startup(self):
        main_box = toga.Box(style=Pack(direction=COLUMN))

        self.x_input = toga.TextInput(placeholder="X coordinates")
        self.y_input = toga.TextInput(placeholder="Y coordinates")
        main_box.add(self.x_input, self.y_input,
                     toga.Button('Plot', on_press=self.plot))

        self.image_view = toga.ImageView(style=Pack(flex=1))
        main_box.add(self.image_view)

        self.main_window = toga.MainWindow(title=self.formal_name)
        self.main_window.content = main_box
        self.main_window.show()

    def plot(self, button):
        import matplotlib.pyplot as plt
        import io

        x = [float(word) for word in self.x_input.value.split()]
        y = [float(word) for word in self.y_input.value.split()]
        fig, ax = plt.subplots()
        ax.plot(x, y)

        f = io.BytesIO()
        plt.savefig(f, format="png")
        self.image_view.image = toga.images.Image(data=f.getvalue())

def main():
    return Hello()

PR Checklist:

mhsmith commented 2 years ago

I've found one more Rubicon API we'll need to emulate: __null__ in toga_android/widgets/progressbar.py. This is easily emulated in terms of Chaquopy's cast, although Chaquopy would also accept a simple None in this context.

@freakboy3742: Since Rubicon's public API isn't documented anywhere, I'm not sure if I've covered everything. Here's what I've got so far:

Can you think of anything else?

EDIT: I've found another one, __cast__: not currently used by Toga, but might be used in user code.

mhsmith commented 2 years ago

Performance comparisons:

Rubicon Chaquopy
Startup after clearing data (seconds) 8.21 4.07
Startup with existing data (seconds) 3.05 3.79
"App size" (MB) 35.64 29.99
"User data" (MB) 28.80 7.76
mhsmith commented 2 years ago

Review of open rubicon-java issues:

freakboy3742 commented 2 years ago

I've found one more Rubicon API we'll need to emulate: __null__ in toga_android/widgets/progressbar.py. This is easily emulated in terms of Chaquopy's cast, although Chaquopy would also accept a simple None in this context.

The current state of None/__null__ handling was a subject of some discussion; it was introduced fairly recently (earlier this year) as an "explicit is better than implicit" approach to resolving which polymorphic method would be selected when passing None as an argument. It's a bit of a wart, but one that I felt was necessary to avoid vastly more complex code in the process of invoking methods. If Chaquopy has a more robust solution here, I'm not opposed to dropping this.

@freakboy3742: Since Rubicon's public API isn't documented anywhere, I'm not sure if I've covered everything. Here's what I've got so far:

  • JavaClass
  • JavaInterface
  • __global__
  • __null__
  • _alternates

Can you think of anything else?

EDIT: I've found another one, __cast__: not currently used by Toga, but might be used in user code.

I would definitely expect to see __cast__ in user code; again, it's a necessary evil required by the polymorph selection code, added earlier this year. I think you're right that it's not currently used in Toga, but we do use the thing that preceded __cast__ - direct access to the __jni__ attribute. This is a bit of a hack that allows Python to pass around the "raw" JNI object rather than wrapping it.

Other than that, I can't think of anything obviously missing from your list.

The performance numbers you've provided are very favourable. I'm intrigued what makes Rubicon marginally faster in the "2+ run" case; I'm guessing it's the space+first start tradeoff? (i.e., Chaquopy doesn't unpack everything, which means it's faster on first run and uses less space, but every other run still needs to do in-memory unpacking which isn't as fast as direct access?)

The Rubicon bugs you've highlighted, though, definitely make Chaquopy an attractive solution. Exception handling is critical for working out when things crash - I'm guessing the bug we've had reported about asycnio/threading crashes is proving difficult to hunt because the stack trace is being eaten by the JNI layer of Rubicon. Subclassing and isinstance handling is also something that will absolutely be required to implement any of the more interesting Android APIs, so we're going to need a solution; if Chaquopy has one out of the box, that is a huge plus.

One thing that would be interesting to see prototyped is whether we can remove DrawHandlerView and IDrawHandler from the template. Those classes exist entirely to support the Canvas widget due to the lack of subclassing support in Rubicon; removing those classes in favour of a "native" Chaquopy subclass would be a good capability demonstrator.

mhsmith commented 2 years ago

The current state of None/__null__ handling was a subject of some discussion; it was introduced fairly recently (earlier this year) as an "explicit is better than implicit" approach to resolving which polymorphic method would be selected when passing None as an argument.

Chaquopy accepts None when there's no ambiguity. If there is ambiguity it throws a TypeError, and you must resolve it with cast(ClassName, None), so I'll add that to the emulation layer as the implementation of __null__.

I would definitely expect to see __cast__ in user code; again, it's a necessary evil required by the polymorph selection code, added earlier this year. I think you're right that it's not currently used in Toga, but we do use the thing that preceded __cast__ - direct access to the __jni__ attribute. This is a bit of a hack that allows Python to pass around the "raw" JNI object rather than wrapping it.

As discussed, __jni__ isn't actually used in Toga, and shouldn't be used in user code either, so we don't need to emulate it. __cast__ can be emulated using Chaquopy's cast function in the same way as __null__.

I'm intrigued what makes Rubicon marginally faster in the "2+ run" case; I'm guessing it's the space+first start tradeoff? (i.e., Chaquopy doesn't unpack everything, which means it's faster on first run and uses less space, but every other run still needs to do in-memory unpacking which isn't as fast as direct access?)

That's correct. It's been a couple of years since I looked at this area in Chaquopy, so it's possible there are some low-hanging fruit, but we agreed that the current numbers are acceptable and it's not worth spending time on at the moment.

mhsmith commented 2 years ago

I tested the Rubicon API emulation using the following app (updated 2022-09-26):

import math
import sys
import toga
from toga.colors import WHITE, rgb
from toga.fonts import SANS_SERIF
from toga.style import Pack
from toga.style.pack import COLUMN, ROW

class Hello(toga.App):
    def startup(self):
        self.progress = toga.ProgressBar(value=60, max=100)
        self.canvas = toga.Canvas(style=Pack(flex=1))

        main_box = toga.Box(style=Pack(direction=COLUMN), children=[
            toga.Label(sys.version),

            # Uses __null__.
            self.progress,

            # Uses ArrayAdapter, which uses _alternates.
            toga.Selection(items=["alpha", "bravo", "charlie"]),

            toga.TextInput(value="TextInput"),
            toga.Button("Button"),
            toga.Slider(range=(0, 100), value=40, on_change=self.on_change_slider),

            self.canvas,
        ])

        self.main_window = toga.MainWindow(title=self.formal_name)
        self.main_window.content = main_box
        self.draw_tiberius()
        self.main_window.show()

    def on_change_slider(self, slider):
        self.progress.value = int(slider.value)

    # Copy draw_tiberius and its subroutines from https://toga.readthedocs.io/en/latest/tutorial/tutorial-4.html,
    # except for draw_text, which crashes on Android because Android's Canvas.measure_text returns None.

The Canvas implementation is incomplete, but it looks exactly the same as it did with Rubicon.

mhsmith commented 2 years ago

After the most recent commit, we're able to pass the Rubicon test suite with the following modifications:

Test suite patch ```patch diff --git a/org/beeware/rubicon/test/Example.java b/org/beeware/rubicon/test/Example.java index 8e490df..2b54f6e 100644 --- a/org/beeware/rubicon/test/Example.java +++ b/org/beeware/rubicon/test/Example.java @@ -94,8 +94,8 @@ public class Example extends BaseExample { int_field = value; } - protected Example(String value) { - // A protected constructor - it exists, but can't be accessed by Python. + private Example(String value) { + // A private constructor - it exists, but can't be accessed by Python. super(999); } @@ -277,10 +277,10 @@ public class Example extends BaseExample { } /* Interface visiblity */ - protected void invisible_method(int value) {} - protected static void static_invisible_method(int value) {} - protected int invisible_field; - protected static int static_invisible_field; + private void invisible_method(int value) {} + private static void static_invisible_method(int value) {} + private int invisible_field; + private static int static_invisible_field; /* Callback handling */ public void set_callback(ICallback cb) { diff --git a/tests/test_rubicon.py b/tests/test_rubicon.py index 76bdd9f..57a6b78 100644 --- a/tests/test_rubicon.py +++ b/tests/test_rubicon.py @@ -1,8 +1,9 @@ import math import sys -from unittest import TestCase +from unittest import TestCase, skip -from rubicon.java import JavaClass, JavaInterface, JavaNull, jdouble, jfloat, jstring, jlong, jshort, jint +from rubicon.java import (JavaClass, JavaInterface, JavaNull, jdouble, jfloat, jstring, jlong, jshort, jint, + jarray, jbyte, jboolean) class JNITest(TestCase): @@ -16,10 +17,8 @@ class JNITest(TestCase): stack.push("Hello") stack.push("World") - # The stack methods are protyped to return java/lang/Object, - # so we need to call toString() to get the actual content... - self.assertEqual(stack.pop().toString(), "World") - self.assertEqual(stack.pop().toString(), "Hello") + self.assertEqual(stack.pop(), "World") + self.assertEqual(stack.pop(), "Hello") # with self.assertRaises(Exception): # stack.pop() @@ -31,7 +30,7 @@ class JNITest(TestCase): class ImpossibleStackSubclass(Stack): pass - self.assertRaises(NotImplementedError, make_subclass) + self.assertRaises(TypeError, make_subclass) def test_field(self): "A field on an instance can be accessed and mutated" @@ -80,6 +79,9 @@ class JNITest(TestCase): def test_static_method(self): "A static method on a class can be invoked." with ExampleClassWithCleanup() as Example: + Example.static_base_int_field = 1137 + Example.static_int_field = 1142 + self.assertEqual(Example.get_static_base_int_field(), 1137) self.assertEqual(Example.get_static_int_field(), 1142) @@ -141,7 +143,7 @@ class JNITest(TestCase): with self.assertRaises(AttributeError): Example.static_method_doesnt_exist() - def test_protected_field(self): + def test_private_field(self): "An attribute error is raised if you invoke a non-public field." Example = JavaClass('org/beeware/rubicon/test/Example') @@ -155,7 +157,7 @@ class JNITest(TestCase): with self.assertRaises(AttributeError): obj1.invisible_field - def test_protected_method(self): + def test_private_method(self): "An attribute error is raised if you invoke a non-public method." Example = JavaClass('org/beeware/rubicon/test/Example') @@ -169,7 +171,7 @@ class JNITest(TestCase): with self.assertRaises(AttributeError): obj1.invisible_method() - def test_protected_static_field(self): + def test_private_static_field(self): "An attribute error is raised if you invoke a non-public static field." Example = JavaClass('org/beeware/rubicon/test/Example') @@ -181,7 +183,7 @@ class JNITest(TestCase): with self.assertRaises(AttributeError): Example.static_invisible_field - def test_protected_static_method(self): + def test_private_static_method(self): "An attribute error is raised if you invoke a non-public static method." Example = JavaClass('org/beeware/rubicon/test/Example') @@ -210,8 +212,8 @@ class JNITest(TestCase): self.assertEqual(obj3.base_int_field, 3342) self.assertEqual(obj3.int_field, 3337) - # Protected constructors can't be invoked - with self.assertRaises(ValueError): + # Private constructors can't be invoked + with self.assertRaises(TypeError): Example("Hello") def test_polymorphic_method(self): @@ -223,7 +225,7 @@ class JNITest(TestCase): self.assertEqual(obj1.doubler("wibble"), "wibblewibble") # If arguments don't match available options, an error is raised - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): obj1.doubler(1.234) def test_byte_array_arg(self): @@ -231,16 +233,20 @@ class JNITest(TestCase): Example = JavaClass('org/beeware/rubicon/test/Example') obj1 = Example() - self.assertEqual(obj1.doubler(b'abcd'), b'aabbccdd') + self.assertEqual(obj1.doubler(jarray(jbyte)(b'abcd')), b'aabbccdd') def test_int_array_arg(self): "Arrays of int can be used as arguments" Example = JavaClass('org/beeware/rubicon/test/Example') obj1 = Example() - self.assertEqual(obj1.doubler([1, 2]), [1, 1, 2, 2]) - self.assertEqual(obj1.doubler([jlong(1), jlong(2)]), [1, 1, 2, 2]) - self.assertEqual(obj1.doubler([jshort(1), jshort(2)]), [1, 1, 2, 2]) - self.assertEqual(obj1.doubler([jint(1), jint(2)]), [1, 1, 2, 2]) + self.assertEqual(obj1.doubler(jarray(jint)([1, 2])), + [1, 1, 2, 2]) + self.assertEqual(obj1.doubler(jarray(jlong)([jlong(1), jlong(2)])), + [1, 1, 2, 2]) + self.assertEqual(obj1.doubler(jarray(jshort)([jshort(1), jshort(2)])), + [1, 1, 2, 2]) + self.assertEqual(obj1.doubler(jarray(jint)([jint(1), jint(2)])), + [1, 1, 2, 2]) def assertAlmostEqualList(self, actual, expected): self.assertEqual(len(expected), len(actual), "Lists are different length") @@ -252,21 +258,26 @@ class JNITest(TestCase): Example = JavaClass('org/beeware/rubicon/test/Example') obj1 = Example() - self.assertAlmostEqualList(obj1.doubler([1.1, 2.2]), [1.1, 1.1, 2.2, 2.2]) - self.assertAlmostEqualList(obj1.doubler([jfloat(1.1), jfloat(2.2)]), [1.1, 1.1, 2.2, 2.2]) - self.assertAlmostEqualList(obj1.doubler([jdouble(1.1), jdouble(2.2)]), [1.1, 1.1, 2.2, 2.2]) + self.assertAlmostEqualList(obj1.doubler(jarray(jfloat)([1.1, 2.2])), + [1.1, 1.1, 2.2, 2.2]) + self.assertAlmostEqualList(obj1.doubler(jarray(jfloat)([jfloat(1.1), jfloat(2.2)])), + [1.1, 1.1, 2.2, 2.2]) + self.assertAlmostEqualList(obj1.doubler(jarray(jdouble)([jdouble(1.1), jdouble(2.2)])), + [1.1, 1.1, 2.2, 2.2]) def test_bool_array_arg(self): "Arrays of bool can be used as arguments" Example = JavaClass('org/beeware/rubicon/test/Example') obj1 = Example() - self.assertEqual(obj1.doubler([True, False]), [True, True, False, False]) + self.assertEqual(obj1.doubler(jarray(jboolean)([True, False])), + [True, True, False, False]) def test_string_array_arg(self): "Arrays of string can be used as arguments" Example = JavaClass('org/beeware/rubicon/test/Example') obj1 = Example() - self.assertEqual(obj1.doubler(["one", "two"]), ["one", "one", "two", "two"]) + self.assertEqual(obj1.doubler(jarray(jstring)(["one", "two"])), + ["one", "one", "two", "two"]) def test_object_array_arg(self): "Arrays of object can be used as arguments" @@ -278,7 +289,7 @@ class JNITest(TestCase): thing2 = Thing('This is two', 2) self.assertEqual( - [str(obj) for obj in obj1.doubler([thing1, thing2])], + [str(obj) for obj in obj1.doubler(jarray(Thing)([thing1, thing2]))], [str(obj) for obj in [thing1, thing1, thing2, thing2]] ) @@ -344,7 +355,7 @@ class JNITest(TestCase): ) # If NULL arguments don't match available options, an error is raised - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): obj1.combiner(3, "Pork", JavaNull(str), handler, [1, 2]), def test_polymorphic_method_null(self): @@ -355,7 +366,7 @@ class JNITest(TestCase): self.assertEqual(obj1.doubler(JavaNull(str)), "Can't double NULL strings") def test_null_return(self): - "Returned NULL objects are typed" + "Returned NULL objects are not typed" Example = JavaClass('org/beeware/rubicon/test/Example') obj = Example() @@ -363,12 +374,10 @@ class JNITest(TestCase): obj.set_thing(Thing.__null__) returned = obj.get_thing() - # Typed null objects are always equal to equivalent typed nulls - self.assertEqual(returned, Thing.__null__) - # All Typed nulls are equivalent - self.assertEqual(returned, Example.__null__) + self.assertIsNone(returned) + # Null is always false - self.assertFalse(returned) + self.assertFalse(Thing.__null__) def test_java_null_construction(self): "Java NULLs can be constructed" @@ -376,7 +385,8 @@ class JNITest(TestCase): obj1 = Example() # Java nulls can be constructed explicitly - self.assertEqual(JavaNull(b"Lcom/example/Thing;")._signature, b"Lcom/example/Thing;") + self.assertEqual(JavaNull(b"Lorg/beeware/rubicon/test/Thing;")._signature, + b"Lorg/beeware/rubicon/test/Thing;") # Java nulls can be constructed from a JavaClass self.assertEqual(JavaNull(Example)._signature, b"Lorg/beeware/rubicon/test/Example;") @@ -384,9 +394,13 @@ class JNITest(TestCase): # Java nulls can be constructed from an instance self.assertEqual(JavaNull(obj1)._signature, b"Lorg/beeware/rubicon/test/Example;") - # A Java Null can be constructed for Python or JNI primitives - self.assertEqual(JavaNull(int)._signature, b"I") - self.assertEqual(JavaNull(jlong)._signature, b"J") + # A Java Null cannot be constructed for Python or JNI primitives + with self.assertRaises(TypeError): + JavaNull(int) + with self.assertRaises(TypeError): + JavaNull(jlong) + + # A Java Null can be constructed for strings self.assertEqual(JavaNull(str)._signature, b"Ljava/lang/String;") self.assertEqual(JavaNull(jstring)._signature, b"Ljava/lang/String;") @@ -406,7 +420,8 @@ class JNITest(TestCase): self.assertEqual(JavaNull([Example])._signature, b"[Lorg/beeware/rubicon/test/Example;") # A Java Null for an array of explicit JNI references - self.assertEqual(JavaNull([b'Lcom/example/Thing;'])._signature, b"[Lcom/example/Thing;") + self.assertEqual(JavaNull([b'Lorg/beeware/rubicon/test/Thing;'])._signature, + b"[Lorg/beeware/rubicon/test/Thing;") # Arrays are defined with a type, not a literal with self.assertRaises(ValueError): @@ -427,13 +442,13 @@ class JNITest(TestCase): def test_null_repr(self): "Null objects can be output to console" # Output of a null makes sense - self.assertEqual(repr(JavaNull(b'Lcom/example/Thing;')), "") - self.assertEqual(repr(JavaNull(str)), "") - self.assertEqual(repr(JavaNull(int)), "") + self.assertEqual(repr(JavaNull(b'Lorg/beeware/rubicon/test/Thing;')), + "cast('Lorg/beeware/rubicon/test/Thing;', None)") + self.assertEqual(repr(JavaNull(str)), "cast('Ljava/lang/String;', None)") - self.assertEqual(str(JavaNull(b'Lcom/example/Thing;')), "") - self.assertEqual(str(JavaNull(str)), "") - self.assertEqual(str(JavaNull(int)), "") + self.assertEqual(str(JavaNull(b'Lorg/beeware/rubicon/test/Thing;')), + "cast('Lorg/beeware/rubicon/test/Thing;', None)") + self.assertEqual(str(JavaNull(str)), "cast('Ljava/lang/String;', None)") def test_polymorphic_static_method(self): "Check that the right static method is activated based on arguments used" @@ -443,7 +458,7 @@ class JNITest(TestCase): self.assertEqual(Example.tripler("wibble"), "wibblewibblewibble") # If arguments don't match available options, an error is raised - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): Example.tripler(1.234) def test_type_cast(self): @@ -457,14 +472,15 @@ class JNITest(TestCase): obj1.set_thing(thing) # Retrieve a generic reference to the object (java.lang.Object) - obj = obj1.get_generic_thing() + Object = JavaClass('java/lang/Object') + obj = Object.__cast__(obj1.get_generic_thing()) ICallback_null = JavaNull(b'Lorg/beeware/rubicon/test/ICallback;') # Attempting to use this generic object *as* a Thing will fail. with self.assertRaises(AttributeError): obj.currentCount() - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): obj1.combiner(3, "Ham", obj, ICallback_null, JavaNull([int])) # ...but if we cast it to the type we know it is @@ -509,27 +525,26 @@ class JNITest(TestCase): def test_heterogenous_list(self): """A list of mixed types raise an exception when trying to find the right Java method.""" Example = JavaClass("org/beeware/rubicon/test/Example") - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): Example.sum_all_ints(["two", 3]) - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): Example.sum_all_ints([1, "two"]) - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): Example.sum_all_floats([1.0, "two"]) - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): Example.sum_all_doubles([1.0, "two"]) def test_list_that_cannot_be_turned_into_java_primitive_array(self): """A list that can't turn into a Java primitive array raises an exception when trying to find the right Java method.""" Example = JavaClass("org/beeware/rubicon/test/Example") - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): Example.sum_all_ints([object()]) def test_empty_list(self): - """An empty list results in an inability to find the right Java method.""" + """An empty list is still able to find the right Java method.""" Example = JavaClass("org/beeware/rubicon/test/Example") - with self.assertRaises(ValueError): - Example.sum_all_ints([]) + self.assertEqual(Example.sum_all_ints([]), 0) def test_pass_double_array(self): """A list of Python floats can be passed as a Java double array.""" @@ -554,25 +569,22 @@ class JNITest(TestCase): self.assertEqual(0, Example.xor_all_bytes(b'xx')) def test_static_access_non_static(self): - "An instance field/method cannot be accessed from the static context" + "A static field/method can be accessed from an instance context" Example = JavaClass('org/beeware/rubicon/test/Example') obj = Example() - with self.assertRaises(AttributeError): - obj.static_int_field - - with self.assertRaises(AttributeError): - obj.get_static_int_field() + self.assertEqual(obj.static_int_field, 11) + self.assertEqual(obj.get_static_int_field(), 11) def test_non_static_access_static(self): - "A static field/method cannot be accessed from an instance context" + "An instance field/method cannot be accessed from a static context" Example = JavaClass('org/beeware/rubicon/test/Example') with self.assertRaises(AttributeError): Example.int_field - with self.assertRaises(AttributeError): + with self.assertRaises(TypeError): Example.get_int_field() def test_string_argument(self): @@ -717,6 +729,7 @@ class JNITest(TestCase): self.assertFalse(TruthInverter().invert(true_maker)) self.assertTrue(TruthInverter().invert(false_maker)) + @skip("_alternates is not in the public API") def test_alternatives(self): "A class is aware of it's type hierarchy" Example = JavaClass('org/beeware/rubicon/test/Example') ```

The main differences are:

I don't think any of these are likely to cause problems in user code. And if they do, they'll be easy to fix.

mhsmith commented 2 years ago

There should be no more significant changes needed to this template: the remaining work to release this is in other components:

Briefcase:

Chaquopy: see milestone 13.0. Some notes on specific issues:

freakboy3742 commented 2 years ago

The one thing that isn't on this list is porting Toga to use "native" Chaquopy, rather than Rubicon-Java. If I'm understanding correctly, chaquo/chaquopy#10 is the only true pre-requisite of that port; chaquo/chaquopy#660 and beeware/briefcase#813 are effective pre-requisites, because developing a Toga update without them will be a PITA.

The related question is release strategy for this change. Do we:

  1. Wait for that port to be completed before we make this template official, remove the Rubicon "shim" that you've included here, and do a single release switching over to Chaquopy; or
  2. Do an initial release that uses the Rubicon shim, and port Toga as the next piece of work.

(2) would let us defer at least chaquo/chaquopy#10; there's no particular deadline for landing this work, and it might be desirable to get some user testing before we switch. However, if experience is any guide, I'm not sure we'll actually get that much user testing until we actually make the change official.

Any thoughts on release strategy?

mhsmith commented 2 years ago

I think for safety we should have a transitional release where Toga continues to use the Rubicon shim and doesn't depend on any Chaquopy-only features. That way, if Chaquopy causes some unforeseen problem, users will have the option of going back to the Rubicon template without losing all the other improvements in the new Toga version.

Even after Toga switches over to using Chaquopy directly, we'll stil have to keep the shim for some time to support any existing user code that uses Android APIs. But we can give the shim module a DeprecationWarning to encourage those users to switch to Chaquopy as well.

mhsmith commented 2 years ago

Speaking of DeprecationWarning, I think we should enable that in the Android log for the same reason as we should in briefcase dev (https://github.com/chaquo/chaquopy/issues/664).

mhsmith commented 2 years ago

Removed most of the Chaquopy issues list above, as everything is now listed under the Chaquopy milestone 13.0.

mhsmith commented 2 years ago

As discussed, in order to get the Chaquopy release out this week and the Briefcase release out next week, we'll postpone both https://github.com/chaquo/chaquopy/issues/10 and https://github.com/chaquo/chaquopy/issues/659.

freakboy3742 commented 1 year ago

(Original comment moved to https://github.com/beeware/briefcase/discussions/1289)

@ccinsz Attaching a comment to a year-old pull request isn't an effective way to get assistance with a problem.

If you'd like help diagnosing a problem start a discussion in the Briefcase discussion forum.

If you believe you've found a bug, open a new issue, either here (if you believe it's a bug with the Android template), or on the Briefcase repository if you believe it's a more generic problem with Briefcase.