czwartek, 18 września 2014

Pojedynek z Androidem - odc.8 Android numer 4 (kalendarz)

Oficjalne API do kalendarza to była kolejna istotna nowość Android 4.0.  Windows Phone 7.5 miał zdaje się po raz pierwszy obsługę takich rzeczy, też za pomocą zapytań, aczkolwiek jakoś bardziej wygodnych. Windows i Windows Phone 8.1 funcjonalności odnośnie kalendarza mają podobne, czyli dodawanie, edycja, usuwanie, listowanie itp. Też możemy wykonywać operacje za pomocą systemowego UI.  Możliwość definiowania własnych kalendarzy również została wprowadzona.   Wracając do Androida to jego intencje dla kalendarza to jakby odpowiednik systemowych tasków. Content provider z operacjami na tabelkach to znacznie bardziej złożone rozwiązanie, ale ujdzie. Nie rozumiem tylko po co obnażanie wnętrzności i danie możliwości znalezienia eventów, które ktoś usunął, ale nie zostały jeszcze fizycznie wyczyszczone.

 

Przed Android 4.0 nie było oficjalnego API  - przez standardowy content provider trzeba było używać niskopoziomowych danych

Android 4.0

  • Klasa CalendarContract dla ułatwienia dostępu
  • Wiele często używanych operacji za pomocą intencji
  • Bezpośrednie programowanie danych kalendarza nadal wymaga content providera (CalendarContract upraszcza korzystanie z niego)

CalendarContract

centralny punkt dla wszystkich stałych kalendarza

  • definiuje uri identyfikujące dane powiązane z kalendarzem
    • używane przez klucz-wartość intencji
    • nazwy tabel dla content providera
  • definiuje nazwy dla elementów powiązanych z kalendarzem
    • identyfikuje extras intencji
    • identyfikuje kolumny tabel

Intencje kalendarza

  • zapewniają najprostszy dostęp do kalendarza
  • zalety w stosunku do korzystania z content providera kalendarza
    • prezentują użytkownikowi standardowy interfejs kalendarza  (startActivity z odpowiednim intent)
    • nie wymagają specjalnych pozwoleń
    • nie trzeba znać szczegółów storage
  • kilka ograniczeń
    • wymagają interakcji użytkownika do zakończenia akcji
    • nie dostarczają informacji z powrotem do naszej aplikacji

Otwieranie kalendarza z podanym czasem i datą

  • akcja:  Intent.ACTION_VIEW
  • dane:  CalendarContract.CONTENT_URI (dodajemy time w ms od północy 1 stycznia 1970)
    • content://com.android.calendar/time/1334168996092

Calendar dateToShow = Calendar.getInstance();

dateToShow.set(2014, Calendar.SEPTEMBER, 17, 8, 00);

 

long ms = dateToShow.getTimeInMillis();

 

Uri.Builder uriBuilder = CalendarContract.CONTENT_URI.buildUpon();

uriBuilder.appendPath(“time”);

ContentUris.appendId(uriBuilder,  ms);

Uri uri = uriBuilder.build();

 

Intent intent = new Intent(Intent.ACTION_VIEW);

intent.setData(uri);

 

startActivity(intent);

Przeglądanie i modyfikacja eventów z intencjami

  • dane:  CalendarContract.Events.CONTENT_URI  (dołączamy event id)
    • content://com.android.calendar/events/125
  • oglądanie istniejącego eventu:  akcja = Intent.ACTION_VIEW
  • edycja istniejącego eventu:  akcja = Intent.ACTION_EDIT

Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI,  eventID);

Intent intent = new Intent(Intent.ACTION_EDIT).setData(uri);

startActivity(intent);

 

Tworzenie nowych eventów

Calendar begin = Calendar.getInstance();

begin.set(2014,  Calendar.SEPTEMBER,  17,  8, 0);

Calendar end = Calendar.getInstance();

end.set(2014,  Calendar.NOVEMBER, 20, 20, 0);

 

Intent intent = new Intent(Intent.ACTION_INSERT).setData(CalendarContract.Events.CONTENT_URI);

intent.putExtra(CalendarContract.Events.TITLE,  title);

intent.putExtra(CalendarContract.Events.EVENT_LOCATION,  location);

intent.putExtra(CalendarContract.Events.DESCRIPTION,  description);

intent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME,  begin.getTimeInMillis());

intent.putExtra(CalendarContract.EXTRA_EVENT_END_TIME,  end.getTimeInMillis());

startActivity(intent);

Calendar content provider

Najbardziej bezpośrednia kontrola nad danymi kalendarza

  • zalety w stosunku do intencji
    • dostęp do wszystkich danych kalendarza
    • dostęp do programów i modyfikacja danych bez interakcji z użytkownikiem
  • wyzwania
    • deklaracja przez naszą aplikację specyficznych dla kalendarza uprawnień

Uprawnienia content providera kalendarza

  • odczytywanie zawartości:   “android.permission.READ_CALENDAR”
  • zapisywanie zawartości:   “android.permission.WRITE_CALENDAR”  (prawo do zapisu nie oznacza prawa do odczytu)

Główne klasy CalendarContract

  • CalendarContract.Calendars - wszystkie kalendarze
  • CalendarContract.Events - wszystkie eventy
  • CalendarContract.Instances - czas rozpoczęcia i zakończenia każdego wydarzenia
  • CalendarContract.Attendees - osoby uczestniczące w każdym wydarzeniu
  • CalendarContract.Reminders - przypominanie o eventach

Calendars –> Events –> Instances,  Attendees,  Reminders

  • Calendars:  CalendarContract.Calendars.CONTENT_URI
  • Events: CalendarContract.Events.CONTENT_URI,   CalendarContract.Events.CALENDAR_ID
  • Instances: CalendarContract.Instances.CONTENT_URI,  CalendarContract.Instances.EVENT_ID
  • Attendees:  CalendarContract.Attendees.CONTENT_URI,  CalendarContract.Attendees.EVENT_ID
  • Reminders:  CalendarContract.Reminders.CONTENT_URI,   CalendarContract.Reminders.EVENT_ID

Tabela kalendarzy

Lista wszystkich kalendarzy na urządzeniu

  • unikalne ID dla każdego kalendarza:   CONTENT_URI & _ID
  • możliwość wyszukiwania za pomocą ContentResolver.query
  • wyszukiwanie konkretnego kalendarza wymaga kombinacji trzech pól
    • CalendarContract.Calendars.ACCOUNT_NAME   xxx@gmail.com
    • CalendarContract.Calendars.ACCOUNT_TYPE
      • “com.google”  - konta synchronizowane przez Google Calendar
      • CalendarContract.Calendars.ACCOUNT_TYPE_LOCAL - dla kalendarzy niesynchronizowanych
    • CalendarContract.Calendars.NAME - nazwa kalendarza, kiedy został utworzony

long calendarID = 1;

Uri uri = ContentUris.withAppendedId(CalendarContract.CONTENT_URI, calendarID);

Listowanie kalendarzy

String[] returnColumns = new String[]  {

       CalendarContract.Calendars._ID,

       CalendarContract.Calendars.ACCOUNT_NAME,

       CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,

       CalendarContract.Calendars.ACCOUNT_TYPE

};

 

Cursor cursor = null;

ContentResolver cr = getContentResolver();

 

cursor = cr.query(CalendarContract.Calendars.CONTENT_URI,  returnColumns,  null, null, null);

 

while (cursor.moveToNext())  {

        long calID = cursor.getLong(0);

        String displayName = cursor.getString(1);

        String accountName = cursor.getString(2);

        String accountType = cursor.getString(3);

        …

}

       cursor.close();

Znalezienie kalendarza

String[] returnColumns = new String[] {

         CalendarContract.Calendars._ID

};

 

String selection = “((“  + CalendarContract.Calendars.ACCOUNT_NAME + “  =  ?)  AND  ”  +

              “(“   + CalendarContract.Calendars.ACCOUNT_TYPE  + “  =  ?)   AND  ”  +

              “(“   + CalendarContract.Calendars.CALENDAR_DISPLAY_NAME   +  “  = ?))”;

String[]  selectionArgs = {accountName,  accountType,  displayName};

 

Cursor cursor = null;

ContentResolver cr = getContentResolver();

 

cursor = cr.query(CalendarContract.Calendars.CONTENT_URI,  returnColumns,  selection,  selectionArgs, null);

 

long calendarID = –1;

 

if (cursor.moveToNext())

          calendarID = cursor.getLong(0);

 

return calendarID;

Tabela zdarzeń

Lista wszystkich zdarzeń dla wszystkich kalendarzy

  • Identyfikacja zdarzeń
    • bezpośredni dostęp przez unikalne _ID
    • powiązanie z kalendarzem za pomocą kolumny CALENDAR_ID
  • Wyszukiwanie przez ContentResolver.query
  • Ważne kolumny
    • TITLE
    • EVENT_LOCATION
    • DESCRIPTION
    • DTSTART:  czas rozpoczęcia  (ms od północy 1 stycznia 1970)
    • DTEND: czas zakończenia  (-//-)
    • EVENT_TIMEZONE:  nazwa strefy czasowej miejsca, gdzie odbywa się zdarzenie np. US/Eastern  (lista wszystkich dostępnych – TimeZone.getAvailableIDs())

Dodawanie nowych zdarzeń

Dodawanie za pomocą ContentResolver.insert

  • Wartości dostarczone w tablicy ContentValues
  • Wymagane kolumny
    • CALENDER_ID
    • DTSTART
    • DTEND  (dla niecyklicznych)
    • EVENT_TIMEZONE
  • zwracane jest URI nowego zdarzenia (można wyciągnąć id zdarzenia przy pomocy ContentUris.parseId)

Modyfikacja i usuwanie zdarzeń

  • Modyfikacja za pomocą ContentResolver.update
    • idetyfikacja zdarzeń do update’u przy użyciu event URI lub query
    • specyfikacja kolumn do zmiany i ich wartości przy użyciu klasy ContentValues
  • Usuwamy za pomocą ContentResolver.delete
    • identyfikacja zdarzeń do zmiany przy użyciu event URI lub query
    • skasowane zdarzenia mogą nie być natychmiast usunięte
      • zdarzenie nie jest usuwane dopóki urządzenie nie zsynchronizuje się z serwerem
      • należy sprawdzić, czy kolumna DELETED ma wartość 0, aby być pewnym że zdarzenie jest nadal poprawne

Aktualizacja zdarzenia

ContentResolver cr = getContentResolver();

 

Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventID);

 

ContentValues values = new ContentValues();

values.put(CalendarContract.Events.EVENT_LOCATION,  location);

values.put(CalendarContract.Events.DESCRIPTION,  description);

 

int rows = 0;

       rows = cr.update(uri,  values,  null, null);

Usuwanie zdarzenia

ContentResolver cr = getContentResolver();

 

Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI,  eventID);

 

int rows = 0;

      rows = cr.delete(uri, null, null);

Sprawdzenie czy event został naprawdę usunięty

String[] EVENT_PROJECTION = new String[] {

        CalendarContract.Events._ID,

        CalendarContract.Events.TITLE,

        CalendarContract.Events.DELETED

};

 

String queryFilter = CalendarContract.Events.DELETED  +  “  =  ?”;

String[]  queryFilterValues = {“0”};

 

Cursor cur = null;

ContentResolver cr = getContentResolver();

Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI,  eventID);

cur  = cr.query(uri,  EVENT_PROJECTION,  queryFilter,  queryFilterValues,  null);

 

if (cur.moveToNext())  {

          long returnedEventID = cur.getLong(0);

          String title = cur.getString(1);

          int isDeleted = cur.getInt(2);

          …

}

Uczestnicy eventów

Tabela Attendees

  • Uczestnika dodajemy do wydarzenia przy użyciu ContentResolver.insert (dla każdego uczestnika musimy podać prawidłowe event id)
  • Ważne kolumny
    • ATTENDEE_NAME
    • ATTENDEE_EMAIL
    • ATTENDEE_RELATIONSHIP (relacja do użytkownika,  stałe Attendees.RELATIONSHIP_xxx:  Attendee, Organizer, Performer, Speaker)
    • ATTENDEE_TYPE (stałe Attendees.TYPE_xxx: Optional, Required, Resource,  dozwolone typy Calendars.ALLOWED_ATTENDEE_TYPES)
    • ATTENDEE_STATUS  (stałe Attendees.STATUS_xxx:  Accepted, Declined, Invited, Tentative)

Przypominanie o wydarzeniach

  • Dodajemy przypomnienie do spotkania używając ContentResolver.insert
    • trzeba podać poprawne event id
    • limit na kalendarz:  Calendars.MAX_REMINDERS
  • Ważne kolumny
    • METHOD: typ (stałe Reminders.METHOD_xxx:  Alert, Default, Email, SMS)
    • MINUTES:  minuty do rozpoczęcia alarmu

Zdarzenia cykliczne

Informacje o cykliczności w tabeli Instances

  • Kolumny dla dat/czasu cyklów eventów
  • Zapytania zawsze muszą mieć podany przedział czasu (dołączamy czas początkowy i końcowy w CONTENT_URI)
  • Widok na wystąpienia eventu
    • Tabela może być tylko odpytywana
    • Nie można utworzyć powtórzeń

Uri.Builder uriBuilder = CalendarContract.Instances.CONTENT_URI.buildUpon();

 

ContentUris.appendedId(uriBuilder, startDate.getTimeInMillis());

ContentUris.appendedId(uriBuilder, endDate.getTimeInMillis());

 

return uriBuilder.build();

Tworzenie cyklicznych eventów

Cykliczność jest cechą zdarzenia

ContentResolver cr = getContentResolver();

ContentValues values = new ContentValues();

values.put(CalendarContract.Events.CALENDAR_ID, calendarID);

values.put(CalendarContract.Events.TITLE, title);

values.put(CalendarContract.Events.DESCRIPTION, description);

values.put(CalendarContract.Events.EVENT_LOCATION, location);

values.put(CalendarContract.Events.DTSTART, startDate.getTimeInMillis());

values.put(CalendarContract.Events.EVENT_TIMEZONE, “US/Eastern”);

values.put(CalendarContract.Events.DURATION, duration);  //”PT1H”

values.put(CalendarContract.Events.RRULE, rRule);  //”FREQ=WEEKLY;COUNT=4”

 

Uri uri = cr.insert(CalendarContract.Events.CONTENT_URI, values);

long eventID = ContentUris.parseId(uri); 

środa, 17 września 2014

WinJS 3.0 - WinJS Everywhere

Krótki wpis, w którym odsyłam do wyczerpującego linka http://blogs.windows.com/buildingapps/2014/09/17/winjs-everywhere/, gdzie management bardzo dokładnie przedstawił, jakie korzyści odniesiemy przesiadając się z wersji drugiej na trzecią. Znacząco zostało poprawione wsparcie dla przeglądarek innych niż IE, wyeliminowano zależność od CSS Grid czy innych specyficznych rzeczy niedostępnych w innych przeglądarkach czy platformach. Pewne kontrolki dostępne do tej pory tylko w Windows Phone zyskały na uniwersalności np. Pivot. Wprowadzono też modularyzację biblioteki, by każdy mógł używać czego potrzebuje, niekoniecznie od razu całości.

wtorek, 16 września 2014

Windows 9 Preview (Build 9834) - kolejne informacje

Oficjalny event prezentujący po raz pierwszy nową wersję Windows został zapowiedziany na 30 września. Nie czekając jednak połowy miesiąca, już dziś mamy kolejne nieoficjalne informacje z builda, który stał się bohaterem ostatnich przecieków. Jakich nowości możemy się jeszcze potencjalnie spodziewać?  Oto one:

 

Centrum notyfikacji  - jak wiemy WP 8.1 otrzymał taką funkcjonalność, teraz czas na Windows

 

Storage Sense & WiFi Sense  - funkcjonalności znajome z WP

Windows-9-Preview-Build-9834-1410712077-0-0

 

Cortana  - na razie znaleziono na pewno pliki wskazujące na obecność Cortany, podobno jest to aplikacja uruchamiana w oknie

jQssuHn

 

Internet Explorer 12 - też jest ponoć związany z nowym Windows,  przecieki zaczynają mówić o… renesansie wtyczek!  Prowadzone są ponoć prace nad większą interakcją ze stronami webowymi niż pozwalają na to obecne webowe standardy. Drugim zagadnieniem jest uruchamianie za pomocą mikrowirtualizacji starego kodu w przeglądarce podobnie jak w środowisku aplikacyjnym. Nie wiadomo na ile pewne są te przecieki dotyczące koncepcji nowego IE …

 

Linki

poniedziałek, 15 września 2014

Pojedynek z Androidem - odc.7 Android numer 4 (jeszcze o fragmentach, ActionBar, sieć)

Przyszła pora na kolejny odcinek w stylu chłodnym okiem. Dziś na warsztat wziąłem część funkcjononalności wprowadzonych w Android 4.  Co prawda u bram już puka wersja piąta, ale od czegoś trzeba zacząć. Co dziś mam do skomentowania?

Fragmenty - moim zdaniem przerost infrastrukturalny nad treścią w stosunku do kontrolek XAML czy JavaScript.  Nie będę już wspominać o idei komunikowania się z aktywnością za pomocą interfejsu, aby się nie powtarzać.  Twórcy przewidzieli, że fragmenty mogą się odtwarzać i że powinniśmy zapewnić odtwarzanie ich stanu.  Trudno zrozumieć czemu obrócenie ekranu wszystko zabija i martw tu się developerze, by odtworzyć stan wszystkiego. Każdy ekran to musi być osobna aktywność, do tego stopnia że na wąskich ekranach otwiera się dodatkową aktywność, a na szerokich kontrolki z dwóch aktywności umieszcza się w jednej.  W XAML/HTML obrócenie ekranu nie wpływa na żywotność strony, ot wszystko się dynamicznie przemieszcza i tyle, pomysł z otwieraniem nowej strony na wąskich ekranach smartfonów też czasami się wykorzystuje, ale można też zrobić jedną kontrolkę przełączającą dwa widoki.

ActionBar - dobrze, że powstało coś bardziej funkcjonalnego niż podstawowe menu z dawniejszych czasów. Tak porównując to Windows Phone jako powstały znacznie później od razu wyszedł od idei paska aplikacyjnego i minimum przycisków sprzętowych.  Rodzina Windows/Windows Phone oferuje różne paski w zależności od typu urządzenia. Pomiędzy Windows 8.1 a WP 8.1 kontrolki paska XAML/WinJS z tym samym kodem mogą być już używane.  W Androidzie po prostu jest jeden system na smartfonach i tabletach. Zakładki możemy najbliżej porównać do kontrolek Hub i Pivot (tylko na WP), które wydają mi się bardziej nowoczesne, aczkolwiek nie są częścią paska aplikacyjnego czy nawigacyjnego. Sama obsługa zakładek w Android wydaje mi się niepotrzebnie skomplikowana, od razu listenery, jakieś ręczne sterowanie dodawaniem itd.

Komunikacja sieciowa - obsługa statusu wydaje się dość typowa, obsługa Wi-Fi Direct może nie jest najgorsza, ale przez brak zwykłych eventów nieco skomplikowana choćby przez broadcast receiver i implementacje listenerów. Z kolei obsługa czytania tagów NFC wydaje bardzo prosta, prostsza niż w WP, przy czym powstaje pytanie, jakie jeszcze inne scenariusze z NFC są realizowane przez Androida (w WP mamy także komunikację z innymi urządzeniami oraz płatności). Jak natknę się jeszcze na NFC, to być może zyskam szersze spojrzenie.

 

Android 4.0

  • więcej niż prosta ewolucja
  • platforma obsługująca różne czynniki
    • fragmenty
    • ActionBar
  • właściwości sieciowe zgodne z nowoczesnymi sieciami mobilnymi
    • limity na ilość danych,  kontrola użytkownika
    • komunikacja peer-to-peer pomiędzy urządzeniami
      • Wi-Fi Direct
      • NFC (odczyt tagów)
  • lepszy dostęp do funkcjonalności platformy
    • integracja interfejsu kalendarza z aplikacją
    • uproszczony dostęp do danych kalendarza
      • kalendarze –> zdarzenia –> instancje,  uczestnicy,  przypomnienia

 

Jeszcze o fragmentach

Fragmenty mogą być używane do

  • tworzenia adaptywnego UI
  • zakładek
  • menu kontekstowego
  • modyfikacji action bar

Klasy bazujące na fragmentach

  • Fragment
  • ListFragment
  • DialogFragment
  • PreferenceFragment

Dostawanie aktywności z poziomu fragmentu

  • metoda onAttach wywoływana w momencie podpinania fragmentu do aktywności
  • metoda getActivity

FragmentManager

  • każda aktywność ma własny, getFragmentManager
  • przechwytuje szczegóły tworzenia, pokazywania, ukrywania fragmentów
  • przechowuje referencje do wszystkich fragmentów w aktywności,  findFragmentById

Utrzymywanie stanu fragmentu

  • zmiany w layout często powodują tworzenie nowych instancji fragmentów
  • bieżący stan fragmentu zapisujemy za pomocą onSaveInstanceState
  • zapisany stan jest przekazywany do nowej instancji w metodach onCreate i onCreateView

public class CourseDescriptionFragment extends Fragment  {

         …

         int   _courseIndex;

         …

         @Override

         public View onCreateView(LayoutInflater inflater,  ViewGroup container,  Bundle savedInstanceState)  {

                 …

                 int initialIndex = savedInstanceState == null ? 0 : savedInstanceState.getInt(“courseIndex”, 0);

                 setDisplayedDescription(initialIndex);

                 …

         }

         @Override

         public void onSaveInstanceState(Bundle outState)  {

                  outState.putInt(“courseIndex”,  _courseIndex);

         }

         …

}

Dynamiczne zarządzanie wyświetlaniem fragmentu

  • często zachodzi potrzeba do przełączania między widokami jednej i wielu aktywności
  • FragmentManager pozwala ustalić, które fragmenty są aktualnie wyświetlane, metoda fragmentu isVisible

W zależności od orientacji ekranu możemy wyświetlać jedną aktywność z dwoma fragmentami lub dwie aktywności z jednym fragmentem

public class DynamicActivity extends Activity {

         …

         public void onSelectedCourseChanged (int courseIndex)  {

                   if  (descriptionFragment != null && descriptionFragment.isVisible())

                            //orientacja pozioma - master-details

                            descriptionFragment.setDisplayedDescription(courseIndex);

                   else  {

                            //orientacja pionowa - nowy ekran

                           Intent intent = new Intent(this,  DynamicDetailsActivity.class);

                           intent.putExtra(“courseIndex”,  courseIndex);

                           startActivity(intent);

                   }

         }     

}

 

ActionBar

  • lepiej dostosowany do różnych urządzeń i ekranów
  • także tradycyjne zachowanie menu i obsługa wychodzącego z użycia przycisku sprzętowego ‘menu’
  • różne warianty nawigacji
  • domyślnie aplikacje używają starego zachowania zgodnego z menu
  • dostępny tylko na aktywnościach używających skórek wywodzących się z Theme.Halo
    • automatycznie przy min sdk / target sdk 11 lub nowszym w manifeście
  • ikona na pasku może być klikalna
    • powrót do ekranu home aplikacji
    • przeniesienie użytkownika o jeden poziom wyżej w hierarchii aplikacji
  • opcja podziału na dwa paski (górny i dolny) na wąskim ekranie - w manifeście (w aktywności lub całej aplikacji)
  • zastępuje TabActivity w nawigacji zakładkowej

Potrzeba zarządzania aktywnościami taska przy implementacji home

  • użytkownik powinien mieć wrażenie powrotu
  • nowa aktywność jest domyślnie dodawana na końcu bieżącego stosu
  • przy powrocie “home” wszystkie inne aktywności w stosie powinny zostać wyczyszczone
    • osiągamy za pomocą FLAG_ACTIVITY_CLEAR_TOP

private void onHome()  {

         Intent intent = new Intent(this,  MyActivity.class);

         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);

         startActivity(intent);

}

Nawigacja zakładkowa

  • zakładki są umieszczane bezpośrednio na ActionBar
  • adaptacja do dostępnej przestrzeni na ekranie
    • na wąskich ekranach - pasek z zakładkami pod ikoną aplikacji
    • na szerokich ekranach - zakładki są umieszczone między ikoną aplikacji a opcjami
  • tworzenie
    • wywołujemy setNavigationMode na ActionBar z wartością NAVIGATION_MODE_TABS
    • dla każdej zakładki tworzymy instancję ActionBar.Tab
      • tworzymy Fragment dla zawartości
      • dostarczamy implementację interfejsu ActionBar.TabListener do zarządzania zakładką
        • onTabSelected  - przy pierwszym wywołaniu musi dodać fragment do aktywności, przy sukcesie musi uczynić fragment widocznym
        • onTabUnselected - musi ukryć fragment
        • onTabReselected - naciśnięcie wybranej już zakładki, zwykle nic nie robimy
    • każdą ActionBar.Tab dodajemy do ActionBar
      • poprzez metodę ActionBar.addTab

public class MyTabListener implements ActionBar.TabListener  {

         Fragment _tabFragment;

         boolean _isFirstSelect  = true;

 

         public MyTabListener(Fragment fragment)  {

                 _tabFragment = fragment;

         }

         public void onTabSelected(ActionBar.Tab tab,  FragmentTransaction fragmentTransaction)  {

                  if (_isFirstSelect)  {

                           fragmentTransaction.add(R.id.content,  _tabFragment,  null);

                           _isFirstSelect = false;

                  }

                  else  {

                           fragmentTransaction.attach(_tabFragment);

                  }                 

         }

         public void onTabUnselected(ActionBar.Tab tab,  FragmentTransaction fragmentTransaction)  {

                  fragmentTransaction.detach(_tabFragment);

         }

         public void onTabReselected(ActionBar.Tab tab,  FragmentTransaction fragmentTransaction)  {

         }

}

public class TabNavigatedActivity extends Activity {

         public void onCreate(Bundle savedInstanceState)  {

                 super.onCreate(savedInstanceState);

                 ActionBar actionBar = getActionBar();

                 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

 

                  Fragment tab1Fragment = Fragment.instantiate(this,  Tab1Fragment.class.getName());

                  MyTabListener tab1Listener = new MyTabListener(tab1Fragment);

                  ActionBar.Tab tab1 = actionBar.newTab().setText(“Tab 1”).setTabListener(tab1Listener);

                  …

 

                  actionBar.addTab(tab1);

                  …

         }

}

 

Sieć

Konfigurowalność sieci mobilnej

  • całkowity limit
  • limit dla konkretnej aplikacji
  • blokowanie korzystania z sieci w tle dla danej aplikacji

Ustalanie stanu sieci za pomocą ConnectivityManager

  • Context.getSystemService(CONNECTIVITY_SERVICE);
  • ConnectivityManager.getActiveNetworkInfo
  • zwraca instancję NetworkInfo dla bieżącego połączenia lub null jeśli nie ma w ogóle łączności
  • NetworkInfo.isConnected  - czy można skorzystać z połączenia
  • Nie używać ConnectivityManager.getBackgroundDataSetting, deprecated, zwraca zawsze true

Udostępnienie użytkownikom kontroli nad korzystaniem z sieci przez aplikację

  • udostępnienie preferencji do ograniczeń z korzystania z sieci
  • udostępnienie PreferenceActivity dla strony Android “Data Usage”
    • zdefiniowanie Intent Filter dla akcji MANAGE_NETWORK_USAGE
    • potrzebna kategoria DEFAULT

<activity …>

           <intent-filter>

                     <action android:name=”android.intent.action.MANAGE_NETWORK_USAGE” />

                     <category android:name=”android.intent.category.DEFAULT” />

          </intent-filter>

</activity>

w aktywności lub serwisie

ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();

HttpGet httpGet = new HttpGet(_url);

HttpResponse httpResponse;

HttpClient httpClient = newHttpClientWithThreadSafeClientConnManager();

InputStreamReader streamReader;

….

httpResponse = httpClient.execute(httpGet);

HttpEntity httpEntity = httpResponse.getEntity();

if  (httpEntity != null && httpEntity.isStreaming()) {

         InputStream inStream = httpEntity.getContent();

         if  (inStream != null)  {

                 streamReader = new InputStreamReader(inStream);

                 while ((bytesRead = streamReader.read(buffer,  0, size)) > 0) {  … }

                 inStream.close();

         }

}

Blokada na łączność w tle nie musi zadziałać na serwis, wykonywanie w tle zależy od tego, czy aktywność jest w tle

Wi-Fi Direct

Wymagane deklaracje w manifeście

<uses-sdk android:minSdkVersion=”14” />   (Android 4.0 lub nowszy)

<uses-feature android:name=”android.hardware.wifi.direct”  android:required=”true” />

<uses-permission android:name=”android.permission.ACCESS_WIFI_STATE” />

<uses-permission android:name=”android.permission.CHANGE_WIFI_STATE”/>

<uses-permission android:name=”android.permission.INTERNET” />

Implementacja

Główny przebieg

  • inicjalizacja systemu
  • inicjalizacja żądań by wykonać specyficzne akcje

Utworzony w runtime broadcast receiver

  • nasłuchiwanie notyfikacji o zakończeniu wykonywanych akcji
  • w niektórych przypadkach wykrywanie sukcesu lub błędu
  • w niektórych przypadkach inicjalizacja następnego kroku wieloetapowej akcji

WiFiP2pManager

  • initialize - przygotowanie API, zwraca WiFiP2pManager.Channel
  • discoverPeers - wyszukanie dostępnych peer-ów
  • requestPeers - pobranie bieżącej listy dostępnych peer-ów
  • connect - ustanowienie połączenia z peer-ami

Wi-Fi Direct informuje aplikacje o istotnych zdarzeniach za pomocą broadcast intents

  • WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION (przy włączaniu/wyłączaniu Wi-Fi Direct,  WiFiP2pManager.EXTRA_WIFI_STATE - bieżący stan)
  • WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION (zmiana informacji o Wi-Fi Direct tego urządzenia)
  • WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION (informuje, że wyniki zapytania o listę dostępnych peer-ów są już dostępne)
  • WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION (informuje, że połączenie zostało ustanowione lub zerwane)

public class MainActivity extends Activity implements WifiP2pManager.ChannelListener {

          private WifiP2pManager _wfdManager;

          private WifiP2pManager.Channel  _wfdChannel;

          private WifiDirectReceiver  _wfdReceiver;

         

          @Override

          public void onCreate(Bundle savedInstanceState)  {

                  …

                  _wfdManager = (WifiP2pManager) getSystemService(WIFI_P2P_SERVICE);

                  _wfdChannel = _wfdManager.initialize(this, getMainLooper(), this);

          }

 

          private void registerWfdReceiver() {

                   _wfdReceiver = new WiFiDirectReceiver(_wfdManager,  _wfdChannel,  this);

                   _wfdReceiver.registerReceiver();

          }

 

          private void unregisterWfdReceiver()  {

                   if (_wfdReceiver != null)

                           _wfdReceiver.unregisterReceiver();

                    _wfdReceiver = null;

          }       

 

          public void onChannelDisconnected()  {

                   _wfdChannel = _wfdManager.initialize(this, getMainLooper(), this);

                   …

          }

         

          public void onClickMenuDiscover(MenuItem item)  {

                    if (isWfdReceiverRegisteredAndFeatureEnabled())  {

                             _wfdManager.discoverPeers(_wfdChannel,  new ActionListenerHandler(this));

                    }

          }

 

          public void onClickMenuConnect(MenuItem item)  {

                   if (isWfdReceiverRegisteredAndFeatureEnabled())  {

                            WifiP2pDevice theDevice = _wfdReceiver.getFirstAvailableDevice();

                            if (theDevice != null)  {

                                    WifiP2pConfig config = new WifiP2pConfig();

                                    config.deviceAddress = theDevice.deviceAddress;

                                    config.wps.setup = WpsInfo.PBC;

                                    _wfdManager.connect(_wfdChannel,  config,  new ActionListenerHandler(this));

                            }

                   }

          }

}

 

public class ActionListenerHandler implements  WifiP2pManager.ActionListener  {

          MainActivity _activity;

          public ActionListenerHandler(MainActivity activity)  {

                  _activity = activity;

          }

          public void onSuccess()  {

          }

          public void onFailure(int i)  {

          }

}

 

public class WiFiDirectReceiver  extends BroadcastReceiver implements

          WifiP2pManager.PeerListListener,  WifiP2pManager.ConnectionInfoListener

{

          …

         public void registerReceiver() {

                   _appMainActivity.registerReceiver(this, getIntentFilter());

         }

         public void unregisterReceiver() {

                  _appMainActivity.unregisterReceiver(this);

         }

         private IntentFilter getIntentFilter() {

                  if (_intentFilter == null) {

                            _intentFilter = new IntentFilter();

                            _intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);

                            _intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);

                            _intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);

                            _intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);

                  }

                  return _intentFilter;

         }

         public void onReceive(Context context, Intent intent)  {

                  String action = intent.getAction();

                  …

                  if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action))  {

                           int wfdState = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE,  -1);

                           /*  WifiP2pManager.WIFI_P2P_STATE_ENABLED ? */

                  }

                  else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action))  {

                            WifiP2pDevice thisDevice = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_DEVICE);

                            …

                  }

                  else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action))  {

                             _wfdManager.requestPeers(_wfdChannel, this);

                  }

                  else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action))  {

                             NetworkInfo info = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);

                             if (info != null && info.isConnected())  {

                                      _wfdManager.requestConnectionInfo(_wfdChannel);

                             }

                  }

         }

         public void onPeersAvailable(WifiP2pDeviceList wifiP2pDeviceList)  {

                   //warunki

                  _wfdDevices = wifiP2pDeviceList.getDeviceList().toArray(new WifiP2pDevice[0]);

         }

         public void onConnectionInfoAvailable(WifiP2pInfo wifiP2pInfo)  {

                   if  (wifiP2pInfo.groupFormed) {

                            if (wifiP2pInfo.isGroupOwner) {

                                     //otwieramy ServerSocket

                            }

                            else  {

                                     //otwieramy socket do wifiP2pInfo.groupOwnerAddress

                            }

                   }

         }

}

NFC

Android mapuje czytanie tagów NFC na standardowe intencje

  • Kiedy tag zostanie odczytany, system wysyła intent
    • akcja “android.nfc.action.NDEF_DISCOVERED”
    • URI / MIME z taga staje się danymi w intencji
  • Aktywności przechwytują tagi NFC przez utworzenie odpowiedniego filtra intencji
    • powinien zawierać kategorię “DEFAULT”

<activity …>

        <intent-filter>

                 <action android:name=”android.nfc.action.NDEF_DISCOVERED” />

                 <category android:name=”android.intent.category.DEFAULT” />

                 <data android:mimeType=”text/plain” />

        </intent-filter>

</activity>

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();

                          }

                }

       }

}