dist: Support adding icons to Android applications
[skip ci]
This commit is contained in:
parent
17e00c5786
commit
267907d329
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue