Neue APIs für Android 12 21.09.2021, 10:22 Uhr

Stabilisierung der API

Die dritte Beta von Android 12 ermöglicht Entwicklern einen ersten Blick auf die finalen APIs der neuen Betriebssystemversion.
(Quelle: Android Developers)
Googles Aktualisierungen des Android-Betriebssystems verursachen bei vielen Entwicklern nicht nur Freude. Android ist allerdings unangefochtener Marktführer im Bereich der Smartphone-Betriebssysteme, weshalb Entwickler ihren Ärger darüber wohl oder übel in Grenzen halten müssen. Sei dem wie es sei, ist sich Google der für Android-Entwickler anfälligen Probleme durchaus bewusst. Die Auslieferung neuer Android-Betas erfolgt deshalb durch den in Bild 1 gezeigten Prozess, der Entwicklern einen klaren Zeitpunkt für die Stabilisierung der API verspricht.
Google hat den Android-Beta-Prozess vor einiger Zeit formalisiert (Bild 1)
Quelle: Hanna
Wir haben die Neuerungen, die durch diesen Formalisierungsprozess entstehen, in einer früheren Ausgabe schon behandelt. Mit Beta drei ist Android 12 nun knapp vor der Stabilisierung der API. Im offiziellen Google-Blog verspricht das Entwicklerteam allerdings schon jetzt, dass es keine gravierenden Neuerungen mehr geben wird. Aus diesem Grund ist es nun abermals an der Zeit, einen Blick auf Verschlechterungen, Ärgernisse und Neuerungen zu werfen.

Aktualisierung der Arbeitsumgebung

Wenn sie Android 12 schon bisher getestet haben, verwenden Sie die API-Version Android S Preview. Mit der Beta drei bietet Google das Android S-SDKs über den offiziellen Channel an. Wenn Sie die neue Variante des Betriebssystems verwenden möchten, so sollten Sie die Android S Preview deinstallieren und stattdessen die Android 12 Preview (S) selektieren (Bild 2). Neben der eigentlichen SDK-Plattform benötigen Sie - normalerweise - auch ein Emulatorimage, der Autor empfiehlt wie immer die Verwendung von x86-Varianten. Im Rahmen der Aktualisierung des SDKs sollten Sie darauf achten, im Tab SDK Tools Aktualisierungen der Toolchain zu autorisieren. Das Verwenden veralteter Toolchain-Versionen führt bei der Android-Entwicklung erfahrungsgemäß zu undefiniertem Verhalten und die dadurch entstehenden Fehler sind oft nur sehr schwer auffindbar.
Neben dem Beta-SDK gibt es nun auch eine Beta des finalen SDKs (Bild 2)
Quelle: Hanna
Die Android 12-Versuche in den folgenden Schritten werden unter Nutzung einer virtuellen Maschine durchgeführt. Mit Android 12 änderte Google den Auslieferungsprozess für Betriebssystem-Betas: Die Verantwortung für die Bereitstellung geeigneter Images liegt nun beim OEM des jeweiligen Telefons. Unter der URL https://developer.android.com/about/versions/12/devices findet sich eine Liste geeigneter Geräte. Zum Zeitpunkt der Drucklegung werden die folgenden elf Anbieter unterstützt: Asus, Google (Pixel), OnePlus, Oppo, RealMe, Sharp, TECNO, TCL, Vivo, Xiaomi und ZTE. Es fällt auf, dass Samsung in dieser Liste nicht auftaucht. Mit einer aktuellen Version von Android Studio samt den neuesten Buildtools erzeugte Projektskelette weisen nun folgenden Gradle-Einsprungpunkt auf:

android {
    compileSdk 31
    buildToolsVersion "31.0.0"
Interessant ist in diesem Zusammenhang auch noch, dass statt dem bisher verwendeten String S nun die Versionsnummer 31 zum Ansprechen von Android 12 dient:

defaultConfig {
    applicationId "com.example.nmg12skeleton"
    minSdk 31
    targetSdk 31
    versionCode 1
    versionName "1.0"
    testInstrumentationRunner
    "androidx.test.runner.AndroidJUnitRunner"
}
Es ist empfehlenswert, sofort nach der Installation des SDK ein neues Projektskelett zu erzeugen und dieses im zweiten Schritt im Android-Emulator auszuführen. Neue Versionen des Android-Emulators brauchen zum Start übrigens ungefähr 14 GByte Festwertspeicher, und starten sonst mit verwirrenden Fehlermeldungen nicht.
Nach dem erfolgreichen Start der IDE wirft Android Studio im Rahmen der Kompilation der Applikation häufig Fehler, die nach dem Schema Installed Build Tools revision 31.0.0 is corrupted. Remove and install again using the SDK Manager. aufgebaut sind. Ursache des Problems ist, dass Google in der ausgelieferten Version des SDKs einige erforderliche Dateien vergessen hat, deren Fehlen Android Studio moniert. Unter Ubuntu lässt sich dieses Problem durch Kopieren der Files nach folgendem Schema bewerkstelligen:

tamhan@TAMHAN18:~/Android$ cd Sdk/build-tools/31.0.0/
tamhan@TAMHAN18:~/Android/Sdk/build-tools/31.0.0$ mv d8 dx
tamhan@TAMHAN18:~/Android/Sdk/build-tools/31.0.0$ cd lib
tamhan@TAMHAN18:~/Android/Sdk/build-tools/31.0.0/lib$ mv d8.jar dx.jar
tamhan@TAMHAN18:~/Android/Sdk/build-tools/31.0.0/lib$
Unter Windows arbeitende Entwickler müssen zusätzlich einige Dateien umbenennen, was in Stack Overflow unter der URL https://stackoverflow.com/questions/68387270/android-studio-error-installed-build-tools-revision-31-0-0-is-corrupted beschrieben wird. Beachten Sie, dass das von Android Studio angeforderte Neu-Installieren des SDKs im SDK Manager keine Abhilfe schafft. Nach der Durchführung dieser Änderungen sollte die Applikation jedenfalls im Emulator erscheinen. Auf Projekt-Repositories hinweisende Fehler sind zu diesem Zeitpunkt unkritisch.

Spiele im Zentrum

Erfahrene Entwickler mögen bei der Vorstellung des modernen Android-Spielemarkts erschaudern: Dass man mit Psychoanalytiker, In-App-Purchase und Co. viel Geld verdienen kann, zeigt sich an den Börsenwerten mancher Unternehmen. Mit Android 12 führt Google Erweiterungen für Spiele Programmierer ein, die die Gaming Experience der Benutzerschaft verbessern.
Auf Seiten des Benutzer-Interfaces bringt Game Mode dabei die in Bild 3 und Bild 4 gezeigten Erweiterungen. In den zur Verfügung gestellten Emulator-Images stehen diese noch nicht zur Verfügung, wir entnahmen die beiden Screenshots deshalb aus der Präsentation Android 12 Game APIs. Für Entwickler von Spielen ist Game Mode vor allem deshalb relevant, weil Google die Abwägung zwischen Grafik-Qualität und Energieverbrauch fortan auf Benutzerebene realisiert.
Game Mode-kompatible Spiele beziehungsweise Geräte bieten Spielern zusätzliche Funktionen an (Bild 3)
Quelle: Hanna
Solche Spiele wollen den durch Benutzerschnittstellen verursachte Störungen des Flows minimieren (Bild 4)
Quelle: Hanna
Analog zur Grafik-Einstellungsseite eines Desktop-Spiels erlaubt das Android Game Mode dem Benutzer die Auswahl der gewünschten Grafikqualität. Aufgabe als Entwickler ist in diesem Fall im ersten Schritt das Erreichen der vom Betriebssystem bereitgestellten Qualitätseinstellung. Dies müssen Sie nach jedem Aufruf von onResume neu ermitteln und in ihrer Engine danach nach bestem Wissen und Gewissen umsetzen.
Erste Aktion zur Aktivierung dieser Funktion ist die Nutzung des Kategorie-Felds. Die Manifestdatei ihrer Applikation kann dem Betriebssystem so mitteilen, dass das vorliegende APK ein Computerspiel realisiert:

<application
    android:allowBackup="true"
    android:icon="@mipmap/
    ic_launcher"
    android:label="@string/
    app_name"
    android:appCategory="game"
Im nächsten Schritt müssen Sie außerdem Metadaten einschreiben, die sowohl den Performance- als auch den Batteriesparmodus aktivieren. Die Dokumentation ist zum Zeitpunkt, an dem der Artikel geschrieben wurde, noch nicht eindeutig, ob diese Information nur vom Play Store, oder aber auch vom Betriebssystem ausgewertet wird. Da der Gutteil des Vertriebs von Applikationen allerdings über den Playstore erfolgt, spricht nichts dagegen, die schon geöffnete Manifestdatei auch noch um die folgenden Passagengruppen zu erweitern:

<application>
  <meta-data
    android:name=
    "com.android.app.gamemode.performance.enabled"
    android:value="true"/>
  <meta-data
    android:name=
    "com.android.app.gamemode.battery.enabled"
    android:value="true"/>
  ...
</application>
Im nächsten Schritt müssen sie die Game Mode-Statusinformationen abernten. Dies erfolgt über einen neuen Systemdienst, der ausschließlich für das Zurückgeben des Statuszustands erforderlich ist. Seine Abfrage erfolgt nach dem von anderen Systems Services bekannten Zweikampf aus Aufruf von getApplicationContext().getSystemService und Durchführung der eigentlichen Auswertung:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if ( Build.VERSION.SDK_INT >=
    Build.VERSION_CODES.S ) {
        GameManager gameManager =
        getApplicationContext().getSystemService
        (GameManager.class);
        int gameMode = gameManager.getGameMode();
        Log.e("NMG", "Game Mode steht auf: " +
        Integer.toString(gameMode));
    }
    . . .
Der Rückgabewert der Methode gameManager.getGameMode() ist dabei eines von vier Enum-Flags. Aktuell sind diese noch nicht dokumentiert, eine Reflektion gegen die Klasse führt folgende Werte zutage:

public final class GameManager {
    public static final int GAME_MODE_BATTERY = 3;
    public static final int GAME_MODE_PERFORMANCE = 2;
    public static final int GAME_MODE_STANDARD = 1;
    public static final int GAME_MODE_UNSUPPORTED = 0;
Am wichtigsten ist dabei der Wert GAME_MODE_UNSUPPORTED, die auf das Nicht-Vorhandensein von Game Mode-Unterstützung auf der aktuellen Hardware hinweist. Wer das Beispiel im Emulator ausführt, bekommt deshalb das in Bild 5 gezeigte Ergebnis.
Der Android-Emulator bietet keine Unterstützung für Game Mode (Bild 5)
Quelle: Hanna
Wichtig ist in diesem Zusammenhang noch, dass Game Mode-Geräte von Seiten des OEMs mit Patches ausgestattet werden können. Diese haben die Aufgabe, vorliegende Geräte an populäre Spiele anzupassen. Ein gutes Beispiel wäre das zwangsweise Reduzieren der Rendering-Auflösung, um Ruckeln und / oder Hitzeentwicklung zu reduzieren. Entwickler haben in diesem Bereich normalerweise nur wenig Mitspracherecht Die unter https://developer.android.com/games/gamemode/gamemode-interventions bereitstehende Dokumentation erklärt allerdings, wie sie diese als Game Mode Interventions bezeichneten Eingriffe in ihre Rendering-Logik so weit wie möglich eliminieren. Zum Ansprechen bestimmter Versionen von SOCs führt Google zudem die folgenden neun Konstanten in Android.OS ein:

if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ) {
    . . .
    Log.e("NMG",Build.SOC_MANUFACTURER);
    Log.e("NMG", Build.SOC_MODEL);
}
Sie informieren über den Hersteller und den Typ des SOCs, der für die Ausführung des vorliegenden Programms verantwortlich ist. Im Android-Emulator bekommen wir folgende Ausgabe:

com.example.nmg12skeleton E/NMG: AOSP
com.example.nmg12skeleton E/NMG: ranchu
Search: Suchen mit eingeschränktem Funktionsumfang
Palm OS implementierte mit Global Feed ein grenzgeniales Feature, das auch heute noch von Palm-Benutzern schmerzlich vermisst wird. Es ermöglichte das Eingeben eines mehr oder weniger beliebigen Suchstrings, den das Betriebssystem danach bei allen am Telefon befindlichen Applikationen anmeldete. Diese hatten die Aufgabe, ihre lokalen Datenspeicher nach Resultaten zu durchsuchen und diese beim Betriebssystem anzumelden. Lohn der Mühen war die Einblendung einer kompakten Resultatliste. Das Anklicken eines Resultats öffnete dieses danach (idealerweise) in der Zielapplikation.
Hardwarebeschleunigung unter Linux
Haben Sie einen AMD-Prozessor, so ist der Android-Emulator unter Windows nicht mit der Fähigkeit zur Hardwarebeschleunigung ausgestattet. Der beste Weg zur Umgehung des Problems ist die Nutzung von Linux – selbst der vergleichsweise alte FX8320 des Autors liefert unter Ubuntu mehr als akzeptable Performance.
Seit dem Erscheinen der ersten Test-Version von Android 12 geistert ein als App Search bezeichnetes Feature durch die Entwicklerschaft - bisher vor allem deshalb, weil seine Implementierung Metadaten, Datenbankschemata und Co. voraussetzt. Viele fragten sich, wie Google diese doch sehr applikationsspezifischen Informationen in ein einheitliches, in Betriebssystem-Benutzerschnittstellen darstellbares Format zu bringen gedenkt. Mit Beta drei erklärt Google sich an dieser Stelle. Die wenig erfreuliche Antwort lautet kurz gefasst: gar nicht. Spezifischerweise sieht der Text folgendermaßen aus: »your application can allow its data to be displayed on system UI surfaces by the system’s pre-installed intelligence component. Exactly which data gets displayed on system UI surfaces is dependent on the OEM.«
Während außer Frage steht, dass populäre Applikationen wie WhatsApp und Co. von OEMs eher kurz als langfristig in ihrer jeweiligen Suche integriert werden, dürfte der Normalentwickler in diesem Bereich wenig Chancen haben. Google möchte ihn allerdings trotzdem zur Implementierung animieren und promotet den Search-Dienst deshalb nun als hochperformante lokale Suchengine. Dahinter steht der Gedanke, dass Suchprozesse gegen die von ihrer Applikation verwalteten Datenbestände so ohne den von SQLite verursachten Datenbank-Zugriffsaufwand ablaufen können.

Search-Dienst in zwei Versionen

Zur Erleichterung des Umstiegs bietet Google den Search-Dienst in zwei Versionen an. Die als Local Storage bezeichnete Kompatibilitätsvariante steht dabei ab inklusive Android 4.0 zur Verfügung. Sie ist im Prinzip eine hoch performante lokale Suchengine, die ihre Metadaten und Indices im Daten-Verzeichnis ihrer Applikation ablegt (Bild 6). Erst ab Android 12 steht die neue Variante zur Verfügung, Google spricht in diesem Zusammenhang von Plattform Storage.
Google beschleunigt die Adoption von Storage durch Anbieten von zwei Varianten (Bild 6)
Quelle: Hanna
Interessant ist in diesem Zusammenhang, dass Google die Zugriffs-API bei der Datenbank-Varianten vereinheitlicht. Wie seine Applikation ab Android 12 mit Plattform Storage arbeiten lassen möchte, erledigt die Beschaffung des Zugriffs-Kontextobjekts einfach nach folgendem Schema:

if (BuildCompat.isAtLeastS()) {
  mAppSearchSessionFuture.setFuture
  (PlatformStorage.createSearchSession(
    new PlatformStorage.SearchContext.Builder
    (mContext, DATABASE_NAME)
    .build()));
} else {
  mAppSearchSessionFuture.setFuture
  (LocalStorage.createSearchSession(
    new LocalStorage.SearchContext.Builder
    (mContext, DATABASE_NAME)
    .build()));
}
Die Auslieferung der für Search notwendigen Komponenten erfolgt über die Android X-Erweiterung. Die spezifisch zur Suchengine gehörenden Bibliotheken finden sich dabei unter der URL https://developer.android.com/jetpack/androidx/releases/appsearch. Wir wollen an dieser Stelle einen Versuch durchführen. Als erstes müssen wir in die zum Modul App gehörende build.gradle-Datei wechseln, wo wir nach folgendem Schema die neuen Bibliotheken einbinden:

dependencies {
    def appsearch_version = "1.0.0-alpha03"
    implementation "androidx.appsearch:appsearch:
    $appsearch_version"
    annotationProcessor "androidx.appsearch:
    appsearch-compiler:$appsearch_version"
    implementation "androidx.appsearch:
    appsearch-local-storage:$appsearch_version"
    implementation "androidx.appsearch:
    appsearch-platform-storage:$appsearch_version"
Der hier verwendete Versionsstring war zum Zeitpunkt, als der Artikel geschrieben wurde, aktuell. Es wäre vorstellbar, dass Google hier weitere Aktualisierungen vornimmt. Interessant ist hier allerdings noch, dass wir die mit den Paketen androidx.appsearch:appsearch-local-storage und androidx.appsearch:appsearch-platform-storage unterschiedliche Implementierungsbibliotheken laden. Das liegt daran, dass Google Local Storage und Plattform Storage über separate Bibliotheken ausgeliefert. Die doch etwas umfangreichere Local-Version der API muss so nur dann in des Projekt eingebunden werden, wenn der Entwickler diese Funktion auch wirklich benötigt.
Im nächsten Schritt müssen wir ein POJO generieren, das durch Hinzufügen der @Document-Annitation als für Search relevantes Datenbank-Schema deklariert ist:

import androidx.appsearch.annotation.Document;
@Document
public class TamsDocument {
   
}
Das Hinzufügen der Klasse reicht allerdings nicht zur Erzeugung eines solchen Dokuments aus. Die in Android Studio bei Bedarf eingeblendete Dokumentation informiert uns darüber, dass eine Dokument-Klasse unbedingt zumindest einen Namespace und eine Id aufweisen muss. Dabei handelt es sich um zwei String-Attribute, die je eine zusätzliche Annotation aufnehmen:

@Document
public class TamsDocument {
    @Document.Namespace
    private final String namespace;
    @Document.Id
    private final String id;
Eine Reflexion der Dokument-Klasse informiert uns darüber, dass es mit score eine zusätzliche Annotation für die Analyse gibt. Diese legt das Gewicht des jeweiligen Eintrages fest. Da wir hier kein Score-Feld exponieren, werden alle in der Datenbank angelegten Elemente als mit einem Score von null (also neutral) angenommen. Unser POJO deklariert die Membervariablen namespace und id als private. Für den Zugriff durch die Datenbank-Engine sind deshalb Akzessoren erforderlich:

@Document
public class TamsDocument {
    . . .
    @NonNull
    public String getNamespace() {
        return namespace;
    }
    @NonNull
    public String getId() {
        return id;
    }
Im nächsten Schritt müssen wir die eigentlich von der Datenbank zu indizierenden Elemente anlegen. Dies erfolgt durch Property-Annotationen. Aktuell kennt die App Search-Bibliothek dabei die folgende Auswahl:
  • Document.BooleanProperty
  • Document.BytesProperty
  • Document.DoubleProperty
  • Document.LongProperty
  • Document.StringProperty
Für unser kleines Beispiel reicht ein String aus, weshalb wir auf Document.StringProperty setzen. In den Eigenschaften beziehungsweise Parametern der Annotation dürfen wir an dieser Stelle dann noch festlegen, wie die Datenbank mit den gegen die Strings gerichteten Suchanfragen umzugehen hat:

@Document.StringProperty(indexingType =
AppSearchSchema.StringPropertyConfig.
INDEXING_TYPE_PREFIXES)
private final String text;
Wie im Fall der zwangsweise exponierten Felder gilt auch hier, dass sie zum erfolgreichen Funktionieren der Datenbank-Engine eine Akzessor-Methode bereitstellen müssen:

@NonNull
public String getText() {
    return text;
}
Der Abschluss der Dokument-Klasse ist dann ein nach folgendem Schema aufgebauter Konstruktor, der uns die Erzeugung von Instanzen ermöglicht:

TamsDocument(@NonNull String id, @NonNull String
namespace, @NonNull String text) {
    this.id = Objects.requireNonNull(id);
    this.namespace = Objects.requireNonNull(namespace);
    this.text = Objects.requireNonNull(text);
}
Im nächsten Schritt benötigen wir eine Datenbank-Verbindung. Für unser kleines Beispiel nutzen wir die Methode createSearchSesson aus LocalStorage:

public void onViewCreated(@NonNull View view, Bundle
savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    Context context = getContext();
    ListenableFuture<AppSearchSession> sessionFuture =
    LocalStorage.createSearchSession(
            new LocalStorage.SearchContext.Builder
            (context, /*databaseName=*/ "tams_app")
              .build()
    );
Interessant ist hier vor allem die Klasse ListenableFuture, die Android Studio von Haus aus unterwellt anzeigen wird. Aktuelle Versionen bieten an dieser Stelle die Möglichkeit zur automatischen Hinzufügung der Espresso-Core-Bibliothek als Schnellaktion an. Wer sich für diese Option entscheidet, stellt aber fest, dass keine Änderungen am Projektskelett erfolgen und der Fehler eingeblendet bleibt.

Hinzufügung der Espresso-Core-Bibliothek

Interessanterweise erfolgt die Einbindung der Klasse nicht über die Vollversion der Guava-Bibliothek. Das Android X-Bibliothekssystem bietet mit Concurrent eine eigene Variante des Elements an, die unter der URL https://developer.android.com/jetpack/androidx/releases/concurrent dokumentiert ist. Um unsere Datenbank-Beschaffung kompilierbar zu machen, reicht es aus, in der zum Modul App gehörenden .gradle-Datei folgende neue Abhängigkeit hinzuzufügen:

dependencies {
    . . .
    implementation
    "androidx.concurrent:concurrent-futures:1.1.0"
Im Rahmen der Einrichtung der Datenbank müssen wir im nächsten Schritt dafür sorgen, dass das weiter oben angesprochene Schema Teil der Datenbankinformationen wird. Alle in App Search implementierten Operationen laufen asynchron ab. Designschema der Kommunikation ist dabei das Erzeugen eines request-Objekts, das im nächsten Schritt gegen die Datenbank angewendet wird:

SetSchemaRequest setSchemaRequest = null;
try {
    setSchemaRequest =
    new SetSchemaRequest.Builder().addDocumentClasses
    (TamsDocument.class).build();

} catch (AppSearchException e) {
    e.printStackTrace();
}
Da Google einen Gutteil der Generator-Klassen eine oder mehrere Exception werfen lässt, ist das Verwenden von Try-Catch-Blöcken zur kompilierbar-machung der Applikation unbedingt erforderlich. Der zweite Akt des Anmelden des Schemas erfolgt dann nach folgender Weise:

SetSchemaRequest finalSetSchemaRequest =
setSchemaRequest;
ListenableFuture<SetSchemaResponse> setSchemaFuture =  Futures.transformAsync(sessionFuture, session ->
session.setSchema(finalSetSchemaRequest),
ContextCompat.getMainExecutor(getContext()));
Die konsequente Nutzung von Futures als Programmier-Pattern zeigt an dieser Stelle insofern ihre Zähne, als die Futures-Unterstützung in der Java-Programmiersprache als Ganzes eher mangelhaft ist. Die für das Zusammenfassen der beiden Futures verantwortliche Methode Futures.transformAsync ist von Haus aus nicht Teil des Standards und auch nicht in der Android X-Bibliothek enthalten. Zur Lösung empfiehlt es sich, an dieser Stelle zusätzlich die Android-Variante des gesamten Frameworks in den Built Path hinzuzufügen. Dies erfolgt durch eine weitere Anpassung der zum Modul App gehörenden build.gradle-Datei:

dependencies {
    . . .
    implementation 'com.google.guava:guava:28.2-android'
Durch die Implosion des kompletten Guava-Pakets kommt es übrigens nicht zu Namespace-Kollisionen: wir deklarieren im Import-Teil jeder einzelnen .java-Datei, welche externen Elemente im jeweiligen File zum Einsatz kommen.

Datenbankstruktur bevölkern

Sei dem wie es sei, wollen wir unsere Datenbankstruktur im nächsten Schritt bevölkern. Hierzu erzeugen wir unter Nutzung des weiter oben besprochenen Konstruktors eine neue Instanz von TamsDocument:

TamsDocument testD = new TamsDocument("user1","noteId", "Das ist ein Test!");
Das eigentliche Einfügen des Testdokuments erfolgt abermals über ein request-Objekt. Die für das Einpflegen neuer Dokumente in die Such-Infrastruktur zuständige Variante der Klasse hört auf den Namen addDocuments und lässt sich folgendermaßen anstoßen:

PutDocumentsRequest putRequest;
try {
    putRequest = new PutDocumentsRequest.Builder().
    addDocuments(testD).build();
    ListenableFuture<AppSearchBatchResult<String,
    Void>>
    putFuture =
          Futures.transformAsync(sessionFuture,
          session -> session.put(putRequest),
          ContextCompat.getMainExecutor(getContext()));
Das permanente Aufrufen der Funktion Futures.transformAsync sorgt dafür, dass die von den Aktivierungen der request-Klassen zurückgelieferten Futures zusammengefasst werden. Wir erhalten am Ende also ein finales Future, dass uns das Abwarten aller in ihm enthaltenen Subereignisse ermöglicht. Dazu müssen wir natürlich im nächsten Schritt ein Callback hinzufügen, was nach folgendem Schema erfolgt:

Futures.addCallback(putFuture, new FutureCallback
<AppSearchBatchResult<String, Void>>() {
    @Override
    public void onSuccess(@Nullable AppSearchBatchResult
    <String, Void> result) {
        Map<String, Void> successfulResults =
        result.getSuccesses();
        Map<String, AppSearchResult<Void>>
        failedResults = result.getFailures();
        }
    @Override
    public void onFailure(@NonNull Throwable t) {  }
    }, ContextCompat.getMainExecutor(getContext()));
    } catch (AppSearchException e) {
        e.printStackTrace();
}
Im nächsten Schritt können wir mit der Suche beginnen - auch hierzu ist eine SearchSpec-Klasse erforderlich:

SearchSpec searchSpec = new SearchSpec.Builder()
    .addFilterNamespaces("noteId")
    .build();
ListenableFuture<SearchResults> searchFuture =
    Futures.transform(sessionFuture, session ->
    session.search("Das", searchSpec),
    ContextCompat.getMainExecutor(getContext()));
    Futures.addCallback(searchFuture,
    new FutureCallback<SearchResults>() {
        @Override
        public void onSuccess(@Nullable SearchResults searchResults) {
            iterateSearchResults(searchResults);
        }
        @Override
        public void onFailure(@NonNull
        Throwable t) { }
    },  ContextCompat.getMainExecutor(getContext()));
Im Großen und Ganzen handelt es sich dabei um Boilerplate-Code. Wir legen abermals ein Callback an, indem wir das bei der erfolgreichen Abarbeitung der Suche zurückgelieferte Objekt an eine weitere Methode übergeben. Dieses hat dann die Aufgabe, nach folgendem Schema für die eigentliche Verarbeitung der zurückgelieferten Elemente zu sorgen:

private void iterateSearchResults(SearchResults searchResults) {
    Futures.transform
    (searchResults.getNextPage(), page -> {
        GenericDocument genericDocument =
        page.get(0).getGenericDocument();
        String schemaType =
        genericDocument.getSchemaType();
        TamsDocument doc = null;
Aufgrund des in der Einleitung dieses Abschnitt besprochenen Aufbaus ist nicht sicher, dass unsere Anfrage immer nur Informationen vom gewünschten Typ zurückliefert. Zudem paginiert Google die angelieferten Informationen. Die erste Aufgabe der Routine besteht darin, diese Pensionierung auf Zuruf aufzuheben. Für kleines Beispiel reicht es aus, immer konstant mit der ersten Seite des Staates zu arbeiten. In einem produktiven System würden sie hier natürlich, analog zu einem Cursor, umfangreichere Auswertungslogik vorziehen.
Teil der Java-Bibliothek
Google leistet sich mit Guava seit längerer Zeit eine globale Utility-Bibliothek für Java-Programmierer, die immer wieder erneut benötigte Funktionen bereitstellt. Ihre Webseite findet sich unter https://guava.dev/ und enthält Dutzende nützlicher Komponenten - die Dokumentation unseres hier verwendeten ListenableFuture findet sich übrigens unter https://guava.dev/releases/21.0/api/docs/com/google/common/util/concurrent/ListenableFuture.html.
Die nächste Aktion besteht darin, den im Wert schemaType zurückgelieferten Datentyp gegen das von uns gewünschte Schema zu überprüfen. Die Auswertung, die hier durch das ausgeben von Informationen im LogCat-Fenster besteht, erfolgt nur im Fall eines Treffers:

if (schemaType.equals("TamsDocument")) {
    try {
        doc = genericDocument.toDocumentClass
        (TamsDocument.class);
        Log.e("NMG", doc.getText());
    } catch (AppSearchException e) {    }
}
return doc;
}, ContextCompat.getMainExecutor(getContext()));
}
An dieser Stelle ist diese kleine Fingerübung bereit – führen Sie sie aus, um sich in LogCat an der folgenden Ausgabe zu erfreuen:

com.example.nmg12skeleton E/NMG:
Das ist ein Test!
Beachten Sie, dass unsere hier durchgeführten Experimente ausschließlich im Arbeitsspeicher des Telefons liegen. Möchten Sie Ihre Informationen permanent speichern, so müssen Sie die Arbeit an dieser Stelle signalisieren:

ListenableFuture<Void>
requestFlushFuture = Futures.transformAsync(sessionFuture,
        session ->
        session.requestFlush(),
        mExecutor);
Futures.addCallback(requestFlushFuture, new FutureCallback<Void>() {
    @Override
    public void onSuccess(@Nullable Void result) {
        . . .
    }
    @Override
    public void onFailure(@NonNull Throwable t) {
        Log.e(TAG, "Failed to flush database updates."
        , t);
    }
}, mExecutor);
Screenshots haben sich - auch bei technisch vergleichsweise herausgeforderten Benutzern - als weit verbreitetes Kommunikationsmittel etabliert.
Während das Aufnehmen von Einzelbildschirmen heute kein besonderes Problem mehr darstellt, ist der Gutteil der in Android enthaltenen Formulare lang. Das manuelle Herumscrollen, Screenshot-Auslösen und anschließende Scitexen der Resultate ist eine Aufgabe, der Google in Android 12 den Kampf angesagt.
Mit Android 12 nimmt Google auch in diesem Bereich Erleichterungen vor. In der Theorie erkennt das Betriebssystem das Vorhandensein scrollbarer Inhalte und bietet dem Nutzer in diesem Fall die Möglichkeit, einen vollwertigen Screenshot anzufertigen (Bild 7).
Entsteht das scrollbare Fenster unter Nutzung der Android-Widgets, so kann die Screenshot-Funktion es zusammenfassen (Bild 7)
Quelle: Foto: Google
In Tests funktionierte dies mit dem Android-Emulator noch nicht. Das Kamera-Symbol in der Statusleiste löst einen für Entwickler vorgesehenen Framebuffer-Dump aus. Der Hotkey sorgt für das Auftauchen der neuen Screenshot-Oberfläche, die Option zur Zusammenfassen bietet sie allerdings nicht an.
Insbesondere im Fall von Geschäftsapplikationen gilt, dass bildschirmüberschreitende Inhalte oft nicht unter Nutzung des Android-GUI-Stacks entstehen. Ein gutes Beispiel dafür wäre eine Aktien- oder Kryptowährungs-Applikation, die ihre Diagramme mit einem eigenen Charting-Framework auf den Bildschirm bringt. Zur Lösung dieses Problems bietet Android 12 das ScrollCaptureCallback-Interface an. Dabei handelt es sich um eine Programmierschnittstelle, über die ihre Applikation einen Teil des Bildschirms als für Screenshots relevant markieren darf und danach in diesem die in der Screenshot-File zu schreibenden Inhalte ablegt. Google zeigt sich dabei insofern flexibel, als die eigentliche Anmeldung entweder in einem Fenster oder in einer Queue erfolgen kann - die korrekten Methoden haben die folgenden Signaturen:

View#setScrollCaptureCallback
Window#registerScrollCaptureCallback
Die eigentliche Einbindung ist nicht besonders kompliziert - wichtig ist vor allem, dass sie sich das Stamm-Widget ihrer Applikation greifen und nach folgendem Schema mit einem Callback ausstatten:

public void onViewCreated(@NonNull View view, Bundle
savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    view.setScrollCaptureCallback(this);
Die in Android Studio enthaltene Auto-Vervollständigungsengine greift uns an dieser Stelle insofern unter die Arme, als sie sich automatisch um das Festlegen der Inklusion kümmert:

public class SecondFragment extends Fragment implements ScrollCaptureCallback {
    private FragmentSecondBinding
    binding;
Bei intelligenter Verwendung von Android Studio können Sie die eigentliche Generierung der Methoden anweisen. Die Methodenskelette präsentieren sich dann nach folgendem Schema:

public void onScrollCaptureSearch
(@NonNull CancellationSignal
cancellationSignal, @NonNull Consumer<Rect> consumer)
public void onScrollCaptureStart
(@NonNull ScrollCaptureSession
scrollCaptureSession, @NonNull
CancellationSignal cancellationSignal, @NonNull Runnable runnable)
public void onScrollCaptureImageRequest(@NonNull
ScrollCaptureSession scrollCaptureSession, @NonNull
CancellationSignal cancellationSignal, @NonNull Rect rect, @NonNull Consumer<Rect> consumer)
public void onScrollCaptureEnd(@NonNull Runnable
runnable)
Für einen ersten Analyse-Versuch bietet es sich dann an, in jeder der vier Methoden nach folgendem Schema Instrumentierungscode unterzubringen:

@Override
public void onScrollCaptureEnd(@NonNull Runnable
runnable) {
    Log.e("NMG", "onScrollCaptureEnd");

}
Ein Versuch der Programmausführung im Android-Emulator informiert uns dann darüber, dass die vier Methoden aktuell noch nicht aufgerufen werden können. Google dürfte dieses Problem in naher Zukunft beheben, weshalb es sich schon jetzt auszahlt, Überlegungen zum wahrscheinlichen Datenverarbeitungs-Workflow anzustellen.
Im ersten Akt dürfte dabei ein Aufruf von onScrollCaptureSearch erfolgen. Die unter https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ScrollCaptureTargetResolver.java bereitstehende Klasse ScrollCaptureTargetResolver nutzt die von dieser Methode zurückgegebenen Koordinaten-Informationen zur Feststellung des Callbacks, der für die erforderliche beziehungsweise anstehende Screenshot-Operation verantwortlich ist.
Wichtig ist dabei vor allem, dass die Aufrufe immer am Main Thread erfolgen, der Zugriff auf GUI-Elemente und Co. also keine Manipulationen mit Runnables und anderen Elementen voraussetzt.
Dies ist unter anderem deshalb wichtig, weil die eigentliche Verarbeitungs-Methode der Klasse ScrollCaptureTargetResolver mit einem Time-out ausgestattet ist. Zu lange dauernde Verarbeitungen der Methode würde dazu führen, dass das Betriebssystem den jeweiligen Callback ignoriert. Wichtig ist hier, dass das Betriebssystem durch  onScrollCaptureSearch im ersten Schritt eine Liste ansprechbarer Screenshot-Generatoren erzeugt.
Sofern sich das Betriebssystem für ihren Callback entschieden hat, folgt ein Aufruf der Methode onScrollCaptureStart. Die eigentliche Bereitstellung der Bilddaten erfolgt dann durch onScrollCaptureImageRequest, wobei jeder Aufruf Informationen über die in zurückgelieferten Bereich erwartete Verschiebung enthält. Am Ende des Aufnahme-Prozesses steht ein Aufruf von onScrollCaptureEnd. Das permanent mitgelieferte Objekt vom Typ CancellationSignal ermöglicht ihrer Applikation dabei den Abbruch von Screenshot-Prozessen. Dies kann zum Beispiel dann sinnvoll sein, wenn ihre Applikation feststellt, dass zur Fertigstellung eines umfangreichen Screenshots nicht ausreichend Informationen zur Verfügung stehen.
Nebeneffekt der immer weiteren Verbreitung organischer Displays ist, dass Hersteller nicht auf rechteckige oder quadratische Displays beschränkt sind. Wohl im Interesse eines gefälligen Industriedesigns werden Telefone mit runden Bildschirmkanten immer populärer. Dies ist für Entwickler insofern von Nachteil, als im runden Bereich dargestellte Teile der Applikation unschön abgeschnitten erscheinen (Bild 8).
Auch im Emulator gilt: Abgeschnittene Teile des Programms (siehe Statusleiste) sind hässlich (Bild 8)
Quelle: Hanna
Da der Radius der Ecken-Rundung von Gerät zu Gerät unterschiedlich ist, sollten im Fullscreen-Modus arbeitende Entwickler nicht auf einen von Haus aus festgelegten Abstands-Wert setzen. Dieser müsste nämlich so bemessen sein, dass er auf Geräten mit rechteckigen Bildschirm entweder zu unschönen Darstellung oder zu Verschwendung von Bildschirmplatz führen würde. Android 12 erleichtert Entwicklern das Leben hier durch eine neue Gruppe von APIs, die Informationen über Bildschirmkanten zur Verfügung stellt. Als Grunddesignparadigma kommt dabei das in Bild 9 gezeigte Design zum Einsatz – ein Mittelpunkt und ein Radius bestimmen den Abrundungsgrad.
Die folgende Geometrie dient zur Beschreibung runder Bildschirmkanten (Bild 9)
Quelle: Foto  Google
Problematisch ist an dieser neuen API, dass sie den Bildschirm des Telefons nicht absolut betrachtet. Die Berechnung der Rundung erfolgt immer im Verhältnis zum von ihrer Applikation beackerten Teil des Bildschirms. Betrifft die Rundung nur die Statuszeile, so wird sie von der API ignoriert, wenn ihr Programm nicht im Fullscreen-Modus arbeitet. Für einen ersten Versuch bietet es sich an, im Fragment-Beispiel von Android Studio in der Activity folgenden Code zu platzieren:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = ActivityMainBinding.
    inflate(getLayoutInflater());
    setContentView(binding.getRoot());
    if ( Build.VERSION.SDK_INT >=
    Build.VERSION_CODES.S ) {
        final WindowInsets insets =
        binding.getRoot().getRootWindowInsets();
        final RoundedCorner topRight =
        insets.getRoundedCorner
        (RoundedCorner.POSITION_TOP_RIGHT);
}
Die API arbeitet in einem zweistufigen Prozess: Als erstes müssen wir einen als Insets bezeichneten Datensatz abrufen, der Informationen über die dem Fenster auferlegten Abrundungen anliefert. Die Methode getRoundedCorner beschafft dann die eigentlichen RoundedCorner-Objekte, die nach folgendem Schema über den Rundungsgrad informieren:

public final class RoundedCorner implements Parcelable {
    public static final int POSITION_BOTTOM_LEFT = 3;
    public static final int POSITION_BOTTOM_RIGHT = 2;
    public static final int POSITION_TOP_LEFT = 0;
    public static final int POSITION_TOP_RIGHT = 1;
    public int getPosition() { . . .
    public int getRadius() { . . .
    public Point getCenter() { . . .
Leider liefert der Emulator in insets den Wert null, was den darauffolgenden Aufruf von getRoundedCorner fehlschlagen lässt. Ein Versuch mit den Unterviews liefert nach folgendem Schema dasselbe Ergebnis:

public void onViewCreated(@NonNull View view, Bundle
savedInstanceState) {
    super.onViewCreated(view,
    savedInstanceState);
    if ( Build.VERSION.SDK_INT >=
    Build.VERSION_CODES.S ) {
        final WindowInsets insets =
        view.getRootWindowInsets();
Wichtig ist in diesem Zusammenhang noch, dass Entwickler von Homescreen-Widgets bei der Arbeit mit Android 12 ebenfalls Änderungen in Kauf nehmen müssen. Am Ärgerlichsten ist, dass die Kanten fortan abgerundet erscheinen (Bild 10).
Auch Widgets bekommen in Android 12 abgerundete Kanten (Bild 10)
Quelle: Foto  Google
Auch sonst legt Google den Fokus klar auf rekonfigurierbare Widgets mit höherem Nutzwert. Liefert der Entwickler beispielsweise eine Konfigurations-Activity mit seinem Widget aus, so blendet Android diese nun bei einem Lang-Tap ein (Bild 11).
Android 12 erleichtert den Zugriff auf die Konfigurations-Seite eines Widgets (Bild 11)
Quelle: Foto  Google
Eine weitere interessante Änderung betrifft die Widgetliste. Entwickler dürfen nun in der Manifestdatei einen Beschreibungsstring anliefern, der die Funktion des Widgets beschreibt:

<appwidget-provider
  ...
  android:description="@string/my_widget_description">
</appwidget-provider>
Wie im Fall der App Search weist Google allerdings auch hier darauf hin, dass die Anzeige des Strings nicht garantiert ist – es steht jedem OEM frei, seinen eigenen Programmstarter zu realisieren. Weitere Informationen zu den Neuerungen finden sich in der unter https://developer.android.com/about/versions/12/features/widgets bereitstehenden Übersichtsliste.




Das könnte Sie auch interessieren