Android Work Manager

Smartphones machen so einige Dinge im Hintergrund, von denen man erst dann erfährt, wenn man denkt, Wow, schön das ich jetzt daran erinnert werde oder wenn das einfach nur nervt. Die Frage für mich, wie funktioniert das eigentlich wenn Facebook zwar nicht aufgerufen wurde und noch nicht einmal pausiert, und trotzdem im Hintergrund prüft ob etwa neue Nachrichten eingegangen sind?

Weiterlesen

Linux, User Management

Ich beginne hier mit dem Titelgebenden Thema. Es geht darum wer den eigentlich der sogenannte root und die anderen user sind. Als Szenario habe ich mir dabei vorgestellt, dass man sich gerade ein Linuxoides System auf seinem Rechner Installiert hat. Die Beispiel habe ich mit Linux Mint getestet.

Was wir lernen:

  • Den eigenen user identifizieren. Gruppenzugehörigkeit ermitteln.
  • Den aktuellen user wechseln.
  • Vorübergehende root- Rechte für den eigenen user erlangen.
  • Ein neues root– Password nach der Erstinstallation des Systems anlegen und selbst zum root werden.
Weiterlesen

Job Intent Service

Google empfiehlt ab Android Oreo (API 26) oder aktueller für länger laufende Background Tasks anstelle von Service, JobIntentService zu verwenden.

Services werden ein paar Minuten nachdem die App beendet wurde und im Hintergrund läuft, zerstört. In diese Stadium führt ein erneutes Starten des Services zur einer Exception. Mit dem JobIntentService wurden diese Nachteile beseitigt.

Kompatibilität

An dieser Stelle lohnt es sich einmal kurz ein paar Gedanken über die Versionierung von Android zu machen. Ich schaue ich dabei kurz in die nähere Vergangenheit und die Gegenwart, bis Heute, das ist der Mai 2021.

Aktuell wird Android Oreo 8.1 (API 27) noch von Google unterstützt. Oreo 8.0 (API 26) nicht mehr. Oreo gibt es seit 2017.

Nach Oreo folgen: Android Pi (API 28), Android 10 (API 29).

Das Aktuellste Version ist: Android 11 (API 30) das am 9. September 2020 veröffentlicht wurde.

Beispiel

Es werden Daten über eine Internetverbindung abgerufen und über einen Intent an eine BroadcastReceiver geschickt. Wenn kein Verbindungsfehler auftritt oder keine fehlerhafte Suchanfrage übergeben wurde, dann läuft der Code innerhalb der while– schleife so lange, bis die App zerstört wurde.

/**
 * Example implementation of a JobIntentService.
 */
public class MyJobIntentService extends JobIntentService {

    static final int JOB_ID = 1000;
    private boolean isDestroyed;
    private List<CovidSearchResultData> result = new ArrayList<>();

    /**
     * Convenience method for enqueuing work in to this service.
     */
    static void startNew(Context context, Intent work) {
        enqueueWork(context, MyJobIntentService.class, JOB_ID, work);
    }

    @Override
    protected void onHandleWork(@NonNull Intent intent) {

        // We have received work to do.  The system or framework is already
        // holding a wake lock for us at this point, so we can just go.
        Log.i("SimpleJobIntentService", "Executing work: " + intent);

        // Get parameters.
        String town = intent.getStringExtra("town");

        String label = intent.getStringExtra("label");
        if (label == null) {
            label = intent.toString();
        }

        // This flag is set to true from inside the while- loop when
        // something goes wrong (e.g. Network connection break down or the query could
        // not be executed.....
        isDestroyed = false;

        //
        // This will run forever.
        // There is mot way to stop it.
        //

        // Result
        StringBuffer covidDataBuffer = new StringBuffer();
        String favLocURL = getURLForCurrentLocation(town);
        List<CovidSearchResultData> result = new ArrayList<>();

        while (!isDestroyed) {

            //
            // Get covid data from the server...
            //
            try {

                URL _url = new URL(favLocURL);
                HttpURLConnection urlConnection = (HttpURLConnection) _url.openConnection();

                InputStreamReader isr = new InputStreamReader(urlConnection.getInputStream());
                BufferedReader br = new BufferedReader(isr);

                String line;

                line = br.readLine();
                covidDataBuffer.append(line);

                result = DecodeJsonResult.getResult(covidDataBuffer.toString());

                isr.close();

                Thread.sleep(150);

            } catch (Exception e) {
                Log.v("ERRORERROR----", e + "");
            }

            //
            // This sends the received data to all registered receivers.
            //
            String r;
            if (result.size() > 0)
                r = result.get(0).getCasesPer10K() + "";
            else {
                //
                // Destroy this service if nothing could be found...
                //
                r = town + " not found.....";
                isDestroyed = true;
                Log.v("RESULTRESULT", r+" Not found");
            }

            Intent myIntent = new Intent();
            myIntent.putExtra("covidData", r);
            myIntent.setAction("com.berthold.servicejobintentservice.CUSTOM_INTENT");
            sendBroadcast(myIntent);

            Log.v("RESULTRESULT", r + "");

            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
            }
        }

        Log.v("RESULTRESULT", "Done! Going to destroyed state.....");
    }

    @Override
    public void onDestroy() {
        isDestroyed = true;
        toast("All work complete");
        Log.v("RECEIVEDRECEIVED"," Destroyed!");
        super.onDestroy();
    }

    /**
     * Returns an url that searches the network for covid info
     * regarding the users favourite location.
     *
     * @return
     */
    public String getURLForCurrentLocation(String town) {
        String url = (MessageFormat.format("https://public.opendatasoft.com/api/records/1.0/search/?dataset=covid-19-germany-landkreise&q={0}&facet=last_update&facet=name&facet=rs&facet=bez&facet=bl", town));
        return url;
    }

    @SuppressWarnings("deprecation")
    final Handler mHandler = new Handler();

    // Helper for showing tests
    void toast(final CharSequence text) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(MyJobIntentService.this, text, Toast.LENGTH_SHORT).show();
            }
        });
    }
}

Erkenntnisse

Ein JobIntentService läuft, wenn er mal gestartet wurde so lange, bis er nicht von „innen“ heraus“ zerstört worden ist (zum Beispiel wenn ein Netzwerkfehler aufgetreten ist usw).


Er kann nicht von „außerhalb“, also beispielsweise aus der implementierten Activity heraus, gestoppt werden.


Lang laufende Aufgaben müssen innerhalb des JobIntentService nicht in einem Thread ablaufen.


Eignet sich ebenso wie der Service nicht für wiederkehrende Aufgaben!

Android Service

Ein Service der sich beliebig oft starten oder stoppen läßt wird implementiert. Ausserdem wird gezeigt, wie der Service mit den restlichen Bestandteilen der App kommuniziert, und es werden die Fragen geklärt wie dem Service Parameter beim Start übergeben werden können und wie der Service Nachrichten an die App/ das Android System senden kann. letzteres wird am Beispiel Intent/ Broadcast Receiver implementiert.

Wozu eignen sich Services? Android bietet unterschiedliche Möglichkeiten wie sich „länger“ laufende Aufgaben, die im Hintergrund ablaufen sollen, ohne das der sogenannte UI- Thread geblockt wird, implementiert werden können.

  • Thread
    Nützlich für wirklich kurze Bearbeitungszeiten. Mit kurz ist damit nach meiner Erfahrung der Bereich von Sekunden und nicht Minuten gemeint. Kurze Netzwerkzugriffe, das Laden von Dateien aus dem Dateisystem. Dekodieren von Datenstrukturen, zum Beispiel JSON oder XML Datensätze.

    Die Herausforderung ist sicherzustellen das für die Selbe Aufgabe nicht zwei Threads gleichzeitig gestartet werden. Des Weiteren ist es nicht ganz so einfach Threads „punktgenau“ zu stoppen. Ein Beispiel: Suchanfragen die synchron zur Benutzereingabe gestartet und aktualisiert werden sollen. In diesen Fällen habe ich auf die sogenannte Asyc Task zurückgegriffen und tue es immer wieder, obwohl die von Google nicht mehr weiter unterstützt wird. Mal sehen, wie ich diese Funktionalität ersetzten kann…..

  • Async Task
    Nützlich für kürzere Bearbeitungszeiten. Vergleichbar mit dem Einsatzbereich von Threads. Async Task’s sind aber besser steuerbar. Damit kann man über den Start, die Verarbeitung, publizieren von Zwischenergebnissen und das Beenden alles in einer sauberen Struktur abarbeiten. Wird leider aus verschiedenen Gründen ab API 30 (Android Version 11) nicht mehr unterstützt. Executor, ThreadPoolExecutor und FutureTask werden von Google als Ersatz angeboten.

  • Service
    Eignet sich für Aufgaben die einen längeren Zeitraum beanspruchen. Kann undefiniert lange laufen. Wäre zum Beispiel für größere Downloads geeignet.

    Meiner Erfahrung ist die, dass man Services schlecht für periodische Abfragen einsetzen kann.Zum Beispiel abfragen von Daten aus dem Netzwerk und den Benutzer in Abhängigleit von bestimmten Bedingungen zu informieren.

Im Folgenden nun, der Reihe nach, die Implementation eines Services:

Manifest vorbereiten

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.berthold.servicedemo">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ServiceDemo">

        <!-- Our custom service -->
        <!-- The 'exported' parameter declares this service as private (false)  -->
        <!-- or as public (true). if a service is public it can be accessed from -->
        <!-- outside of the application package -->

        <service
            android:name=".MyService"
            android:exported="false">
        </service>

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>


    </application>

</manifest>

Den Service als Klasse implementieren

package com.berthold.myapplication;


import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.Nullable;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;

public class MyService extends Service {

    private boolean isDestroyed;
    private long serviceID;

    /**
     * A bound service will run as long as the app s alive.
     * It will be destroyed when all apps binding it are
     * destroyed....
     *
     * @param intent
     * @return
     */
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    /**
     * When a service is started this way, it will run indefinitely.
     * When implementing this, it is one's one responsibility to
     * stop the service, for example when the app is destroyed.
     *
     * @param intent
     * @param flags
     * @param startId
     * @return
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        isDestroyed = false;
        serviceID = System.currentTimeMillis();

        String town = intent.getStringExtra("town");

        Toast.makeText(this, "Service Started for:" + town, Toast.LENGTH_LONG).show();

        // intent will be 'null' if the activity which started that
        // service was destroyed. So we have to check to prevent a
        // exception.
        //
        // If one uses the 'Return START_REDELIVER_INTENT' option, the same
        // intent which was used for the old precess is reused, it will
        // be redelivered.
        if (intent != null)
            intent.setAction("com.berthold.myapplication.CUSTOM_INTENT");

        // This runs and fires a intent to all registered receivers
        // until this service is destroyed.
        new Thread(new Runnable() {
            @Override
            public void run() {

                // Result
                StringBuffer covidDataBuffer = new StringBuffer();
                String favLocURL = getURLForCurrentLocation(town);
                List<CovidSearchResultData> result = new ArrayList<>();

                while (!isDestroyed) {

                    //
                    // Get covid data from the server...
                    //
                    try {

                        URL _url = new URL(favLocURL);
                        HttpURLConnection urlConnection = (HttpURLConnection) _url.openConnection();

                        InputStreamReader isr = new InputStreamReader(urlConnection.getInputStream());
                        BufferedReader br = new BufferedReader(isr);

                        String line;

                        line = br.readLine();
                        covidDataBuffer.append(line);

                        result = DecodeJsonResult.getResult(covidDataBuffer.toString());

                        isr.close();

                        Thread.sleep(150);

                    } catch (Exception e) {
                        Log.v("ERRORERROR----", e + "");
                    }

                    //
                    // This sends the received data to all registered receivers.
                    //
                    String r;
                    if (result.size() > 0)
                        r = result.get(0).getCasesPer10K() + "";
                    else {
                        //
                        // Destroy this service if nothing could be found...
                        r = town + " not found.....";
                        isDestroyed = true;
                        stopSelf();
                    }

                    Intent myIntent = new Intent();
                    myIntent.putExtra("covidData", r);
                    myIntent.setAction("com.berthold.myapplication.CUSTOM_INTENT");
                    sendBroadcast(myIntent);

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                }
                Log.v("SERVICE", "Done!");
            }
        }).start();

        return START_STICKY;
    }

    /**
     * Stops the service by intent or when the app is
     * destroyed by he system (e.g. when the device orientation changes)
     */
    @Override
    public void onDestroy() {
        super.onDestroy();
        isDestroyed = true;
        Log.v("SERVICE", serviceID + " Destroyed");
    }

    /**
     * Returns an url that searches the network for covid info
     * regarding the users favourite location.
     *
     * @return
     */
    public String getURLForCurrentLocation(String town) {
        String url = (MessageFormat.format("https://public.opendatasoft.com/api/records/1.0/search/?dataset=covid-19-germany-landkreise&q={0}&facet=last_update&facet=name&facet=rs&facet=bez&facet=bl", town));
        return url;
    }
}

Den Broadcast Receiver implementieren

Der Receiver in der Activity:

        //
        // This registers the broadcast receiver and receives
        // any broadcast send from our service....
        //
        BroadcastReceiver r = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String covidData=intent.getStringExtra("covidData");
                outputV.setText(covidData);
                Log.v("RECEIVED___", covidData+"--");
            }
        };

        registerReceiver(r, new IntentFilter("com.berthold.myapplication.CUSTOM_INTENT"));

Der Sender in der Service– Klasse:

  Intent myIntent = new Intent();
                    myIntent.putExtra("covidData", r);
                    myIntent.setAction("com.berthold.myapplication.CUSTOM_INTENT");
                    sendBroadcast(myIntent);

Life Cycles

Eine Activity oder ein Fragment reagiert auf Änderungen im Life- Cycle einer Android App , indem die entsprechenden Call- Back Methoden aufgerufen werden (onCreate(), onStart(),onResume() etc…).

Dabei kann es aber unter Anderem passieren das, während der Code der in onStart() ausgeführt wird – nehmen wir an es findet der Verbindungsaufbau zu einem Bluetooth Gerät statt – onStop() vom System aufgerufen wird. wenn man nun dort die Bluetooth– Verbindung über diese Änderung informieren möchte, dann wird es zu einem Fehler kommen weil es ja noch keine Verbindung gibt (am ehestens bekommt man eine null pointer exception wenn man beispielsweise mit dem connectedThread– Objekt kommunizieren möchte).

Das oben beschriebene Fehlverhalten kann vermieden werden, indem man jene Komponenten einer Activity oder eines Fragmentes (also zum Beispiel das oben erwähnte bluetoothConnectedThread– Objekt – selbst auf die Änderungen im Life- Cycle reagieren läßt. Dabei ist die Activity oder das Fragment immer der Life- Cycle Owner. Die jeweilige Komponente ist der sogenannte Observer der über die Änderungen entsprechend informiert wird und darauf reagieren kann.

Neben dem Vorteil der Fehlervermeidung wird der Code lesbarer. Wenn man mehrere Komponenten hat die Life- Cycle abhängig sind, dann hat man in jeder Komponente den kompletten Code stehen und nicht teilweise in der jeweiligen Call- Back Methode der Activity oder des Fragemntes.

Parent Layout füllen

Klar, der Parameter match-parent hat den gewünschten, namensgebenden Effekt. Aber, wenn ich mehrere view– Elemente in einem parent darstelle und ich möchte, dass das am unteren Bildschirmrand steht genau die Größe hat, die noch zur Verfügung steht, was dann? Vor allem wenn es sich bei diesem Element um eine list- view handelt is das ein bisschen problematisch. eine Lösung steht hier.

  <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/fragment_fav_covid_data_view"
        app:layout_constraintBottom_toTopOf="@+id/nav_view"
        app:navGraph="@navigation/mobile_navigation" />

Entscheidend ist hier: Man setzte die Höhe auf null und den constraint auf den Kopf des folgenden Elements, das die Höhe einschränken soll (deswegen heist das auch constraint 😉

Singleton Pattern

Ist eine Klasse von der – während der Laufzeit eines Programms – nur ein Objekt abgeleitet werden kann.

Wird oft in Verbindung mit Datenbank- Zugriffen genannt und anhand solcher Beispiele auch erklärt. Ich persönlich habe noch keinen Fall gehabt, wo ich das gebraucht hätte. Ich beschränke mich deshalb erst einmal darauf das Pattern zu zeigen:

class SingletonExample {

// private field that refers to the object
   private static SingletonExample singleObject;

   private SingletonExample() {
      // constructor of the SingletonExample class
   }

   public static SingletonExample getInstance() {
      // write code that allows us to create only one object
      // access the object as per our need
   }
}

Das Code- Beispiel stammt aus der unter [1] verlinkten Quelle.

Man beachte: Der Leere Konstruktor ist als privat deklariert. Das bedeutet, neue Objekte können nicht außerhalb der eigenen Klasse erzeugt werden. Über die getInstance() Methode besorgt man sich eine Instanz dieser Klasse. Diese Instanz kann nur einmal erzeugt werden, wenn man sicherstellt, dass es nicht schon eine gibt. Beispiel:

 public static ThreadGetCovidData getInstance(getCovidDataInterface g,String sK,String sQ){
        if (getCovidData==null) {
            gC=g;
            apiAddressStadtkreise=sK;
            searchQuery=sQ;
            getCovidData = new ThreadGetCovidData();
        }
        return getCovidData;
    }

Das war es auch schon. Wenn ich mit eigenen Worten, ein Beispiel beschreiben kann, in dem es notwendig war ein Objekt genau einmal zu erzeugen und nicht etwa einfach eine statische Klasse zu verwenden, dann schreibe ich das hier hin.

[1] Prgrammiz.com: Singleton

Android App, Update prüfen

Ich beschreibe hier, wie man es schafft, die Nutzer einer in der Play- Store veröffentlichten App über Updates zu informieren.

Grundsätzlich habe ich mir zwei Varianten angeschaut. Die erste funktioniert indem man sich der Play Core Library bedient. Die Zweite Methode funktioniert über den Abgleich der aktuellen Versionsnummer der App auf dem Android Gerät mit der in der Play Store veröffentlichen Variante.

Play Core Library

Unter [1] findet man den Link zur offiziellen Dokumentation von Goggle.

Ich habe zunächst einmal nur ein Stück Code getestet, dass prüft ob ein Update verfügbar ist.

//
        // Play core library
        //
        AppUpdateManager appUpdateManager = AppUpdateManagerFactory.create(getApplicationContext());

        // Returns an intent object that you use to check for an update.
        Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo();

        appUpdateInfoTask.addOnSuccessListener(new OnSuccessListener<AppUpdateInfo>() {
            @Override
            public void onSuccess(AppUpdateInfo result) {
                if (result.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
                    Toast.makeText(getApplicationContext(), "Update available.....", Toast.LENGTH_LONG).show();
                    Log.v("UPDATEUPDATE:", result.availableVersionCode() + "");
                }else
                    Log.v("UPDATEUPDATE:","No Update");
            }
        });

Ich habe den Code mit meiner App „Beam Calc“ getestet (Verlinkt unter [2]). Ob das funktioniert wäre noch zu prüfen. Ich werde berichten, wenn ich es getestet habe, also, spätestens nach dem nächsten Update der App.

Wenn ein neues Update da ist, dann kann man den Update Prozess direkt aus der App heraus starten.

Abgleich über die Versionsnummern im Play- Store und der App die auf dem Android Gerät läuft

Die hier gezeigte Lösung ist nichts anderes als Copy- Paste. Der zugrundeliegende Artikel findet sich unter [3]. Der hier abgebildete Code ist daraus entnommen.

Eine wichtige Voraussetzung ist: Wenn man einen neuen Release erzeugt, dann muss man unbedingt dafür sorgen, dass keine älteren APK’s dem neuen Release beigefügt werden. Ansonsten wird bei der Angabe der Versions- Nummer im Play Store (im übrigen gilt das auch für die verfügbaren Bildschirm Auflösungen) „Variiert ja nach Gerät“ angezeigt.

Der Code bedient sich der Jsoup- Library, einem HTML– Parser für Java. Den Link findet man unter [4]. Am schnellsten geht die Integration in das eigene Projekt über Gradle (das unten stehende in die build.gradle auf Modul- Ebene einfügen):

 // jsoup HTML parser library @ https://jsoup.org/
    implementation 'org.jsoup:jsoup:1.13.1'

Der eigentliche Versions- Checker schaut so aus:

import android.os.AsyncTask;
import org.jsoup.Jsoup;
import java.io.IOException;

/**
 * Version checker.
 *
 * Retrieves the version of this app from the Google Play- Store.
 *
 * Source: https://stackoverflow.com/questions/34309564/how-to-get-app-market-version-information-from-google-play-store
 */
public class VersionChecker extends AsyncTask<String, String, String> {

    String newVersion;

    @Override
    protected String doInBackground(String... params) {

        try {
            newVersion = Jsoup.connect("https://play.google.com/store/apps/details?id=" + "berthold.beamcalc" + "&hl=de")
                    .timeout(30000)
                    .userAgent("Mozilla/5.0 (Windows; U; WindowsNT 5.1; en-US; rv1.8.1.6) Gecko/20070725 Firefox/2.0.0.6")
                    .referrer("http://www.google.com")
                    .get()
                    .select("div.hAyfc:nth-child(4) > span:nth-child(2) > div:nth-child(1) > span:nth-child(1)")
                    .first()
                    .ownText();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return newVersion;
    }
}

Der Aufruf wird so erledigt:

 //
        // Version checker
        //
        VersionChecker vc=new VersionChecker();
        try {
            String latest = vc.execute().get();
            Toast.makeText(getApplicationContext(), "Latest Version Code:"+latest, Toast.LENGTH_LONG).show();
        } catch (Exception e){}
    }

Ich werde an dieser Stelle weiter über die Ergebnisse berichten.

[1] Google Play Core Library, ofizelle Dokumentation von Google.
[2] Beam Calc App.
[3] Stack Overflow, Get version from Play Store.
[4] jsoup: Java HTML Parser.