środa, 8 października 2014

Pojedynek z Androidem - odc. 12 zdjęcia i wideo

Zdjęcia i wideo to tematyka, która zawsze jest już wdzięczna sama w sobie. Bo kto nie chciałby zrobić fotki czy nagrać filmu zamiast np. odczytywać dane, coś tam obliczać czy komunikować się  z serwisem?  Przyglądając się rozwiązaniom Androida dla multimediów wydaje mi się, że co drugie pojęcie (albo i częściej) gdzieś już widziałem, była gdzieś analogiczna koncepcja, coś nazywa się podobnie czy nawet tak samo… Są też owszem i pewne przekombinowania, ale całość jest dość podobna do ekosystemu Windows, do którego już tradycyjnie będę czynił porównania.

Zacznijmy od podejść do robienia zdjęć i nagrywania wideo. Podejścia z wywołaniem predefiniowanej aplikacji jak i bardziej zaawansowane korzystanie z API połączone z budową własnego interfejsu jest znane dobrze z Windows/Windows Phone. Android w przypadku pierwszej opcji jest bardziej elastyczny idąc - mówiąc językiem Windows - w kontrakty dla aplikacji robiących zdjęcia i nagrywających filmy (w Windows/Windows Phone mamy jedną systemową aplikację, której elementy mogą być wywoływane przez inne aplikacje). Czemu Android zakłada że jak już coś trzymać to koniecznie w External Storage?  W Windows Phone wyklarowało się bardziej uniwersalne rozwiązania zakładające różne miejsca przechowywania systemowych multimediów - w pamięci telefonu lub na karcie SD, co wynikło z udostępnienia kart SD dopiero w WP 8. Poza tym często najbardziej topowe modele smartfonów z WP często nie obsługują kart SD, ponieważ oferują dużą pojemność wewnętrznej pamięci. Możliwość wiązania plików w multimedialnym folderze z konkretną aplikacją wydaje się w Androidzie interesujące.

Przejdźmy do robienia zdjęć w wariancie używającym predefiniowanej aplikacji. Wykonanie fotki z opcją podającą ścieżkę dla zwracanego pliku wymaga odrobiny kodu, przykład z nazwą pliku wyliczaną według daty i czasu wydaje się być tym, co nawet zostało wbudowane w sam Windows Phone (automatyczne nadawane przez system nazwy zdjęć są podobne). To, że należy rozgłosić że folder z nowo powstałym zdjęciem jest gotowy (sendBroadcast z Intent.ACTION_MEDIA_MOUNTED) by odświeżyła się galeria, przypomniała mi pewną przypadłość, która czasem zdarzała się w WP.

Proste nagrywanie wideo w Androidzie jest tak proste jak proste zrobienie zdjęcia, choć jest pewna niesymetryczność w API do odbierania wyników.

Bezpośredni dostęp do aparatu - obsługa kamer przedniej i tylnej, podstawowych parametrów wydaje się dość prosta. Unikanie korzystania z kamery, gdy tego nie potrzebujemy czy podczas, gdy aplikacja traci foreground jest także znana z Windows. W sumie nie ma nic dziwnego, sprzęt to sprzęt i rządzi się swoimi prawami.  Wyświetlanie podglądu z kamery nie jest może najtrudniejsze, ale trzeba wiedzieć to i owo, nadpisać klasę, przeładować kilka metod. Dobrze jest też zadbać o dobrą orientację pokazywanego obrazu (czemu sami wykonujemy obliczenia?). Obsługa licznych parametrów świadczy o sporych możliwościach, trochę może przekombinowane zapisywanie parametru w zbiorze parametrów, a potem całego zbioru. O metadanych (np. EXIF, geotagi) gdzieś już wcześniej czytałem w czasach poznawania Windows więc jakoś nie wydaje się to zaskakujące.

Nagrywanie wideo klasą MediaRecorder nie jest może najgorsze, ale wymaga wiedzy, co w jakiej kolejności trzeba zrobić, możemy ustawić parametry nieobsługiwane przez nas sprzęt, pewne błędy mogą wyjść dopiero w momencie włączenia nagrywania już po skonfigurowaniu wszystkiego. Jak chcę po przerwie wznowić nagrywanie, to muszę wszystko poresetować i na nowo poinicjować.  Nie jest to chyba najlepiej zaprojektowane API, ale może się lepiej nie dało… Wykonywanie zdjęć podczas nagrywania - gdzieś już jakby słyszałem…

MediaStore - pierwsze skojarzenie to libraries w Windows/Windows Phone, w Androidzie to jakby baza zawierające informacje o plikach multimedialnych czy ich miniatury…  O ile w Windows nikt nie rozbiera libraries na czynniki pierwsze (choć też to są jakieś zbiory danych), tutaj mamy pewne elementy z anatomii, możemy wymusić wymusić odświeżenie informacji lub jawnie zlecić zapisanie informacji o jakimś konkretnym pliku czy plikach…

Mam też dygresję do obsługi miniatur. W Android jest lepiej niż w Windows Phone, ale gorzej niż w Windows. Wszędzie mamy metodę o nazwie GetThumbnail (z dokładnością do async). W Windows bez żadnych dodatkowych bytów, jakiś id-ków po prostu mogę sobie pobrać miniaturę dla danego pliku i w jakim chcę rozmiarze. W Windows Phone to API też już teraz mamy, ale…  jeszcze nie działa. W Android za pomocą Media Store dla pliku o danym id wyciągnę sobie miniaturę w jednym z dwóch predefiniowanych rozmiarów. Jeśli dysponuję tylko nazwą pliku, tu muszę najpierw wykonać niskopoziomowe przeszukanie bazy danych w MediaStore. Mając kursor z zapytania odczytujemy id z pierwszego rekordu, a następnie dopiero pobieramy miniaturę. Czy naprawdę nie dało się prościej tego zaprojektować?  Czy są też obsługiwane miniatury do plików wideo ?

Podsumowując, spodziewałem się większej złożoności tematu, ale całość okazała się stosunkowo przystępna, co nie oznacza że zawsze super wygodna. Autor filmów może nie poruszył wszystkiego, bo wiem, że w Windows/Windows Phone znajdzie się jeszcze całkiem sporo ciekawych funkcjonalności związanej z multimediami, jak choćby przykładowo konwersja plików na inne formaty, robienie sekwencji zdjęć (także zaawansowane z rozstrzałem parametrów w WP 8.1), API do edycji wideo w WP 8.1 czy całkiem spore możliwości integracji z systemem WP.

 

Intro

Dwa podejścia do przechwytywania zdjęć i wideo

  • pełna kontrola
    • bezpośredni dostęp do aparatu
    • nasza aplikacja jest odpowiedzialna za pokazanie podglądu
    • nasza aplikacja jest odpowiedzialna za sterowanie zachowaniem aparatu i interakcję z nim
    • bardziej skomplikowane oprogramowanie
  • proste użycie
    • aplikacja deleguje szczegóły obsługi zdjęć i wideo do standardowej aplikacji zdjęć i wideo
    • niewielka kontrola

Proste robienie zdjęć

  • Wykorzystanie intencji
    • intencja z akcją MediaStore.ACTION_IMAGE_CAPTURE
    • wynik uzyskiwany za pomocą startActivityForResult
      • request code
      • response code
        • RESULT_OK
        • RESULT_CANCELED
      • Intent z fotografią
        • “data” w extras
          • instancja klasy Bitmap
  • Użytkownik zobaczy aktywność zarejestrowaną do przechwytywania tej akcji
    • na większości urządzeń będzie to standardowa aplikacja do zdjęć
    • inne aplikacje mogą rejestrować się do przechwytywania tej akcji
  • User experience
    • wyświetlenie podglądu z aparatu z opcją zamknięcia
    • przycisk do zrobienia zdjęcia
    • podgląd zdjęcia z opcjami akceptacji, anulowania lub powtórzenia wykonania

public void onMenuItem1Click(MenuItem item) {

        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

        startActivityForResult(intent, 1000);

}

@Override

protected void onActivityResult(int requestCode, int resultCode, Intent resultIntent) {

         Bundle extras = null;

         Bitmap imageBitmap = null;

         ImageView imageView = (ImageView) findViewById(R.id.imageView);

         if (resultCode == RESULT_CANCELED) {

                  return;

         }

         switch(requestCode) {

                  case 1000:

                           extras = resultIntent.getExtras();

                           imageBitmap = (Bitmap) extras.get(“data”);

                           break;

         }

         if (imageBitmap != null) {

                  imageView.setImageBitmap(imageBitmap);

         }

}

Określanie miejsca na plik ze zdjęciem

Intent

  • ścieżka dla pliku ze zdjęciem jako extra
    • MediaStore.EXTRA_OUTPUT
  • ścieżka jako instancja Uri
  • podanie ścieżki powoduje zrobienie zdjęcia w pełnej jakości
  • onActivityResult
    • parametr Intent może być zawsze równy null

Miejsce przechowywania zdjęć

  • normalnie w miejscu, do którego może mieć dostęp każdy
    • normalnie nie ograniczamy dostępu tylko dla naszej aplikacji
  • standardowa lokalizacja w Environment.getExternalStoragePublicDirectory
    • dla fotografii stała Environment.DIRECTORY_PICTURES
    • normalnie aplikacja tworzy specyficzny dla siebie podfolder
    • zdjęcia pozostają nawet po odinstalowaniu aplikacji
  • API 7 (Android 2.1.x) lub starsze urządzenia wymagają specjalnego przechwytywania
    • używamy Environment.getExternalStorageDirectory
    • trzeba jawnie dodać do ścieżki podfolder “Pictures”

Bezpieczna praca z external storage

  • zewnętrzy storage może zostać usunięty lub być niedostępny (starsze urządzenia nie zapisują po podłączeniu do komputera)
  • potrzebujemy ustalić, czy możemy zapisywać
    • Environment.getExternalStorageState (Environment.MEDIA_MOUNTED - bezpieczny zapis)
  • Zapis do zewnętrznego storage wymaga w manifeście uprawnienia android.permission.WRITE_EXTERNAL_STORAGE

Zdjęcia mogą być przechowywane w powiązaniu z naszą aplikacją

  • pliki są publicznie dostępne
  • zostaną automatycznie usunięte po odinstalowaniu aplikacji
  • używamy Context.getExternalFilesDir (przekazujemy stałą Environment.DIRECTORY_PICTURES)

File getPhotoDirectory()  {

         File outputDir = null;

         String externalStorageState = Environment.getExternalStorageState();

         if (externalStorageState.equals(Environment.MEDIA_MOUNTED))  {

                  File pictureDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);

                  outputDir = new File(pictureDir, “Xxx”);

                  if (!outputDir.exists())  {

                          if (!outputDir.mkdirs())  {

                                  …

                                  outputDir = null;

                          }

                  }

         }

         return outputDir;

}

Uri generateTimeStampPhotoFileUri()  {

         Uri photoFileUri = null;

         File outputDir = getPhotoDirectory();

         if (outputDir != null)  {

                 String timeStamp = new SimpleDateFormat(“yyyyMMDD_HHmmss”).format(new Date());

                 String photoFileName = “IMG_” + timeStamp + “.jpg”;

                 File photoFile = new File(outputDir, photoFileName);

                 photoFileUri = Uri.fromFile(photoFile);

         }        

         return photoFileUri;

}

public void onMenuItem2Click(MenuItem item) {

        _photoFileUri = generateTimeStampPhotoFileUri();

        if  (_photoFileUri != null) {

                Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

                intent.putExtra(MediaStore.EXTRA_OUTPUT, _photoFileUri);

                startActivityForResult(intent, 1001);

        }

}

@Override

protected void onActivityResult(int requestCode, int resultCode, Intent resultIntent)  {

        …

        switch(requestCode)  {

                case 1001:

                       imageBitmap = BitmapFactory.decodeFile(_photoFileUri.getPath());

                       break;

        }

        …

        sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED,

                 Uri.parse(“file://”  + Environment.getExternalStorageDirectory());

}

Proste nagrywanie wideo

Delegowanie nagrywania wideo do standardowej aplikacji

  • Intent z akcją MediaStore.ACTION_VIDEO_CAPTURE (startActivityForResult)
  • standardowa aplikacja
    • rozpoczęcie / zatrzymanie nagrywania
    • po zatrzymaniu akceptacja, anulowanie lub powtórzenie nagrywania
  • określenie nazwy pliku w MediaStore.EXTRA_OUTPUT
    • do określania folderu należy skorzystać z Environment.DIRECTORY_MOVIES
  • określenie jakości za pomocą MediaStore.EXTRA_VIDEO_QUALITY
    • 1 - wysoka jakość, duży plik
    • 0 - słabsza jakość, mały plik

Odbieranie wyników nagrania

Wywoływana jest metoda onActivityResult

  • response code - sukces/porażka
  • przy sukcesie Intent zawiera URI pliku wideo
    • getData - dostanie nazwy pliku mp4
    • nie ma extra “data”

public void onMenuItem3Click(MenuItem item) {

        _videoFileUri = generateTimeStampVideoFileUri();

        if  (_videoFileUri != null) {

                Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);

                intent.putExtra(MediaStore.EXTRA_OUTPUT, _videoFileUri);

                intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);

                startActivityForResult(intent, 1002);

        }

}

 

Bezpośredni dostęp do aparatu

Należy oznaczyć aplikację, która używa aparatu

  • Google Play
    • napisze o tym w informacjach o aplikacji
    • zapobiegnie pobraniu aplikacji na urządzenia, które nie wspierają
  • deklaracja za pomocą elementu uses-feature
    • domyślnie Google Play udostępnia aplikację tylko na urządzenia wspierające daną funkcjonalność
      • można oznaczyć opcjonalność przez atrybut required=’false’
    • android.hardware.camera - aplikacja może używać dowolnego aparatu (zwykle ten z tyłu)
    • android.hardware.camera.front - aplikacja potrzebuje aparatu z przodu

<manifest …>

         …

         <uses-feature android:name=’android.hardware.camera’ />

         …

</manifest>

Weryfikacja obecności aparatu

PackageManager

  • metoda hasSystemFeature
    • PackageManager.FEATURE_CAMERA (zwykle do aparatu z tyłu)
    • PackageManager.FEATURE_CAMERA_FRONT

PackageManager pm = context.getPackageManager(); //metoda w aktywności

boolean hasCamera = pm.hasSystemFeature(PackageManager.FEATURE_CAMERA);

Pozwolenia dla korzystania z aparatu

  • android.permission.CAMERA - korzystanie z aparatu
  • android.permission.WRITE_EXTERNAL_STORAGE - zapisywanie obrazu do plików
  • android.permission.ACCESS_FINE_LOCATION - tagowanie zdjęć namiarami z GPS

Uzyskiwanie dostępu do aparatu

  • statyczna metoda Camera.open
    • zwraca referencję Camera
    • w wywołaniu bez argumentów otwiera domyślny aparat
  • otwarcie konkretnego aparatu
    • Camera.open(id) - int id (0 .. Camera.getNumberOfCameras() – 1)
  • Camera.release - zakończenie pracy z danym aparatem

Sprawdzenie położenia aparatu przed jego uruchomieniem

  • statyczna metoda Camera.getCameraInfo
    • int id
    • Camera.CameraInfo (tworzy instancję)
  • pole CameraInfo.facing
    • CameraInfo.CAMERA_FACING_FRONT
    • CameraInfo.CAMERA_FACING_BACK

int getFacingCameraId(int facing) {

         int cameraId = CAMERA_ID_NOT_SET;

 

         int nCameras = Camera.getNumberOfCameras();

         Camera.CameraInfo cameraInfo = new Camera.CameraInfo();

 

         for (int cameraInfoId=0; cameraInfoId < nCameras; cameraInfoId++) {

                 Camera.getCameraInfo(cameraInfoId, cameraInfo);

                 if (cameraInfo.facing == facing) {

                         cameraId = cameraInfoId;

                         break;

                 }

         }

         return cameraId;       

}

Zarządzanie aparatem jak zasobem współdzielonym

Należy trzymać referencję do kamery tak krótko jak to możliwe

  • tylko jedna aplikacja może mieć otwartą danę kamerę w zadanym czasie
    • wyjątek przy próbie otwarcia kamery trzymanej przez inną aplikację
  • zawsze zwalniajmy kamerę kiedy użytkownik opuszcza aktywność
    • w metodzie Activity.onPause
    • ponowne otworzenie kamery w metodzie Activity.onResume
    • w metodzie Activity.onSaveInstanceState możemy zapamiętać id kamery

 

Implementacja podglądu z kamery

Wyświetlanie podglądu z kamery w aplikacji

  • Kamera może rysować podgląd bezpośrednio na instancji klasy Surface
  • Umieszczamy Surface w UI

Klasa SurfaceView dostarcza widok opakowujący Surface

  • pozwala na umieszczenie Surface w hierarchii widoków w UI aplikacji
    • Surface zawarty jest w SurfaceHolder, który zarządza szczegółami layoutu
  • rozszerzamy SurfaceView by stworzyć podgląd
    • łączymy kamerę z Surface
  • implementujemy interfejs SurfaceHolder.Callback
    • startujemy i zatrzymujemy podgląd w odpowiedzi na zmiany w dostępności surface

public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback  {

         Camera _camera;

         SurfaceHolder _holder;

         public CameraPreview(Context context, AttributeSet attributeSet) {

                  super(context,  attributeSet);

         }

         public CameraPreview(Context context) {

                  super(context);

         }

         public void ConnectCamera(Camera camera, int cameraId) {

                 _camera = camera;

 

                 int previewOrientation = getCameraPreviewOrientation(cameraId);

                 _camera.setDisplayOrientation(previewOrientation);

 

                 _holder = getHolder();

                 _holder.addCallback(this);

                 startPreview();

         }

         public void releaseCamera() {

                 if (_camera != null)  {

                        stopPreview();

                        _camera = null;

                 }

         }

         void startPreview()  throws IOException {

                  if (_camera != null  && _holder.getSurface() != null)  {

                          try {

                                   _camera.setPreviewDisplay(_holder);

                                   _camera.startPreview();

                          } catch(Exception e) {

                          }

                   }

         }

         void stopPreview() {

                   if (_camera != null)  {

                         try {

                                 _camera.stopPreview();

                          } catch(Exception e) {

                          }

                   }

         }

         public void surfaceCreated(SurfaceHolder surfaceHolder) {

                   startPreview();

         }

         public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {

                  stopPreview();

                  startPreview();

         }

         public void surfaceDestroyed(SurfaceHolder surfaceHolder) {

                   stopPreview();

         }

         int getCameraPreviewOrientation(int cameraId) {

                    int temp = 0;

                    int previewOrientation = 0;

 

                    Camera.CameraInfo cameraInfo = new Camera.CameraInfo();

                    Camera.getCameraInfo(cameraId, cameraInfo);

 

                    int deviceOrientation = getDeviceOrientationDegrees();

                    switch(cameraInfo.facing) {

                              case Camera.CameraInfo.CAMERA_FACING_BACK:

                                       temp = cameraInfo.orientation – deviceOrientation + 360;

                                       previewOrientation = temp % 360;

                                       break;

                               case Camera.CameraInfo.CAMERA_FACING_FRONT:

                                       temp = (cameraInfo.orientation + deviceOrientation)  % 360;

                                       previewOrientation = (360 - temp) % 360;

                                       break;

                    }

                   

                    return previewOrientation;

         }

         int getDeviceOrientationDegrees() {

                    int degrees = 0;

                    WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);

                    int rotation = windowManager.getDefaultDisplay().getRotation();

 

                    switch(rotation)  {

                              case Surface.ROTATION_0:

                                       degrees = 0;

                                       break;

                              case Surface.ROTATION_90:

                                       degrees = 90;

                                       break;

                               …

                    } 

 

                    return degrees;

         }

}

<com.xxx.CameraPreview

         android:id=”@+id/cameraPreview”

         android:layout_width=”fill_parent”

         android:layout_height=”fill_parent”

         android:layout_weight=”1”

/>

Zarządzanie podglądem z kamery

  • Camera wie jak renderować podgląd na Surface
    • Camera.setPreviewDisplay - łączy Camerę z Surface holderem
    • Camera.startPreview - rozpoczyna renderowanie podglądu z kamery na Surface
    • Camera.stopPreview
  • odpowiedź na metody SurfaceHolder.Callback
    • surfaceCreated: rozpoczęcie pokazywania podglądu
    • surfaceDestroyed: zatrzymanie pokazywania podglądu
    • surfaceChanged:  zatrzymanie i restart podglądu

Ustawianie orientacji dla podglądu

Może być potrzebne obrócenie podglądu kamery by dopasować się do orientacji urządzenia

  • kamery często są inaczej zorientowane niż naturalna orientacja urządzenia
  • kamery mogą być odpytywane z orientacji z naturalnej pozycji urządzenia
    • Camera.getCameraInfo; wartość w Camera.CameraInfo.orientation
  • Android 2.2 (API 8) lub nowszy wspiera obrót podglądu
    • Camera.setDisplayOrientation
  • kamery z przodu mają dodatkowe wyzwanie:  podgląd renderuje się jak lustrzane odbicie

 

Robienie zdjęcia

Robienie zdjęcia odbywa się asynchronicznie

  • inicjalizacja przez wywołanie Camera.takePicture
    • powraca od razu bez gotowego zdjęcia
    • trzeba dostarczyć implementację callbacku
  • interfejs Camera.ShutterCallback
    • notyfikacja, kiedy fotografia została po raz pierwszy przechwycona przez sensor
    • odtwarzanie odpowiedniego dźwięku lub inny rodzaj feedbacku
  • interfejs Camera.PictureCallback
    • otrzymuje zdjęcie jako tablicę bajtów

Otrzymywanie danych zdjęcia

Robienie zdjęcia i przetwarzanie zachodzą w fazach

  • maksymalnie trzy możliwe wersje danych zdjęcia
    • wiele urządzeń wspiera tylko podzbiór
  • surowe dane obrazka
    • nieprzetworzone dane obrazka bezpośrednio z sensora
    • większość kamer nie dostarcza
  • dane postview
    • pierwszy raz w pełni przetworzona wersja danych (bez kompresji)
    • większość kamer dostarcza
  • dane obrazka JPEG
    • zkompresowana, w pełni zformatowana wersja danych
    • w większości przypadków tylko jej potrzebujemy

Camera.takePicture ma dwa przeładowania

  • najczęściej używana wersja przyjmuje 3 parametry
    • Camera.ShutterCallback
    • Camera.PictureCallback dla obrazka postview
    • Camera.PictureCallback dla obrazka JPEG
  • alternatywne przeładowanie akceptuje dodatkowo
    • Camera.PictureCallback dla surowego obrazka
  • możemy przekazywać null-e dla parametrów, które nas nie interesują
  • podgląd automatycznie się zatrzymuje po wywołaniu takePicture
    • nie można zrestartować podglądu dopóki nie otrzymamy finalnego obrazka

void takePicture() {

         _selectedCamera.takePicture(null, null, new Camera.PictureCallback()  {

                   public void onPictureTaken(byte[] bytes, Camera camera)  {

                          File f = CameraHelper.generateTimeStampPhotoFile();  // z wcześniejszych sampli 

                          try {

                                   OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(f));

                                   outputStream.write(bytes);

                                   outputStream.flush();

                                   outputStream.close();

                          } catch (Exception e)  {

                          }  

                          sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, 

                                Uri.parse(“file://” + Environment.getExternalStorageDirectory())));                     

                           _selectedCamera.startPreview();

                   }

         });

}

Zapisywanie zdjęcia na karcie SD

  • Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
  • android.permission.WRITE_EXTERNAL_STORAGE
  • FileOutputStream (dla poprawienia wydajności opakować w BufferedOutputStream)

 

Kontrolowanie ustawień aparatu

  • Camera.Parameters
    • najczęściej spotykane zachowania np. flash, autofocus
    • sposób robienia zdjęć np. rozdzielczość
    • metadane zdjęcia w pliku JPEG np. obrót aparatu, geotagi
    • zachowanie sprzętu powiązanego z aparatem np. zoom
  • Camera.getParameters
    • zwraca instancję Camera.Parameters
  • Camera.setParameters
    • zawsze startuje z instancją Camera.Parameters zwróconą przez getParameters
    • dla większości wartości, setParameters jest wołane tuż przed zrobieniem zdjęcia
      • wszystkie wartości najpierw nanosimy na instancję Parameters
    • od razu modyfikuje zachowania

Rozdzielczość fotografii

  • Każdy aparat wspiera określone rozdzielczości (zwykle wiele)
    • Parameters.getSupportedPictureSizes:  List<Camera.Size>
    • Parameters.getPictureSize - aktualny wybór
  • Wybór rozdzielczości za pomocą Parameters.setPictureSize
    • szerokość i wysokość jako int
    • trzeba Parameters przekazać do Camera.setParameters

_cameraParameters = _selectedCamera.getParameters();

_supportedPictureSizes = _cameraParameters.getSupportedPictureSizes();

_selectedPictureSize = _cameraParameters.getPictureSize();

_selectedPictureSize = _supportedPictureSizes.get(sizeIndex);

_cameraParameters.setPictureSize(_selectedPictureSize.width, _selectedPictureSize.height);

_selectedCamera.setParameters(_cameraParameters);

Metadane fotografii

  • EXIF (Exchangeable Image File Format)
  • Parameters.setRotation
    • przechwytywanie zależy od sterowników aparatu
    • niektóre sterowniki zapisują obrót do bloku EXIF
    • niektóre sterowniki obracają obraz i zapisują obrót 0 w EXIF lub nie tworzą w ogóle EXIF
  • Geotagging
    • setGpsLatitude/setGpsLongitude/setGpsAltitude
    • setGpsTimestamp
    • setGpsProcessingMethod (może być dowolny string, standardowe wartości: GPS, WLAN, CELLID, MANUAL)
    • removeGpsData - czyści wszystkie informacje o lokalizacji

int rotation = CameraHelper.getDisplayOrientationForCamera(this, _selectedCameraId) // we wcześniejszych samplach

_cameraParameters.setRotation(rotation);

_cameraParameters.setGpsAltitude(altitude);

_selectedCamera.setParameters(_cameraParameters);

Zoom

  • Parameters.isZoomSupported
  • wartość zoom jest wartością całkowitą
    • Parameters.getZoom/setZoom
    • 0 - brak zoom
    • Parameters.getMaxZoom - najbliższy możliwy zoom
  • w odpowiedzi na żądanie użytkownika modyfikację zoom robimy najszybciej jak to możliwe
    • podgląd i zrobione zdjęcie
    • podgląd otrzymuje zmieniony zoom jak tylko zostanie on przekazany do aparatu
  • niektóre aparaty wspierają płynny zoom (stopniowa zmiana do zadanego poziomu)
    • Parameters.isSmoothZoom
    • bezpośrednia komunikacja z kamerą
      • Camera.startSmoothZoom (ten sam zakres wartości co Parameters.setZoom)
      • asynchroniczne wykonywanie, natychmiastowy powrót samej metody
    • nie można wykonywać innych operacji związanych z zoom podczas płynnego zoomowania
      • implementujemy Camera.onZoomChangeListener by otrzymywać notyfikacje o zmianach zoom i zakończeniu płynnego zoomowania

ZoomControls zoomControls = (ZoomControls)  findViewById(R.id.zoomControls);

zoomControls.setOnZoomInClickListener(

        new View.OnClickListener()  {

                public void onClick(View view)  {

                       zoomIn();

                }

        }

);

void zoomIn() {

         if (_currentZoom < _maxZoom) {

                   _currentZoom++;

                  _cameraParameters.setZoom(_currentZoom);

                  _selectedCamera.setParameters(_cameraParameters);

         }

}

public class XActivity extends Activity implements Camera.OnZoomChangeListener {

        …

        void openCamera() {

                 …

                 _isSmoothZoomSupported = _cameraParameters.isSmoothZoomSupported();

                 if (_isSmoothZoomSupported)

                         _selectedCamera.setZoomChangeListener(this);

        }

        void zoomTo(int value) {

                 if (_currentZoom != value) {

                          //disable zoom buttons

                          _selectedCamera.startSmoothZoom(value);

                 }

        }

         public void onZoomChange(int zoomValue, boolean stopped, Camera camera) {

                 if (stopped)

                        //enable zoom buttons                

                 _currentZoom = zoomValue;

         }

}

 

Nagrywanie wideo

MediaRecorder

  • obsługa różnych źródeł wejścia
  • duża konfigurowalność
    • bardzo szczegółowe ustawienia nagrywania
    • spora liczba ogólnych profili dla uproszczenia konfiguracji
  • nagrania są zapisywane bezpośrednio w storage’u urządzenia

Stany MediaRecorder

  • Przejścia pomiędzy różnymi stanami podczas konfiguracji
    • muszą odbywać się w określonej kolejności

Initial – [setAudioSource()/setVideoSource()]* –>  Initialized – setOutputFormat() / setProfile –> DataSource Configured

- [setOutputFile() / set zachowania (lub setProfile)]* – prepare() –> Prepared – start() –> Recording:

- stop() –> Initial  - release() –> Released

Konfiguracja MediaRecorder

  1. tworzymy instancję
  2. wiążemy z nią aparat
  3. ustawiamy źródła audio i wideo
  4. ustawiamy profil nagrywania
  5. ustawiamy ścieżkę do pliku wynikowego (string)
  6. wywołujemy metodę prepare (try catch)

MediaRecorder mediaRecorder = new MediaRecorder();

mediaRecorder.setOutputFile(outputFile.toString());

mediaRecorder.prepare();

Wiązanie z aparatem

Używana jest kamera dostarczona przez aplikację

  • włączajmy kamerę tylko do robienia zdjęć
  • aplikacja musi zwolnić blokadę na kamerze
    • Camera.unlock
  • przekazujemy kamerę do MediaRecorder
    • MediaRecorder.setCamera
    • od Android 3.2 (API 13) aplikacja może nadal ją używać do robienia zdjęć

_camera = Camera.Open(camId);

_camera.setPreviewDisplay(surfaceHolder);

_camera.startPreview();

_camera.unlock();

mediaRecorder.setCamera(_camera);

Ustawianie źródeł audio i wideo

  • zawsze używamy tych samych wartości przy nagrywaniu wideo
  • MediaRecorder.SetVideoSource - przekazujemy MediaRecorder.VideoSource.CAMERA (kamera przekazana przez MediaRecorder.setCamera)
  • MediaRecorder.setAudioSource - przekazujemy MediaRecorder.AudioSource.CAMCORDER (mikrofon z kamery)

Ustawianie profilu

  • zamiast ustawiania poszczególnych wartości (co może być skomplikowane, ale daje nam bardzo dużą kontrolę)
  • klasa CamcorderProfile reprezentuje każdy profil
    • zawiera wszystkie ustawienia dla danego profilu
    • stałe reprezentujące każdy profil
      • QUALITY_1080P
      • QUALITY_720P
      • QUALITY_480P
      • QUALITY_CIF
      • QUALITY_QVGA
      • QUALITY_QCIF
    • używamy metody get do uzyskania instancji profilu dla danej stałej
    • MediaRecorder.setProfile

CamcorderProfile profile = CamcorderProfile.get(CamcorderProfile.QUALITY_720P);

Nagrywanie wideo

Po przygotowaniu MediaRecorder

  • MediaRecorder.start (try catch)
  • MediaRecorder.stop
    • nie można szybko wznowić nagrywania
    • trzeba na nowo zainicjalizować MediaRecorder przed dalszym nagrywaniem

Sprzątanie MediaRecorder

  • MediaRecorder.reset – przywraca instancję do stanu Initialize
  • MediaRecorder.release - zwolnienie wszystkich zasobów do systemu (MediaRecorder nie trzyma już blokady na kamerze)
  • Camera.lock - ponowne zablokowanie kamery
  • onPause
    • zatrzymanie nagrywania
    • pełne wyczyszczenie instancji
    • MediaRecorder należy sprzątać przed sprzątaniem kamery

mediaRecorder.reset();

mediaRecorder.release();

camera.lock();

sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse(“file://” + Environment.getExternalStorageDirectory())));

//by pokazało się w galerii

Zarządzanie orientacją kamery

Podobnie jak przy zdjęciach musimy przechowywać informację o położeniu kamery w wideo

  • te same obliczenia co przy zdjęciach i poglądzie z jedną różnicą
    • używamy tych samych obliczeń dla kamer z przodu co z tyłu
  • informację o orientacji zapisujemy w MediaRecorder.setOrientationHint

Jeszcze o profilach

Nie wszystkie kamery wspierają wszystkie profile

  • Wyjątek przy próbie użycia profilu powyżej rozdzielczości kamery (podczas rozpoczynania nagrywania)
  • Można sprawdzić wsparcie kamery dla danego profilu
    • CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_xxx)
  • Android posiada dwa specjalne profile, które dostosowują się do kamery
    • QUALITY_HIGH - najwyższa wspierana rozdzielczość w kamerze
    • QUALITY_LOW - najniższa wspierana rozdzielczość

 

Media Store

Najbardziej multimedialne aplikacje i funkcje  nie korzystają bezpośrednio z systemu plików, tylko z Media Store

  • aplikacja Gallery, montowanie USB

Media Scanner Service

  • skanuje pliki multimedialne systemowe, by dostarczyć informacje o nich do Media Store
  • okresowo wykonuje skanowania, by zapewnić aktualność danych
  • skanuje zewnętrzny system plików po zamontowaniu nośnika w urządzeniu
    • monitorowanie intencji zawierającej akcję Intent.ACTION_MEDIA_MOUNTED
    • system automatycznie wysyła taką intencję po podłączeniu medium do urządzenia
    • możliwość wymuszenia przez wywołanie sendBroadcast z taką intencją
  • aplikacje mogą wyzwalać skanowanie danego pliku

sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse(“file://” + Environment.getExternalStorageDirectory())));

Skanowanie pliku

MediaScannerConnection zapewnia łączność do Media Scanner

  • statyczna metoda scanFile do skanowania pojedynczych plików
    • context
    • tablica ścieżek plików do skanowania
    • opcjonalnie typy plików mime (inaczej jest wnioskowany z rozszerzenia)
    • opcjonalnie interfejs callback do informowania o zakończeniu
      • MediaScannerConnection.onScanCompletedListener
      • callback dostarcza URI do informacji o pliku w Media Store
        • content://media/external/images/media/1234
  • Pliki są widoczne w Media Store jak tylko zakończy się skanowanie
    • dużo bardziej efektywniejsze niż używanie Intent.ACTION_MEDIA_MOUNTED

public class XActivity extends Activity {

         …

         void doScanFile(String fileName)  {

                  String[] filesToScan = {fileName};

                  MediaScannerConnection.scanFile(this, filesToScan, null,

                           new MediaScannerConnection.OnScanCompletedListener()  {

                                     public void onscanCompleted(String filePath, Uri uri)  {                                             

                                     }

                           }

                    );

         }

}

Miniatury

Możemy pobierać miniatury obrazków z Media Store

  • MediaStore.Images.Thumbnails - metody i stałe
  • stałe określają pożądany rozmiar miniatur
    • MediaStore.Images.Thumbnails.MINI_KIND - 512 x 512
    • MediaStore.Images.Thumbnails.MICRO_KIND - 96 x 96
  • dostępne przy użyciu id plików multimedialnych z Media Store
    • id uzyskujemy z URI za pomocą ContentUris.parseId
  • dostęp do bitmapy miniatury za pomocą metody getThumbnail
    • content resolver
    • ID obrazka
    • stała z rozmiarem
    • opcjonalnie dowolne BitmapFactory.Options

thumbnail = MediaStore.Images.Thumbnails.getThumbnail(getContentResolver(), id, MediaStore.Images.Thumbnails.MINI_KIND, null);

Pobieranie miniatury przez nazwę pliku

Mając nazwę pliku możemy znaleźć jego id

  • Media Store jest standardowym content providerem
  • Content providery mogą być odpytywane za pomocą ContentResolver.query
    • ContentResolver może uzyskać poprzez Context.getContentResolver
  • Parametry przekazywane do zapytania
    • URI:  MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    • kolumny (tablica stringów): MediaStore.Images.Media._ID
    • where:  MediaStore.Images.Media.Data + “ like ?”
    • wartość where (tablica stringów): ścieżka do pliku z obrazkiem
    • order by: null
  • zwrócony kursor zawiera ID pliku z obrazkiem

final String[] QUERY_COLUMNS = { MediaStore.Images.Media._ID };

final String QUERY_ORDER_BY = MediaStore.Images.Media._ID;

final String QUERY_WHERE = MediaStore.Images.Media.DATA + “ like ? “;

 

File filePath = new File(photoDirectory,  fileName);

 

String[] queryValues = { filePath.toString() };

Cursor imageCursor = getContentResolver().query(

              MediaStore.Images.Media.EXTERNAL_CONTENT_URI,  QUERY_COLUMNS,

              QUERY_WHERE,  queryValues, null);

 

int idColumnIndex = imageCursor.getColumnIndex(MediaStore.Images.Media._ID);

if (imageCursor.moveToFirst()) {

         long id = imageCursor.getLong(idColumnIndex);

}

sobota, 4 października 2014

Pojedynek z Androidem - odc. 11 lokalizacja geograficzna

Przyszła pora na lokalizację. Przebrnięcie przez ileś filmów związanych z tą tematyką wydawało się nużące… Jednak po zapoznaniu się z całością udało mi się zebrać interesujące informacje oraz uzyskać dość jasny obraz całości, który teraz przedstawię.

Różne providery lokalizacji (sieć, GPS) - rzecz znana również w rodzinie Windows, aczkolwiek zrealizowana odrobinę inaczej. W aplikacjach Windows Store nie mamy aż takiej infrastruktury do wyszukiwania providerów spełniających określone wymagania czy predefiniowanego dostępu do formatu NMEA z GPS. W Android aplikacja może w prosty sposób otworzyć okno z ustawieniami sieci Wi-Fi czy trybu lotniczego. W ekosystemie Windows nie spotkałem się z czymś takim jak pasywny provider. Jeśli ktoś zapyta w systemie o lokalizację (np. jakaś inna aplikacja) to my dowiemy się o tym w naszej aplikacji za pomocą tego właśnie providera.

LocationManager - nie rozumiem, po co ktoś wymyślił metodę pobierającą ostatnio ustaloną wartość, która może być przed kilku miesięcy lub być null. Oprócz tego możemy też pobrać aktualną wartość lub zapiąć się na zmiany lokalizacji, co jest również możliwe w Windows/Windows Phone.

Monitorowanie zmian lokalizacji a wątki - tutaj moim zdaniem pewne rzeczy z architektury Androida utrudniają realizację tego zadania, jak choćby niszczenie i odtwarzanie aktywności przy obracaniu ekranu, loopery i handlery może nie są i najgorsze, ale obsługa ich w porównaniu z Windows jest bardziej skomplikowana. Przy okazji Activity.runOnUiThread jednoznacznie kojarzy mi się z wywoływaniem kodu w dispatcherze w aplikacjach XAML. Odbieranie aktualizacji w Androidzie jest możliwe za pomocą callbacków lub pending intent. Callbacki mają jakieś przełożenie na API w Windows.  Pending intent w przypadku lokalizacji w Android wydaje się dobrym sposobem na wygodne nasłuchiwanie zmian, ale z uwagi na większy od callbacka narzut nie jest polecany do scenariuszy z częstą aktualizacją położenia.

Proximity alerts - jak dla mnie odpowiednik geo-fencingu w Windows 8.1/Windows Phone 8.1, też wykrywamy wejścia/wyjścia z jakiegoś obszaru wokół danej lokalizacji geograficznej

Geocoding - mapowanie współrzędnych geograficznych lub nazwy obiektu na adres możemy również zrobić na platformie Windows/Windows Phone. Temat poruszany jest zwykle przy omawianiu map.  Dostajemy usługę Bing. W przypadku Androida mamy doczynienia z analogicznym rozwiązaniem, jest API korzystające z serwisów Google.  Na platformie Windows z uwagi na prowadzoną politykę nie ma możliwości, by geolokalizacja na jakichś urządzeniach nie działała, w przypadku Androida urządzenia z systemem w wersji Open Source mogą tego nie obsługiwać.

 

Podstawy

LocationManager

  • Obsługuje wszystkie funkcjonalności związane z lokalizacją
    • zwrócenie bieżącej lokalizacji
    • monitorowanie zmian w statusie
    • tworzenie notyfikacji po osiągnięciu lokalizacji
  • Dostęp
    • Context.getSystemService
    • nazwa serwisu:  Context.LOCATION_SERVICE

LocationManager lm = (LocationManager)  getSystemService(LOCATION_SERVICE);

Większość urządzeń z Android wspiera 2 providery lokalizacji

  • GPS  (LocationManager.GPS_PROVIDER)
  • bazujący na sieci (LocationManager.NETWORK_PROVIDER  - kombinacja znanych hotspotów Wi-Fi i baz komórkowych)

Uprawnienia

  • różne w zależności od wymaganej dokładności
  • ACCESS_COURSE_LOCATION - sieć, mała dokładność
  • ACCESS_FINE_LOCATION - GPS, wysoka precyzja
  • z nadania uprawnień do wysokiej precyzji nie wynikają uprawnienia do niskiej

Odbieranie informacji o zmianach w lokalizacji i providerze

  • implementacja interfejsu LocationListener
  • onLocationChanged - instancja klasy Location, bieżąca lokalizacja
  • onProviderEnabled/onProviderDisabled - włączenie/wyłączenie powiązanego providera
  • onStatusChanged -istotna zmiana w statusie providera

Klasa Location

  • surowe informacje o lokalizacji (brak np. adresu z ulicą)
  • kluczowe metody: 
    • getLatitude / getLongitude
    • getAccuracy (w metrach)
  • czas: 
    • getTime (czas w UTC w danej lokalizacji, mogą być różnice między różnymi providerami)
    • getElapsedRealtimeNanos (czas lokalizacji w ns od bootowania urządzenia, spójny dla wszystkich providerów, od Android 4.2, API 17)
  • inne informacji w zależności od providera:
    • getSpeed, getBearing, getAltitude
    • metody hasXXX do sprawdzenia, czy zawiera dane informacje

public class MyLocationListener implements LocationListener  {

        …

        public void onLocationChanged(Location location)  {

                 String provider = location.getProvider();

                 double lat = location.getLatitude();

                 double lng = location.getLongitude();

                 float accuracy = location.getAccuracy();

                 long time = location.getTime();

 

                 SimpleDateFormat formatter = new SimpleDateFormat(“yyyy-MM-dd ’T’ HH:mm:ss”);

                 formatter.setTimeZone(TimeZone.getTimeZone(“UTC”));

                 String timeStamp = formatter.format(time);

                 String message = String.format(“%s   |  lat/lng=%f/%f  |  accuracy=%f | time=%s”,  provider, lat, lng, accuracy, timeStamp);

        }

 

        public void onStatusChanged(String s, int i, Bundle bundle)  {

        }

 

         public void onProviderEnabled(String s)  {   //nazwa providera

         }

 

         public void onProviderDisabled(String s)  {

         }

}

Odbieranie informacji o zmianach w lokalizacji

  • metoda requestLocationUpdates w serwisie lokalizacji, do której przekazujemy
    • nazwę providera
    • minimalny czas w ms do notyfikacji (0 - najczęściej jak się da)
    • minimalna zmiana położenia w m do notyfikacji (0 - przy każdym ruchu)
    • referencję do implementacji LocationListener
  • aktualizacje przychodzą dopóki nie wywołamy metody removeUpdates (przekazujemy do niej implementację LocationListener przekazaną do requestLocationUpdates)

LocationManager lm = (LocationManager)  getSystemService(LOCATION_SERVICE);

LocationListener listener = new MyLocationListener();

lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);

Pobieranie pojedynczej wartości

  • szybkie
    • LocationManager.getLastKnownLocation
    • mogą być sprzed iluś minut, godzin, dni
    • może się zdarzyć wartość null (rzadko)
  • pojedyncza, bieżąca wartość
    • LocationManager.requestSingleUpdate
    • callback jak w requestLocationUpdates

LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);

Location recentLocation = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER);

 

LocationListener myListener = new MyListenerClass();

lm.requestSingleUpdate(LocationManager.GPS_PROVIDER, myListener, null); 

//null – odpalenie w wątku UI

 

Location Providers

2 główne providery:

  • LocationManager.GPS_PROVIDER
  • LocationManager.NETWORK_PROVIDER

Providery mogą opisywać swoje charakterystyki

  • klasa LocationProvider
    • do pobrania informacji o providerze używamy LocationManager.getProvider
  • identyfikacja wymaganego poziomu zasilania
    • getPowerRequirements
  • horyzontalna dokładność
    • getAccuracy
    • zwraca Criteria.ACCURACY_COARSE/ACCURACY_FINE
  • rzeczy potrzebne do ustalenia położenia:
    • requiresSatellite,  requiresNetwork,  requiresCell
  • identyfikacja pozyskiwanych informacji
    • supportsAltitude, supportsBearing, supportsSpeed

LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);

LocationProvider gpsProvider = lm.getProvider(LocationManager.GPS_PROVIDER);

Specyfikacja pożądanego zachowania za pomocą klasy Criteria

  • wymagane informacje
    • setAltitudeRequired, setBearingRequired, setSpeedRequired
  • wymagania co do dokładności szerokości i długości geograficznej (Fine / Course)
    • setAccuracy
  • inne wymagania co do dokładności (High/Medium/Low)
    • setSpeedAccuracy, setVerticalAccuracy
  • akceptowalne zużycie zasobów
    • setPowerRequirement, setCostAllowed

Dopasowanie zachowania do providerów za pomocą klasy Criteria

  • sprawdzenie, czy provider spełnia kryteria
    • LocationProvider.meetsCriteria
  • wybranie providerów, które spełniają kryteria
    • LocationManager.getProviders
    • wszystkie pasujące lub tylko obecnie włączone
    • lista ograniczona jest uprawnieniami aplikacji
  • pobranie uaktualnień do lokalizacji przy użyciu providera spełniającego dane kryteria
    • LocationManager.requestLocationUpdates/requestSingleUpdate

Criteria criteria = new Criteria();

criteria.setAccuracy(Criteria.ACCURACY_FINE);

criteria.setSpeedRequired(true);

criteria.setAltitudeRequired(true);

LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);

List<String> matchingProviderNames = lm.getProviders(criteria, false);

for(String providerName:matchingProviderNames)  {

         LocationProvider provider = lm.getProvider(providerName);

         boolean requiresCell = provider.requiresCell();

         …

}

Sprawdzanie dostępności providera

  • LocationListener dostarcza tylko część informacji
    • onEnabled/onDisabled informują jedynie kiedy użytkownik włączył/wyłączył
  • Przed użyciem zawsze sprawdzajmy dostępność
    • LocationManager.isProviderEnabled

Provider sieciowy jest uzależniony od

  • włączenia Wi-Fi (możliwość sprawdzenia aktywności za pomocą ConnectivityManager)
  • włączenia trybu lotniczego (ustalenie stanu za pomocą klasy Settings.Global z AIRPLANE_MODE_ON)

Wykrywanie zmian w providerze sieciowym

BroadcastReceiver

  • w onReceive sprawdzamy Wi-Fi i tryb lotniczy
  • filtr intencji
    • Wi-Fi:  ConnectivityManager.CONNECTIVITY_ACTION
    • Tryb lotniczy:  Intent.ACTION_AIRPLANE_MODE_CHANGED

 

boolean isAvailable = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER);

 

boolean isOff = Settings.System.getInt(getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) == 0;

 

ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);

NetworkInfo wifiInfo = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);

boolean isAvailable = wifiInfo.isAvailable();

Udostępnianie potrzebnych funkcjonalności

  • Android nie pozwala aplikacjom zmieniać większości funkcjonalności
  • Aplikacja powinna automatycznie wyświetlać ekran z właściwościami
    • startActivity z odpowiednią intencją dla ekranu właściwości
    • Settings.ACTION_LOCATION_SOURCE_SETTINGS (providerzy lokalizacji)
    • Settings.ACTION_WIFI_SETTINGS
    • Settings.ACTION_AIRPLANE_MODE_SETTINGS

Provider GPS dostarcza więcej informacji niż tylko bieżące położenie

zdania NMEA (typowe dla urządzeń GPS)

  • implementacja interfejsu GpsStatus.NmeaListener
  • rozpoczęcie słuchania dzięki LocationManager.addNmeaListener

Pasywny provider lokalizacji

Inicjuje niejawnie monitorowanie lokalizacji

  • LocationManager.PASSIVE_PROVIDER
  • otrzymamy informację o lokalizacji, gdy inne źródło zażąda tego
  • prawdziwy provider możemy zidentyfikować za pomocą metody Location.getProvider
  • jak inna aplikacja np. pogoda pobierze lokalizację, u nas zostanie to zalogowane

 

Zarządzanie lokalizacją i wątkami

Domyślnie całe przetwarzanie odbywa się w głównym wątku aplikacji.

Jeśli chcemy wykonywać bardziej czasochłonne operacje jak np. zapis na dysk, do bazy danych lub komunikacja z siecią warto używać looper-a (z HandlerThread) obsługującego komunikaty w innym wątku niż UI. Przekazujemy instancję looper-a do LocationManager.requestLocationUpdates

HnadlerThread thread = new HandlerThread(“locthread”);

thread.start();

Looper looper = thread.getLooper();

locationManager.requestLocationUpdates(locationManager.NETWORK_PROVIDER, 0, 0, listener, looper);

locationManager.removeUpdates(listener);

looper.quit();

Aktualizacja UI z wątku lokalizacji jest wyzwaniem

  • Trzeba użyć międzywątkowego bezpiecznego mechanizmu
    • najłatwiej Activity.runOnUiThread
  • Trudności w synchronizacji UI z listenerem lokalizacji
    • aktywności się niszczone i tworzone od nowa przy obracaniu urządzenia
  • Od API 13 fragmenty mogą być persystowane między aktywnościami (setRetainInstance(true) w onCreate)
  • Przed API 13 trzeba jawnie zarządzać lokalizacją i obiektami wątków
    • zapisanie informacji pomiędzy aktywnościami przez nadpisanie onRetainNonConfigurationInstance
    • pobranie informacji między aktywnościami przez wywołanie getLastNonConfigurationInstance

final Location theLocation = location;

getActivity().runOnUiThread(new Runnable() {

        public void run() {

               setLocation(theLocation);

        }

});

 

public class TrackingActivity extends Activity {

        …

        public void onCreate(Bundle savedInstanceState) {

                …

                LocationState state = getLastNonConfigurationInstance();

                if (state != null)  {

                        _looper = state.getLooper();

                        _locationListener = state.getLocationListener();

                        if (_locationListener != null)

                               _locationListener.setActivity(this);

                }

        }

        …

        @Override

        public Object onRetainNonConfigurationInstance() {

                 return new LocationState(_looper, _locationListener);

        }

}

Trackowanie lokalizacji w tle

  • Serwis umożliwia trackowanie bez aktywności
  • Nadal trzeba używać loopera, by nie blokować głównego wątka aplikacji
  • Nie należy używać IntentService, bo może zostać zamknięty automatycznie

 

Kontrola częstotliwości aktualizacji lokalizacji

Ograniczenie częstotliwości aktualizacji

  • LocationManager - dwa sposoby:
    • czas (np. co 5 min)
    • odległość (np. co 1 km)
  • ustawienia limitów zachowują się inaczej przed i po API 16 (Android 4.1)
    • przed API 16 limity są tylko wskazówką dla providera (może się częściej aktualizować)
    • od API 16 provider musi uwzględniać limity

Ograniczenia czasowe

  • w ms
    • 0 - tak często, jak tylko możliwe
    • <=1000 - w praktyce to samo jak 0
  • provider może wyłączyć się pomiędzy aktualizacjami
    • im większe odstępy czasu, tym bardziej jest to prawdopodobne
    • znacząca oszczędność energii
  • dla dużych wartości odstępy pomiędzy aktualizacjami mogą być większe niż żądane
    • providerzy nie zaczynają szukać lokalizacji zanim nie minie okres przerwy
    • dla providera GPS różnica może być znacząca (10 s lub więcej)

Ograniczenia odległości

  • w metrach
    • 0 - tak często, jak tylko możliwe
    • minimalna znacząca wartość różni się pomiędzy providerami
  • provider rzadko się wyłącza pomiędzy aktualizacjami (musi być aktywny by ustalić dystans, o jaki się przemieściliśmy)
  • używany głównie do ograniczenia przetwarzania przez program

Otrzymywanie aktualizacji

Dwa sposoby

  • callback
    • większość kontroli po stronie aplikacji
    • mniejszy narzut systemowy (dobry wybór dla częstych aktualizacji)
  • pending intents
    • prostszy do oprogramowania
    • większy narzut systemowy (dopasowywanie Intent Filter) (unikać przy częstych aktualizacjach)
  • te same rodzaje limitów
  • te same informacje

Znaczne uproszczenie scenariuszy w lokalizacji

  • background tracking
    • system wyśle do serwisu bez rozwiązywania problemów z czasem życia
    • tworzymy prosty serwis dziedziczący po IntentService
  • wiele odbiorców
    • możliwość powiązania broadcastu z pending intent
    • odbiorcy tworzą broadcast receiver’a z odpowiednim filtrem intencji

Implementacja aktualizacji lokalizacji z Pending Intents

  • zdefiniowanie własnej akcji
  • implementacja serwisu/broadcast receivera z filtrem intencji dla tej akcji
  • stworzenie intencji dla tej akcji
  • stworzenie PendingIntent opakowujący Intent
  • wywoływanie LocationManager.requestLocationUpdates z obiektami Pending Intent
  • kończymy wywołując LocationManager.removeUpdates

Intent intent = new Intent(“com.xxx.ACTION_LOCATION_UPDATE”);

PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, 0);

LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);

lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, pendingIntent);

Dostęp do informacji o lokalizacji wysyłanej dla Pending Intent

Cel Pending Intent otrzymuje Intent z lokalizacją

  • otrzymany Intent zawiera wszystkie informacje w oryginalnym Intent
  • system lokalizacji dodaje specyficzne extras
    • zmiany w lokalizacji:  LocationManager.KEY_LOCATION_CHANGED (zwracana wartość jest instancją klasy Location)
    • włączenie/wyłączenie providera:  LocationManager.KEY_PROVIDER_ENABLED (zwracana wartość 1=enabled, 0=disabled)

public class XActivity extends Activity {

        …

        private PendingIntent _locationChangeServicePendingIntent;

        private PendingIntent _locationChangeBroadcastPendingIntent;

        @Override

        public void onCreate(Bundle savedInstanceState)  {

                …

                Intent intent = new Intent(“com.xxx.ACTION_LOCATION_CHANGED”);

                _locationChangeServicePendingIntent = PendingIntent.getService(this, 0, intent, 0);

                _locationChangeBroadcastPendingIntent =  PendingIntent.getBroadcast(this, 0, intent, 0);

        }

        public void onMenuStartClick(MenuItem item)  {

                LocationManager lm = (LocationManager)  getSystemService(LOCATION_SERVICE);

                lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, _locationChangeServicePendingIntent);

                /* _locationChangeBroadcastPendingIntent */

        }

        public void onMenuStopClick(MenuItem item)  {

                LocationManager lm = (LocationManager)  getSystemService(LOCATION_SERVICE);

                 lm.removeUpdates(_locationChangeServicePendingIntent);

                 /* _locationChangeBroadcastPendingIntent */

        }

}

public class LocationUpdateService extends IntentService  {

        public LocationUpdateService()  {

                super(“LocationUpdateService”);

        }

        @Override

        protected void onHandleIntent (Intent intent)  {

                String action = intent.getAction();

                if (action.equals(“com.xxx.ACTION_LOCATION_CHANGED”))  {

                        Bundle extras = intent.getExtras();

                        Location location = extras.get(LocationManager.KEY_LOCATION_CHANGED);

                        …

                }

        }

}

public class LocationUpdateReceiver1 extends BroadcastReceiver {

         public void onReceive(Context context, Intent intent)  {

                 String action = intent.getAction();

                 …

         }

}

<manifest …>

          <application …>

                     <service android:name=”.LocationUpdateService”>

                               <intent-filter>

                                        <action android:name=”com.xxx.ACTION_LOCATION_CHANGED” />

                               </intent-filter>

                      </service>

                       <service android:name=”.LocationUpdateReceiver1”>

                               <intent-filter>

                                        <action android:name=”com.xxx.ACTION_LOCATION_CHANGED” />

                               </intent-filter>

                      </service>

         </application>

</manifest>

Kiedy unikać lokalizacji z Pending Intent?

  • duża częstotliwość aktualizacji
  • aktywność jako cel dla Pending Intent
    • ciągłe wywoływanie onCreate w aktywności
    • aktywność może stać się aktywnością foreground jeśli była w tle
    • można używać, gdy aktywność ma ustawiony tryb na singleInstance
      • trzeba wywołać removeUpdate jeśli użytkownik przełączy się na inną aktywność

Proximity alerts

System może wysłać alert jeśli znajdziemy się blisko określonej lokalizacji

  • LocationManager.addProximityAlert (może być kilka równocześnie aktywnych)
  • Wewnętrznie używane są zarówno sieć jak i GPS
  • Potrzeba dużej ilości energii
  • Wymagane uprawnienia
    • API 16 (Android 4.1.1)  lub wcześniej:  ACCESS_COURSE_LOCATION (wystarczy)
    • API 17 (Android 4.2) lub wyżej:  ACCESS_FINE_LOCATION

Czego potrzebujemy do proximity alert ?

  • szerokość i długość geograficzną lokalizacji
  • proximity = odległość od lokalizacji w m
  • jak długo monitorować wejście do proximity (w ms)
    • -1: aż do wywołania removeProximityAlert
    • 0: od razu
  • odpalenia Pending Intent, kiedy urządzenie wchodzi do proximity
    • logiczny extra o nazwie LocationManager.KEY_PROXIMITY_ENTERING
      • true - wejście
      • false - wyjście

LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);

long expiration = -1;

lm.addProximityAlert(latitude, longitude, radius, expiration, pendingIntent);

 

Informacje o lokalizacji czytelne dla ludzi

Klasa Address

  • części adresu
    • getThouroughfare: zwykle nazwa ulicy
    • getLocality: zwykle nazwa miasta
    • getAdminArea: zwykle nazwa stanu lub prowinicji
  • adres gotowy do wyświetlenia
    • getMaxAddressLineIndex
    • getAddressLine(index)
  • współrzędne
    • getLatitiude/getLongitude

Klasa Geocoder

  • pełny opis przyjazny dla komputera i dla człowieka
  • getFromLocation
    • jeden lub więcej instancji Address dla współrzędnych szerokości i długości geograficznej
  • getFromLocationName
    • jeden lub więcej instancji Address dla nazwanej lokalizacji
  • obie metody zwracają List<Address>
    • w wywołaniach metod specyfikujemy maksymalną liczbę instancji Address
    • lista zawierać będzie od 0 do max żądanych wartości
    • pierwszy zwracany wynik jest zwykle najbardziej pasujący
  • klasa zależy od własnych bibliotek i usług Google
    • jest dostępna na większości urządzeń kupionych w sklepie
    • może nie być dostępna na urządzeniach z Androidem w wersji Open Source
    • sprawdzenie czy wymagane biblioteki Google są na urządzeniu - statyczna metoda isPresent (API 9/Android 2.3)
  • klasa wykorzystuje web serwis
    • getFromLocation i getFromLocationName wywołują zdalne serwisy
  • IOException
    • biblioteki nie są dostępne
    • błąd w komunikacji z serwerem
  • wartość zwracana pusta lub null
    • błąd w komunikacji z serwerem
    • żaden adres nie pasuje do zapytania
  • metod getFromLocation/getFromLocationName nie należy wywoływać w wątku UI
    • obie oparte są na blokujących wywołaniach sieciowych
    • należy zastosować dowolne rozwiązania pozwalające uniknąć blokowania wywołania w wątku UI (najprostsze: AsyncTask)

Geocoder geocoder = new Geocoder(context);  //w activity: this

List<Address> addrList1 = geocoder.getFromLocation(x,y,5);

List<Address> addrList2 = geocoder.getFromLocationName(place, 5);

Java

  • metoda(String… strings)

środa, 1 października 2014

Windows 10 Technical Preview - nowe fakty i pierwsze wrażenia

Trochę nowych faktów, które zebrały się na obecny moment w mojej głowie

Mamy już także nieoficjalne nagrania w sieci demonstrujące nowe właściwości systemu. Oto jedno z nich:

Pojawiło się też zmontowane wideo z wczorajszego pokazu idei “Continuum”:

Na oficjalnej stronie preview.windows.com wśród opatrzonych już zdjęć pojawiło się zdjęcie akcji i komend w górnym pasku okna aplikacji Modern:

 60c5935f-e7b5-4b9a-8e52-96ab7b74f13f_28 

Oczywiście najlepiej na żywo zapoznać się z nowym systemem instalując go, co też uczyniłem.

win102

Tak, teraz wszystkie aplikacje Modern mają w pasku tytułowym ten przycisk. Nadal może on otwierać Charms Bar, co może trochę dziwnie wyglądać, ale tego należało się spodziewać. Snap Assistant w praktyce nie zawsze jest tak wygodny. Spłaszczone wszystkie okna, przy czym wokół każdego okna jest taka gradientowa poświata. Przycisk Share w eksplorerze rzeczywiście włącza sharowanie do aplikacji Modern na pasku bocznym, co może też trochę wyglądać mniej spójnie, ale na razie wywoływana jest dotychczasowa implementacja. Pozostaje niedosyt związany z platformą dla aplikacji, ale całość wygląda tak jakby ktoś szybko przerobił część Windows 8.1, więc może nie jest jeszcze gotowa. Powstaje pytanie co z paskami. Trzeba klikać prawym przyciskiem myszy by zobaczyć pasek aplikacyjny i nawigacyjny (np. Weather). Czy to jest jeszcze zgodne z nową filozofią systemu “wiele oblicz, jedna platforma” ? Aplikacja “Store” od Windows 8.1 na desktopie niedotykowym wyświetla zawsze górny pasek nawigacyjny i nieco mniejszy. Może to będzie jakiś zaczątek dla nowej wersji platformy? Poza tym trzeba będzie zakładać większą skalowalność interfejsu jak sądzę. Cóż będę używał, pobierał z chmury najnowsze buildy (co przypomina program Developer Preview na Windows Phone) i jak coś zauważę to przekażę to przez Windows Feedback (sama też czasami odpytuje  - mnie np. dziś czy ciężko korzystało mi się z Menu Start w skali od 1 do 4), a o części z nich napiszę może i tutaj.