Search

Nautilus - Columns and Property page Provider for APK files

Contents[Hide]

dropcap-gnome-apk

On Android devices, all programs are installed thru some APK packages. Like well-known deb packages, these packages have some specific descriptive data : package name, version number, version code, requested permissions, ...

If you are an APK developper or if you are handling a large collection of APK packages on a Linux computer, you'll find that Nautilus is providing some very poor informations about these files. It is not even displaying the icon properly !

A previous article explained how to Display official APK icon as Nautilus thumbnail.

This article explains how to use Nautilus python extension capabilities to :

  • add some columns providing specific APK informations (package name, version, ...)
  • add one tab to APK file properties to provide a lot of extra informations

This procedure has been tested under Ubuntu Gnome 16.04 LTS and Ubuntu 16.04 LTS running gnome, but it should be applicable to many other modern Gnome Shell based distributions.

1. Install packages and create environment

Under Ubuntu, Nautilus python extensions are not enabled by default.

To enable them, you need to :

  • install python-nautilus package
  • create the directory that will hold the new extensions

Terminal
# sudo apt-get install python-nautilus
# mkdir --parents $HOME/.local/share/nautilus-python/extensions

If not already done, you also need to install aapt utility that will be used to extract data form APK files.

This utility is provided by the Android SDK, but it is also available straight from Ubuntu package repository.

If you don't want to install the full Android SDK, you can simply install it :

Terminal
# sudo apt-get install aapt

You are now ready to create your first Nautilus extension.

2. APK Columns Extension

A Nautilus columns provider is a Python class should provide a least 2 methods :

  • get_columns : called to list the columns provided by the extension
  • update_file_info : called for every file to provide the columns data

APK column provider will generate 4 new columns with APK package most important data :

  • Official name
  • Version
  • Version code
  • Minimum SDK version needed

nautilus-extension-apk-column

All other data will be provided by the property page extension.

As update_file_info will be called for each and every file displayed by Nautilus, it should be optimised for execution time. So the extension way of working will be :

  • one single execution of aapt utility per file
  • analysis of the result line by line thru regular expressions
  • end of analysis as soon as all data are extracted

The following python script will do the job :

~/.local/share/nautilus-python/extensions/apk-columns.py
#!/usr/bin/env python3
# ---------------------------------------------------
# Nautilus extension to add APK specific columns
# Dependency :
#   * aapt
# Procedure :
#   http://www.bernaerts-nicolas.fr/linux/76-gnome/324-gnome-nautilus-apk-column-property-provider-extension
#
# Revision history :
#   08/11/2014, V1.0 - Creation by N. Bernaerts
#   25/04/2020, v2.0 - rewrite for python3 compatibility
#                      add application name
# ---------------------------------------------------

# -------------------
#  Import libraries
# -------------------

import io
import subprocess
import re
import pipes
from urllib.parse import unquote
from gi.repository import Nautilus, GObject

# --------------------
#   Class definition
# --------------------

class ApkColumnExtension(GObject.GObject, Nautilus.ColumnProvider, Nautilus.InfoProvider):
  def __init__(self): pass
    
  # -----------------------------
  #   List of available columns
  # -----------------------------
  def get_columns(self):
    return (
      Nautilus.Column(name="NautilusPython::Apk1", attribute="apk_pkg_app", label="APK\nApp", description="Application name"),
      Nautilus.Column(name="NautilusPython::Apk2", attribute="apk_pkg_ver", label="APK\nApp ver.", description="Application version"),
      Nautilus.Column(name="NautilusPython::Apk3", attribute="apk_pkg_name", label="APK\nPkg", description="Package name"),
      Nautilus.Column(name="NautilusPython::Apk4", attribute="apk_pkg_code", label="APK\nPkg ver.", description="Package version"),
      Nautilus.Column(name="NautilusPython::Apk5", attribute="apk_sdk_ver", label="APK\nSDK ver.", description="SDK version"),
    )

  # ------------------------
  #   Retrieve file values
  # ------------------------
  def update_file_info(self, file):
 
    # test file type
    if file.get_uri_scheme() != 'file': return
        
    # if mimetype corresponds to APK file, read data and populate tab
    if file.get_mime_type() in ('application/vnd.android.package-archive'):

      # format filename
      filename = unquote(file.get_uri()[7:])

      # aapt command, using pipes to handle filenames including '(', ')', ...
      command_line = "aapt d badging " + pipes.quote(filename)
      proc = subprocess.Popen(command_line , shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

      # initialise data flags  
      found_package = False
      found_application = False
      found_sdkversion = False

      # console return analysis
      for line in io.TextIOWrapper(proc.stdout, encoding="utf-8"):
        handled = False

        # line package:
        if handled == False and found_package == False:
          if re.compile('^package:.*$').match(line):
            handled = True
            found_package == True
            value = re.compile('^.*name=.([^ \']*).*$').sub('\g<1>', line).rstrip('\n')
            file.add_string_attribute('apk_pkg_name', value)
            value = re.compile('^.*versionName=.([^ \']*).*$').sub('\g<1>', line).rstrip('\n')
            file.add_string_attribute('apk_pkg_ver', value)
            value = re.compile('^.*versionCode=.([^ \']*).*$').sub('\g<1>',line).rstrip('\n')
            file.add_string_attribute('apk_pkg_code', value)
           
        # line application:
        if handled == False and found_application == False:
          if re.compile('^application:.*$').match(line):
            handled = True
            found_application = True
            value = re.compile('^.*label=.([^ \']*).*$').sub('\g<1>', line).rstrip('\n')
            file.add_string_attribute('apk_pkg_app', value)
           
        # line sdkVersion:
        if handled == False and found_sdkversion == False:
          if re.compile('^sdkVersion:.*$').match(line):
            handled = True
            found_sdkversion = True
            value = re.compile('^sdkVersion:.([^ \']*).*$').sub('\g<1>', line).rstrip('\n')
            file.add_string_attribute('apk_sdk_ver', value)

    # else, file is not an APK
    else:
      file.add_string_attribute('apk_pkg_name', "")
      file.add_string_attribute('apk_pkg_code', "")
      file.add_string_attribute('apk_pkg_ver', "")
      file.add_string_attribute('apk_pkg_app', "")
      file.add_string_attribute('apk_sdk_ver', "")

You just need to download it under ~/.local/share/nautilus-python/extensions/apk-columns.py and to restart Nautilus to be able to display your new APK specific columns :

Terminal
# wget -O $HOME/.local/share/nautilus-python/extensions/apk-columns.py https://raw.githubusercontent.com/NicolasBernaerts/ubuntu-scripts/master/nautilus/extensions/apk-columns.py
# nautilus -q

You can now add the 4 new APK specific columns.

These new columns are sortable like any other column.

3. APK Property Page Extension

A Nautilus property page provider is a Python class that should provide a least one method called get_property_pages which is called to display a tab in the file property dialog.

So, APK property page provider will generate one Gtk widget in charge of displaying a wide range of data in addition to the previous ones :

  • Target SDK version
  • Supported densities
  • Supported screen families
  • Features used
  • Permissions needed

nautilus-extension-apk-property

The following python script will do the job :

~/.local/share/nautilus-python/extensions/apk-properties.py
#!/usr/bin/env python3
# ---------------------------------------------------------
# Nautilus extension to display properties tab
# for Android APK files
# Dependency :
#   - aapt
# Procedure :
#   http://bernaerts.dyndns.org/linux/...
#
# Revision history :
#   02/03/2014, v1.0 - creation by N. Bernaerts
#   24/04/2020, v2.0 - rewrite for python3 compatibility
# ---------------------------------------------------

# -------------------
#  Import libraries
# -------------------

import io
import subprocess
import re
import pipes
from urllib.parse import unquote
from gi.repository import Nautilus, Gtk, GObject

# --------------------
#   Class definition
# --------------------

class APKInfoPropertyPage(GObject.GObject, Nautilus.PropertyPageProvider):
  def __init__(self): pass
    
  # --------------------
  #   Display one item
  # --------------------
  def dislayItem(self, title, value, x, y):

    # dislay title
    gtk_label = Gtk.Label()
    gtk_label.set_markup("<b>" + title + "</b>")
    gtk_label.set_alignment(1.0, 0)
    gtk_label.set_padding(10, 3)
    gtk_label.show()
    self.grid.attach(gtk_label, x, y, 1, 1)

    # dislay value
    gtk_label = Gtk.Label()
    gtk_label.set_markup(value)
    gtk_label.set_alignment(0.0, 0)
    gtk_label.set_padding(10, 3)
    gtk_label.show()
    self.grid.attach(gtk_label, x + 1, y, 1, 1)
    
    return

  # -------------------------
  #   Display property tab
  # -------------------------
  def get_property_pages(self, files):

    # test file type
    if len(files) != 1: return
    file = files[0]
    if file.get_uri_scheme() != 'file': return

    # if mimetype corresponds to APK file, read data and populate tab
    if file.get_mime_type() in ('application/vnd.android.package-archive'):
    
      # format filename
      filename = unquote(file.get_uri()[7:])

      # create label and grid
      self.property_label = Gtk.Label('APK')
      self.property_label.show()
      self.grid = Gtk.Grid()
      self.grid.set_margin_start(10)
      self.grid.set_margin_end(10)
      self.grid.set_margin_top(5)
      self.grid.set_margin_bottom(5)
      self.grid.show()
      
      # aapt command, using pipes to handle filenames including '(', ')', ...
      command_line = "aapt d badging " + pipes.quote(filename)
      proc = subprocess.Popen(command_line , shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

      # multiline strings
      apk_features = ""
      apk_permissions = ""

      # initialise data flags  
      found_package = False
      found_application = False
      found_sdkversion = False
      found_targetsdk = False
      found_screens = False
      found_densities = False
      found_code = False

      # console return analysis
      for line in io.TextIOWrapper(proc.stdout, encoding="utf-8"):
        handled = False

        # line package:
        if handled == False and found_package == False:
          if re.compile('^package:.*$').match(line):
            handled = True
            found_package == True
            value = re.compile('^.*name=.([^ \']*).*$').sub('\g<1>', line).rstrip('\n')
            self.dislayItem("Name", value, 0, 1)
            value = re.compile('^.*versionName=.([^ \']*).*$').sub('\g<1>', line).rstrip('\n')
            self.dislayItem("Version name", value, 0, 2)
            value = re.compile('^.*versionCode=.([^ \']*).*$').sub('\g<1>',line).rstrip('\n')
            self.dislayItem("Version code", value, 0, 3)

        # line application:
        if handled == False and found_application == False:
          if re.compile('^application:.*$').match(line):
            handled = True
            found_application = True
            value = re.compile('^.*label=.([^ \']*).*$').sub('\g<1>', line).rstrip('\n')
            self.dislayItem("Label", value, 0, 0)


        # line sdkVersion:
        if handled == False and found_sdkversion == False:
          if re.compile('^sdkVersion:.*$').match(line):
            handled = True
            found_sdkversion = True
            value = re.compile('^sdkVersion:.([^ \']*).*$').sub('\g<1>', line).rstrip('\n')
            self.dislayItem("SDK version", value, 0, 4)

        # line targetSdkVersion:
        if handled == False and found_targetsdk == False:
          if re.compile('^targetSdkVersion:.*$').match(line):
            handled = True
            found_targetsdk = True
            value = re.compile('^targetSdkVersion:.([^ \']*).*$').sub('\g<1>', line).rstrip('\n')
            self.dislayItem("Target SDK version", value, 0, 5)

        # line native-code:
        if handled == False and found_code == False:
          if re.compile('^native-code:.*$').match(line): 
            handled = True
            found_code = True
            value = re.compile('^native-code:.(.*)$').sub('\g<1>', line)
            value = re.compile(' ').sub('\n', value)
            value = re.compile('\'').sub('', value).rstrip('\n')
            self.dislayItem("Code", value, 0, 6)

        # line supports-screens:
        if handled == False and found_screens == False:
          if re.compile('^supports-screens:.*$').match(line): 
            handled = True
            found_screens = True
            value = re.compile('^supports-screens:.(.*)$').sub('\g<1>', line)
            value = re.compile(' ').sub('\n', value)
            value = re.compile('\'').sub('', value).rstrip('\n')
            self.dislayItem("Supported screens", value, 0, 7)

        # line densities:
        if handled == False and found_densities == False:
          if re.compile('^densities:.*$').match(line): 
            handled = True
            found_densities = True
            value = re.compile('^densities:.(.*)$').sub('\g<1>', line)
            value = re.compile(' ').sub('\n', value)
            value = re.compile('\'').sub('', value).rstrip('\n')
            self.dislayItem("Supported densities", value, 0, 8)

        # line uses-feature:
        if handled == False:
          if re.compile('^  uses-feature:.*$').match(line):
            handled = True
            apk_features += re.compile('^  uses-feature: name=.(.*).$').sub('\g<1>', line)

        # line uses-permission:
        if handled == False:
          if re.compile('^uses-permission:.*$').match(line):
            handled = True
            apk_permissions += re.compile('^uses-permission: name=.(.*).$').sub('\g<1>', line)

      # dislay features and permissions
      self.dislayItem("Used features", apk_features.rstrip('\n'), 0, 9)
      self.dislayItem("Used permissions", apk_permissions.rstrip('\n'), 0, 10)

      # declare main scrolled window
      self.window = Gtk.ScrolledWindow()
      self.window.add_with_viewport(self.grid)
      self.window.show_all()

      # return result
      return Nautilus.PropertyPage(name="NautilusPython::apk_info", label=self.property_label, page=self.window),

You just need to download it under ~/.local/share/nautilus-python/extensions/apk-properties.py and to restart Nautilus to be able to display your new APK property tab :

Terminal
# wget -O $HOME/.local/share/nautilus-python/extensions/apk-properties.py https://raw.githubusercontent.com/NicolasBernaerts/ubuntu-scripts/master/nautilus/extensions/apk-properties.py
# nautilus -q

 

Hope it helps.

Signature Technoblog

This article is published "as is", without any warranty that it will work for your specific need.
If you think this article needs some complement, or simply if you think it saved you lots of time & trouble,
just let me know at This email address is being protected from spambots. You need JavaScript enabled to view it.. Cheers !

icon linux icon debian icon apache icon mysql icon php icon piwik icon googleplus