dist: Support adding icons to Android applications

[skip ci]
This commit is contained in:
rdb 2021-12-07 23:56:19 +01:00
parent 17e00c5786
commit 267907d329
4 changed files with 125 additions and 14 deletions

View File

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

View File

@ -608,6 +608,10 @@ class build_apps(setuptools.Command):
application.set('android:debuggable', ('false', 'true')[self.android_debuggable])
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:
activity = ET.SubElement(application, 'activity')
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: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.set('android:name', 'android.app.lib_name')
meta_data.set('android:value', appname)
@ -1187,6 +1195,23 @@ class build_apps(setuptools.Command):
# Generate an 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
if self.macos_main_app and 'macosx' in platform:
self.bundle_macos_app(builddir)

View File

@ -32,6 +32,9 @@ class Icon:
return True
def getLargestSize(self):
return max(self.images.keys())
def generateMissingImages(self):
""" Generates image sizes that should be present but aren't by scaling
from the next higher size. """
@ -269,3 +272,35 @@ class Icon:
icns.close()
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))

View File

@ -212,6 +212,11 @@ def create_aab(command, basename, build_dir):
bundle_fn = p3d.Filename.from_os_specific(command.dist_dir) / (basename + '.aab')
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
# alignment needed by Android automatically.
bundle = p3d.ZipArchive()
@ -227,6 +232,31 @@ def create_aab(command, basename, build_dir):
bundle.add_subfile('BundleConfig.pb', p3d.StringStream(config.SerializeToString()), 9)
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)
native = NativeLibraries()
@ -236,10 +266,6 @@ def create_aab(command, basename, build_dir):
native_dir.targeting.abi.alias = getattr(AbiAlias, abi.upper().replace('-', '_'))
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)
# Add the classes.dex.