Merge branch 'support-xml-icons' into 'master'

Support XML icons

Closes #392

See merge request fdroid/fdroidserver!464
This commit is contained in:
Hans-Christoph Steiner 2018-02-19 16:58:28 +00:00
commit 44ebf701e1
3 changed files with 79 additions and 44 deletions

View file

@ -61,7 +61,7 @@ APK_PERMISSION_PAT = \
re.compile(".*(name='(?P<name>.*?)')(.*maxSdkVersion='(?P<maxSdkVersion>.*?)')?.*") re.compile(".*(name='(?P<name>.*?)')(.*maxSdkVersion='(?P<maxSdkVersion>.*?)')?.*")
APK_FEATURE_PAT = re.compile(".*name='([^']*)'.*") APK_FEATURE_PAT = re.compile(".*name='([^']*)'.*")
screen_densities = ['640', '480', '320', '240', '160', '120'] screen_densities = ['65534', '640', '480', '320', '240', '160', '120']
screen_resolutions = { screen_resolutions = {
"xxxhdpi": '640', "xxxhdpi": '640',
"xxhdpi": '480', "xxhdpi": '480',
@ -96,9 +96,10 @@ def px_to_dpi(px):
def get_icon_dir(repodir, density): def get_icon_dir(repodir, density):
if density == '0': if density == '0' or density == '65534':
return os.path.join(repodir, "icons") return os.path.join(repodir, "icons")
return os.path.join(repodir, "icons-%s" % density) else:
return os.path.join(repodir, "icons-%s" % density)
def get_icon_dirs(repodir): def get_icon_dirs(repodir):
@ -1377,7 +1378,7 @@ def process_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk=Fal
.format(apkfilename=apkfile) + str(e)) .format(apkfilename=apkfile) + str(e))
# extract icons from APK zip file # extract icons from APK zip file
iconfilename = "%s.%s.png" % (apk['packageName'], apk['versionCode']) iconfilename = "%s.%s" % (apk['packageName'], apk['versionCode'])
try: try:
empty_densities = extract_apk_icons(iconfilename, apk, apkzip, repodir) empty_densities = extract_apk_icons(iconfilename, apk, apkzip, repodir)
finally: finally:
@ -1442,10 +1443,12 @@ def process_apks(apkcache, repodir, knownapks, use_date_from_apk=False):
def extract_apk_icons(icon_filename, apk, apkzip, repo_dir): def extract_apk_icons(icon_filename, apk, apkzip, repo_dir):
""" """Extracts PNG icons from an APK with the supported pixel densities
Extracts icons from the given APK zip in various densities,
saves them into given repo directory Extracts icons from the given APK zip in various densities, saves
and stores their names in the APK metadata dictionary. them into given repo directory and stores their names in the APK
metadata dictionary. If the icon is an XML icon, then this tries
to find PNG icon that can replace it.
:param icon_filename: A string representing the icon's file name :param icon_filename: A string representing the icon's file name
:param apk: A populated dictionary containing APK metadata. :param apk: A populated dictionary containing APK metadata.
@ -1453,7 +1456,17 @@ def extract_apk_icons(icon_filename, apk, apkzip, repo_dir):
:param apkzip: An opened zipfile.ZipFile of the APK file :param apkzip: An opened zipfile.ZipFile of the APK file
:param repo_dir: The directory of the APK's repository :param repo_dir: The directory of the APK's repository
:return: A list of icon densities that are missing :return: A list of icon densities that are missing
""" """
res_name_re = re.compile(r'res/(drawable|mipmap)-(x*[hlm]dpi|anydpi).*/(.*)_[0-9]+dp.(png|xml)')
pngs = dict()
for f in apkzip.namelist():
m = res_name_re.match(f)
if m and m.group(4) == 'png':
density = screen_resolutions[m.group(2)]
pngs[m.group(3) + '/' + density] = m.group(0)
icon_type = None
empty_densities = [] empty_densities = []
for density in screen_densities: for density in screen_densities:
if density not in apk['icons_src']: if density not in apk['icons_src']:
@ -1461,71 +1474,79 @@ def extract_apk_icons(icon_filename, apk, apkzip, repo_dir):
continue continue
icon_src = apk['icons_src'][density] icon_src = apk['icons_src'][density]
icon_dir = get_icon_dir(repo_dir, density) icon_dir = get_icon_dir(repo_dir, density)
icon_dest = os.path.join(icon_dir, icon_filename) icon_type = '.png'
# Extract the icon files per density # Extract the icon files per density
if icon_src.endswith('.xml'): if icon_src.endswith('.xml'):
png = os.path.basename(icon_src)[:-4] + '.png' m = res_name_re.match(icon_src)
for f in apkzip.namelist(): if m:
if f.endswith(png): name = pngs.get(m.group(3) + '/' + str(density))
m = re.match(r'res/(drawable|mipmap)-(x*[hlm]dpi).*/', f) if name:
if m and screen_resolutions[m.group(2)] == density: icon_src = name
icon_src = f
if icon_src.endswith('.xml'): if icon_src.endswith('.xml'):
empty_densities.append(density) empty_densities.append(density)
continue icon_type = '.xml'
icon_dest = os.path.join(icon_dir, icon_filename + icon_type)
try: try:
with open(icon_dest, 'wb') as f: with open(icon_dest, 'wb') as f:
f.write(get_icon_bytes(apkzip, icon_src)) f.write(get_icon_bytes(apkzip, icon_src))
apk['icons'][density] = icon_filename apk['icons'][density] = icon_filename + icon_type
except (zipfile.BadZipFile, ValueError, KeyError) as e: except (zipfile.BadZipFile, ValueError, KeyError) as e:
logging.warning("Error retrieving icon file: %s %s", icon_dest, e) logging.warning("Error retrieving icon file: %s %s", icon_dest, e)
del apk['icons_src'][density] del apk['icons_src'][density]
empty_densities.append(density) empty_densities.append(density)
if '-1' in apk['icons_src'] and not apk['icons_src']['-1'].endswith('.xml'): # '-1' here is a remnant of the parsing of aapt output, meaning "no DPI specified"
if '-1' in apk['icons_src']:
icon_src = apk['icons_src']['-1'] icon_src = apk['icons_src']['-1']
icon_path = os.path.join(get_icon_dir(repo_dir, '0'), icon_filename) icon_type = icon_src[-4:]
icon_path = os.path.join(get_icon_dir(repo_dir, '0'), icon_filename + icon_type)
with open(icon_path, 'wb') as f: with open(icon_path, 'wb') as f:
f.write(get_icon_bytes(apkzip, icon_src)) f.write(get_icon_bytes(apkzip, icon_src))
im = None if icon_type == '.png':
try: im = None
im = Image.open(icon_path) try:
dpi = px_to_dpi(im.size[0]) im = Image.open(icon_path)
for density in screen_densities: dpi = px_to_dpi(im.size[0])
if density in apk['icons']: for density in screen_densities:
break if density in apk['icons']:
if density == screen_densities[-1] or dpi >= int(density): break
apk['icons'][density] = icon_filename if density == screen_densities[-1] or dpi >= int(density):
shutil.move(icon_path, apk['icons'][density] = icon_filename
os.path.join(get_icon_dir(repo_dir, density), icon_filename)) shutil.move(icon_path,
empty_densities.remove(density) os.path.join(get_icon_dir(repo_dir, density), icon_filename))
break empty_densities.remove(density)
except Exception as e: break
logging.warning(_("Failed reading {path}: {error}") except Exception as e:
.format(path=icon_path, error=e)) logging.warning(_("Failed reading {path}: {error}")
finally: .format(path=icon_path, error=e))
if im and hasattr(im, 'close'): finally:
im.close() if im and hasattr(im, 'close'):
im.close()
if apk['icons']: if apk['icons']:
apk['icon'] = icon_filename apk['icon'] = icon_filename + icon_type
return empty_densities return empty_densities
def fill_missing_icon_densities(empty_densities, icon_filename, apk, repo_dir): def fill_missing_icon_densities(empty_densities, icon_filename, apk, repo_dir):
""" """
Resize existing icons for densities missing in the APK to ensure all densities are available Resize existing PNG icons for densities missing in the APK to ensure all densities are available
:param empty_densities: A list of icon densities that are missing :param empty_densities: A list of icon densities that are missing
:param icon_filename: A string representing the icon's file name :param icon_filename: A string representing the icon's file name
:param apk: A populated dictionary containing APK metadata. Needs to have 'icons' key :param apk: A populated dictionary containing APK metadata. Needs to have 'icons' key
:param repo_dir: The directory of the APK's repository :param repo_dir: The directory of the APK's repository
""" """
icon_filename += '.png'
# First try resizing down to not lose quality # First try resizing down to not lose quality
last_density = None last_density = None
for density in screen_densities: for density in screen_densities:
if density == '65534': # not possible to generate 'anydpi' from other densities
continue
if density not in empty_densities: if density not in empty_densities:
last_density = density last_density = density
continue continue

Binary file not shown.

View file

@ -253,14 +253,14 @@ class UpdateTest(unittest.TestCase):
apps = fdroidserver.metadata.read_metadata(xref=True) apps = fdroidserver.metadata.read_metadata(xref=True)
knownapks = fdroidserver.common.KnownApks() knownapks = fdroidserver.common.KnownApks()
apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False) apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False)
self.assertEqual(len(apks), 12) self.assertEqual(len(apks), 13)
apk = apks[0] apk = apks[0]
self.assertEqual(apk['packageName'], 'com.politedroid') self.assertEqual(apk['packageName'], 'com.politedroid')
self.assertEqual(apk['versionCode'], 3) self.assertEqual(apk['versionCode'], 3)
self.assertEqual(apk['minSdkVersion'], '3') self.assertEqual(apk['minSdkVersion'], '3')
self.assertEqual(apk['targetSdkVersion'], '3') self.assertEqual(apk['targetSdkVersion'], '3')
self.assertFalse('maxSdkVersion' in apk) self.assertFalse('maxSdkVersion' in apk)
apk = apks[5] apk = apks[6]
self.assertEqual(apk['packageName'], 'obb.main.oldversion') self.assertEqual(apk['packageName'], 'obb.main.oldversion')
self.assertEqual(apk['versionCode'], 1444412523) self.assertEqual(apk['versionCode'], 1444412523)
self.assertEqual(apk['minSdkVersion'], '4') self.assertEqual(apk['minSdkVersion'], '4')
@ -298,7 +298,6 @@ class UpdateTest(unittest.TestCase):
raise Exception('This test must be run in the "tests/" subdir') raise Exception('This test must be run in the "tests/" subdir')
apk_info = fdroidserver.update.scan_apk('org.dyndns.fules.ck_20.apk') apk_info = fdroidserver.update.scan_apk('org.dyndns.fules.ck_20.apk')
self.assertEqual(apk_info['icons_src'], {'240': 'res/drawable-hdpi-v4/icon_launcher.png', self.assertEqual(apk_info['icons_src'], {'240': 'res/drawable-hdpi-v4/icon_launcher.png',
'120': 'res/drawable-ldpi-v4/icon_launcher.png', '120': 'res/drawable-ldpi-v4/icon_launcher.png',
'160': 'res/drawable-mdpi-v4/icon_launcher.png', '160': 'res/drawable-mdpi-v4/icon_launcher.png',
@ -319,6 +318,21 @@ class UpdateTest(unittest.TestCase):
self.assertEqual(apk_info['hashType'], 'sha256') self.assertEqual(apk_info['hashType'], 'sha256')
self.assertEqual(apk_info['targetSdkVersion'], '8') self.assertEqual(apk_info['targetSdkVersion'], '8')
apk_info = fdroidserver.update.scan_apk('org.bitbucket.tickytacky.mirrormirror_4.apk')
self.assertEqual(apk_info['icons_src'], {'160': 'res/drawable-mdpi/mirror.png',
'-1': 'res/drawable-mdpi/mirror.png'})
apk_info = fdroidserver.update.scan_apk('repo/info.zwanenburg.caffeinetile_4.apk')
self.assertEqual(apk_info['icons_src'], {'160': 'res/drawable/ic_coffee_on.xml',
'-1': 'res/drawable/ic_coffee_on.xml'})
apk_info = fdroidserver.update.scan_apk('repo/com.politedroid_6.apk')
self.assertEqual(apk_info['icons_src'], {'120': 'res/drawable-ldpi-v4/icon.png',
'160': 'res/drawable-mdpi-v4/icon.png',
'240': 'res/drawable-hdpi-v4/icon.png',
'320': 'res/drawable-xhdpi-v4/icon.png',
'-1': 'res/drawable-mdpi-v4/icon.png'})
def test_scan_apk_no_sig(self): def test_scan_apk_no_sig(self):
config = dict() config = dict()
fdroidserver.common.fill_config_defaults(config) fdroidserver.common.fill_config_defaults(config)
@ -527,7 +541,7 @@ class UpdateTest(unittest.TestCase):
knownapks = fdroidserver.common.KnownApks() knownapks = fdroidserver.common.KnownApks()
apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False) apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False)
fdroidserver.update.translate_per_build_anti_features(apps, apks) fdroidserver.update.translate_per_build_anti_features(apps, apks)
self.assertEqual(len(apks), 12) self.assertEqual(len(apks), 13)
foundtest = False foundtest = False
for apk in apks: for apk in apks:
if apk['packageName'] == 'com.politedroid' and apk['versionCode'] == 3: if apk['packageName'] == 'com.politedroid' and apk['versionCode'] == 3: