[Tutorial] Xposed Module selbst erstellen

Kiwi++Soft

Kiwi++Soft

Ehrenmitglied
32.707
Hallo zusammen,

ich möchte hier mal ein Tutorial vorstellen:

Xposed Module selbst erstellen

Ich habe gerade nach einiger Android-Abstinenz wieder ein Projekt auf Android implementiert. Ich fand das Thema Xposed sehr spannend, und ich hatte zufällig selbst Bedarf an einem Modul, das ich nicht finden konnte: Notification-OnlyIcon.

Da das mein erstes Xposed-Projekt ist, kann ich sehr genau die Entstehung des Moduls reproduzieren, und somit anderen Usern einen Einsteiger-geeigneten Weg in die Xposed-Entwicklung zeigen.

Inhalt:

Die Entwicklungsumgebung Teil 1
Grundlagen zu Xposed
Das Projekt erzeugen
Einsprung-Punkte finden
Das Modul Teil 1 (Erster Test)
Das Modul Teil 2
Die Konfigurations-App
Der Settings-Service: Kommunikation mit dem Modul
Die Entwicklungsumgebung Teil 2 (Weitere OS Versionen)
Das Modul Teil 3 (Android 6.0)
Schutz vor Code-Piraterie
Das Modul im Xposed-Repository hochladen
 
Zuletzt bearbeitet:
  • Danke
Reaktionen: TSC Yoda, TomTim und Darkman
Zu Android-Entwicklung benötigt man auf jeden Fall einen Computer. Ich habe einen PC mit Win 7 verwendet, aber es werden auch andere Betriebssysteme unterstützt. Da sind meine Anleitung analog zu verwenden, ich denke dass es zur Xposed-Entwicklung nichts Betriebssystem spezifisches zu beachten gibt.

Derzeit wird zur Android-Entwicklung das Android-Studio empfohlen. Ich persönlich empfand Eclipse deutlich besser, aber wenn man aktuell bleiben will, muss man sich Android-Studio installieren. Aber ganz ehrlich: Die Code-Completion und Refactoring Fähigkeiten sind im Vergleich mit Eclipse echt lausig.

Download Android-Studio. Hierbei muss man sich entscheiden, ob mit oder ohne Android-SDK (Falls man bereits ein Android-SDK installiert hat). Ebenso die Version für das eigene Betriebssystem herunter laden.

Die herunter geladene Datei Installieren, ich habe hierbei keine besonderen Einstellungen vorgenommen.

Des weiteren benötigt man das Xposed-Api. Dieses wird zwar beim installieren des Xposed-frameworks auf dem Mobiil-Gerät. Jedoch benötigt man eine spezielle Version, gegen die man entwickeln kann. Diese findet man hier: [OFFICIAL] Xposed for Lollipop/Marshmallow [Android 5.0/5.1/6.0, v80, 2016/02/02] Am unteren Ende des Ersten Beitrages kann man eine Datei namens XposedBridgeApi-xxxxxxxx.jar, diese muss man herunterladen und aufbewahren.

Hinweis: Ich spezifiziere keine bestimmte Version und poste keinen Link, um nicht irgendwann eine Veraltete Version zu referenzieren.. Bitte einfach die aktuellste Version von der Seite runter laden.

Damit hätte man eine Entwicklungsumgebung bereit gestellt.
 
  • Danke
Reaktionen: TSC Yoda, TomTim und Darkman
Um ein Xposed Modul zu erstellen, muss man natürlich erst eine genaue Vorstellung haben, was das Xposed-Framework ist.

Hier möchte ich auf meinen Artikel verweisen: Grundlagen zum Xposed Framework

Ich habe den Artikel zwar für dieses Tutorial geschrieben, möchte Ihn aber auch für 'Nichtenwickler' anbieten, und habe ihn daher ausgegliedert.

Des weiteren möchte ich auch noch auf diese Artikel des Xposed-Entwicklers rovo89@xda.developers.com verweisen:

 
Zuletzt bearbeitet:
  • Danke
Reaktionen: TSC Yoda und Darkman
Als ersten Schritt erzeugen wir ein leeres Projekt (ohne sichtbare Komponente) in Android Studio.

  • Im AndroidStudio auf Menü Datei -> Neu -> Neues Projekt
  • In dem Dialog als Projekt-Name Tutorial und als Domaine beispielsweise tutorial.tut eintragen. -> Weiter
  • Telefon und Tablet auswählen und Minimum SDK auf API21 -> Weiter
  • Keine Activity hinzufügen -> Fertig
In dem neuen Projekt-Fenster auf den Tab 'Projekt' an der linken Seite klicken, um die Projekt-Sicht zu aktivieren, und dann in der kleinen Dropdown-Box oben in der Projekt-Sicht die Ansichtsart 'Projekt' wählen.

Der nächste Schritt wird sein, aus einer 'Normalen App' ein Xposed Modul zu machen:

Im Tree in der Projekt-Sicht den Zweig Tutorial -> app -> src -> main die Datei AndroidManifest.xml anklicken.

In der Datei 'AndroidManifest.xml' im XML Pfad <manifest><application> die folgende Code-Passage einfügen:
Code:
        <meta-data
            android:name="xposedmodule"
            android:value="true"/>
        <meta-data
            android:name="xposedminversion"
            android:value="79"/>
        <meta-data
            android:name="xposeddescription"
            android:value="Shows only icon for notifications of selected apps"/>

Damit haben wir unsere App zu einem Xposed-Modul gemacht, das mindestens Xposed-Version 79 verlangt mit der Beschreibung 'Shows only icon for notifications of selected apps'.

In der Projekt-Sicht in dem Zweig Tutorial -> app -> src -> main auf den Ordner 'main' mit rechts Klicken und Neu -> Ordner -> Asset Ordner anwählen, den Dialog bestätigen.

In der Projekt-Sicht in dem Zweig Tutorial -> app -> src -> main -> assets auf den Ordner 'assets' mit rechts Klicken und Neu -> Datei anwählen, und in dem Dialog den Name 'xposed_init' eingeben und bestätigen.

In der Datei 'xposed_init' den folgenden Text eingeben:
Code:
tut.tutorial.tutorial.Module

Damit teilen wir dem Xposed-Framework mit, dass die Klasse 'tut.tutorial.tutorial.Module' diejenige sein wird, die das Xposed-Framework aufrufen soll.

Jetzt müssen wir im Datei-Explorer (welchen auch immer man verwendet) in das Projekt-Verzeichnis gehen und unter dem Ordner 'app' einen Unterordner 'xlib' anlegen und dort das vorhin heruntergeladene XposedBridgeApi-xxxxxxxx.jar rein kopieren. In Android-Studio auf das 'Synchronisieren' Icon (3. Icon von Links) klicken.

Jetzt sollte das XposedBridgeApi.jar im Baum der Projekt-Sicht verfügbar sein.

In der Projekt-Sicht in dem Zweig Tutorial -> app auf die Datei 'build.gradle' klicken, (Vorsicht, es zwei Dateien diese Namens, den richtigen Zweig beachten!).

In der Datei 'build.gradle' in der Section 'dependencies' die folgende Zeile einfügen:
Code:
    provided fileTree(dir: 'xlib', include: ['*.jar'])

Jetzt kann man mal einen Probe-Build anstoßen. Dieser sollte ohne Probleme durchlaufen.

Dann ist unser Tutorial-Projekt erst einmal erstellt.

Ich hänge den von mir bis hierhin erzeugten Projekt-Ordner in gezippter Form an. Wenn man Probleme hatte, kann den runter laden und statt des selbst Erzeugten Projektes verwenden.
 

Anhänge

  • Tutorial.zip
    3,4 MB · Aufrufe: 220
  • Danke
Reaktionen: TSC Yoda und Darkman
Die größte Herausforderung bei der Entwicklung eines Xposed-Moduls ist es, die geeigneten Einsprung-Punkte im Code zu finden.

Dazu muss man sich einerseits im Klaren darüber sein, was man benötigt, bzw was das Xposed-Modul können soll (falls noch nicht geschehen, bitte nochmals die Beschreibung des Moduls lesen: Notification-OnlyIcon):

  • Wenn eine neue Benachrichtigung erzeugt wurde, prüfen ob diese unterdrückt werden soll.
  • Wenn die Benachrichtigung unterdrückt wurde, Ihr Icon in die Benachrichtigungs-Leiste oder in die StatusLeiste einhängen.
  • Wenn die Benachrichtigung geupdatet wird, das Icon anpassen
  • Wenn die Beanchrichtigung gelöscht wird, das Icon entfernen.

Die Sourcen von Andorid kann man entweder über Googles GIT finden, oder man kann sie auf GrepCode anschauen.

Da die Sourcen von Android recht umfangreich sind, bleibt die Frage, wie man die geeignete Klasse finden kann. Wenn man in Google nach den Begriffen 'StatusBar add Notification' findet man auch irgendwann einen Link in dem man die Information erfährt, dass die Klasse 'com.android.systemui.statusbar.PhoneStatusBar' eine Methode 'addNotification(StatusBarNotification, NotificationListenerService.RankingMap)' hat.

Damit kann man in GrepCode gezielt gucken: GC: PhoneStatusBar - com.android.systemui.statusbar.phone.PhoneStatusBar (.java) - GrepCode Class Source

Diese Methode scheint aufgerufen zu werden, wenn eine neue Benachrichtigung zur Anzeige gebracht werden soll, und scheint daher für unser Projekt der geeignete Einsprung-Punkt zu sein.

Da wir nicht nur wissen wollen, wenn eine neue Benachrichtigung gesetzt wird, sondern auch wenn diese wieder von der App gelöscht wird, und wenn sie geändert wird (und sich somit Ihr Icon ändern könnte) müssen wir noch weitere Methoden suchen. In der Klasse PhoneStatusBar finden wir auch noch eine 'removeNotification(String, NotificationListenerService.RankingMap)' Methode. In der BasisKlasse der PhoneStatusBar, BaseStatusBar werden wir noch fündig mit einer Methode 'updateNotification(StatusBarNotification, NotificationListenerService.RankingMap)'.

Weiterhin benötigen wir einen Einsprung, wenn die NotificationIcons verändert werden. Dazu finden wir in der Klasse PhoneStatusBar die Methode 'updateNotifcationIcons()'.

Jetzt benötigen wir noch einen Einsprung, wenn die Anzeige der StatusBar, der StatusBarView erzeugt wurde, um unsere Icons in die StatusBar eindocken zu können. Dazu finden wir in der Klasse PhoneStatusBar die Methode 'makeStatusBarView()'

Mit diesen Informationen gerüstet, kann man einmal anfangen.
 
  • Danke
Reaktionen: TSC Yoda und Darkman
Der nächste Schritt ist es, das Modul selbst zu erstellen.

Dazu in der Projekt-Sicht auf dem Zweig Tutorial -> app -> src -> main -> java -> tut.tutorial.tutorial auf das Pakage 'tut.tutorial.tutorial' rechts Klick und Neu -> Java Klasse auswählen. Im Dialog als Name 'Module' eingeben und bestätigen.

In der Datei 'Module.java' hinter dem Klassenname folgenden Text einfügen:
Code:
 implements IXposedHookLoadPackage

In der Klasse müssen wir jetzt eine Methode zufügen, um dem implementierten Interface zu genügen:
Code:
    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable
    {
     
    }

Das ist die Methode, die das Xposed-Framework aufruft, wenn der Eingang 'Load Package' genutzt werden soll.

Bevor wir diese mit Leben füllen, legen wir uns Konstanten an, mit den Namen der Klassen und Methoden, in die wir uns einhängen wollen:
Code:
   public static final String KIWI = Module.class.getPackage().getName();
    //package and classes
    public static final String SYSTEMUI = "com.android.systemui";
    public static final String PHONE_STATUS_BAR = SYSTEMUI +".statusbar.phone.PhoneStatusBar";
    public static final String BASE_STATUS_BAR = SYSTEMUI +".statusbar.BaseStatusBar";
    //hooked methods
    public static final String MAKE_STATUS_BAR_VIEW = "makeStatusBarView";
    public static final String ADD_NOTIFICATION = "addNotification";
    public static final String REMOVE_NOTIFICATION = "removeNotification";
    public static final String UPDATE_NOTIFICATION = "updateNotification";
    public static final String UPDATE_NOTIFICATION_ICONS = "updateNotificationIcons";

Jetzt definieren wir uns noch ein paar Instanz-Variablen:
Code:
    private Set<String> i_pack = new HashSet<>();
    private LinkedHashMap<String, View> i_icons = new LinkedHashMap<>();
    private LinearLayout i_status;
    private int i_sdk;

Der Zweck der Instanz-Variablen:
  • i_pack ist die Liste der Apps, deren Icons unterdrückt werden soll.
  • i_icons ist eine HashMap, in der wir unter dem Key der Beanchrichtigung das Icon der Benachrichtigungen speichern, die wir unterdrückt haben.
  • i_status ist ein LinearLayout, das wir in die Status-Leiste inhängen, um dor unsere Icons anzeigen zu können.
  • i_sdk ist die SDK-Version des Gerätes, auf dem unser Modul gestartet wird.

Dann können wir damit beginnen, unsere Methode mit leben zu füllen. Als erstes prüfen wir, ob das geladene Package das ist, das uns interessiert, in unserem Fall 'com.android.systemui' (für das wir die Konstante SYSTEMUI definiert haben). Dazu setzen wir diesen Code in die Methode ein:

Code:
        if (SYSTEMUI.equals(lpparam.packageName))
        {
            i_pack = new HashSet<>();
            i_pack.add("dev.ukanth.ufirewall");
            i_sdk = Build.VERSION.SDK_INT;
            XposedBridge.log(KIWI + " started on Sdk " + i_sdk);
        }


Die meisten Anweisungen sollten selbsterklärend sein, und die Anweisung 'i_pack.add("dev.ukanth.ufirewall");' dient dazu, dass wir eine App (hier: AFWall+) in der Liste haben, um einen Test-Case für unser Modul zu haben.

Jetzt haken wir in die erste Methode (addNotification) ein. Dazu fügen wir unter der Log-Anweisung, aber noch vor der schließend Klammer der If-Bedingung folgenden Code ein:
Code:
            XposedHelpers.findAndHookMethod(PHONE_STATUS_BAR, lpparam.classLoader, ADD_NOTIFICATION,
                    StatusBarNotification.class, NotificationListenerService.RankingMap.class, new XC_MethodHook()
                    {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) throws Throwable
                        {
                            StatusBarNotification sbn = (StatusBarNotification) param.args[0];
                            XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " got " + sbn.getPackageName() + ":" + sbn.getKey() + ":" + sbn.getNotification().icon);
                            if (!sbn.isClearable())
                            {
                                if (i_pack.contains(sbn.getPackageName()))
                                {
                                    param.setResult(null);
                                    XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " suppressed " + sbn.getPackageName());
                                }
                                else
                                {
                                    XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " did not find " + sbn.getPackageName() + " in " + i_pack);
                                }
                            }
                            else
                            {
                                XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " found clearable " + sbn.getPackageName());
                            }
                        }
                    });

Was haben wir damit getan?

Wir haben dem Xposed-Framework mitgeteilt, dass wir in die Methode 'addNotification(StatusBarNotification, RankingMap)' einhaken wollen und haben als 'Haken' eine anonyme Klasse, die XC_MethodHook extended, übergeben. Diese anonyme Klasse enthält eine Methode 'beforeHookedMethod(MethodHookParam param)' die vor dem Aufruf von addNotification gerufen wird.

In dieser Methode greifen wir auf das erste Argument (param.args[0]) zu, dass der addNotification Methode übergeben würde, die StatusBarNotification.

Da StatusBarNotificatin eine Klasse ist, die in dem API vorhanden ist, können wir diese Klasse in unserem Code verwenden, und mit einem cast haben wir eine Variable sbn, in der die Notification drin ist.

Wir prüfen nun, ob die Benachrichtigung nicht löschbar ist, und ob sie von einem Package kommt, dessen Benachrichtigungen wir unterdrücken wollen. Wenn ja, passiert folgendes: Wir setzen an dem MethodHookParam param ein Rückgabe-Ergebnis mit 'param.setResult(null);'. Das setzen des Rückgabe-Ergebnisses bewrikt, dass der Aufruf der eingehakten Methode 'addNotification(...)' unterdrückt wir, also dass die Benachrichtigung nicht bei der PhoneStatusBar ankommt.

Jetzt ist ein guter Zeitpunkt, für einen ersten Funktions-Test:

Code:
package tut.tutorial.tutorial;

import android.os.Build;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.view.View;
import android.widget.LinearLayout;

import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Set;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

/**
* Created by uwek on 24.02.2016.
*/
public class Module  implements IXposedHookLoadPackage
{
    public static final String KIWI = Module.class.getPackage().getName();
    //package and classes
    public static final String SYSTEMUI = "com.android.systemui";
    public static final String PHONE_STATUS_BAR = SYSTEMUI +".statusbar.phone.PhoneStatusBar";
    public static final String BASE_STATUS_BAR = SYSTEMUI +".statusbar.BaseStatusBar";
    //hooked methods
    public static final String MAKE_STATUS_BAR_VIEW = "makeStatusBarView";
    public static final String ADD_NOTIFICATION = "addNotification";
    public static final String REMOVE_NOTIFICATION = "removeNotification";
    public static final String UPDATE_NOTIFICATION = "updateNotification";
    public static final String UPDATE_NOTIFICATION_ICONS = "updateNotificationIcons";

    private Set<String> i_pack = new HashSet<>();
    private LinkedHashMap<String, View> i_icons = new LinkedHashMap<>();
    private LinearLayout i_status;
    private int i_sdk;

    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable
    {
        if (SYSTEMUI.equals(lpparam.packageName))
        {
            i_pack = new HashSet<>();
            i_pack.add("dev.ukanth.ufirewall");
            i_sdk = Build.VERSION.SDK_INT;
            XposedBridge.log(KIWI + " started on Sdk " + i_sdk);

            XposedHelpers.findAndHookMethod(PHONE_STATUS_BAR, lpparam.classLoader, ADD_NOTIFICATION,
                    StatusBarNotification.class, NotificationListenerService.RankingMap.class, new XC_MethodHook()
                    {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) throws Throwable
                        {
                            StatusBarNotification sbn = (StatusBarNotification) param.args[0];
                            XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " got " + sbn.getPackageName() + ":" + sbn.getKey() + ":" + sbn.getNotification().icon);
                            if (!sbn.isClearable())
                            {
                                if (i_pack.contains(sbn.getPackageName()))
                                {
                                    param.setResult(null);
                                    XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " suppressed " + sbn.getPackageName());
                                }
                                else
                                {
                                    XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " did not find " + sbn.getPackageName() + " in " + i_pack);
                                }
                            }
                            else
                            {
                                XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " found clearable " + sbn.getPackageName());
                            }
                        }
                    });
        }
    }
}

Dazu schliessen wir unser Gerät (muss bisher Android 5.0 oder 5.1 sein, Android 6.0 Unterstützung kommt noch) per USB-Kabel an den PC an. Entwickler-Optionen und USB Debugging müssen aktiviert sein.

Hat man kein Gerät mit Android 5.0 oder 5.1 zur Hand, bitte in da Kapitel Die Entwicklungsumgebung Part 2 (Weitere OS Versionen) schauen.

Jetzt den grünen Pfeil in AndroidStudio klicken und das Gerät als Ziel-Gerät bestätigen.

Da unsere App noch keine Oberfläche hat, wird man auf dem Gerät nicht viel bemerken, aber der XposedInstaller wird bemerken, dass eine neue App, die als XposedModul gekennzeichnet ist, installiert wurde. Daher sended er eine Benachrichtigung, in der er anbietet, das Modul zu aktivieren und das Gerät neu zu starten.

Wurde das Modul aktiviert und neu gestartet, sollten dauerhafte Benachrichtigungen der AFWall+ unterdrückt werden.

Dieses sollte sich auch in dem Xposed-Log (das man im Xposed-Installer unter Logs findet) niederschlagen.

Es sollten folgende Zeilen zu finden sein:

Code:
02-24 17:12:22.425 I/Xposed  (  916): tut.tutorial.tutorial addNotification got dev.ukanth.ufirewall:0|dev.ukanth.ufirewall|33341|null|10149:2130837613
02-24 17:12:22.426 I/Xposed  (  916): tut.tutorial.tutorial addNotification suppressed dev.ukanth.ufirewall
 
Zuletzt bearbeitet:
  • Danke
Reaktionen: TSC Yoda und Darkman
Wenn der Test erfolgreich verlief, sollte man sich daran machen, die abgefangene Benachrichtigung zur Anzeige als Icon vorzunehmen.

Im Verlauf dieses Schrittes werden noch ein paar Konstanten gebraucht, die ich hier schon mal angebe und bei Verwendung erkläre:
Code:
   //accessed methods and fields
    public static final String CREATE_NOTIFICATION_VIEWS = "createNotificationViews";
    public static final String M_CONTEXT = "mContext";
    public static final String M_NOTIFICATION_ICONS = "mNotificationIcons";
    public static final String M_SYSTEM_ICON_AREA = "mSystemIconArea";
    public static final String ICON = "icon";

Durch Studium des Codes der Methode 'addNotification(...)' (die übrigens erfrischend wenig Kommentare enthält :scared:, ganz wie ich es mag, Kommentare sind für Anfänger und verdecken den Blick auf das, wo die Wahrheit steht: Der Code :thumbup:) findet man, wie man das Icon der Benachrichtigung so vorbereiten kann, dass es sich als View darstellen lässt. Man ruft die Methode createNotificationViews(StatusBarNotification) auf, und kann an dem zurück gegebenen Object in dem Instanz-Feld 'icon' einen View für da Icon bekommen. Diesen View speichern wir in der Map der anzuzeigenden Icons. Als Key verwenden wir den Key der Statusbar-Notification, denn mit diesem Key müssen wir es identifizieren, wenn ein Aufruf der einzuhakenden Methode 'removeNotification(String, RankingMap)' kommt. Das geschieht mit folgendem Code, der in den if-Zwei einzuhängen ist, in dem die Benachrichtigung durch Nichtaufruf der Methode 'addNotification(...)' (siehe 'setResult(null)') unterdrückt wird.
Code:
                                        Object o = XposedHelpers.callMethod(param.thisObject, CREATE_NOTIFICATION_VIEWS, new Class[]{StatusBarNotification.class}, sbn);
                                        View icon = (View) XposedHelpers.getObjectField(o, ICON);
                                        XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " got icon " + icon);
                                        i_icons.put(sbn.getKey(), icon);




Die Icons sichtbar zu machen regeln wir, wenn die einzuhakende Methode updateNotificationIcons(), denn dort werden die Icons der Benachrichtigungs-Leiste neu angeordnet, so dass wir nach dieser Methode erst Icons in die Benachrichtungs-Leiste einhängen können.

Daher muss man die Methode updateNotificationIcons aufrufen, (das wäre in addNotifications(...) auch passiert), damit das Icon der abgefangenen Benachrichtigung angezeigt wird. Dazu direkt unter dem eben eingefügten Code, noch diese Zeile einfügen:
Code:
                                        XposedHelpers.callMethod(param.thisObject, UPDATE_NOTIFICATION_ICONS, new Class[0]);

Nun ist es an der Zeit, die weiteren Ziel-Methoden an den Haken zu nehmen.

In dem Haken von 'removeNotification(...)' wird nur das Icon aus der Map der darzustellenden Icons gelöscht. Ansonsten ist er Analog zum Haken von 'addNotification(...)'. Hier der Code:
Code:
            XposedHelpers.findAndHookMethod(PHONE_STATUS_BAR, lpparam.classLoader, REMOVE_NOTIFICATION,
        String.class, NotificationListenerService.RankingMap.class, new XC_MethodHook()
        {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable
            {
                String key = (String) param.args[0];
                XposedBridge.log(KIWI + " " + REMOVE_NOTIFICATION + " got " + key);
                if (i_icons.containsKey(key))
                {
                    param.setResult(null);
                    i_icons.remove(key);
                    XposedBridge.log(KIWI + " " + REMOVE_NOTIFICATION + " removed " + key);
                    XposedHelpers.callMethod(param.thisObject, UPDATE_NOTIFICATION_ICONS, new Class[0]);
                }
            }
        });

Auch der Haken von updateNotification(...) enthält nichts vorher schon Dagewesenes:
Code:
            XposedHelpers.findAndHookMethod(BASE_STATUS_BAR, lpparam.classLoader, UPDATE_NOTIFICATION, StatusBarNotification.class, NotificationListenerService.RankingMap.class, new XC_MethodHook()
            {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable
                {
                    StatusBarNotification sbn = (StatusBarNotification) param.args[0];
                    XposedBridge.log(KIWI + " " + UPDATE_NOTIFICATION + " got " + sbn.getPackageName() + ":" + sbn.getKey() + ":" + sbn.getNotification().icon);
                    if (i_icons.containsKey(sbn.getKey()))
                    {
                        param.setResult(null);
                        Object o = XposedHelpers.callMethod(param.thisObject, CREATE_NOTIFICATION_VIEWS, new Class[]{StatusBarNotification.class}, sbn);
                        View icon = (View) XposedHelpers.getObjectField(o, "icon");
                        XposedBridge.log(KIWI + " " + UPDATE_NOTIFICATION + " stored icon " + icon);
                        i_icons.put(sbn.getKey(), icon);
                            XposedHelpers.callMethod(param.thisObject, UPDATE_NOTIFICATION_ICONS, new Class[0]);
                    }
                }
            });

Der Haken der Methode 'makeStatusBarView()' hat die Aufgabe, den Platz in der Status-Leiste für unsere abgefangenen Benachrichtigungs-Icons bereit zu stellen, falls wir die Icons dort darstellen wollen. Wir lassen uns erst aufrufen, nachdem der StatusBarView erzeugt wurde. In der 'afterHookedMethod(MethodHookParam param)' Methode greifen wir auf zwei Instanz-Felder der PhoneStatusBar zu, und hängen unser eigenes LinearLayout ein, um dort später IconViews einhängen zu können.
Code:
            XposedHelpers.findAndHookMethod(PHONE_STATUS_BAR, lpparam.classLoader, MAKE_STATUS_BAR_VIEW, new XC_MethodHook()
            {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable
                {
                    Context ctx = (Context) XposedHelpers.getObjectField(param.thisObject, M_CONTEXT);
                    Object psb = param.thisObject;
                    ViewGroup iconArea = (ViewGroup) XposedHelpers.getObjectField(psb, M_SYSTEM_ICON_AREA);
                    i_status = new LinearLayout(ctx);
                    iconArea.addView(i_status, 0);
                }
            });

Es fehlt noch der Haken für die Methode 'updateNotificationIcons()'. Hier greifen auf ein anderes Instanz-Feld zu, der Notification-Icon-Bereich, um dort die IconViews einzuhängen, falls wir sie in der Benachrichtigungs-Leiste anzeigen möchten. Ich möchte dabei darauf hinweisen, dass ich die Einstellung, die Icons in der Status-Leiste anzuzeigen, auch in der Liste der Packages, deren Benachtigungen abgefangen werden, speichere. Ich verwende dafür einen Eintrag in der Liste, der niemals ein Package-Name sein kann, da er Zeichen enthält, die ein Java-Bezeichner nicht enthalten darf. Ich verwende den String "<status>", für den eine weitere Konstante:
Code:
    public static final String STATUS = "<status>";
Weiterhin muss man wissen, dass nach der 'updateNotificationIcons()' Methode alle unsere aus der NotificationIcons Ansicht entfernt sind, aber in unsererm selbst eingehängten LinearLayout nicht. Daher entferne ich auch dort alle Icons, und hänge die Icons in der Map in die ausgewählte Leiste ein.
Ansonsten ist der Haken unspektakulär:
Code:
            XposedHelpers.findAndHookMethod(PHONE_STATUS_BAR, lpparam.classLoader, UPDATE_NOTIFICATION_ICONS, new XC_MethodHook()
            {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable
                {
                    XposedBridge.log(KIWI + " " + UPDATE_NOTIFICATION_ICONS + " called");
                    i_status.removeAllViews();
                    if (i_pack.contains(STATUS))
                    {
                        for (View icon : i_icons.values())
                            i_status.addView(icon, 0);
                    }
                    else
                    {
                        LinearLayout ll = (LinearLayout) XposedHelpers.getObjectField(param.thisObject, M_NOTIFICATION_ICONS);
                        for (View icon : i_icons.values())
                            ll.addView(icon);
                    }
                }
            });

Wenn man nun einen Test startet, sollten die Icons in die Beenachrichtigungs Leiste eingehängt werde. Möchte man die Icons in der Status Leiste haben, muss einfach den folgenden Code nach der Instanziierung der Package-Liste einhängen:
Code:
            i_pack.add(STATUS);

Hier wieder der komplette Code:

Code:
package tut.tutorial.tutorial;

import android.content.Context;
import android.os.Build;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Set;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

/**
* Created by uwek on 24.02.2016.
*/
public class Module  implements IXposedHookLoadPackage
{
    public static final String KIWI = Module.class.getPackage().getName();
    public static final String STATUS = "<status>";
    //package and classes
    public static final String SYSTEMUI = "com.android.systemui";
    public static final String PHONE_STATUS_BAR = SYSTEMUI +".statusbar.phone.PhoneStatusBar";
    public static final String BASE_STATUS_BAR = SYSTEMUI +".statusbar.BaseStatusBar";
    //hooked methods
    public static final String MAKE_STATUS_BAR_VIEW = "makeStatusBarView";
    public static final String ADD_NOTIFICATION = "addNotification";
    public static final String REMOVE_NOTIFICATION = "removeNotification";
    public static final String UPDATE_NOTIFICATION = "updateNotification";
    public static final String UPDATE_NOTIFICATION_ICONS = "updateNotificationIcons";

    //accessed methods and fields
    public static final String CREATE_NOTIFICATION_VIEWS = "createNotificationViews";
    public static final String M_CONTEXT = "mContext";
    public static final String M_NOTIFICATION_ICONS = "mNotificationIcons";
    public static final String M_SYSTEM_ICON_AREA = "mSystemIconArea";
    public static final String ICON = "icon";

    private Set<String> i_pack = new HashSet<>();
    private LinkedHashMap<String, View> i_icons = new LinkedHashMap<>();
    private LinearLayout i_status;
    private int i_sdk;

    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable
    {
        if (SYSTEMUI.equals(lpparam.packageName))
        {
            i_pack = new HashSet<>();
            i_pack.add(STATUS);
            i_pack.add("dev.ukanth.ufirewall");
            i_sdk = Build.VERSION.SDK_INT;
            XposedBridge.log(KIWI + " started on Sdk " + i_sdk);

            XposedHelpers.findAndHookMethod(PHONE_STATUS_BAR, lpparam.classLoader, MAKE_STATUS_BAR_VIEW, new XC_MethodHook()
            {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable
                {
                    Context ctx = (Context) XposedHelpers.getObjectField(param.thisObject, M_CONTEXT);
                    Object psb = param.thisObject;
                    ViewGroup iconArea = (ViewGroup) XposedHelpers.getObjectField(psb, M_SYSTEM_ICON_AREA);
                    i_status = new LinearLayout(ctx);
                    iconArea.addView(i_status, 0);
                }
            });

            XposedHelpers.findAndHookMethod(PHONE_STATUS_BAR, lpparam.classLoader, ADD_NOTIFICATION,
                    StatusBarNotification.class, NotificationListenerService.RankingMap.class, new XC_MethodHook()
                    {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) throws Throwable
                        {
                            StatusBarNotification sbn = (StatusBarNotification) param.args[0];
                            XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " got " + sbn.getPackageName() + ":" + sbn.getKey() + ":" + sbn.getNotification().icon);
                            if (!sbn.isClearable())
                            {
                                if (i_pack.contains(sbn.getPackageName()))
                                {
                                    param.setResult(null);
                                    Object o = XposedHelpers.callMethod(param.thisObject, CREATE_NOTIFICATION_VIEWS, new Class[]{StatusBarNotification.class}, sbn);
                                    View icon = (View) XposedHelpers.getObjectField(o, ICON);
                                    XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " got icon " + icon);
                                    i_icons.put(sbn.getKey(), icon);
                                    XposedHelpers.callMethod(param.thisObject, UPDATE_NOTIFICATION_ICONS, new Class[0]);
                                }
                                else
                                {
                                    XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " did not find " + sbn.getPackageName() + " in " + i_pack);
                                }
                            }
                            else
                            {
                                XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " found clearable " + sbn.getPackageName());
                            }
                        }
                    });
            XposedHelpers.findAndHookMethod(PHONE_STATUS_BAR, lpparam.classLoader, REMOVE_NOTIFICATION,
                    String.class, NotificationListenerService.RankingMap.class, new XC_MethodHook()
                    {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) throws Throwable
                        {
                            String key = (String) param.args[0];
                            XposedBridge.log(KIWI + " " + REMOVE_NOTIFICATION + " got " + key);
                            if (i_icons.containsKey(key))
                            {
                                param.setResult(null);
                                i_icons.remove(key);
                                XposedBridge.log(KIWI + " " + REMOVE_NOTIFICATION + " removed " + key);
                                XposedHelpers.callMethod(param.thisObject, UPDATE_NOTIFICATION_ICONS, new Class[0]);
                            }
                        }
                    });

            XposedHelpers.findAndHookMethod(BASE_STATUS_BAR, lpparam.classLoader, UPDATE_NOTIFICATION, StatusBarNotification.class, NotificationListenerService.RankingMap.class, new XC_MethodHook()
            {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable
                {
                    StatusBarNotification sbn = (StatusBarNotification) param.args[0];
                    XposedBridge.log(KIWI + " " + UPDATE_NOTIFICATION + " got " + sbn.getPackageName() + ":" + sbn.getKey() + ":" + sbn.getNotification().icon);
                    if (i_icons.containsKey(sbn.getKey()))
                    {
                        param.setResult(null);
                        Object o = XposedHelpers.callMethod(param.thisObject, CREATE_NOTIFICATION_VIEWS, new Class[]{StatusBarNotification.class}, sbn);
                        View icon = (View) XposedHelpers.getObjectField(o, "icon");
                        XposedBridge.log(KIWI + " " + UPDATE_NOTIFICATION + " stored icon " + icon);
                        i_icons.put(sbn.getKey(), icon);
                        XposedHelpers.callMethod(param.thisObject, UPDATE_NOTIFICATION_ICONS, new Class[0]);
                    }
                }
            });
            XposedHelpers.findAndHookMethod(PHONE_STATUS_BAR, lpparam.classLoader, UPDATE_NOTIFICATION_ICONS, new XC_MethodHook()
            {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable
                {
                    XposedBridge.log(KIWI + " " + UPDATE_NOTIFICATION_ICONS + " called");
                    i_status.removeAllViews();
                    if (i_pack.contains(STATUS))
                    {
                        for (View icon : i_icons.values())
                            i_status.addView(icon, 0);
                    }
                    else
                    {
                        LinearLayout ll = (LinearLayout) XposedHelpers.getObjectField(param.thisObject, M_NOTIFICATION_ICONS);
                        for (View icon : i_icons.values())
                            ll.addView(icon);
                    }
                }
            });
        }
    }
}
 
  • Danke
Reaktionen: TSC Yoda und Darkman
Es ist nun an der Zeit, eine Konfigurations-App bereitzustellen. Da ich davon ausgehe, dass die meisten Leser dieses Tutorials schon mal eine App erstellt haben, werde ich das Konzept des Android-Life-Cycle als bekannt voraussetzen. Wenn das Thema jedoch unbekannt sein sollte, verweise ich auf dieses Tutorial: Managing the Activity Lifecycle | Android Developers

Jetzt erstellen wir eine neue Activity. Dazu gehen wir in der Projekt-Sicht auf den Zweig Tutorial -> app -> src -> main -> java -> tut.tutorial.tutorial und rechts Klick auf das Package tut.tutorial.tutorial Neu -> Activity -> 'Empty Activity' (Wichtig: Es gibt auchen Menü-Punkt Blank Activity, das ist etwas anderes und nicht geeignet! Ich weiß aber leider nicht wie es auf Deutsch genau bezeichnet wird, denn ich habe ein Englisches OS und AndroidStudio). Im Dialog den Namen 'SettingActivity' angeben und das Layout mit Namen 'activity_settings.xml'. Launcher-Icon anhaken! Dann bestätigen.

Das sollte uns ein Klasse SettingsActivity.java erzeugt haben, die so aussieht:
Code:
package tut.tutorial.tutorial;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class SettingsActivity extends AppCompatActivity
{

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_settings);
    }
}

Ebenso wurde ein Layout activity_settings.xml erzeugt das so aussieht:

Code:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="tut.tutorial.tutorial.SettingsActivity">

</RelativeLayout>

Dieses ergänzen wir mit einem ListView (Ich setze voraus, das die Android-XML-Notation bekannt ist:
Code:
    <ListView
        android:id="@+id/list"
        android:layout_height="wrap_content"
        android:layout_width="match_parent">

    </ListView>

Nun erzeugen wir noch ein Menü. In der ProjektSicht auf den Zweig Tutorial -> app -> main -> src -> res und auf res rechts Klick. Neu -> Ordner. Im Dialog den Namen 'menu' eingeben, und bestätigen. Auf dem neuen Ordner 'menu' erneut rechts Klick. Neu -> Datei. Im Dialog den Namen settings_menu.xml eingeben und in die neue Datei folgendes Eintragen (Auch hier gehe ich nicht weiter auf die Funktion ein, sondern setze diese Android-Kentnis vorraus):
Code:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <group android:checkableBehavior="single">
        <item android:id="@+id/item_notif"
              android:title="Icons in notificationbar" />
        <item android:id="@+id/item_status"
              android:title="Icons in statusbar" />
    </group>
</menu>

In der Klasse 'SettingsActivity' benötigen wir ein paar Konstanten:
Code:
    public static final String KIWI = SettingsActivity.class.getPackage().getName();
    public static final String SETTINGS_ACTIVITY = SettingsActivity.class.getSimpleName();
    public static final String ACTION_PACK_ADD = KIWI+".pack_add";
    public static final String ACTION_PACK_REMOVE = KIWI+".pack_remove";
    public static final String ACTION_ALL_PACK = KIWI+".all_packs";
    public static final String ACTION_SEND_PACKS = KIWI+".send_packs";
    public static final String PACKAGES = "packages";
    public static final String PACKAGE = "package";
    public static final String PREFS_PATH = "/data/"+KIWI+"/shared_prefs";
    public static final String PREFS_NAME = "notoi.prefs";
    public static final String STATUS = "<status>";

und Instanzvariablen:
Code:
    private Set<String> i_pack;
    private PackageManager i_pm;
Die Verwendung der Instanz-Variablen:
i_pack: Die Liste der Apps (Packages), deren Benachrichtigungen abzufangen sind.
i_pm: Der PackageManager, der an verschiedenen Stellen benötigt wird.

Nun ergänzen wir die 'onCreate(...)' Methode. Wir lesen unsere Settings aus, holen uns vom PackageManger die Liste der installierten Packages, filtern nur die 'Enableten' heraus uns sortieren die nach App-Name. Zum Schluss erzeugen wir einen ArrayAdapter (mit der internen Klasse AppArrayAdapter, siehe weiter unten) und setzen den bei unserem ListView.
Code:
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        i_pack = readPrefs();
        setContentView(R.layout.activity_settings);
        ArrayList<ApplicationInfo> list = new ArrayList<>();
        i_pm = getPackageManager();
        for (ApplicationInfo info: i_pm.getInstalledApplications(PackageManager.GET_META_DATA))
        {
            if (info.enabled)
            {
                list.add(info);
            }
        }
        Collections.sort(list, new Comparator<ApplicationInfo>()
        {
            public int compare(ApplicationInfo a, ApplicationInfo b)
            {
                return a.loadLabel(i_pm).toString().compareTo(b.loadLabel(i_pm).toString());
            }
        });
        AppArrayAdapter aaa = new AppArrayAdapter(this,list);
        ((ListView)findViewById(R.id.list)).setAdapter(aaa);

    }

Die nächsten drei Methoden überschreiben das Event-Handler für Menüs in Android und sind auch relativer Standard. Zu beachten ist jedoch, dass als Reaktion auf das Ändern eines Menü-Eintrages ein Intent, eine App-Übergreifende Botschaft, gesendet wird. Dazu gleich noch mehr. Auch hier wieder beachten, dass die Konfiguration der Position der Icons einfach mit dem Set i_pack ist (siehe dazu das vorherige Kapitel). Hier der Code:
Code:
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.settings_menu, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    Intent i;
    item.setChecked(true);
    switch (item.getItemId()) {
        case R.id.item_notif:
            i_pack.remove(STATUS);
            i = new Intent(ACTION_PACK_REMOVE);
            i.putExtra(PACKAGE, STATUS);
            sendBroadcast(i);
            writePrefs();
            return true;
        case R.id.item_status:
            i_pack.add(STATUS);
            i = new Intent(ACTION_PACK_ADD);
            i.putExtra(PACKAGE, STATUS);
            sendBroadcast(i);
            writePrefs();
            return true;
        default:
            return super.onOptionsItemSelected(item);
    }
}

@Override
public boolean onPrepareOptionsMenu(Menu menu)
{
    MenuItem i;
    i = menu.findItem(i_pack.contains(STATUS)?R.id.item_status:R.id.item_notif);
    i.setChecked(true);
    return super.onPrepareOptionsMenu(menu);
}

Die nächsten beiden Methoden widmen sich dem Lesen und Schreiben der Einstellungen. Da ich es schlicht mag, habe ich nicht die standard Mechanismen von Android genommen, sondern die Tatsache genututz, dass die ganze Konfiguration in einer einzigen Liste von Strings gespeichert ist, und kein String ein Leerzeeichen enthalten darf. Also packe ich diese Strings in einen langen StringBuilder, stets mit Leerzeichen getrennt. Den tieferen Grund bespreche ich später. Hier der Code:
Code:
    private Set<String> readPrefs()
    {
        HashSet<String> pack = new HashSet<>();
        File dir = new File(Environment.getDataDirectory() + PREFS_PATH);
        File fi = new File(dir,PREFS_NAME);
        if (fi.exists())
        {
            try
            {
                FileReader fr = new FileReader(fi);
                StringBuilder sb = new StringBuilder();
                char[] buffer = new char[1024];
                int r = fr.read(buffer);
                while (r > 0)
                {
                    sb.append(buffer, 0, r);
                    r = fr.read(buffer);
                }
                fr.close();
                StringTokenizer tok = new StringTokenizer(sb.toString()," ");
                while(tok.hasMoreTokens())
                {
                    pack.add(tok.nextToken());
                }
            }
            catch (Exception e)
            {
                Log.e("Xposed", KIWI+" "+SETTINGS_ACTIVITY+" Exception reading prefs " + e);
            }
        }
        return pack;
    }

    private void writePrefs()
    {
        File dir = new File(Environment.getDataDirectory() + PREFS_PATH);
        dir.mkdirs();
        File fo = new File(dir,PREFS_NAME);
        StringBuilder sb = new StringBuilder();
        for (String s:i_pack)
        {
            sb.append(s).append(" ");
        }
        try
        {
            FileWriter fw = new FileWriter(fo);
            fw.write(sb.toString());
            fw.close();
            fo.setReadable(true,false);
        }
        catch (Exception e)
        {
            Log.e("Xposed", KIWI+" "+SETTINGS_ACTIVITY+" Exception writing prefs " + e);
        }
    }

Nun fehlt noch der Array-Adapter, der ist als innere Klasse AppArrayAdapter implementiert. Auch hier ist die Auffälligkeit, dass bei einer Änderung der Einstllung ein Intent gesendet wird. Hier der Code:

Code:
    private class AppArrayAdapter extends ArrayAdapter<ApplicationInfo>
    {
        private final Context i_context;
        private final ArrayList<ApplicationInfo> i_list;

        private  AppArrayAdapter(Context context, ArrayList<ApplicationInfo> list)
        {
            super(context, R.layout.app_row, list);
            this.i_context = context;
            this.i_list = list;
        }

        @Override
        public View getView(int pos, View convertView, ViewGroup parent)
        {
            View row = getLayoutInflater().inflate(R.layout.app_row, parent, false);
            ApplicationInfo info = i_list.get(pos);
            final String pack = info.packageName;
            ((ImageView)row.findViewById(R.id.item_icon)).setImageDrawable(info.loadIcon(i_pm));
            ((TextView)row.findViewById(R.id.item_text)).setText(info.loadLabel(i_pm));
            CheckBox check = (CheckBox) row.findViewById(R.id.item_check);
            check.setChecked(i_pack.contains(pack));
            check.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener()
            {
                @Override
                public void onCheckedChanged(CompoundButton button, boolean checked)
                {
                    if (checked && !i_pack.contains(pack))
                    {
                        i_pack.add(pack);
                        Intent i = new Intent(ACTION_PACK_ADD);
                        i.putExtra(PACKAGE, pack);
                        sendBroadcast(i);
                        writePrefs();
                    }
                    else if (!checked && i_pack.contains(pack))
                    {
                        i_pack.remove(pack);
                        Intent i = new Intent(ACTION_PACK_REMOVE);
                        i.putExtra(PACKAGE, pack);
                        sendBroadcast(i);
                        writePrefs();
                    }
                }
            });
            return row;

        }
    }

In diesem Code wird allerdings noch das Lauyout für die Zeilen benötigt. Dazu muss man in der Projekt-Sicht den Zweig Tutorial -> app -> src -> main -> res -> layout aud den Ordner 'layout' rechts Klick und Neu -> Neue Layout Res Datei. m Dialog den Namen app_row.xml und als Root-Element RelativeLayout wählen. Dieses mit diesem Inhalt überschreiben:
Code:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="56dp">
    <ImageView
        android:id="@+id/item_icon"
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:layout_alignParentLeft="true"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginTop="12dp"
        />

    <!-- title -->
    <TextView
        android:id="@+id/item_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@+id/item_icon"
        android:layout_toLeftOf="@+id/item_check"
        android:layout_alignBottom="@+id/item_check"
        android:textSize="18dp" />

    <!-- counter -->
    <CheckBox
        android:id="@+id/item_check"
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:layout_alignParentRight="true"
        android:layout_marginRight="8dp"
        android:layout_marginTop="12dp"
        android:gravity="center"
        android:backgroundTint="#FFFFFF" />

</RelativeLayout>

Jetzt sollte unser Code wieder 'Ohne offene Stellen' sein, so dass ein guter Zeitpunkt für einen Test wäre.

Da in diesem Schritt wieder sehr viel verschiedene Dateien erzeugt wurden, zeige ich nicht nur den Source einer einzelnen Datei an, sondern hänge wieder den ganzen Projekt-Ordner als Attachment an diesen Beitrag.

Die SettingsActivity sollte in der Lage sein, ihre Einstellung zu persitieren und bei der nächsten Sitzung wieder herzustellen. Allerdings besteht noch keine Interaktion mit dem Modul. Damit befassen wir uns in dem nächsten Abschnitt.
 

Anhänge

  • Tutorial.zip
    3,5 MB · Aufrufe: 193
  • Danke
Reaktionen: Darkman und TSC Yoda
Bei der Entwicklung eines Xposed-Modules muss einen Aspekt beachten: Während die Konfigurations-App im ihrem ganz normal Kontext läuft, ist das Modul im Kontext der App, die es modifiziert. Im unserem Fall bedeutet das, dass unser Modul im Kontext des SystemUI läuft.

Daher ist eine Interaktion zwischen Modul und Konfigurations-App ganz elegant über Broadcasts zu lösen. Im vorherigen Kaptiel haben wir bereits gesehen, dass die Konfiguration Intents sendet, wenn die Konfiguration verändert wurde. Wenn nun das Modul diese empfangen würde, könnte es leicht zur Laufzeit auf Änderungen der Konfiguration reagieren.

Ein guter Zeitpunkt, sich als BroadcatReceiver zu registrieren, ist in der 'afterHookedMethod()' Methode des MethodHooks der 'makeStatusBarView()' Methode. Die wir zu einem sehr frühen Zeitpunkt genau einmal aufgerufen. Und den Context des SystemUI haben wir zu dieser Methode bereits ausgelesen. Ein weiterer Vorteil ist es, dass man zu diesem Zeitpunkt gut die Initiale Konfiguration lesen könnte. Dafür verwendet das Modul einen Intent, mit dem es einen Service im Kontext der Konfiguration startet, der dann die Konfiguration über einen Intent an das Modul zurück sendet.

Hier die veränderte Methode:
Code:
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable
                {
                    final Context ctx = (Context) XposedHelpers.getObjectField(param.thisObject, M_CONTEXT);
                    final Object psb = param.thisObject;
                    IntentFilter filter = new IntentFilter();
                    filter.addAction(SettingsActivity.ACTION_PACK_ADD);
                    filter.addAction(SettingsActivity.ACTION_PACK_REMOVE);
                    filter.addAction(SettingsActivity.ACTION_ALL_PACK);
                    ctx.registerReceiver(new BroadcastReceiver()
                    {
                        @Override
                        public void onReceive(Context context, Intent intent)
                        {
                            if (intent.getAction().equals(SettingsActivity.ACTION_PACK_ADD))
                            {
                                String s = intent.getStringExtra(SettingsActivity.PACKAGE);
                                i_pack.add(s);
                                XposedBridge.log(KIWI + " Broadcast add got " + intent.getStringExtra(SettingsActivity.PACKAGE));
                                if (STATUS.equals(s))
                                {
                                    XposedHelpers.callMethod(psb, UPDATE_NOTIFICATION_ICONS, new Class[0]);
                                }
                            }
                            else if (intent.getAction().equals(SettingsActivity.ACTION_PACK_REMOVE))
                            {
                                String s = intent.getStringExtra(SettingsActivity.PACKAGE);
                                i_pack.remove(s);
                                XposedBridge.log(KIWI + " Broadcast remove got " + intent.getStringExtra(SettingsActivity.PACKAGE));
                                if (STATUS.equals(s))
                                {
                                    XposedHelpers.callMethod(psb, UPDATE_NOTIFICATION_ICONS, new Class[0]);
                                }
                            }
                            else if (intent.getAction().equals(SettingsActivity.ACTION_ALL_PACK))
                            {
                                HashSet<String> pack = new HashSet<String>();
                                StringTokenizer tok = new StringTokenizer(intent.getStringExtra(SettingsActivity.PACKAGES), " ");
                                while (tok.hasMoreTokens())
                                    pack.add(tok.nextToken());
                                XposedBridge.log(KIWI + " Broadcast all got " + intent.getStringExtra(SettingsActivity.PACKAGES));
                                i_pack = pack;
                            }
                        }
                    }, filter);
                    try
                    {
                        Intent i = new Intent(SettingsActivity.ACTION_SEND_PACKS);
                        i.setPackage(KIWI);
                        ctx.startService(i);
                        XposedBridge.log(KIWI + " " + MAKE_STATUS_BAR_VIEW + " sent intent...");
                    }
                    catch (Throwable t)
                    {
                        XposedBridge.log(KIWI + " " + MAKE_STATUS_BAR_VIEW + " failed intent: " + t);
                    }
                    ViewGroup iconArea = (ViewGroup) XposedHelpers.getObjectField(psb, M_SYSTEM_ICON_AREA);
                    i_status = new LinearLayout(ctx);
                    iconArea.addView(i_status, 0);
                }

Jetzt muss man das Setzen der Debug-Konfiguration ausschalten, in dem man folgende Zeilen entfernt:
Code:
            i_pack.add(STATUS);
            i_pack.add("dev.ukanth.ufirewall");

Nun muss noch der Service erzeugt werden. Wenn man (zumindest bei meiner Version von AndroidStudio) den Service erzeugen läst, wird eine unglaubliche Menge Müll erzeugt. Ich habe es dennoch getan. In der Projekt-Sicht auf den Zweig Tutorial -> app -> src -> main -> java -> tut.turial.tutorial aud das Package 'tut.turial.tutorial' rechts Klick. Neu -> Service -> Service (IntentService) Im Dialog den Namen 'SettingsService' angeben und den Hacken zum Erzeugen von Stubs entfernen. Den Inhalt der Datei hiermit überschreiben:
Code:
package tut.tutorial.tutorial;

import android.app.IntentService;
import android.content.Intent;
import android.os.Environment;
import android.util.Log;

import java.io.File;
import java.io.FileReader;

public class SettingsService extends IntentService
{
    public static final String SETTINGS_SERVICE = "SettingsService";

    public SettingsService()
    {
        super(SETTINGS_SERVICE);
    }

    @Override
    protected void onHandleIntent(Intent intent)
    {
        if (intent != null)
        {
            Log.v("Xposed", SettingsActivity.KIWI + " " + SETTINGS_SERVICE + " got intent...");
            try
            {
                String s = "";
                File dir = new File(Environment.getDataDirectory() + SettingsActivity.PREFS_PATH);
                File fi = new File(dir, SettingsActivity.PREFS_NAME);
                if (fi.exists())
                {
                    try
                    {
                        FileReader fr = new FileReader(fi);
                        StringBuilder sb = new StringBuilder();
                        char[] buffer = new char[1024];
                        int r = fr.read(buffer);
                        while (r > 0)
                        {
                            sb.append(buffer, 0, r);
                            r = fr.read(buffer);
                        }
                        fr.close();
                        s = sb.toString();
                    }
                    catch (Exception e)
                    {
                        Log.e("Xposed", SettingsActivity.KIWI+" "+SETTINGS_SERVICE+" Exception reading prefs " + e);
                    }
                }
                Intent i = new Intent(SettingsActivity.ACTION_ALL_PACK);
                i.putExtra(SettingsActivity.PACKAGES, s);
                sendBroadcast(i);
                Log.v("Xposed", SettingsActivity.KIWI+" "+SETTINGS_SERVICE+" got intent...");
            }
            catch (Throwable t)
            {
                Log.e("Xposed", SettingsActivity.KIWI+" "+SETTINGS_SERVICE+" Exception in handle intent " + t);
            }
        }
    }
}

Dann muss der Service in der AndroidManifest.xml mit folgendem überschrieben werden:
Code:
        <service
            android:name=".SettingsService"
            android:exported="true">
            <intent-filter>
                <action android:name="tut.tutorial.tutorial.send_packs"/>
            </intent-filter>
        </service>

Und wieder einmal ist der Punkt für einen Testlauf erreicht. Das Modul sollte jetzt auf einem Android 5.0 oder 5.1 Gerät einwandfrei laufen. Da auch diesmal mehrere Dateien geändert wurden, hänge ich wieder das ganze Projekt-Verzeichnis an diesen Beitrag an.
 

Anhänge

  • Tutorial.zip
    3,5 MB · Aufrufe: 170
  • Danke
Reaktionen: Darkman und TSC Yoda
Möchte man weitere Android Versionen unterstützen (hier am Beispiel Android 6.0) aber es steht kein geeignete Gerät zur Verfügung, so kann man sich sehr gut behelfen, indem man den Android-Emulator 'Genymotion' verwendet. Dieser setzt der Virtuellen Maschine 'Vitrual Box' und ist performant genug, um als Testumgebung zu fungieren.

Der 'Genymotion' Emulator ist für privat-Anwender kostenlos und kann hier heruntergeladen werden: https://www.genymotion.com/

Wichtig: Beim Download ist zu entscheiden, ob man einen Version im Bundle mit Oracle Virtual Box oder ohne Bundle herunter lädt. Wenn man auf seinem PC bereits Virtual Box installiert hat, besser nur Genymotion runter laden, ansonsten das Bundle.

Nun die Software Installieren. Wenn das erfolgreich abgeschlossen ist, kann man GenyMotion starten, und ein neues Virtuelles Android-Gerät konfigurieren.

Ich habe mich für ein 'Custom Phone - 6.0.0 - API 23 - 768x1280' entschieden. Nach dem das Gerät erzeugt wurde, muss man es starten, um das Xposed-Framework zu installieren.

Das Xposed-Framework auf Genymotion installieren:


Zunächst muss man folgende Dateien herunterladen:
Zur Installation ist folgende Vorgehensweise (Reihenfolge!) empfehlenswert:
  • Das Virtuelle Gerät starten.
  • Das GenyFlash.zip entpacken und in dem entpackten Ordner die Datei install.bat ausführen.
  • Das XposedInstaller_xxx.apk per 'Drag and Drop' in das Fenster des Virtuellen Gerätes ziehen, dort fallen lassen. Dadurch wird der XposedInstaller installiert.
  • Den Xposed-Installer starten, um die Erzeugung der App-Datei-Struktur zu erzwingen.
  • Das xposed-vxxx-sdk23-x86.zip per 'Drag and Drop' in das Fenster des Virtuellen Gerätes ziehen, dort fallen lassen. In dem Dialog das Installieren des Zips bestätigen.
  • Das Virtuelle Gerät neu starten.
  • Den Xposed Installer starten und den Xposed-Zustand unter 'Framework' überprüfen. Jetzt sollte die installierte Version aktiv sein.
Somit kann man sich diverse virtuelle Geräte erstellen, um sein Modul auf mehreren Android-Versionen zu testen.

Jetzt können wir uns im nächsten Abschnitt mit den Anpassungen für Android 6.0 beschäftigen.
 
  • Danke
Reaktionen: Darkman und TSC Yoda
Hier beschäftigen wir uns damit, das Modul auch für Android 6.0 nutzbar zu machen.

Wir betrachten die Unterschiede im API zwischen Android 5.1 und Android 6.0. Folgende Punkte können als für uns relevant gefunden werden:

  • Die Methode 'updateNotificationIcons()' wurde von der Klasse PhoneStatusBar in die Klasse StatusBarIconController verlagert. Das zwingt uns, beim Aufruf dieser Methode ein 'This-Object' entsprechenden Typs zu haben. Das 'This-Object' ist abhängig von dem gewählten SDK zu verwenden.
  • Wir benötigen aus o.a. Grund eine Instanz der Klasse StatusBarIconController. Diese finden wir in der PhoneStatusBar in der Instanz-Variablen 'mIconController'. Diese Feld können wir auslesen wenn die afterHookedMethod(...) Methode der Klasse
  • Wir müssen, abhängig von der SDK-Version somit die Methode 'updateNotificationIcons()' an verschieden zum Einhaken registrieren.
  • Die Signatur der Methode addNotification wurde geändert, auch diese ist abhängig von der SDK-Version zum 'Einhaken' zu registrieren.
Die Codeänderungen sind mit dieser Vorkenntnis gut zu verstehen, es tauchen an verschiedenen Stelle if/else Zweige auf, die den o.a.Unterschieden Rechnung tragen.

Ich veröffentliche hier den Code im Ganzen, aber man kann die Android-Version abhängigen Zweige leicht erkennen.

Code:
package tut.tutorial.tutorial;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.StringTokenizer;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

/**
* Created by uwek on 24.02.2016.
*/
public class Module  implements IXposedHookLoadPackage
{
    public static final String KIWI = Module.class.getPackage().getName();
    public static final String STATUS = "<status>";
    //package and classes
    public static final String SYSTEMUI = "com.android.systemui";
    public static final String PHONE_STATUS_BAR = SYSTEMUI +".statusbar.phone.PhoneStatusBar";
    public static final String BASE_STATUS_BAR = SYSTEMUI +".statusbar.BaseStatusBar";
    public static final String ICON_CONTROLLER = SYSTEMUI+".statusbar.phone.StatusBarIconController";
    public static final String NOTIFICATION_DATA = SYSTEMUI+".statusbar.NotificationData";
    public static final String NOTIFICATION_DATA_ENTRY = NOTIFICATION_DATA+"$Entry";
    //hooked methods
    public static final String MAKE_STATUS_BAR_VIEW = "makeStatusBarView";
    public static final String ADD_NOTIFICATION = "addNotification";
    public static final String REMOVE_NOTIFICATION = "removeNotification";
    public static final String UPDATE_NOTIFICATION = "updateNotification";
    public static final String UPDATE_NOTIFICATION_ICONS = "updateNotificationIcons";

    //accessed methods and fields
    public static final String CREATE_NOTIFICATION_VIEWS = "createNotificationViews";
    public static final String M_CONTEXT = "mContext";
    public static final String M_NOTIFICATION_ICONS = "mNotificationIcons";
    public static final String M_SYSTEM_ICON_AREA = "mSystemIconArea";
    public static final String ICON = "icon";
    public static final String M_ICON_CONTROLLER = "mIconController";
    public static final String M_NOTIFICATION_DATA = "mNotificationData";

    private Set<String> i_pack = new HashSet<>();
    private LinkedHashMap<String, View> i_icons = new LinkedHashMap<>();
    private LinearLayout i_status;
    private Object i_iconCrtl;
    private int i_sdk;

    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable
    {
        if (SYSTEMUI.equals(lpparam.packageName))
        {
            i_pack = new HashSet<>();
            i_sdk = Build.VERSION.SDK_INT;
            XposedBridge.log(KIWI + " started on Sdk " + i_sdk);
            XposedHelpers.findAndHookMethod(PHONE_STATUS_BAR, lpparam.classLoader, MAKE_STATUS_BAR_VIEW, new XC_MethodHook()
            {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable
                {
                    final Context ctx = (Context) XposedHelpers.getObjectField(param.thisObject, M_CONTEXT);
                    final Object psb = param.thisObject;
                    if (i_sdk==23)
                        i_iconCrtl = XposedHelpers.getObjectField(psb, M_ICON_CONTROLLER);
                    IntentFilter filter = new IntentFilter();
                    filter.addAction(SettingsActivity.ACTION_PACK_ADD);
                    filter.addAction(SettingsActivity.ACTION_PACK_REMOVE);
                    filter.addAction(SettingsActivity.ACTION_ALL_PACK);
                    ctx.registerReceiver(new BroadcastReceiver()
                    {
                        @Override
                        public void onReceive(Context context, Intent intent)
                        {
                            if (intent.getAction().equals(SettingsActivity.ACTION_PACK_ADD))
                            {
                                String s = intent.getStringExtra(SettingsActivity.PACKAGE);
                                i_pack.add(s);
                                XposedBridge.log(KIWI + " Broadcast add got " + intent.getStringExtra(SettingsActivity.PACKAGE));
                                if (STATUS.equals(s))
                                {
                                    if (i_sdk < 23)
                                        XposedHelpers.callMethod(psb, UPDATE_NOTIFICATION_ICONS, new Class[0]);
                                    else
                                        XposedHelpers.callMethod(i_iconCrtl, UPDATE_NOTIFICATION_ICONS, XposedHelpers.getObjectField(psb, M_NOTIFICATION_DATA));
                                }
                            }
                            else if (intent.getAction().equals(SettingsActivity.ACTION_PACK_REMOVE))
                            {
                                String s = intent.getStringExtra(SettingsActivity.PACKAGE);
                                i_pack.remove(s);
                                XposedBridge.log(KIWI + " Broadcast remove got " + intent.getStringExtra(SettingsActivity.PACKAGE));
                                if (STATUS.equals(s))
                                {
                                    if (i_sdk < 23)
                                        XposedHelpers.callMethod(psb, UPDATE_NOTIFICATION_ICONS, new Class[0]);
                                    else
                                        XposedHelpers.callMethod(i_iconCrtl, UPDATE_NOTIFICATION_ICONS, XposedHelpers.getObjectField(psb, M_NOTIFICATION_DATA));
                                }
                            }
                            else if (intent.getAction().equals(SettingsActivity.ACTION_ALL_PACK))
                            {
                                HashSet<String> pack = new HashSet<String>();
                                StringTokenizer tok = new StringTokenizer(intent.getStringExtra(SettingsActivity.PACKAGES), " ");
                                while (tok.hasMoreTokens())
                                    pack.add(tok.nextToken());
                                XposedBridge.log(KIWI + " Broadcast all got " + intent.getStringExtra(SettingsActivity.PACKAGES));
                                i_pack = pack;
                            }
                        }
                    }, filter);
                    try
                    {
                        Intent i = new Intent(SettingsActivity.ACTION_SEND_PACKS);
                        i.setPackage(KIWI);
                        ctx.startService(i);
                        XposedBridge.log(KIWI + " " + MAKE_STATUS_BAR_VIEW + " sent intent...");
                    }
                    catch (Throwable t)
                    {
                        XposedBridge.log(KIWI + " " + MAKE_STATUS_BAR_VIEW + " failed intent: " + t);
                    }
                    ViewGroup iconArea = (ViewGroup) XposedHelpers.getObjectField(i_sdk < 23 ? psb : i_iconCrtl, M_SYSTEM_ICON_AREA);
                    i_status = new LinearLayout(ctx);
                    iconArea.addView(i_status, 0);
                }
            });
            if (i_sdk<23)
            {
                XposedHelpers.findAndHookMethod(PHONE_STATUS_BAR, lpparam.classLoader, ADD_NOTIFICATION,
                        StatusBarNotification.class, NotificationListenerService.RankingMap.class, new XC_MethodHook()
                        {
                            @Override
                            protected void beforeHookedMethod(MethodHookParam param) throws Throwable
                            {
                                StatusBarNotification sbn = (StatusBarNotification) param.args[0];
                                XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " got " + sbn.getPackageName() + ":" + sbn.getKey() + ":" + sbn.getNotification().icon);
                                if (!sbn.isClearable())
                                {
                                    if (i_pack.contains(sbn.getPackageName()))
                                    {
                                        param.setResult(null);
                                        Object o = XposedHelpers.callMethod(param.thisObject, CREATE_NOTIFICATION_VIEWS, new Class[]{StatusBarNotification.class}, sbn);
                                        View icon = (View) XposedHelpers.getObjectField(o, ICON);
                                        XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " got icon " + icon);
                                        i_icons.put(sbn.getKey(), icon);
                                        XposedHelpers.callMethod(param.thisObject, UPDATE_NOTIFICATION_ICONS, new Class[0]);
                                    }
                                    else
                                    {
                                        XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " did not find " + sbn.getPackageName() + " in " + i_pack);
                                    }
                                }
                                else
                                {
                                    XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " found clearable " + sbn.getPackageName());
                                }
                            }
                        });
            }
            else
            {
                XposedHelpers.findAndHookMethod(PHONE_STATUS_BAR, lpparam.classLoader, ADD_NOTIFICATION,
                        StatusBarNotification.class, NotificationListenerService.RankingMap.class, NOTIFICATION_DATA_ENTRY, new XC_MethodHook()
                        {
                            @Override
                            protected void beforeHookedMethod(MethodHookParam param) throws Throwable
                            {
                                StatusBarNotification sbn = (StatusBarNotification) param.args[0];
                                XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " got " + sbn.getPackageName() + ":" + sbn.getKey() + ":" + sbn.getNotification().icon);
                                if (!sbn.isClearable())
                                {
                                    if (i_pack.contains(sbn.getPackageName()))
                                    {
                                        param.setResult(null);
                                        Object o = XposedHelpers.callMethod(param.thisObject, CREATE_NOTIFICATION_VIEWS, new Class[]{StatusBarNotification.class}, sbn);
                                        View icon = (View) XposedHelpers.getObjectField(o, ICON);
                                        XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " got icon " + icon);
                                        i_icons.put(sbn.getKey(), icon);
                                        XposedHelpers.callMethod(i_iconCrtl, UPDATE_NOTIFICATION_ICONS, XposedHelpers.getObjectField(param.thisObject, M_NOTIFICATION_DATA));
                                    }
                                    else
                                    {
                                        XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " did not find " + sbn.getPackageName() + " in " + i_pack);
                                    }
                                }
                                else
                                {
                                    XposedBridge.log(KIWI + " " + ADD_NOTIFICATION + " found clearable " + sbn.getPackageName());
                                }
                            }
                        });
            }
            XposedHelpers.findAndHookMethod(PHONE_STATUS_BAR, lpparam.classLoader, REMOVE_NOTIFICATION,
                    String.class, NotificationListenerService.RankingMap.class, new XC_MethodHook()
                    {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) throws Throwable
                        {
                            String key = (String)param.args[0];
                            XposedBridge.log(KIWI+" "+REMOVE_NOTIFICATION+" got "+key);
                            if (i_icons.containsKey(key))
                            {
                                param.setResult(null);
                                i_icons.remove(key);
                                XposedBridge.log(KIWI + " " + REMOVE_NOTIFICATION + " removed " + key);
                                if (i_sdk < 23)
                                    XposedHelpers.callMethod(param.thisObject, UPDATE_NOTIFICATION_ICONS, new Class[0]);
                                else
                                    XposedHelpers.callMethod(i_iconCrtl, UPDATE_NOTIFICATION_ICONS, XposedHelpers.getObjectField(param.thisObject, M_NOTIFICATION_DATA));
                            }
                        }
                    });

            XposedHelpers.findAndHookMethod(BASE_STATUS_BAR, lpparam.classLoader, UPDATE_NOTIFICATION, StatusBarNotification.class, NotificationListenerService.RankingMap.class, new XC_MethodHook()
            {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable
                {
                    StatusBarNotification sbn = (StatusBarNotification) param.args[0];
                    XposedBridge.log(KIWI + " " + UPDATE_NOTIFICATION + " got " + sbn.getPackageName() + ":" + sbn.getKey() + ":" + sbn.getNotification().icon);
                    if (i_icons.containsKey(sbn.getKey()))
                    {
                        param.setResult(null);
                        Object o = XposedHelpers.callMethod(param.thisObject, CREATE_NOTIFICATION_VIEWS, new Class[]{StatusBarNotification.class}, sbn);
                        View icon = (View) XposedHelpers.getObjectField(o, "icon");
                        XposedBridge.log(KIWI + " " + UPDATE_NOTIFICATION + " stored icon " + icon);
                        i_icons.put(sbn.getKey(), icon);
                        if (i_sdk < 23)
                            XposedHelpers.callMethod(param.thisObject, UPDATE_NOTIFICATION_ICONS, new Class[0]);
                        else
                            XposedHelpers.callMethod(i_iconCrtl, UPDATE_NOTIFICATION_ICONS, XposedHelpers.getObjectField(param.thisObject, M_NOTIFICATION_DATA));
                    }
                }
            });
            if (i_sdk<23)
            {
                XposedHelpers.findAndHookMethod(PHONE_STATUS_BAR, lpparam.classLoader, UPDATE_NOTIFICATION_ICONS, new XC_MethodHook()
                {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable
                    {
                        XposedBridge.log(KIWI + " " + UPDATE_NOTIFICATION_ICONS + " called");
                        i_status.removeAllViews();
                        if (i_pack.contains(STATUS))
                        {
                            for (View icon : i_icons.values())
                                i_status.addView(icon, 0);
                        }
                        else
                        {
                            LinearLayout ll = (LinearLayout) XposedHelpers.getObjectField(param.thisObject, M_NOTIFICATION_ICONS);
                            for (View icon : i_icons.values())
                                ll.addView(icon);
                        }
                    }
                });
            }
            else
            {
                XposedHelpers.findAndHookMethod(ICON_CONTROLLER, lpparam.classLoader, UPDATE_NOTIFICATION_ICONS, NOTIFICATION_DATA, new XC_MethodHook()
                {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable
                    {
                        XposedBridge.log(KIWI + " " + UPDATE_NOTIFICATION_ICONS + " called");
                        i_status.removeAllViews();
                        if (i_pack.contains(STATUS))
                        {
                            for (View icon : i_icons.values())
                                i_status.addView(icon, 0);
                        }
                        else
                        {
                            LinearLayout ll = (LinearLayout) XposedHelpers.getObjectField(param.thisObject, M_NOTIFICATION_ICONS);
                            for (View icon : i_icons.values())
                                ll.addView(icon);
                        }
                    }
                });
            }
        }
    }
}

Jetzt ist das Modul auch für ein Gerät mit Android 6.0 geeignet. Einem Test steht nichts mehr im Wege.
 
  • Danke
Reaktionen: Darkman und TSC Yoda
Schutz vor Code-Piraterie

Das Problem an Java, und damit an Android-Entwicklung ist, dass die erstellte Software relativ schlecht gegen miese kleine Code-Piraten zu schützen ist.

Um sich vor diesen 'Bastel-Buben' ein wenig zu schützen, gibt es eine einfache Lösung:

Man schreibt seinen Code (zumindest die anspruchsvollen Stücke) einfach in C. Das ist auch unter Android möglich, dafür kann man das Android NDK (Native Development Kit) und JNI (Java Native Interface) verwenden.

Ich habe dafür ein kleines Tutorial geschrieben:
[Tutorial] JNI-Projekte mit AndroidStudio

Man muss sich zwar im klaren darüber sein, dass auch nativer Code angegriffen werden kann, jedoch iste es deutlich anspruchsvoller nativen Code zu dekompilieren.
 
Zuletzt bearbeitet:
Zuletzt bearbeitet:

Ähnliche Themen

SM-T110 UND GT-I9300
Antworten
1
Aufrufe
4.323
SM-T110 UND GT-I9300
SM-T110 UND GT-I9300
Kiwi++Soft
Antworten
11
Aufrufe
8.086
Kardroid
Kardroid
Zurück
Oben Unten