#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2012 Canonical, Ltd.
#
# Authors:
#  Olivier Tilloy <olivier.tilloy@canonical.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
Add an 'Android Applications' scope to Unity’s applications lens.
"""

import subprocess
import sys
import base64
import os
import shutil

from gi._glib import GError
from gi.repository import GObject, Gio
from gi.repository import Unity


DBUS_NAME = 'com.canonical.Unity.Scope.AndroidApps'
DBUS_PATH = '/com/canonical/unity/scope/androidapps'

ANDROIDAPP_SCHEME = 'androidapp://'

ICON_CACHE_DIR = os.environ['HOME'] + '/.cache/android/'

LABEL_PATTERN_BLACKLIST = ('AT&T', 'Webtop', 'Phone Portal', 'Settings')


class Application(object):
    def __init__(self, entries):
        tmpapp = {}
        tmpapp["packageName"] = entries[0]
        iconData = base64.b64decode(entries[2])
        iconFileName = ICON_CACHE_DIR+tmpapp["packageName"]+'.png'
        f = open(iconFileName, 'w')
        f.write(iconData)
        f.close()
        tmpapp["label"] = entries[1]
        tmpapp["icon"] = iconFileName
        tmpapp["launchAction"] = entries[3]
        tmpapp["launchActivity"] = entries[4]
        self.__dict__.update(**tmpapp)


class Daemon(object):

    def __init__(self):
        self.scope = Unity.Scope.new(DBUS_PATH)
        self.scope.search_in_global = False
        self.scope.props.sources.add_option("Android", "Android", None)
        self.bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
        self.scope.connect('search-changed', self.on_search_changed)
        self.scope.connect('filters-changed', self.on_filters_changed)
        self.scope.connect('active-sources-changed', self.on_active_sources_changed)
        self.scope.connect('activate-uri', self.on_activate_uri)
        self.scope.export()
        self.init_android_proxy()
        self.init_apps_cache()

    def connect_android_bus(self):
        try:
            self.android_proxy = Gio.DBusProxy.new_sync(self.bus, 0, None,
                                                    'com.canonical.Android',
                                                    '/com/canonical/android/applications/Applications',
                                                    'com.canonical.android.applications.Applications',
                                                    None)
            self.android_proxy.connect('g-signal', self.got_signal)
        except GError, e:
            # bus not available yet
            pass

    def init_android_proxy(self):
        self.connect_android_bus()
        self.android_bus_watcher = Gio.DBusProxy.new_sync(self.bus, 0, None,
                                                    'org.freedesktop.DBus',
                                                    '/org/freedesktop/DBus',
                                                    'org.freedesktop.DBus',
                                                    None)
        self.android_bus_watcher.connect('g-signal', self.got_signal_owner_changed)

    def init_apps_cache(self):
        self.apps_cache = {}
        shutil.rmtree(ICON_CACHE_DIR, ignore_errors=True)
        os.makedirs(ICON_CACHE_DIR)
        try:
            applications = self.android_proxy.listInstalledApplications()
        except GError, e:
            print >> sys.stderr, 'Error: %s' % e.message
        else:
            for application in applications:
                app = Application(application)
                self.apps_cache[app.packageName] = app

    def got_signal_owner_changed(self, proxy, sender, signal, parameters):
        if signal == 'NameOwnerChanged':
            busname = parameters[0]
            old = parameters[1]
            new = parameters[2]
            if busname == 'com.canonical.Android':
                if len(new) != 0:
                    self.connect_android_bus()
                    self.init_apps_cache()
                else:
                    self.apps_cache = {}

    def got_signal(self, proxy, sender, signal, parameters):
        if signal in ('applicationAdded', 'applicationRemoved', 'applicationReplaced'):
            packageName = parameters.get_child_value(0).get_string()
            updated = True
            if signal == 'applicationAdded':
                self.apps_cache[packageName] = Application(self.android_proxy.getApplicationDetails('(s)', packageName))
            elif signal == 'applicationRemoved':
                try:
                    del self.apps_cache[packageName]
                except KeyError:
                    updated = False
            elif signal == 'applicationReplaced':
                self.apps_cache[packageName] = Application(self.android_proxy.getApplicationDetails('(s)', packageName))
            if updated:
                self.scope.invalidate_search(Unity.SearchType.DEFAULT)

    def get_application(self, packageName):
        try:
            application = self.apps_cache[packageName]
        except KeyError:
            application = None
        if application is None:
            try:
                application = self.android_proxy.getApplicationDetails('(s)', packageName)
            except GError, e:
                print >> sys.stderr, 'Error: %s' % e.message
            else:
                application = Application(application)
                self.apps_cache[application.packageName] = application
        return application

    def launch_application(self, packageName):
        application = self.get_application(packageName)
        try:
            applications = self.android_proxy.launchApplication('(ss)', application.launchAction, application.launchActivity)
        except GError, e:
            print >> sys.stderr, 'Error: %s' % e.message

    def is_filtering(self):
        for f in self.scope.props.filters:
            if f.props.filtering:
                return True
        return False

    def update_results_model(self, search, model):
        if self.scope.props.sources.get_option('Android').get_property('active') or not self.scope.props.sources.get_property('filtering'):
            for packageName in self.apps_cache:
                application = self.get_application(packageName)
                if application is None:
                    continue
                if application.launchAction == '' or application.launchActivity == '':
                    continue
                blacklisted = False
                for pattern in LABEL_PATTERN_BLACKLIST:
                    if pattern in application.label:
                        blacklisted = True
                        break
                if blacklisted:
                    continue
                if (search in packageName.lower()) or (search in application.label.lower()):
                    uri = '%s%s' % (ANDROIDAPP_SCHEME, packageName)
                    category = 3 # ANDROID
                    mime = 'application/vnd.android.package-archive'
                    comment = ''
                    dnd_uri = ''
                    model.append(uri, application.icon, category, mime, application.label, comment, dnd_uri)

    def on_search_changed(self, scope, search, search_type, cancellable):
        search_string = search.props.search_string.lower().strip()
        results = search.props.results_model
        results.clear()
        if not self.is_filtering():
            # The filters defined by the applications lens do not
            # make sense when talking about Android applications.
            self.update_results_model(search_string, results)
        search.emit('finished')

    def on_active_sources_changed(self, scope, uri, source):
        self.scope.queue_search_changed(Unity.SearchType.DEFAULT)

    def on_filters_changed(self, scope):
        self.scope.queue_search_changed(Unity.SearchType.DEFAULT)

    def on_activate_uri(self, scope, uri):
        packageName = uri[len(ANDROIDAPP_SCHEME):]
        self.launch_application(packageName)
        return Unity.ActivationResponse(handled=Unity.HandledType.HIDE_DASH, goto_uri='')


if __name__ == '__main__':
    # See http://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-names
    bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
    proxy = Gio.DBusProxy.new_sync(bus, 0, None,
                                   'org.freedesktop.DBus',
                                   '/org/freedesktop/DBus',
                                   'org.freedesktop.DBus', None)
    result = proxy.RequestName('(su)', DBUS_NAME, 0x4)
    if result != 1 :
        print >> sys.stderr, "Name '%s' is already owned on the session bus. Aborting." % DBUS_NAME
        sys.exit(1)

    daemon = Daemon()
    GObject.MainLoop().run()

