Android se nepřipojuje k WiFi síti bez internetu

Android od verze KitKat (4.4) se automaticky nepřipojuje k WiFi sítím, u kterých nezjistí dostupnost internetu. Asi to dává smysl, proč by se pojil k síti, která nemá internet? Raději zkusí jinou síť kde internet bude. Ale jak to vyřešit pokud chceme aby s k této síti připojoval? Uděláme malý hack, kterým nasimulujeme servery, na kterých Android zjišťuje, že je na internetu 🙂 Android po připojení zkontroluje URL (je natvrdo zadrátovaná v Androidu a může se měnit v závislosti na verzi Androidu). Pokud dostane zpět požadovanou odpověď, považuje WiFi síť za připojenou k internetu (i když internet nemá) 🙂

Požadavky na danou adresu jsou klasickým GETem a zařízení požaduje odpověď 204 (No content) to je celé. Jenže jak si tyto servery nasimulovat?

Nejjednodušší je v DNS překládat URL (clients3.google.com a connectivitycheck.gstatic.com) na náš server, kde spustíme nginx s touto konfigurací:

vim /etc/nginx/sites-enabled/default
# ====================================
# = android internet hack
# ====================================

server {
    server_name
        clients3.google.com
        connectivitycheck.gstatic.com;
    listen 80;

    location /generate_204 {
        return 204;
    }
}

 

Android: Retrofit

Před několika lety jsem komunikoval se serverem přes balíček Volley. Protože komunikuji hlavně přes REST rozhraní, nahradil jsem Volley balíčkem Retrofit, který je pro tyto účely doporučovaný.

Retrofit je HTTP klient pro Android (Javu), který vám usnadňuje připojení k webové službě REST API. Usnadňuje konzumaci dat JSON a XML. Podporuje příkazy: GET, POST, PUT, PATCH a DELETE. Pojďme si ukázat jak na to.

Nainstalujeme závislosti:

// Retrofit
compile 'com.squareup.retrofit2:retrofit:2.3.0'

// JSON Parsing
compile 'com.google.code.gson:gson:2.8.2'
compile 'com.squareup.retrofit2:converter-gson:2.3.0'

+ internet permission

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.chikeandroid.retrofittutorial">
 
    <uses-permission android:name="android.permission.INTERNET" />
 
    <application>
       ....
    </application>
 
</manifest>

Retrofit je knihovna, která používá anotované rozhraní pro volání REST API. Vytvoříme jednoduchý požadavek GET pro seznam knih, který se automaticky namapuje na objekty. API pro seznam knih vrací tento JSON:

{  
    "books":[  
        {  
            "author":"Božena Němcová",
            "title":"Babička"
        },
        {  
            "author":"Božena Němcová",
            "title":"V zámku a v podzámčí"
        },
        {  
            "author":"Karel Čapek",
            "title":"Krakatit"
        },
        {  
            "author":"Karel Čapek",
            "title":"Jak se co dělá"
        }
   ]
}

jsonschema2pojo

Z JSONu necháme vygenerovat objekty přes službu jsonschema2pojo. Vložíme JSON, vybereme Source type: JSON, Annotation style: Gson a odškrtneme Allow additional properties.

Rozhraní služby

Modelovou třídu máme vytvořenou, vytvoříme rozhraní služby, které bude spravovat API endpointy. Vytvoříme GET požadavek pro načtení všech knih:

package cz.vencax.retrofit;

import cz.vencax.retrofit.model.Book;
import cz.vencax.retrofit.model.Books;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;

public interface BookService {
    @GET("books")
    Call<Books> getAllBooks();
}

Všimněte si anotace @GET označující typ požadavku a nabývající hodnotu books, která označuje endpoint (tato hodnota se přidá k BASE_URL popsané níže).

RetrofitClient

požadavky na REST API budeme odesílat přes třídu RetrofitClient:

package cz.vencax.retrofit;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class RetrofitClient {
    static final String BASE_URL = "http://demo7646366.mockable.io/";
    private static Retrofit retrofit = null;

    public static Retrofit getClient() {
        if (retrofit==null) {

            Gson gson = new GsonBuilder()
                    .setLenient()
                    .create();

            retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .build();
        }
        return retrofit;
    }
}

Kde BASE_URL nabývá hodnoty se základním URL API, ke kterému se přidávají hodnoty z rozhraní služby popsané výše.

Získání dat v activity

V activity poté data jednoduše získáme a můžeme s nimi dále pracovat. U pozorňuji, že data jsou získána asynchronně:

    private void getBooksFromApi() {

        Log.d("xxx", "getBooksFromApi");

        BookService bookService = RetrofitClient.getClient().create(BookService.class);
        Call<Books> books = bookService.getAllBooks();
        books.enqueue(new Callback<Books>() {

            @Override
            public void onResponse(Call<Books> call, Response<Books> response) {
                Log.d("xxx", "onResponse: " + response);
                if (response.isSuccessful()) {
                    Log.d("xxx", "response.isSuccessful()" + response.body());
                    Books books = response.body();

                    Log.d("xxx", "response.isSuccessful(), books count: " + books.getBooks().size());
                    for (Book book : books.getBooks()) {
                        Log.d("xxx", "Book: " + book.getTitle() + ", " + book.getAuthor());
                    }
                } else {
                    Log.d("xxx", "response NOT isSuccessful()" + response.errorBody().source());
                }
            }

            @Override
            public void onFailure(Call<Books> call, Throwable t) {
                Log.d("xxx", "onFailure: " + t);
            }
        });
    }

Touto jednoduchou ukázkou jsem vám představil jak na to. Jednoduché, že?

Odeslání dat na server – POST

Pokud však potřebujete posílat nějaká data, je nutné je ukládat přes metody PUT nebo POST. Pojďme se podívat jak na to s metodou POST (PUT by byla analogická).

V rozhraní služby vytvoříme metodu POST s endpointem a parametrem @body:

package cz.vencax.retrofit;

import cz.vencax.retrofit.model.Book;
import cz.vencax.retrofit.model.Books;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;

public interface BookService {
    @POST("send")
    Call<Book> postBook(@Body Book book);
}

Na endpoint BASE_URL/send odešleme objekt Book – tento objekt se automaticky převede na JSON.

Odeslání POST požadavku z activity:

    private void postBookToApi() {

        Log.d("xxx", "postBookToApi");

        BookService bookService = RetrofitClient.getClient().create(BookService.class);

        Book bookPost = new Book();
        bookPost.setAuthor("vEnCa-X");
        bookPost.setTitle("Jak na Android - Retrofit");

        Call<Book> book = bookService.postBook(bookPost);
        book.enqueue(new Callback<Book>() {

            @Override
            public void onResponse(Call<Book> call, Response<Book> response) {
                Log.d("xxx", "onResponse: " + response);
                if (response.isSuccessful()) {
                    Log.d("xxx", "response.isSuccessful(): " + response.body());
                    Book book = response.body();
                    Log.d("xxx", "Book: " + book.getTitle() + ", " + book.getAuthor());
                } else {
                    Log.d("xxx", "response NOT isSuccessful()" + response.errorBody().source());
                }
            }

            @Override
            public void onFailure(Call<Book> call, Throwable t) {
                Log.d("xxx", "onFailure: " + t);
            }
        });
    }

Další způsob bez call

Response<Book> book = bookService.postBook(bookPost).execute();
if (book.isSuccessful()) {
   Log.d("xxx", "book reponse: " + book);
}

Problémy

  • máš aktuální verze balíčků?

Android: životní cyklus activity a fragmentu při otočení displeje

Activity

Pojďme si ukázat životní cyklus activity. Červeně je zvýrazněn životní cyklus při otočení displeje. Pokud chcete přenést nějaký stav do nově otočené activity, máte možnost si tento stav uložit v metodě onSaveInstanceState() a obnovit ho v onRestoreInstanceState() nebo v onCreate()

Fragment

U fragmentu máme podobné možnosti. Stavy opět ukládáme v metodě  onSaveInstanceState() a obnovíme v onRestoreInstanceState() nebo v onCreate() nebo v onCreateView()

Nebo můžeme v metodě onCreate() zavolat setRetainInstance(true); čímž se životní cyklus při rotaci změní takto:

Tento způsob je vhodný například pro použití services, které při rotaci v onDestroy nezabijeme ale chce s nimi dále pracovat.

Android: dialog a jeho zachování při rotaci displeje

Pokud v Androidu používáte dialogy pro zobrazení nějaké informace uživateli, nesmíte zapomenout na znovuvytvoření activity (při rotaci displeje, zasunutím zařízení do doku, …) při které je activity ukončena a znovu vytvořena. Tím dojde ke zrušení dialogu. Vytvoří se vám v aplikaci při tomto úkonu správně znovu?

Pokud používáte klasický AlertDialog (po nějakém úkonu – ne zobrazení v onCreate) nebude dialog při otočení displeje zobrazen

DialogFragment

Řešením tohoto problému je DialogFragment, který zůstane zobrazen i po restartování activity. Uvádím jednoduchou kontru kódu jak na to:

MyAlertDialogFragment.java

import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog;

public class MyAlertDialogFragment extends DialogFragment {

    public static MyAlertDialogFragment newInstance(String title) {
        MyAlertDialogFragment frag = new MyAlertDialogFragment();
        Bundle args = new Bundle();
        args.putString("title", title);
        frag.setArguments(args);
        return frag;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        String title = getArguments().getString("title");

        return new AlertDialog.Builder(getActivity())
                .setTitle(title)
                .setPositiveButton("YES",
                        new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int whichButton) {
                                //...
                            }
                        }
                )
                .setNegativeButton("CANCEL",
                        new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int whichButton) {
                                //...
                            }
                        }
                ).create();
    }
}

Activity:

Button button= (Button) findViewById(R.id.button2);
button.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
      DialogFragment newFragment = MyAlertDialogFragment.newInstance("time : " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Calendar.getInstance().getTime()) );
      newFragment.show(getSupportFragmentManager(), "dialog");
   }
});

 

 

Android: zpřístupnění nově vytvořeného souboru přes USB v počítači

Řeším zajímavý úkol: vytvoř v Android zařízení soubor, který bude po připojení telefonu k PC vidět v PC a bude možné jej překopírovat z telefonu do PC.

Dejte si pozor, protože od Android 5 musíte jednotlivá oprávnění povolovat na vyžádání, my budeme potřebovat oprávnění pro ukládání na úložiště. Pro jednoduchost vynechám kód nutný pro získání oprávnění zápisu do úložiště.

Do AndroidManifext.xml přidáme oprávnění:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

A jdeme na zdrojový kód (nezapomeňte kontrolovat oprávnění zápisu na úložiště – od SDK Android 5!):

//vytvorime slozku do ktere budeme pridavat soubory
//slozka bude v "rootu" uloziste a jmenuje se xml
File rootFolder = new File(Environment.getExternalStorageDirectory(), "xml");
if (!rootFolder.exists()) {
   rootFolder.mkdir();
}

try {
   //vytvorime soubor ktery bude pristupny v pocitaci
   File file = new File(rootFolder , "xml" + getCurrentTimeStampForFile() + ".xml");

   FileWriter writer = new FileWriter(file);
   try {
      writer.write("obsah souboru");
   } finally {
      writer.close();
   }

   //toto je dulezite, timto prikazem se soubor zviditelni a bude po pripojeni telefonu pres USB videt v pocitaci
   MediaScannerConnection.scanFile(mContext, new String[] {file.toString()}, null, null);

} catch (Exception e) {
   //problem
}

Nejdůležitější příkaz je:

MediaScannerConnection.scanFile(mContext, new String[] {file.toString()}, null, null);

Tímto příkazem se soubor zviditelní mezi ostatními soubory a bude možné ho překopírovat do počítače

Android: jak na responzivní zobrazení komponent na celou výšku displeje

Pokud budete chtít zobrazit komponenty rovnoměrně na celou výšku displeje, můžete využít ConstraintLayout. Jedná se o nový layout, který je dostupný v Android Studiu. Nezapomeňte ošetřit malé displeje, aby se komponenty na displej vešly.

constraint-layout

Kód pro inspiraci:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="cz.vencax.constraintlayout.MainActivity">

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!1"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView2"
        app:layout_constraintBottom_toBottomOf="@+id/textView3"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@+id/textView1"
        app:layout_constraintVertical_bias="0.5" />

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView3"
        app:layout_constraintBottom_toBottomOf="@+id/textView4"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@+id/textView2"
        app:layout_constraintVertical_bias="0.5" />

    <TextView
        android:id="@+id/textView4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView4"
        app:layout_constraintBottom_toBottomOf="@+id/textView5"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@+id/textView3"
        app:layout_constraintVertical_bias="0.5" />

    <TextView
        android:id="@+id/textView5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView5"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />

</android.support.constraint.ConstraintLayout>

 

Android: layout pro různá zařízení

Pokud si přejete zobrazit různý layout pro telefon, 7″ tablet a 10″ tablet určitě vám pomohou tyto poznámky.

Starý a již nepreferovaný způsob:

  • layout výchozí složka – v té máte základní layout
  • layout-large pro 7″ tablet (funguje pro emulátor Nexus 7)
  • layout-xlarge pro 10″ tablet (funguje pro emulátor Galaxy Tab 10.1)

Nový preferovaný způsob je vybrání layoutu podle hodnoty dp

Jednotka dp (density independent piuxel) vyjadřuje fyzickou velikost displeje. a je definován vztahem:

1dp = 160px/dpi
  • 320dp: typické rozlišení pro telefon (240×320 ldpi, 320×480 mdpi, 480×800 hdpi, atd.).
  • 480dp: phablet – něco mezi telefonem a tabletem (480×800 mdpi).
  • 600dp: 7” tablet (600×1024 mdpi).
  • 720dp: 10” tablet (720×1280 mdpi, 800×1280 mdpi, atd)

Šablony pro zůzně velká zařízení

  • sw600dp = nejmenší rozměr (smallestWidth) 600dp – použije se pokud nejmenší rozměr displeje je alespoň 600dp – bez ohledu na to zda je to šířka nebo výška
  • w600dp = šířka displeje 600dp
  • h600dp = výška displeje 600dp

Příklad: Nexus 6 se má velikost displeje 5,96″ a Quad HD v rozlišení 2560 x 1440 (493 ppi). To odpovídá ~ 730 x 410 dp

Vaše zařízení můžete dohledat na adrese http://www.emirweb.com/ScreenDeviceStatistics.php

Typická zařízení

  • Nexus 7: 1280 x 800 px (961 x 600 dp) / tvdpi / Large screen
  • Nexus 10: 2560 x 1600 px (1280 x 800 dp) / xhdpi / XLarge screen

Pokud tedy chci layout pro Nexus 7, založím ve složce app\src\main\res\layout-w600dp\ požadovaný layout

Pro Nexus 10 založím v app\src\main\res\layout-w720dp\ požadovaný layout

Honneywell Dolphin 75e

Do práce jsme vybírali mobilní terminál, který bude mít hardwarovou čtečku čárových kódu. Po prozkoumání nabízených zařízení nám zůstaly pouze zařízení s OS Android (snad s tím W10 ještě zahejbe ale asi to potrvá). Nakonec jsme vybrali Honneywell Dolphin 75e ale pro ten nefungují oficiální ovladače. WTF?

Nakonec jsem to obešel takto a ovladače fungují:

  1. přesvědčte se že máte nainstalovány ovladače pro Google USB Driver
    doolphin75e_1
  2. Úprava android_winusb.inf
    Soubor android_winusb.inf jsem v mém případě nalezl na: C:\Users\pc-x\AppData\Local\Android\sdk\extras\google\usb_driver\android_winusb.infProvedeme jeho editaci a za sekce Under both the [Google.NTx86] [Google.NTamd64] přidáme:

    ;Honeywell Dolphin Android Product[A0:fastboot,A1:adb,A2:UMS,A3:ums-adb]
    %SingleAdbInterface% = USB_Install, USB\VID_0C2E&PID_0BA0&MI_00
    %SingleAdbInterface% = USB_Install, USB\VID_0C2E&PID_0BA1
    %CompositeAdbInterface% = USB_Install, USB\VID_0C2E&PID_0BA3&MI_01
    %SingleBootLoaderInterface% = USB_Install, USB\VID_0C2E&PID_0BA0
    
  3. Ve Správci zařízení zvolíme na neznámém zařízení Aktualizace software ovladače… a vybereme Vyhledat ovladač v počítači kde zvolíme cestu:
    C:\Users\pc-x\AppData\Local\Android\sdk\extras\google\usb_driver

    vyskočí upozornění, že Windows nemůže oveřit vydavatele tohoto software, to potvrdíme a ovladače máme nainstalované 🙂

ADB

Nyní když máme ovladače, můžeme použít adb příkazy.

adb shell
adb devices

 

Android jak na služby (services)

Pokud v Androidu vykonáváte větší množství operací, není vhodné tuto práci dělat v hlavním vlákně aplikace. Tuto práci musíte udělat v novém vlákně (AsynsTask) nebo ve službě (services). Dnes si ukážeme jak udělat jednoduchou službu.

Na rozdíl od aktivit běží služby na pozadí a nepotřebují uživatelské rozhraní. Služba běží paralelně s hlavním vláknem. Klasickým příkladem služby je přehrávač hudby na pozadí. Když hudba hraje, uživatel se může přepnout do jiné aplikace a přehrávání hudby se nepřeruší. Klasickým případem je stahování/odesílání dat z internetu, práce s databází, zpracování velkých souborů, přehrávání hudby,…

Služby je nutné registrovat v AndroidManifest.xml

    <application ....>
        <activity android:name=".activity.MainActivity" >
           ...
        </activity>

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

    </application>

Poslední dobou se mi líbí přístup definování volání služby jako statickou metodu přímo v service:

import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import my.namespace.Config;

public class MyService extends IntentService {

    private static final String MY_TASK = "my.namespace.action.MY_TASK";
    private static final String MY_PARAM = "myParam";


    public MyService() {
        super("MyService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {

        if (intent != null) {
            final String action = intent.getAction();
            String myParam = intent.getStringExtra(MY_PARAM);

            if (MY_TASK.equals(action)) {

                //my task hard work
                if (Config.DEBUG) Log.d(Config.TAG, "my task hard work with my param: " + myParam );

            }
        }
    }

    public static void runMyService(Context context, String parameter) {

        if (Config.DEBUG) Log.d(Config.TAG, "runMyService()");

        Intent intent = new Intent(context, MyService.class);
        intent.setAction(MY_TASK);
        intent.putExtra(MY_PARAM, parameter);
        context.startService(intent);

    }

}

Metodou runMyService spustíme službu, která vynutí zavolání metody onHandleIntent kde se podle proměnné action rozhodne jaká z úloh se bude provádět (v budoucnu se předpokládá více činnosti pro službu).

Zavolání služby je pak jednoduché:

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (Config.DEBUG) Log.d(Config.TAG, "onCreate()");

        MyService.runMyService(getContext(), "muj parametr");

    }

 

 

Android aplikace + New Relic

Pro analýzu používání a padání Android aplikací se mi osvědčil New Relic (mimochodem, je super i na server). V základní verzi je zadarmo. Obsahuje základní informace které pro malé aplikace bohatě stačí (v kombinaci New relic + Google play API mi momentálně nechybí žádná funkčnost). Pro mne nejužitečnější je crash list report, kde vidíte všechny pády vaší aplikace včetně mnoha informací (jaké to bylo zařízení, verze Androidu , …), Je naivní si myslet, že když aplikace funguje na mém zařízení, bude fungovat všude… Vždy jsem matně vzpomínal jsem jsem New Relic do aplikace dostal. To co stačí udělat naleznete v tomto videu:

Ať slouží 🙂