Explorar o código

dist: Support adding icons to Android applications

[skip ci]
rdb %!s(int64=4) %!d(string=hai) anos
pai
achega
267907d329

+ 35 - 10
direct/src/dist/_android.py

@@ -12,13 +12,13 @@ AbiAlias = Abi.AbiAlias
 
 
 
 
 def str_resource(id):
 def str_resource(id):
-    def compile(attrib):
+    def compile(attrib, manifest):
         attrib.resource_id = id
         attrib.resource_id = id
     return compile
     return compile
 
 
 
 
 def int_resource(id):
 def int_resource(id):
-    def compile(attrib):
+    def compile(attrib, manifest):
         attrib.resource_id = id
         attrib.resource_id = id
         if attrib.value.startswith('0x') or attrib.value.startswith('0X'):
         if attrib.value.startswith('0x') or attrib.value.startswith('0X'):
             attrib.compiled_item.prim.int_hexadecimal_value = int(attrib.value, 16)
             attrib.compiled_item.prim.int_hexadecimal_value = int(attrib.value, 16)
@@ -28,7 +28,7 @@ def int_resource(id):
 
 
 
 
 def bool_resource(id):
 def bool_resource(id):
-    def compile(attrib):
+    def compile(attrib, manifest):
         attrib.resource_id = id
         attrib.resource_id = id
         attrib.compiled_item.prim.boolean_value = {
         attrib.compiled_item.prim.boolean_value = {
             'true': True, '1': True, 'false': False, '0': False
             'true': True, '1': True, 'false': False, '0': False
@@ -37,14 +37,14 @@ def bool_resource(id):
 
 
 
 
 def enum_resource(id, /, *values):
 def enum_resource(id, /, *values):
-    def compile(attrib):
+    def compile(attrib, manifest):
         attrib.resource_id = id
         attrib.resource_id = id
         attrib.compiled_item.prim.int_decimal_value = values.index(attrib.value)
         attrib.compiled_item.prim.int_decimal_value = values.index(attrib.value)
     return compile
     return compile
 
 
 
 
 def flag_resource(id, /, **values):
 def flag_resource(id, /, **values):
-    def compile(attrib):
+    def compile(attrib, manifest):
         attrib.resource_id = id
         attrib.resource_id = id
         bitmask = 0
         bitmask = 0
         flags = attrib.value.split('|')
         flags = attrib.value.split('|')
@@ -54,14 +54,17 @@ def flag_resource(id, /, **values):
     return compile
     return compile
 
 
 
 
-def ref_resource(id, type):
-    def compile(attrib):
+def ref_resource(id):
+    def compile(attrib, manifest):
         assert attrib.value[0] == '@'
         assert attrib.value[0] == '@'
         ref_type, ref_name = attrib.value[1:].split('/')
         ref_type, ref_name = attrib.value[1:].split('/')
         attrib.resource_id = id
         attrib.resource_id = id
+        attrib.compiled_item.ref.name = ref_type + '/' + ref_name
+
         if ref_type == 'android:style':
         if ref_type == 'android:style':
             attrib.compiled_item.ref.id = ANDROID_STYLES[ref_name]
             attrib.compiled_item.ref.id = ANDROID_STYLES[ref_name]
-            attrib.compiled_item.ref.name = ref_type + '/' + ref_name
+        elif ':' not in ref_type:
+            attrib.compiled_item.ref.id = manifest.register_resource(ref_type, ref_name)
         else:
         else:
             print(f'Warning: unhandled AndroidManifest.xml reference "{attrib.value}"')
             print(f'Warning: unhandled AndroidManifest.xml reference "{attrib.value}"')
     return compile
     return compile
@@ -175,6 +178,7 @@ ANDROID_ATTRIBUTES = {
     'glEsVersion': int_resource(0x1010281),
     'glEsVersion': int_resource(0x1010281),
     'hasCode': bool_resource(0x101000c),
     'hasCode': bool_resource(0x101000c),
     'host': str_resource(0x1010028),
     'host': str_resource(0x1010028),
+    'icon': ref_resource(0x1010002),
     'immersive': bool_resource(0x10102c0),
     'immersive': bool_resource(0x10102c0),
     'installLocation': enum_resource(0x10102b7, "auto", "internalOnly", "preferExternal"),
     'installLocation': enum_resource(0x10102b7, "auto", "internalOnly", "preferExternal"),
     'isGame': bool_resource(0x010103f4),
     'isGame': bool_resource(0x010103f4),
@@ -189,10 +193,11 @@ ANDROID_ATTRIBUTES = {
     'required': bool_resource(0x101028e),
     'required': bool_resource(0x101028e),
     'scheme': str_resource(0x1010027),
     'scheme': str_resource(0x1010027),
     'stateNotNeeded': bool_resource(0x1010016),
     'stateNotNeeded': bool_resource(0x1010016),
+    'supportsRtl': bool_resource(0x010103af),
     'supportsUploading': bool_resource(0x101029b),
     'supportsUploading': bool_resource(0x101029b),
     'targetSandboxVersion': int_resource(0x101054c),
     'targetSandboxVersion': int_resource(0x101054c),
     'targetSdkVersion': int_resource(0x1010270),
     'targetSdkVersion': int_resource(0x1010270),
-    'theme': ref_resource(0x01010000, 'android:style'),
+    'theme': ref_resource(0x01010000),
     'value': str_resource(0x1010024),
     'value': str_resource(0x1010024),
     'versionCode': int_resource(0x101021b),
     'versionCode': int_resource(0x101021b),
     'versionName': str_resource(0x101021c),
     'versionName': str_resource(0x101021c),
@@ -204,6 +209,8 @@ class AndroidManifest:
         super().__init__()
         super().__init__()
         self._stack = []
         self._stack = []
         self.root = XmlNode()
         self.root = XmlNode()
+        self.resource_types = []
+        self.resources = {}
 
 
     def parse_xml(self, data):
     def parse_xml(self, data):
         parser = ET.XMLParser(target=self)
         parser = ET.XMLParser(target=self)
@@ -241,10 +248,28 @@ class AndroidManifest:
             attrib.name = key
             attrib.name = key
 
 
             if res_compile:
             if res_compile:
-                res_compile(attrib)
+                res_compile(attrib, self)
 
 
     def end(self, tag):
     def end(self, tag):
         self._stack.pop()
         self._stack.pop()
 
 
+    def register_resource(self, type, name):
+        if type not in self.resource_types:
+            self.resource_types.append(type)
+            type_id = len(self.resource_types)
+            self.resources[type] = []
+        else:
+            type_id = self.resource_types.index(type) + 1
+
+        resources = self.resources[type]
+        if name in resources:
+            entry_id = resources.index(name)
+        else:
+            entry_id = len(resources)
+            resources.append(name)
+
+        id = (0x7f << 24) | (type_id << 16) | (entry_id)
+        return id
+
     def dumps(self):
     def dumps(self):
         return self.root.SerializeToString()
         return self.root.SerializeToString()

+ 25 - 0
direct/src/dist/commands.py

@@ -608,6 +608,10 @@ class build_apps(setuptools.Command):
         application.set('android:debuggable', ('false', 'true')[self.android_debuggable])
         application.set('android:debuggable', ('false', 'true')[self.android_debuggable])
         application.set('android:extractNativeLibs', 'true')
         application.set('android:extractNativeLibs', 'true')
 
 
+        app_icon = self.icon_objects.get('*', self.icon_objects.get(self.macos_main_app))
+        if app_icon:
+            application.set('android:icon', '@mipmap/ic_launcher')
+
         for appname in self.gui_apps:
         for appname in self.gui_apps:
             activity = ET.SubElement(application, 'activity')
             activity = ET.SubElement(application, 'activity')
             activity.set('android:name', 'org.panda3d.android.PandaActivity')
             activity.set('android:name', 'org.panda3d.android.PandaActivity')
@@ -616,6 +620,10 @@ class build_apps(setuptools.Command):
             activity.set('android:configChanges', 'orientation|keyboardHidden')
             activity.set('android:configChanges', 'orientation|keyboardHidden')
             activity.set('android:launchMode', 'singleInstance')
             activity.set('android:launchMode', 'singleInstance')
 
 
+            act_icon = self.icon_objects.get(appname)
+            if act_icon and act_icon is not app_icon:
+                activity.set('android:icon', '@mipmap/ic_' + appname)
+
             meta_data = ET.SubElement(activity, 'meta-data')
             meta_data = ET.SubElement(activity, 'meta-data')
             meta_data.set('android:name', 'android.app.lib_name')
             meta_data.set('android:name', 'android.app.lib_name')
             meta_data.set('android:value', appname)
             meta_data.set('android:value', appname)
@@ -1187,6 +1195,23 @@ class build_apps(setuptools.Command):
             # Generate an AndroidManifest.xml
             # Generate an AndroidManifest.xml
             self.generate_android_manifest(os.path.join(builddir, 'AndroidManifest.xml'))
             self.generate_android_manifest(os.path.join(builddir, 'AndroidManifest.xml'))
 
 
+            # Write out the icons to the res directory.
+            for appname, icon in self.icon_objects.items():
+                if appname == '*' or (appname == self.macos_main_app and '*' not in self.icon_objects):
+                    # Conventional name for icon on Android.
+                    basename = 'ic_launcher.png'
+                else:
+                    basename = f'ic_{appname}.png'
+
+                res_dir = os.path.join(builddir, 'res')
+                icon.writeSize(48, os.path.join(res_dir, 'mipmap-mdpi-v4', basename))
+                icon.writeSize(72, os.path.join(res_dir, 'mipmap-hdpi-v4', basename))
+                icon.writeSize(96, os.path.join(res_dir, 'mipmap-xhdpi-v4', basename))
+                icon.writeSize(144, os.path.join(res_dir, 'mipmap-xxhdpi-v4', basename))
+
+                if icon.getLargestSize() >= 192:
+                    icon.writeSize(192, os.path.join(res_dir, 'mipmap-xxxhdpi-v4', basename))
+
         # Bundle into an .app on macOS
         # Bundle into an .app on macOS
         if self.macos_main_app and 'macosx' in platform:
         if self.macos_main_app and 'macosx' in platform:
             self.bundle_macos_app(builddir)
             self.bundle_macos_app(builddir)

+ 35 - 0
direct/src/dist/icon.py

@@ -32,6 +32,9 @@ class Icon:
 
 
         return True
         return True
 
 
+    def getLargestSize(self):
+        return max(self.images.keys())
+
     def generateMissingImages(self):
     def generateMissingImages(self):
         """ Generates image sizes that should be present but aren't by scaling
         """ Generates image sizes that should be present but aren't by scaling
         from the next higher size. """
         from the next higher size. """
@@ -269,3 +272,35 @@ class Icon:
         icns.close()
         icns.close()
 
 
         return True
         return True
+
+    def writeSize(self, required_size, fn):
+        if not isinstance(fn, Filename):
+            fn = Filename.fromOsSpecific(fn)
+        fn.setBinary()
+        fn.makeDir()
+
+        if required_size in self.images:
+            image = self.images[required_size]
+        else:
+            # Find the next size up.
+            sizes = sorted(self.images.keys())
+            if required_size * 2 in sizes:
+                from_size = required_size * 2
+            else:
+                from_size = 0
+                for from_size in sizes:
+                    if from_size > required_size:
+                        break
+
+            if from_size > required_size:
+                Icon.notify.warning("Generating %dx%d icon by scaling down %dx%d image" % (required_size, required_size, from_size, from_size))
+            else:
+                Icon.notify.warning("Generating %dx%d icon by scaling up %dx%d image" % (required_size, required_size, from_size, from_size))
+
+            from_image = self.images[from_size]
+            image = PNMImage(required_size, required_size)
+            image.setColorType(from_image.getColorType())
+            image.quickFilterFrom(from_image)
+
+        if not image.write(fn):
+            Icon.notify.error("Failed to write %dx%d to %s" % (required_size, required_size, fn))

+ 30 - 4
direct/src/dist/installers.py

@@ -212,6 +212,11 @@ def create_aab(command, basename, build_dir):
     bundle_fn = p3d.Filename.from_os_specific(command.dist_dir) / (basename + '.aab')
     bundle_fn = p3d.Filename.from_os_specific(command.dist_dir) / (basename + '.aab')
     build_dir_fn = p3d.Filename.from_os_specific(build_dir)
     build_dir_fn = p3d.Filename.from_os_specific(build_dir)
 
 
+    # Convert the AndroidManifest.xml file to a protobuf-encoded version of it.
+    axml = AndroidManifest()
+    with open(os.path.join(build_dir, 'AndroidManifest.xml'), 'rb') as fh:
+        axml.parse_xml(fh.read())
+
     # We use our own zip implementation, which can create the correct
     # We use our own zip implementation, which can create the correct
     # alignment needed by Android automatically.
     # alignment needed by Android automatically.
     bundle = p3d.ZipArchive()
     bundle = p3d.ZipArchive()
@@ -227,6 +232,31 @@ def create_aab(command, basename, build_dir):
     bundle.add_subfile('BundleConfig.pb', p3d.StringStream(config.SerializeToString()), 9)
     bundle.add_subfile('BundleConfig.pb', p3d.StringStream(config.SerializeToString()), 9)
 
 
     resources = ResourceTable()
     resources = ResourceTable()
+    package = resources.package.add()
+    package.package_id.id = 0x7f
+    for attrib in axml.root.element.attribute:
+        if attrib.name == 'package':
+            package.package_name = attrib.value
+
+    # Were there any icons referenced in the AndroidManifest.xml?
+    for type_i, type_name in enumerate(axml.resource_types):
+        res_type = package.type.add()
+        res_type.name = type_name
+        res_type.type_id.id = type_i + 1
+
+        for entry_id, res_name in enumerate(axml.resources[type_name]):
+            entry = res_type.entry.add()
+            entry.entry_id.id = entry_id
+            entry.name = res_name
+
+            for density, tag in (160, 'mdpi'), (240, 'hdpi'), (320, 'xhdpi'), (480, 'xxhdpi'), (640, 'xxxhdpi'):
+                path = f'res/mipmap-{tag}-v4/{res_name}.png'
+                if (build_dir_fn / path).exists():
+                    bundle.add_subfile('base/' + path, build_dir_fn / path, 0)
+                    config_value = entry.config_value.add()
+                    config_value.config.density = density
+                    config_value.value.item.file.path = path
+
     bundle.add_subfile('base/resources.pb', p3d.StringStream(resources.SerializeToString()), 9)
     bundle.add_subfile('base/resources.pb', p3d.StringStream(resources.SerializeToString()), 9)
 
 
     native = NativeLibraries()
     native = NativeLibraries()
@@ -236,10 +266,6 @@ def create_aab(command, basename, build_dir):
         native_dir.targeting.abi.alias = getattr(AbiAlias, abi.upper().replace('-', '_'))
         native_dir.targeting.abi.alias = getattr(AbiAlias, abi.upper().replace('-', '_'))
     bundle.add_subfile('base/native.pb', p3d.StringStream(native.SerializeToString()), 9)
     bundle.add_subfile('base/native.pb', p3d.StringStream(native.SerializeToString()), 9)
 
 
-    # Convert the AndroidManifest.xml file to a protobuf-encoded version of it.
-    axml = AndroidManifest()
-    with open(os.path.join(build_dir, 'AndroidManifest.xml'), 'rb') as fh:
-        axml.parse_xml(fh.read())
     bundle.add_subfile('base/manifest/AndroidManifest.xml', p3d.StringStream(axml.dumps()), 9)
     bundle.add_subfile('base/manifest/AndroidManifest.xml', p3d.StringStream(axml.dumps()), 9)
 
 
     # Add the classes.dex.
     # Add the classes.dex.