Links| Links
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:
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:
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:
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:
@UsesAssets(fileNames = "flash_auto.png,flash_off.png")
@SimpleObject(external = true)
public class UrsAI2...
/**
* 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:
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:
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:
Download der Programme und Quellen
@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;
}
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".
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
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:
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.
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
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.
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:
form.ErrorOccurred(Component component, String functionName, int errorNumber,
String message)
new Notifier(this).ShowAlert("Error
" + errorNumber + ": " + message);
form.ErrorOccurredDialog(Component component, String functionName,
int errorNumber, String message, String title, String buttonText)
new Notifier(this).ShowMessageDialog("Error " + errorNumber + ": " + message, title,
buttonText);
form.ErrorOccurred(this, "TestErrorOccurred", 777, "Testing ErrorOccurred");
form.ErrorOccurredDialog(this, "TestErrorOccurred", 777, "Testing ErrorOccurred", "Title of Dialog", "Button Text");
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.
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.
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.