Link Inhalt
KIO4.com Die Installation der Werkzeuge und die Entwicklung einer Beispiel-Extension ist hier sehr gut beschrieben. Leider ein Gemisch aus Englisch und Spanisch, aber trotzdem gut verständlich und vor allen Dingen Schritt für Schritt erklärt. Für den Fall, dass die Seite von Juan Antonio Villalpando (KIO4.com) nicht mehr erreichbar ist, habe ich seine Anleitung PDF ausgedruckt.
Oracle Java Documentation Oracle Java Dokumentation.
AI2 Extension Reference Referenz für die Erstellung von eigenen App Inventor Erweiterungen.
How to Add a Property to a Component Peter Zhong erläutert, wie man einen PropertyEditor erstellt (als PDF ausgedruckt am  2020-08-23).
AI2 Annotation Source Die Annotation für die Spezifizierung der Extension-Elemente sind nirgendwo gut erklärt. Im Source-Code findet man einiges.
Pura Vida Apps Snippets, Tutorials, Extensions und viel mehr. Umfangreicher Content!
AppyBuilder ... ist eine von mehreren alternativen Entwicklungsplattformen. Es gibt viele Hintergrundinformation, u.a. eine Dokumentation der AI2-Klassen: 3nportal.com/AIBridge/API/
Klassenreferenz Mit Doxigen erstellte Dokumentation mit Stand 23.1.2020.
Android-API-Referenz Startseite der Android API Referenz.
Material Design Richtlinien und Materialien, z.B. Icons, für die Entwicklung von Android Apps.

Screen von einem AI2-Projekt in ein anderes kopieren| Screen von einem AI2-Projekt in ein anderes kopieren

Zunächst beide Projekte auf den eigenen Computer exportieren (.aia-Dateien). Die Dateien sin im ZIP-Format.

Beide Dateien öffnen und den Ordner "src" durchsuchen, bis man in einem Unterordner die Screen-Definitionen findet. Z.B. so:

Liste der Screen

Im Projektmappenexplorer rechte Maustaste auf das Projekt. Im Menüpunkt "Ansicht" gibt es den Unterpunkt "Klassendiagramm anzeigen".


Extensions entwickeln| Extensions entwickeln

Um eigene Erweiterungen entwickeln zu können, muss man einige Werkzeuge installieren. Die Installation der Werkzeuge und die Entwicklung einer Beispiel-Extension ist sehr gut bei KIO4.com beschrieben. Leider ein Gemisch aus Englisch und Spanisch, aber trotzdem gut verständlich und vor allen Dingen Schritt für Schritt erklärt.

Nur bei der Position der Quelldateien ist diese Anleitung etwas veraltet. Das dort vorgeschlagene Verzeichnis

     "~\appinventor-sources\appinventor\components\src\com\google\appinventor\components\runtime"


sollte NICHT benutzt werden! Man richtet statt dessen besser eine eigene Sourcen-Struktur ein, die der Java-Package-Nomenklatur entspricht. Diese Struktur startet, wie die Google-Sourcen, im Ordner

     "~\appinventor-sources\appinventor\components\src\".


Bei mir sieht das dann so aus:

Verzeichnis-Struktur

Die Package-Bezeichnung in den Quellen ist dann z.B. "package de.UllisRoboterSeite.UrsAI2UDPv3;" (Bezeichnung des Projekts ändern).

Auch der App Inventor hat eine Tutorial-Seite. Mehr Details zu Komponenten findet man unter dem Link How to Add a Component.

Für den Fall, dass die Seite von Juan Antonio Villalpando (KIO4.com) nicht mehr erreichbar ist, habe ich seine Anleitung PDF ausgedruckt.

 

Wenn man alles installiert hat, ist es mühselig immer wieder nachzuschauen, wo sich die einzelnen Komponenten befinden. Das folgende kleine Batch-Programm (AI2Ex.cmd) öffnet die entsprechenden Fenster (Pfad zu den Quellen ersetzen!):

cd %userprofile%\appinventor-sources\appinventor
start "" "C:\Program Files\Git\git-bash.exe"
start %userprofile%\appinventor-sources\appinventor\components\build\extensions
start %userprofile%\appinventor-sources\appinventor\components\src\de\UllisRoboterSeite

Zum Erstellen der Extension muss in das Git-Bash-Fenster der Befehl ant extensions eingegeben werden.


Develop Extensions| Develop Extensions

To develop your own extensions, you have to install some tools. The installation of the tools and the development of an example extension is very well described at KIO4.com. Unfortunately, a mixture of English and Spanish, but easy to understand and explained step by step.

With the position of the source files, this manual is outdated. The directory proposed there

     "~\appinventor-sources\appinventor\components\src\com\google\appinventor\components\runtime"

should NOT be used! Instead, it is better to set up your own source structure, which corresponds to the Java Package nomenclature. This structure, like the Google sources, starts in the folder

     "~\appinventor-sources\appinventor\components\src\"
.

 For me it looks like this:

 

Verzeichnis-Struktur

The package name in the sources is then e.g. "package de.UllisRoboterSeite.UrsAI2UDPv3;" (change name of project).

The App Inventor also has a tutorial page too. More details on components can be found under the link How to Add a Component.

In the event that the page of Juan Antonio Villalpando (KIO4.com) is no longer available, I have printed his instructions to PDF.

 

When everything is installed, it is tedious to keep checking where the individual components are. The following small batch program (AI2Ex.cmd) opens the needed windows (replace the path to the sources!):

cd %userprofile%\appinventor-sources\appinventor
start "" "C:\Program Files\Git\git-bash.exe"
start %userprofile%\appinventor-sources\appinventor\components\build\extensions
start %userprofile%\appinventor-sources\appinventor\components\src\de\UllisRoboterSeite

To build the extension, you have to enter the command ant extensions in the git-bash window.


Update der AI2-Entwicklungsumgebung für Extension | Update der AI2-Entwicklungsumgebung für Extension

Git Bash starten:

Das folgende Kommando eingeben:

git clone https://github.com/mit-cml/appinventor-sources.git

Die Entwicklungsumgebung wird in das Verzeichnis "C:\Users\<user>\appinventor-sources" kopiert. Der Vorgang dauert einige Minuten.


Interne Ressourcen für eine Extension (Assets) | Interne Ressourcen für eine Extension (Assets)

Wenn eine Extension Ressourcen benötigt, z.B. eine Grafik, lässt sich dies wie folgt realisieren:

  1. Im Ordner der Programmquellen wird ein Verzeichnis mit dem Namen assets angelegt:
    Asset-Verzeichnis
  2. In diesem Verzeichnis werden alle benötigten Ressourcen als Dateien abgelegt:
    Asset-Verzeichnis
  3. In der Deklaration der Extensionklasse sind die Namen der benötigten Dateien in der Annotation UsesAssets anzugeben:
    @UsesAssets(fileNames = "flash_auto.png,flash_off.png")
    @SimpleObject(external = true)
    public class UrsAI2...
  4. Den kompletten Pfad auf diese Datei erhält man über die Funktionen String getAssetPathForExtension(Component component, String asset) bzw. InputStream openAssetForExtension(Component component, String asset) der Klasse Form. Beide Funktionen werfen Exceptions, sollten also mit einem try/catch-Block versehen werden.
    /**
     * Ersetzt das Image der ButtonBaseComponente mit dem Image aus der Datei
     * @param extension Die Extension-Instanz
     * @param buttonBase Die Komponente, in der das Image ersetz werden soll
     * @param assetName Name der Image-Datei
     * @throws Exception z.B. File not found
     */
    static public void setButtonsImage(UrsAI2Camera extension, ButtonBase buttonBase, String assetName) throws Exception {
      String fn = extension.thisForm.getAssetPathForExtension(extension, assetName);
    
      if (!(extension.thisForm instanceof ReplForm))
        fn = fn.replace("file:///android_asset/", "");
    
      BitmapDrawable bd = MediaUtil.getBitmapDrawable(extension.thisForm, fn);
      ViewUtil.setBackgroundDrawable(buttonBase.getView(), bd);
    }

Patchen des AI2 Companion - zusätzliche Permissions | Patchen des AI2 Companion - zusätzliche Permissions (2021-11-06)

Einige Extensions benötigen besondere (Standard-) Permissions. Diese werden üblicherweise in die @UsesPermissions-Annotation zur Extension-Deklaration eingetragen. Beim Kompilieren des Projekts werden diese Permissions dann in das Manifest der App eingestellt.

Der AI2-Companion ist eine eigenständige App, die den Test des Projekts erlaubt. Als eigenständige App hat sie aber ein eigenes Manifest, das nachträglich nicht mehr geändert werden kann. Somit können die notwendigen Permissions dort nicht eingetragen sein. Die Anforderung der Permission durch Screen.AskForPermission klappt bei den Standard-Permissions leider auch nicht (Permissions mit dem Protection Level: normal in der Klasse Manifest.permission, eine Liste s.u.).

Will man dennoch eine Extension mit zusätzlich benötigten Permissions im AI2-Companion testen, muss man diesen Patchen. Gut geeignet ist das APK Editor Studio. Weiterhin benötigt man die Installationsdatei (.apk) für den AI2-Companion. Die Suchmaschine des Vertrauens mit der Anfrage "download apk online" hilft weiter.

Die APK-Datei wird mit den APK Editor Studio geöffnet. Die Schaltfläche Open Contents öffnet ein Fenster mit den extrahierten Komponenten der APK, u.a. auch die Manifest-Datei. Die kann nun mit einem Text-Editor angepasst werden. Die Schaltfläche Save APK erstellt eine neue APK-Datei mit dem angepassten Manifest. Diese überträgt auf das Android-Gerät (z.B. per USB) und installiert sie dort.

Um diesen Vorgang nicht wiederholen zu müssen, fordert man sinnvollerweise gleich alle Permissions an.

Some extensions require special (standard, normal) permissions. These are usually entered in the @UsesPermissions annotation for the extension declaration. When the project is compiled, these permissions are then transferred into the app's manifest.

The AI2 Companion is a stand-alone app that allows the project to be tested. As an independent app, however, it has its own manifest that cannot be changed afterwards. This means that the necessary permissions are not transferred. The request for permissions by Screen.AskForPermission does not work with the standard permissions either (permissions with the protection level: normal in the Manifest.permission class, list see below).

If you still want to test an extension with additional permissions required in the AI2 Companion, you have to patch it. The APK Editor Studio is well suited to do so. You also need the installation file (.apk) for the AI2 Companion. A search engine with query "download apk online" will help.

The APK file is opened with the APK Editor Studio. The Open Contents button opens a window with the extracted components of the APK, including the manifest file. This can now be modified with a text editor. The Save APK button creates a new APK file with the customized manifest. This could be transferred to the Android device (e.g. via USB) and installed there.

In order not to have to repeat this process, it makes sense to insert all permissions at once.

(possibly incomplete) List of Android Permission with protection level normal and AI2 special permissions. Just add all of them to the manifest file. Duplicate permissions are no problem.

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS"/>
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCOUNT_MANAGER"/>
<uses-permission android:name="android.permission.ACTION_MANAGE_OVERLAY_PERMISSION"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BROADCAST_STICKY"/>
<uses-permission android:name="android.permission.CALL_COMPANION_APP"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION"/>
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR"/>
<uses-permission android:name="android.permission.FLASHLIGHT"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<uses-permission android:name="android.permission.INSTALL_SHORTCUT"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.NFC"/>
<uses-permission android:name="android.permission.NFC_PREFERRED_PAYMENT_INFO"/>
<uses-permission android:name="android.permission.NFC_TRANSACTION_EVENT"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<uses-permission android:name="android.permission.READ_APP_BADGE"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_LOGS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.READ_SYNC_STATS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.REORDER_TASKS"/>
<uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_WATCH"/>
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND"/>
<uses-permission android:name="android.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND"/>
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.REQUEST_PASSWORD_COMPLEXITY"/>
<uses-permission android:name="android.permission.SET_ALARM"/>
<uses-permission android:name="android.permission.SET_TIME_ZONE"/>
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
<uses-permission android:name="android.permission.SET_WALLPAPER_HINTS"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.TRANSMIT_IR"/>
<uses-permission android:name="android.permission.UNINSTALL_SHORTCUT"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.USE_CREDENTIALS"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<uses-permission android:name="com.anddoes.launcher.permission.UPDATE_COUNT"/>
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
<uses-permission android:name="com.android.launcher.permission.READ_SETTINGS"/>
<uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT"/>
<uses-permission android:name="com.android.launcher.permission.WRITE_SETTINGS"/>
<uses-permission android:name="com.android.vending.BILLING"/>
<uses-permission android:name="com.google.android.apps.googlevoice.permission.RECEIVE_SMS"/>
<uses-permission android:name="com.google.android.apps.googlevoice.permission.SEND_SMS"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<uses-permission android:name="com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE"/>
<uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH"/>
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>
<uses-permission android:name="com.htc.launcher.permission.READ_SETTINGS"/>
<uses-permission android:name="com.htc.launcher.permission.UPDATE_SHORTCUT"/>
<uses-permission android:name="com.huawei.android.launcher.permission.CHANGE_BADGE"/>
<uses-permission android:name="com.huawei.android.launcher.permission.READ_SETTINGS"/>
<uses-permission android:name="com.huawei.android.launcher.permission.WRITE_SETTINGS"/>
<uses-permission android:name="com.majeur.launcher.permission.UPDATE_BADGE"/>
<uses-permission android:name="com.oppo.launcher.permission.READ_SETTINGS"/>
<uses-permission android:name="com.oppo.launcher.permission.WRITE_SETTINGS"/>
<uses-permission android:name="com.sec.android.provider.badge.permission.READ"/>
<uses-permission android:name="com.sec.android.provider.badge.permission.WRITE"/>
<uses-permission android:name="com.sonyericsson.home.permission.BROADCAST_BADGE"/>
<uses-permission android:name="com.sonymobile.home.permission.PROVIDER_INSERT_BADGE"/>
<uses-permission android:name="me.everything.badger.permission.BADGE_COUNT_READ"/>
<uses-permission android:name="me.everything.badger.permission.BADGE_COUNT_WRITE"/>

2021-09-11: Mix of AI2, Kodular and missing permission.
2021-11-06: android.permission.MANAGE_EXTERNAL_STORAGE added.


Ereignisreihenfolge beim Start der App | Ereignisreihenfolge beim Start der App

Der Start einer App entspricht i.W. dem folgend Ablauf:

ExtensionInit

Die erste Aktion ist der Aufruf der Konstruktoren aller Komponenten. Als nächstes werden die Designer-Eigenschaften eingestellt. Dies nur, wenn die Werte nicht (mehr) der Voreinstellung entsprechen, also im Designer geändert wurden. Das Setzen der Eigenschaften kann im App-Inventor erzwungen werden, indem bei der DesignerProperty-Annotation die Angabe alwaysSend eingestellt wird:

@DesignerProperty(alwaysSend = true, ...)

Bei Kodular (1.4 Eagle, 2019-08-17) funktioniert das leider nicht.

Als Nächstes wird die Methode onResume der Extension aufgerufen, sofern sie das Interface OnResumeListener implementiert und sich bei der Übergeordneten Form-Klasse entsprechend registriert. Zu diesem Zeitpunkt ist es leider nicht möglich, Ereignisse auszulösen. Will man auch die erste Öffnung des Forms als Ereignis weitergeben muss dies später in Extension.onInitialze geschehen.

Darauf folgend wird das Ereignis Screen.Initialize ausgelöst. Nach der Ausführung des zugehörigen Handlers wird die Methode onInitialize der Extension aufgerufen, sofern sich diese mit registerForOnInitialize im Konstruktor bei der zugehörige Instanz von Form (Screen) registriert hat. D.h. die App kann im Event-Handler zu Screen.Initialize bereits auf die Eigenschaften und Methoden der Extension zugreifen, bevor diese aufgefordert wird, sich zu initialisieren. In kritischen Fällen muss man hier also entsprechende Vorsorge betreiben.

Zuletzt geht sie Steuerung an die GUI über (Message-Queue).


Multiscreen Apps im MIT AI2 Companion| Multiscreen Apps im MIT AI2 Companion (2021-03-22)

Die folgenden Angaben beziehen sich auf die Version 2.60 des MIT AI2 Companion.

Das wesentliche GUI-Element einer Android App ist die Activity. Jede Display-Ansicht entspricht i.W. einer von Activity abgeleiteten Klasse. Dies trifft auch auf einen AI2 Screen zu, der indirekt von Activity abgeleitet ist. Wenn von einer Ansicht zu einer anderen gewechselt wird, wird die neue Activity auf die Spitze eines Stapels (Stack) gelegt. Wenn diese dann wieder verlassen wird (Back), wird sie vom Stapel entfernt und die vorher aktive Activity-Instanz wird in dem Zustand fortgesetzt, in dem sie verlassen wurde. Eine mit dem AI2 erstellte App verhält sich genau so.

Wird ein AI2-Projekt im Companion ist dies leider nicht so. Im Companion wird nur eine einzige Screen-Instanz (Activity) verwaltet. Die aufrufende Screen-Instanz wird verworfen und durch die aufgerufene ersetzt. Hieraus resultieren zwei Unterschiede:

  • Die Back-Taste bezieht sich nicht auf die ausgeführte Screen-Instanz, sondern auf die Companion-Instanz. D.h. wird die Back-Taste ausgelöst, wird nicht zum vorhergehenden Screen zurück gekehrt, sondern die Companion-Instanz wird geschlossen. Will man das vermeiden, muss man im Screen.Backpressed-Ereignis den Screen explizit schließen. Dann wird der vorhergehende Screen erneut angezeigt.


  • Die Rückkehr zum vorhergehenden Screen wird dadurch simuliert, dass eine neue Instanz dieser Screen-Klasse angelegt und initialisiert wird. Das gilt auch für alle enthaltenen Komponenten. Vorher eingestellte Werte sind also im Gegensatz zu einer kompilierten App verloren. Sie werden durch die Initialwerte ersetzt.
  • Die Ereignisreigenfolge bei der Rückkehr in den vorhergehenden Screen (z.B. von Screen2 zurück zu Screen1) ist:
    1. Screen1.Initialize
    2. Screen1.OtherScreenClosed

Build-Nummer bei AI2-Extensions| Build-Nummer bei AI2-Extensions

Bei der Entwicklung von Extension passiert es gelegentlich, dass man beim Erstellen und dem anschließenden Upload in ein AI2-Testprogramm einen Fehler macht und mit einer alten Version weiter testet. Die anschließende Fehlersuche ist dann recht unerquicklich, weil man an der ganz falschen Stelle sucht. Andere Entwicklungsumgebungen bieten die Möglichkeit bei jeder Erstellung eines Programms einer Versions- oder eine Build-Nummer hochzuzählen. AI2 bietet diese Möglichkeit leider nicht.

So geht's dennoch:

  1. Die Quelle der Extension so vorbereiten, dass ein Versionsname in definierter Weise enthalten ist.
  2. Man braucht ein Programm, dass die zu übersetzenden Quellen untersucht, ob der definierte Versionsname enthalten ist und diesen dann abändert.
  3. Man muss den Build-Prozess (per ant) so anpassen, dass das externe Programm an passender Stelle aufgerufen wird.

Download der Programme und Quellen

1. Quelle vorbereiten

@DesignerComponent(version = 1,versionName = MyExtension.VersionName", ...)
@SimpleObject(external = true)
public class MyExtension extends AndroidNonvisibleComponent {
   static final String VersionName = "1.7";
   ...

Die Klasse erhält die Variable static final String VersionName = "x.y";. Wichtig ist, dass der rot markierte Bereich genauso geschrieben wird. Hiernach wird gesucht. x kann ein beliebiger Text sein, gefolgt von einem Punkt und einer Zahl (y, auch mehrstellig). Wird dieser Text gefunden, wird y inkrementiert und die Datei neu geschrieben.

Die Versionsnummer kann auch in der Annotation @DesignerComponent verwand werden, muss aber mit dem Klassennamen der Extension spezifiziert werden.

Die Versionsnummer kann über eine Eigenschaft veröffentlicht werden:

@SimpleProperty(description = "Returns the component's version name.")
public String Version() {
   return VersionName;
}

2. Das Ersetzungsprogramm

Ein kleines VB-Programm, AI2VersionUpdate, übernimmt den Ersatz:

Imports System.IO

Module Main
   Sub Main()
      Console.WriteLine("=========================")
      Console.WriteLine("   AI2 Version Update")
      Console.WriteLine("=========================")

      Dim Dir = My.Application.CommandLineArgs(0)
      If Dir.EndsWith("""") Then
         Dir = Dir.Substring(0, Dir.Length - 1)
      End If

      If Dir.EndsWith("\") Then
         Dir = Dir.Substring(0, Dir.Length - 1)
      End If

      Const searchFor = "VersionName = """

      For Each foundFile As String In My.Computer.FileSystem.GetFiles(Dir, 
                              FileIO.SearchOption.SearchAllSubDirectories, "*.java")
         Dim fileContent As String
         fileContent = My.Computer.FileSystem.ReadAllText(foundFile)
         Dim ind = fileContent.IndexOf(searchFor)
         If ind > -1 Then
            Console.WriteLine(foundFile)

            Dim firstPart = fileContent.Substring(0, ind + searchFor.Length)
            Dim secondPart = fileContent.Substring(ind + searchFor.Length)
            ind = secondPart.IndexOf("""")
            Dim version = secondPart.Substring(0, ind)
            secondPart = secondPart.Substring(ind)

            ind = version.LastIndexOf(".")
            If (ind > -1) Then
               firstPart &= version.Substring(0, ind + 1)
               version = version.Substring(ind + 1)
            End If

            Dim build = CInt(version) + 1
            Console.WriteLine("=== Version number updated to " & build)

            fileContent = firstPart & CStr(build) & secondPart
            Dim utf8WithoutBom As New System.Text.UTF8Encoding(False)
            My.Computer.FileSystem.WriteAllText(foundFile, fileContent, False, utf8WithoutBom)

            Using sink As New StreamWriter(foundFile)
               sink.Write(fileContent)
            End Using

            Console.WriteLine("=========================")
         End If
      Next
   End Sub
End Module

Das übersetze Programm (die .exe-Datei) habe ich in ein Verzeichnis kopiert, das in der PATH-Systemvariablen enthalten ist. Um flexibler zu sein, rufe ich das Programm nicht direkt, sondern über eine Batch-Datei (AI2Precompile.bat).

AI2VersionUpdate.exe "src\de\ullisroboterseite\"

Auch diese Datei liegt in einem über PATH erreichbaren Ordner. Die Batch-Datei enthält aktuell nur eine Zeile, die das Programm aufruft und das Verzeichnis übergibt, in dem nach den Quelle gesucht werden soll. Zum Zeitpunkt der Ausführung des Programms ist das aktuelle Verzeichnis "C:\Users\<user>\appinventor-sources\appinventor\components".

3. Build-Prozess anpassen

Die Erstellung der Komponenten wird über die Datei "build.xml" im Verzeichnis "C:\Users\<user>\appinventor-sources\appinventor\components" gesteuert. Man suche den Eintrag <target name="AndroidRuntime". Dort fügt den Aufruf des Ersetzungsprogramms ein(<exec>-Tag):

<!-- =====================================================================
     AndroidRuntime: library providing runtime support for components
     ===================================================================== -->
<property name="AndroidRuntime-class.dir" location="${class.dir}/AndroidRuntime" />
<target name="AndroidRuntime"
        description="Generate runtime library implementing components"
        unless="AndroidRuntime.uptodate"
        depends="common_CommonVersion,HtmlEntities,CopyComponentLibraries,AnnotationProcessors,AndroidRuntime.uptodate">

   <!-- Ulli: Version number update -->
   <exec executable="AI2Precompile.bat">
   </exec>

   <mkdir dir="${AndroidRuntime-class.dir}" />
...

Beim Erstellen der Extension erfolgt dann folgender Eintrag in das Build-Log:

[exec]
[exec] C:\Users\Ulli\appinventor-sources\appinventor\components>AI2VersionUpdate.exe "src\de\ullisroboterseite\"
[exec] =========================
[exec]    AI2 Version Update
[exec] =========================
[exec] C:\Users\Ulli\appinventor-sources\appinventor\components\src\de\ullisroboterseite\ursai2surface\UrsAI2Surface.java
[exec] === Version number updatet to 8
[exec] =========================

 

Sichtbare externe Komponente entwickeln| Sichtbare externe Komponente entwickeln

Bei der Beispiel-Extension UrsAnalogClock wird gezeigt, wie man eine sichtbare Komponente im AI2 entwickeln kann.


Android Log schreiben / anzeigen / löschen| Android Log schreiben / anzeigen / löschen

Log schreiben

Das Schreiben von Log-Einträgen zu Debug-Zwecken ist recht einfach. Zunächst muss das entsprechende Package eingebunden werden:

import android.util.Log;

Das Schreiben in das Log erfolgt über die statischen Methoden v(), d(), i(), w() und e() der Klasse Log. Die einbuchstabigen Methodennamen spiegeln gleichzeitig eine Priorität wieder und bedeuten:

  • v: verbose (ausführlich)
  • d: debug (Test)
  • i:  information
  • w: warning
  • e: error (Fehler)

Alle diese Methoden gibt es in drei Parameter-Konfigurationen:

static int X(String tag, String msg)
static int X(String tag, Throwable tr)
static int X(String tag, String msg, Throwable tr)

Der Parameter tag ist eine beliebige Zeichenfolge, die ein späteres Filtern erlaubt. Am bestem definiert man hierfür eine String-Konstante, damit man garantiert immer den gleichen Wert übergibt:

private static final String LOG_TAG = "MyLogTag";

Ein typischer Aufruf sähe dann etwa so aus:

Log.i(LOG_TAG, "Eintritt in Methode mmm. Parameter aaa: " + aaa);

Die Klasse Log enthält weitere nützliche Methoden. Z.B. lässt sich ein aktueller Stack-Trace als String abrufen. Eine ausführliche Dokumentation gibt es in der Android Referenz Log.

Log anzeigen

Das Anzeigen des Logs geschieht auf dem PC über das Programm adb. adb gehört zu den Android SDK Platform Tools. Die Platform Tools wiederum sind Teil der Command line tools. Hier sind eine Reihe weiterer nützlicher Tools für Entwickler enthalten.

Wenn man das Android Studio installiert werden die Tools und die notwendigen Treiber mit installiert (ggf. als Option). Zu finden sind sie (Version 3.6) im Verzeichnis "C:\Users\<username>\AppData\Local\Android\Sdk\platform-tools". Man kann die Tools aber auch unabhängig vom Android Studio herunter laden und benutzen (s. Links). Es gibt Windows- und Linux-Versionen. Ggf. sind noch Treiber zu installieren. Wegen der unterschiedlichen Android Version sollte man hier nach aktuellen Information per Suchmaschine suchen.

Auf dem Android-Gerät muss das USB-Debugging aktiviert sein und das Gerät muss per USB mit dem PC verbunden sein.

Zur Anzeige des Logs ruft man adb logcat [weitere Optionen] in einer DOS-Box (PowerShell-Box) auf. Genaue Spezifikationen findet man in der Android Referenz zu logcat. Ein typischer Aufruf ist

adb logcat "MyLogTag":I *:S

Alle Log-Einträge mit dem Tag MyLogTag der Stufe I und höher (also I, W und E, jedoch nicht V und D) werden angezeigt. Alle anderen Tags werden nicht angezeigt ("*:S", S = Silent). Verschiedene Tags lassen sich mit verschiedenen Stufen kombinieren. Die Ausgabe sieht dann etwa wie folgt aus:

04-17 18:07:01.880 27088 27088 W MyLogTag: text.length() == 0
04-17 18:07:13.790 27088 27088 I MyLogTag: Eintritt in Methode mmm. Parameter aaa: 47
04-17 18:09:42.934 27450 27450 I MyLogTag: Eintritt in Methode mmm. Parameter aaa: 723

Löschen

Das Löschen aller Log-Einträge erfolgt über die Anweisung

adb logcat -b all -c

Auch hier kann man feiner spezifizieren (s. Dokumentation).


Standard-Error-Handling in Extensions| Standard-Error-Handling in Extensions

Anstatt eigene Error-Events anzubieten, kann man das Standard-Fehler-Ereignis ErrorOccurred der Screen-Komponente auslösen.

error-occurred

Der Vorteil ist, dass, wenn in der App kein Ereignis-Handler für dieses Ereignis implementiert ist, ein Hinweisfenster (Notifier) aufgeblendet wird. Die Dokumentation hierzu, leider nur als Kommentar im Code zu finden, sagt:

If dispatchEvent returned false, then no user-supplied error handler was run. If in addition, the screen initializer was run, then we assume that the user did not provide an error handler. In this case, we run a default error handler, namely, showing a notification / message dialog to the end user of the app. The app writer can override this by providing an error handler.

Das Ereignis kann folgendermaßen ausgelöst werden:

  1. Die Basisklasse für Komponenten (AndroidNonvisibleComponent) stellt über das geschützte Feld form eine Referenz auf das Screen-Objekt zur Verfügung, in das die Extension eingebunden ist.
  2. Das Auslösen des Ereignisses geschieht dann über die Methoden
    1. form.ErrorOccurred(Component component, String functionName, int errorNumber, String message)

      löst

        new Notifier(this).ShowAlert("Error " + errorNumber + ": " + message);

      aus, wenn kein Event-Handler für das Ereignis implementiert ist.

    2. form.ErrorOccurredDialog(Component component, String functionName, int errorNumber, String message, String title, String buttonText)

      löst

         new Notifier(this).ShowMessageDialog("Error " + errorNumber + ": " + message, title, buttonText);

      aus, wenn kein Event-Handler für das Ereignis implementiert ist.

Beispiele:

Aufruf von ErrorOccurred ohne implementierten Event-Handler:
form.ErrorOccurred(this, "TestErrorOccurred", 777, "Testing ErrorOccurred");

show-error-message

Aufruf von ErrorOccurredDialog ohne implementierten Event-Handler:
form.ErrorOccurredDialog(this, "TestErrorOccurred", 777, "Testing ErrorOccurred", "Title of Dialog", "Button Text");

show-error-dialog

Aufruf von ErrorOccurred bzw. ErrorOccurredDialog mit implementierten Event-Handler:

Ergibt folgende Ausgabe:

Error:
de.ullisroboterseite.mycomponent.MyComponent@5e1d2eb
TestErrorOccurred
777
Testing ErrorOccurred

"5e1d2eb" ist die ID der spezifischen Komponente. Unterschiedliche Komponenten haben unterschiedliche IDs.


Google App Engine starten| Google App Engine starten

Die Google App Engine kann auch per Kommando-Zeile gestartet werden. Dort stehen eine Reihe von Optionen zur Verfügung, die der Launcher nicht bietet, z.B. die Spezifizierung der Host-Adresse und der Port über den die Anwendung kommunizieren soll. Wenn Google App Engine korrekt installiert wurde, befindet sich dev_appserver.py im Order C:\Program Files (x86)\Google\google_appengine\ und die Umgebungsvariable PATH enthält einen Eintrag zu dem Ordner. Wenn nicht, neu installieren, Pfad selbst eintragen oder bei den Kommandos explizit angeben.

 Nun wechselt man in das Verzeichnis, in dem sich die zur Applikation gehörende Datei app.yaml befindet. Dann z.B.

dev_appserver.py app.yaml --host 192.168.178.99 --port 8080

Nun kommuniziert die Applikation nicht mehr über localhost sondern über 192.168.178.99.

Alle möglichen Einstellungen bekommt man per

dev_appserver.py -h

Tiefergehende Informationen| Tiefergehende Informationen

Die Dokumentation für die Entwicklung von eigenen Extensions ist recht dürftig. Mit solchen Informationen kommt man dann deutlich weiter. Z.B.:

Der Konstruktor einer Extension-Klasse erhält eine Referenz container auf die Instanz einer ComponentContainer-Klasse. Diese bietet u.a. die beiden Methoden container.$context() und container.$form(). Dies sind Referenzen auf die zu Grunde liegende Activity bzw. Form-Instanz. Die Form-Klasse wiederum bietet viele zusätzliche Methoden, z.B. registerForOnPause(OnPauseListener component) mit der man sich informieren lassen kann, wenn die App in den Zustand Paused wechselt. Leider muss man in den Quellcode schauen, um zu verstehen, was genau passiert.

Ich habe mit Doxigen eine Klassenreferenz der Version nb184 (30.7.2020). Es lohnt sich, hier ein wenig zu stöbern.

Nicht jede Methode ist sauber dokumentiert. Weitere Informationen erhält man, wenn man von Verzeichnis "C:\Users\<meinUser>\appinventor-sources\appinventor\components\src\com" mit findstr nach Stellen sucht, wo die gesuchte Klasse oder Methode benutzt wird, und dann in der entsprechenden Quelle nachschaut:

findstr /S /C:"funktionxyz" *.java

Installierte APK kopieren| Installierte APK kopieren

Bei TECHWISER gibt es eine Reihe von Anleitungen: Top 5 Ways to Extract APK File of Any App on Your Android Phone. Jedoch funktionieren die nicht immer. Was bisher immer geklappt hat ist, ist das Command-Line-Tool Android Debug Bridge (adb). Mit dem Kommando

adb shell pm list packages -f | findstr /I ...

erhält man eine Liste der Paketnamen aller installierten Apps. Über findstr sollte man eine Auswahl treffen, ansonsten kann die Liste sehr lang werden. Man erhält z.B. folgende Ausgabe:

package:/data/app/appinventor.ai_bienonline.ForeGroundTest-J0tMkoToueawIRiMV44VsQ==/base.apk=appinventor.ai_bienonline.ForeGroundTest

Der rote Text enthält den Pfad der APK-Datei. Die Datei muss man zunächst in ein ein von außen zugreifbares Verzeichnis kopieren. Z.B. ins Download-Verzeichnis (eine Zeile!):

adb shell cp 
   ➥/data/app/appinventor.ai_bienonline.ForeGroundTest-J0tMkoToueawIRiMV44VsQ==/base.apk 
   ➥/storage/emulated/0/Download

Dort landet sie unter dem Namen "base.apk". Aus dem Download-Verzeichnis kann man sie dann per File-Transfer abholen.


Java: Felder eines Objekts per Reflection ausgeben | Java: Felder eines Objekts per Reflection ausgeben

Der folgende Code erledigt das:

import java.util.Arrays;
import java.lang.reflect.*;
...
String spyFields(Object obj) {
    StringBuffer buffer = new StringBuffer();
    Field[] fields = obj.getClass().getDeclaredFields();
    for (Field f : fields) {
        if (!Modifier.isStatic(f.getModifiers())) {
            buffer.append(f.getType().getSimpleName());
            buffer.append(" ");
            buffer.append(f.getName());
            buffer.append(" = ");
            Object value = "*not accessible*";
            try {
                f.setAccessible(true);
                value = f.get(obj);
            } catch (Exception e) {
                // nichts zu tun
            }
            if (f.getType().isArray())
               buffer.append(spyArray(value));
            else
                buffer.append("" + value);
            buffer.append("\n");
        }
    }
    return buffer.toString();
}

String spyArray(Object  obj){
    try { return Arrays.toString((boolean[]) obj); } catch(Exception e) {}
    try { return Arrays.toString((byte[]) obj); } catch(Exception e) {}
    try { return Arrays.toString((char[]) obj); } catch(Exception e) {}
    try { return Arrays.toString((double[]) obj); } catch(Exception e) {}
    try { return Arrays.toString((float[]) obj); } catch(Exception e) {}
    try { return Arrays.toString((int[]) obj); } catch(Exception e) {}
    try { return Arrays.toString((long[]) obj); } catch(Exception e) {}
    try { return Arrays.toString((short[]) obj); } catch(Exception e) {}
    try { return Arrays.deepToString((Object[]) obj); } catch(Exception e) {}
    return "*Invalid Array definition";
}

View/Viewgroup-Hierarchie| View/Viewgroup-Hierarchie

Beim Entwickeln von Extensions ist es manchmal notwendig, in die Hierarchie der Komponenten (Android View/ViewGroup) einzugreifen. Die folgende Grafik zeigt die Hierarchie der View/ViewGroup-Komponenten für eine einfache App mit einem VerticalScrollArrangement, das zwei Schaltflächen enthält. Die Hierarchie gilt für die Ausführung im Companion.

View Tree


Dit und Dat| Dit und Dat

Bei der Annotation @DesignerProperty kann man einen defaultValue angeben. Dieser Wert ist dann die Voreinstellung der Eigenschaft im Designer-Fenster des App Inventor. Für Eigenschaften vom Typ boolean (editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN) sind der Wert für die Voreinstellung true der Text "True" (mit großem "T"!). Alle anderen Angaben führen zu Voreinstellung false.

Voreinstellung


Funktionsweise des AI2 Timers| Funktionsweise des AI2 Timers

Der Timer (Teil der Komponente Clock) benutzt wider Erwarten keinen Timer des Systems, sondern nutzt den die Funktion postDelayed einer android.os.Handler-Instanz. Verantwortlich für den Ablauf ist die Klasse com.google.appinventor.components.runtime.util.TimerInternal. Diese Klasse implementiert das Runnable-Interface.

Ein Handler ermöglicht das Senden und Verarbeiten von Nachrichten für Runnable-Objekte, die mit der Message-Queue eines Threads verbunden sind. postDelayed erhält als erstes Argument einen Verweis auf ein Runnable-Objekt. Nachdem die bei postDelayed angegebene Zeit verstrichen ist, wird die Methode run des übergebenen  Runnable-Objekts aufgerufen. Da TimerInternal das Interface Runnable implementiert, kann sich die TimerInternal-Instanz selbst aufrufen. In der Methode run erfolgt dann ein erneuter Aufruf von postDelayed. Dadurch entsteht eine zeitgesteuerte Endlosschleife:


public void Enabled(boolean enabled) {
...

  if (enabled) {
    handler.postDelayed(this, interval); // Bewirkt den Aufruf von Methode run, nachdem interval Millisekunden vergangen sind.
  }
}

...

public void run() {
    if (enabled) {
      component.alarm();

      // During the call to component.alarm, the enabled field may have changed.
      // We need to make sure that enabled is still true before we call handler.postDelayed.
      if (enabled) {
        handler.postDelayed(this, interval);
      }
    }
}

Dadurch, dass zuerst component.alarm aufgerufen wird, ist es möglich, den Timer im Event-Handler abzustellen (Timer.enabled = false).

Das Timing ist nicht sehr genau. Zum einen kann Runnable.run verzögert aufgerufen werden, wenn das System unter Last steht. Zum anderen wird die Zeit, die der Event-Handler benötigt, nicht berücksichtigt. Wenn man ein genaueres Verhalten benötigt, müsste man die vergangene Systemzeit ermitteln und das Intervall entsprechend korrigieren.


Gesetzte WakeLocks anzeigen| Gesetzte WakeLocks anzeigen

Die Android-Debug-Bridge (ADB, Download) ermöglicht es, auf Systeminterna zuzugreifen. Mit diesem Kommando lassen sich alle gesetzten WakeLocks anzeigen:

adb shell dumpsys power|grep -i wake_lock

Die Filterangabe bei grep kann variert werden um weitere Informationen zu erhalten, z.B. nur "wake" oder "lock".

Damit das funktioniert muss das Gerät per USB an den PC angeschlossen sein und der Dateitransfer aktiviert sein.


Scannen der App bei manueller Installation deaktivieren| Scannen der App bei manueller Installation deaktivieren

Wenn man Apps mit dem App Inventor erstellt und die kompilierte App auf dem Handy installieren will, will Google Play Protect die App jedes Mal scannen. Das nervt insbesondere dann, wenn die Netzwerk-Verbindung schwach ist. Dann muss man den Scannvorgang meist mehrfach wiederholen. Man kann das Scannen abstellen. Dazu:

  1. die App Google Play Store öffnen
  2. oben rechts auf auf das Profilbild tippen
  3. auf Menü-Punkt Play Protect tippen
  4. oben rechts auf das Symbol Einstellungen tippen
  5. danach kann man das Scannen der App deaktivieren.