sobota, 13 września 2014

Pojedynek z Androidem - odc.6 intro po raz drugi (architektura, preferencje, menu, okna dialogowe, deployment)

Następny odcinek z cyklu poświęconemu systemowi Android.  Po obejrzeniu kolejnego szkolenia mającego być wprowadzeniem, tym razem starszego wychwyciłem kilka rzeczy dotąd nieporuszanych jak architektura systemu i API,  ustawienia aplikacji, obsługa menu i okien dialogowych oraz przygotowanie aplikacji do publikacji w sklepie.

Oczywiście nie mogę obejść się bez odrobiny publicystyki, tak więc i dziś zanotuję kilka swoich subiektywnych wrażeń oraz porównań.

Preferencje - taki odpowiednik ApplicationData.LocalSettings plus panel ustawień jak dla mnie, przy czym przynajmniej na razie nie zauważyłem odpowiednika RoamingSettings.  Jak zwykle trochę przekombinowana obsługa, choć kreator i automatyczny interfejs dla różnego rodzaju ustawień może być ciekawą opcją.

Menu - obsługa kontekstowego menu jak zwykle przekombinowana, nie rozumiem upodobania do nadpisywania metod w aktywności, brakuje mi swobody jaką dają kontrolki XAML czy HTML5.  Pomysł na wybieralne menu czy z checkami jakiś taki też nadmiarowy,  patrząc tak wizualnie

Okna dialogowe - tutaj znowu się powtórzę, po kij nadpisywać metody w aktywności, definiowanie callbacków i akcji nawet zgrabne, choć podejście z async w WinRT bardziej mi się podoba. Dialog progress?  Czy nie jest to wizualny przerost formy nad treścią, zwłaszcza jeśli nie można go anulować ? IMHO wystarczyłby sam pasek czy kółko, ale Android ma swoje wytyczne.  Obsługa znów mało banalna, coś notyfikuj, coś nadpisz, odpal coś jawnie w innym wątku itp.  Niestandardowy dialog ma choćby odpowiednika w ContentDialog w WP 8.1.  W Android mimo deklaratywnego XML i tak nadal mamy w cenie imperatywne sterowanie elementami UI, czyli odszukanie ich z poziomu code-behind, by coś na nich wykonać.

Publikacja - w szczegółach różna, ale jednak coś podobnego. Podpisywanie, pakowanie, sklep brzmi  dość znajomo. Przy czym tu jakby sami bawimy się w obfuskację, w Windows Phone Microsoft zabezpiecza nasze app-ki za nas, w Windows nie ma zabezpieczeń, ale mają być wprowadzone.

Zaznaczam że to starsze szkolenie, więc jak dotrę do nowszych materiałów to być może zweryfikuję, to co tutaj napisałem. Ale czasami od czegoś trzeba zacząć.

 

Architektura

  • Jądro Linux
    • abstrakcja sprzętu, sterowniki
  • Biblioteki
    • Standardowa biblioteka C
    • Biblioteki multimedialne
    • Menadżery surfowania (m.in dostęp do wyświetlacza)
    • LibWebCore (WebKit)
    • SGL (2D)
    • Biblioteki 3D (OpenGL, ES)
    • FreeType (renderowanie czcionek)
    • SQLLite
  • Android Runtime
    • podstawowe biblioteki zawierające większość standardowych bibliotek Java
    • nie używa JVM, a Dalvik VM
      • wykonuje pliki .dex
      • każda aplikacja uruchamia swoją własną VM
  • Framework dla aplikacji
    • pełny zbiór serwisów napisanych w Javie
    • widoki i okna
    • zasoby, dostawcy zawartości
    • serwisy telefonu / API
    • notyfikacje
    • cykl życia
  • Aplikacje
    • jeden lub więcej
      • aktywności
      • serwisów
      • broadcast receiver-ów
      • content provider-ów (odpytywanie zbiorów danych np. SQL)

Inne rodzaje API

  • serwisy lokalizacji
  • serwisy telefoniczne
  • audio & video
  • przeglądarka webowa
  • google maps

Bezpieczeństwo i uprawnienia

  • Każda aplikacja wykonuje się ze swoim własnym Linux user ID
  • Wszystkie aplikacje wykonują się domyślnie w piaskownicy
  • Uprawnienia
    • Aplikacje deklarują w manifeście, czego potrzebują (w tym możliwe definicje własnych)
    • W czasie instalacji użytkownik decyduje o zgodzie na wymagane uprawnienia
  • Aplikacje są podpisywane lokalnie (pliki APK)

Instrumentacja

  • możliwość określenia w manifeście
  • junit

public class MainActivity extends Activity {

        @InjectView

        private TextView textView;

}

 

Layout

Layout absolutny

<AbsoluteLayout …

      android:layout_width=”fill_parent”  android:layout_height=”fill_parent”>

      <Button android:text=”Button”  android:layout_width=”wrap_content” android:layout_height=”wrap_content”

         …  android:layout_x=”126dip”  android:layout_y=”61dip”></Button>

</AbsoluteLayout>

Table Layout,  List View, Grid View, Tab View

Date/Time Picker, Dropdown (Spinner), Auto Complete, Gallery, Frame Layout, Google Map, Web View

android:textSize=”16sp”

 

Preferencje

Współdzielone preferencje

  • Sposób na przechowywanie informacji dla aplikacji
  • Dozwolone jest używanie wielu zbiorów preferencji
  • Dwa poziomy
    • aktywność
    • aplikacja
  • To coś innego niż zapisywanie stanu
  • Dlaczego używać
    • Małe ilości danych, które potrzebujemy zapisać
    • Łatwiejsze niż zapisywanie do pliku lub bazy danych
    • Ustawienia użytkownika
  • Kiedy stosować
    • przechowywanie danych pomiędzy wywołaniami aplikacji
    • przechowywanie danych pomiędzy aktywnościami w aplikacji
    • odpowiadanie na zmiany preferencji za pomocą handlerów

public class XApplication extends Application {

         @Override

         public void onCreate()  {

                 super.onCreate();

                 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());

                 Editor editor = preferences.edit();

                 editor.putString(“test”, “xxx”);

                 editor.commit();

         }

}

Nazwę własnej klasy aplikacji umieszczamy w manifeście

public class MainActivity extends Activity {

         …

         private OnClickListener button1Listener = new OnClickListener()  {

                 @Override

                 public void onClick(View arg0)  {

                         SharedPreferences preferences = getSharedPreferences(“default”, MODE_PRIVATE);

                         string value = preferences.getString(“test2”, “”);

                         …

                 }

         }

         …

        @Override

        public void onCreate(Bundle savedInstanceState)  {

                 …

                 //SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());

                 //String value = preferences.getString(“test”, “”);

                 …

                 SharedPreferences preferences = getSharedPreferences(“default”, MODE_PRIVATE / MODE_WORLD_READABLE / MODE_WORLD_WRITEABLE);

                 Editor editor = preferences.edit();

                 editor.putString(“test2”, “yyy”);

                 editor.commit();

        }

}

Ekran ustawień

Eclipse

  • New Android XML File  -> Preference
    • Add:  CheckBoxPreference, EditTextPreference, ListPreference, Preference, PreferenceCategory,  PreferenceScreen, RingtonePreference
    • Attributes for EditTextPreference
      • Attributes from DialogPreference
      • Attributes from Preference:  Key, …

<PreferenceScreen …>

         <EditTextPreference  android:dialogMessage=”Xxxx”  android:key=”test3” android:dialogTitle=”Yyy”  android:title=”Zzz”></EditTextPreference>

</PreferenceScreen>

 

public class MainPreferenceActivity extends PreferenceActivity {

          private OnPreferenceChangeListener  onPreferenceChangeListener  = new OnPrefeenceChangeListener()  {

                  @Override

                  public boolean onPreferenceChange(Preference arg0,  Object arg1)  {

                          return false;

                  }

          }

 

          @Override

           protected void onCreate(Bundle savedInstanceState)  {

                   super.onCreate(savedInstanceState);

                   addPreferencesFromResource(R.xml.main_preferences);

 

                   Preference p;

                   p.setOnPreferenceChangeListener(onPreferenceChangeListener);

           }

}

Manifest

  • Deklaracja MainPreferenceActivity

Inne opcje przechowywania danych

  • Internal Storage
    • jak isolated storage
    • usuwany z aplikacją
    • proste pisanie do pliku
  • External Storage
    • jak internal, ale może nim być karta SD lub inny nośnik dostępny dla użytkownika
    • może być zamontowany przez USB
    • należy sprawdzić dostępność przed użyciem
    • dostępne specjalne foldery Androida
  • Bazy danych
    • SQLLite
    • Sqllite3 tool
    • kursory do odpytywania danych

 

Menu

Option menu

  • wyświetlane po naciśnięciu sprzętowego przycisku menu
  • 6 pozycji, pozycja “more”
  • Zwykle link do ustawień aplikacji
  • Specyficzne dla aktywności

Context menu

  • dostępne po dłuższym dotknięciu elementu
  • przykłady:  usuwanie,  kopiowanie,  wycinanie,  zmiana nazwy

Submenu

  • tylko jeden poziom zagnieżdżenia

Pozycje z checkiem

  • wszystkie pozycje menu mogą mieć checka
  • tylko menu kontekstowe wyświetla checki
  • mogą być pojedyncze lub grupa
    • pojedynczy wybór
    • multi-select

Eclipse

  • New Android XML File  -> Menu
    • Checkable behavior

<menu  …>

          <group  android:id=”@+id/group1”>

                     <item android:id=”@+id/item1”></item>

                    <item  android:id=”@+id/item2”></item>

          </group>

</menu>

      public class MainActivity extends Activity  {

               …

               @Override

               public void onCreate(Bundle savedInstanceState)  {

                         …

                         registerForContextMenu(button);

               }

 

               @Override

               public void onCreateContextMenu(ContextMenu menu,  View v,  ContextMenuInfo menuInfo)  {

                          super.onCreateContextMenu(menu, v, menuInfo);

                          MenuInflater inflater = getMenuInflater();

                          inflater.inflate(R.menu.context_menu), menu);

               }

                @Override

                public boolean onContextItemSelected(MenuItem item)  {

                          …

                          return super.onContextItemSelected(item);

                }

      }

<menu  …>

      <group …>

            <item …></item>

            <item …>

                   <menu>

                          <item …></item>

                          <item …></item>

                   </menu>

            </item>

      </group>

</menu>

 

Dialogi

showDialog

         onCreateDialog

         onPrepareDialog

dismissDialog   (pozostaje w pamięci, jedynie potem onPrepareDialog)

removeDialog

Alert dialog

  • Zastosowanie
    • Ogólne powiadomienie
    • Pytania OK/Anuluj
    • Proste wybory
  • 1, 2 lub 3 przyciski
  • Możliwość używania list z wyborem

Progress dialog

  • Dwa tryby
    • Spinner (kółko)
    • Horizontal (pasek)
  • Bez Cancel

Custom dialog

  • ten sam layout, co aktywności
  • może wchodzić w interakcję z widokiem w layout
  • zawsze pokazuje tytuł
  • zarządzanie jak w Alert Dialog

public class MainActivity extends Activity  {

         final private int RESET_DIALOG = 0;

         …

         private OnClickListener resetButtonListener = new OnClickListener()  {

                   @Override

                   public void onClick(View arg0)  {

                           showDialog(RESET_DIALOG);

                         

                           //lub dla progress dialog  (drugi sposób)

                           dialog = ProgressDialog.show(MainActivity.this, “Waiting”,  “Doing something…”);

                            Thread thread = new Thread(new Runnable()  {

                                       @Override

                                        public void run()  {

                                                 Thread.sleep(3000);

                                                  handler.sendEmptyMessage(0)

                                        }

                              });                             

                              thread.start();

                   }

         }

         protected android.app.Dialog  onCreateDialog(int id)  {

                 /* 

                 switch(id)  {

                           case  RESET_DIALOG: 

                                    AlertDialog.Builder  builder  = new Builder(this);

                                    return builder

                                           .setMessage(“Xxx …”)

                                           .setNegativeButton(“No”, new DialogInterface.OnClickListener()  {

                                                      @Override

                                                      public void onClick(DialogInterface arg0,  int arg1)  {

                                                               Toast.makeText(MainActivity.this,  “…”, 5).show();

                                                      }

                                           })

                                           .setPositiveButton(“Yes”, new DialogInterface.OnClickListener()  {

                                                      @Override

                                                      public void onClick(DialogInterface arg0,  int arg1)  {

                                                               Toast.makeText(MainActivity.this,  “…”, 5).show();

                                                      }

                                           })

                                           .create();

                     }  */

 

                     /*

                      switch(id)  {

                           case  RESET_DIALOG: 

                                    AlertDialog.Builder  builder  = new Builder(this);

                                    return builder

                                           .setTitle(“Xxx …”)

                                           .setNegativeButton(“No”, new DialogInterface.OnClickListener()  {

                                                      @Override

                                                      public void onClick(DialogInterface arg0,  int arg1)  {

                                                               Toast.makeText(MainActivity.this,  “…”, 5).show();

                                                      }

                                           })

                                           .setPositiveButton(“Yes”, new DialogInterface.OnClickListener()  {

                                                      @Override

                                                      public void onClick(DialogInterface arg0,  int arg1)  {

                                                               Toast.makeText(MainActivity.this,  “…”, 5).show();

                                                      }

                                           })

                                           .setMultiChoiceItems(new CharSequence[] { “A”,  “B”,  “C”},  new boolean[]  { false,  false,  false},  null)

                                           .create();

                     }  */

 

                     /*

                     switch(id)  {

                            case RESET_DIALOG: 

                                        ProgressDialog  progressDialog = new ProgressDialog(this);

                                        progressDialog.setMessage(“…”);

                                        progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);

 

                                        Thread thread = new Thread(new Runnable()  {

                                                 @Override

                                                 public void run()  {

                                                          Thread.sleep(3000);

                                                          handler.sendEmptyMessage(0)

                                                 }

                                        });

                                        dialog  = progressDialog;

                                        thread.start();

                                        return dialog;

                     }  */

 

                     switch  (id)  {

                               case RESET_DIALOG:

                                         dialog = new Dialog(this);

                                         dialog.setContentView(R.layout.custom_dialog);

                                         dialog.setTitle(“…”);

                                         TextView textView = (TextView) dialog.findViewById(R.id.textView1);

                                         textView.setText(“…”);

                                         return dialog;

                     }

 

                      return null;

         }

       

        private Dialog dialog  = null;

        private Handler handler = new Handler()  {

                  public void handleMessage (android.os.Message msg)  {

                            dialog.dismiss();

                  } 

        }

         …

}

 

Deployment

Podpisywanie aplikacji

  • wszystkie aplikacje muszą być podpisane
  • wersja debug jest podpisywana kluczem debug
  • certyfikaty muszą wygasać po 22.10.2033
  • własne podpisywanie (bez centrum autoryzacyjnego)
  • ten sam certyfikat dla
    • aktualizacji
    • współdzielenia kodu

Eclipse

  • Projekt w Package Explorer (menu kontekstowe)
    • –> Android Tools –>  Export (Un)Signed Application Package…
    • -> Export:  Android/Export Android Application

Własne podpisywanie

  • {Program Files}\Java\{wersja}\bin\keytool  -genkey -v -keystore ścieżka -alias Demo  -keyalg RSA  -keysize 2048  -validity 10000
  • jarsigner  -verbose  -keystore ścieżkaCert  ścieżkaPakietu Demo
  • jarsigner -verify -verbose ścieżkaPakietu
  • {android_sdk\tools}zipalign -v 4 ścieżkaPakietu plikPakietu

Proguard

  • obfuscator
  • narzędzie open source w ADT
  • redukuje rozmiar kodu
  • License Manager nie jest wystarczający
  • trudny do konfiguracji z zewnętrznymi bibliotekami
  • trudne debugowanie po zdeployowaniu
  • {android_sdk}\tools\proguard\bin

Android License Manager

  • tylko dla płatnych aplikacji
  • urządzenie lub emulator z API level 8+

Aplikacja – LVL(License Veryfication Library) –> Market App –> Market License Server

Android SDK and AVD Manager

Third party Add-ons

  • Google Inc. (dl-ssl.google.com)
    • Google Market Licensing package, revision 1

Eclipse

  • Kreowanie projektu z biblioteki

Manifest

<uses-permission android:name=”com.android.vending.CHECK_LICENSE” />

public class MainActivity extends Activity {

        private static final byte[] SALT = new byte[] { … };

        private static final String GOOGLE_KEY = “ABC”;

        …

        private LicenseCheckerCallback licenseCheckerCallback;

        private LicenseChecker licenseChecker;

        …

        private OnClickListener helpButtonListener = new OnClickListener()  {

                @Override

                public void onClick(View arg0)  {

                       licenseChecker.checkAccess(licenseCheckerCallback);

                }

        }

        …

 

        @Override

        public void onCreate(Bundle savedInstanceState)  {

                …

                licenseCheckerCallback  = new LicenseCheckerCallbackImpl();

                String deviceId = Secure.getString(this.getContentResolver(),  Secure.ANDROID_ID);

                licenseChecker = new LicenseChecker(getApplicationContext(), 

                new ServerManagedPolicy(getApplicationContext(),  new AESObfuscator(SALT, getPackageName(), deviceId),

                GOOGLE_KEY);

        }

 

        …       

        private class LicenseCheckerCallbackImpl implements LicenseCheckerCallback  {

                 @Override

                 public void allow()  {

                           if (isFinishing())

                                      return;

                           ShowMessage(“…”);

                 }

                 @Override

                 public void dontAllow()  {

                           if (isFinishing())

                                      return;

                           ShowMessage(“…”);

                 }

                 @Override

                 public void applicationError(ApplicationErrorCode errorCode)  {

                 }

        }

 

       private void ShowMessage(String message)  {

                handler.post(new Runnable()  {

                          @Override

                          public void run()  {

                                   Toast.makeText(MainActivity.this, message, 5).show();

                          }

                }

       }

}

Brak komentarzy: