En av de mest populære bruksområder for mobile enheter er å spille av lyd gjennom musikkstrømningstjenester, nedlastede podcaster eller et annet antall lydkilder. Selv om dette er en ganske vanlig funksjon, er det vanskelig å implementere, med mange forskjellige stykker som må bygges riktig for å gi brukeren din full Android-opplevelse.
I denne opplæringen lærer du om MediaSessionCompat
fra Android support-biblioteket, og hvordan det kan brukes til å skape en riktig bakgrunnslydtjeneste for brukerne.
Det første du må gjøre er å inkludere Android support-biblioteket i prosjektet ditt. Dette kan gjøres ved å legge til følgende linje i modulens build.gradle filen under avhengighetsnoden.
kompilere 'com.android.support:support-v13:24.2.1'
Etter at du har synkronisert prosjektet ditt, opprett en ny Java-klasse. For dette eksempelet vil jeg ringe til klassen BackgroundAudioService
. Denne klassen må utvides MediaBrowserServiceCompat
. Vi vil også implementere følgende grensesnitt: MediaPlayer.OnCompletionListener
og AudioManager.OnAudioFocusChangeListener
.
Nå som din MediaBrowserServiceCompat
implementering er opprettet, la oss ta et øyeblikk å oppdatere AndroidManifest.xml før du går tilbake til denne klassen. På toppen av klassen må du be om WAKE_LOCK
tillatelse.
Deretter, innenfor applikasjon
node, erklære din nye tjeneste med følgende intent-filteret
elementer. Disse vil tillate at tjenesten din kan avskjære kontrollknapper, hodetelefonhendelser og mediesøking for enheter, for eksempel Android Auto (selv om vi ikke vil gjøre noe med Android Auto for denne opplæringen, er det fortsatt nødvendig med grunnleggende støtte for den. MediaBrowserServiceCompat
).
Til slutt må du erklære bruken av MediaButtonReceiver
fra Android supportbiblioteket. Dette vil tillate deg å avskjære mediekontrollknappens interaksjoner og hodetelefonhendelser på enheter som kjører KitKat og tidligere.
Nå som din AndroidManifest.xml filen er ferdig, du kan lukke den. Vi skal også lage en annen klassen som heter MediaStyleHelper
, som ble skrevet av Ian Lake, Developer Advocate på Google, for å rydde opp etableringen av mediestilvarsler.
offentlig klasse MediaStyleHelper / ** * Bygg et varsel ved hjelp av informasjonen fra den aktuelle mediesesjonen. Gjør tung bruk * av @link MediaMetadataCompat # getDescription () for å trekke ut relevant informasjon. * @param-kontekst Kontekst brukt til å konstruere varselet. * @param mediaSession Media økt for å få informasjon. * @return En forhåndsbygd melding med informasjon fra den aktuelle mediesesjonen. * / offentlig statisk NotificationCompat.Builder fra (Kontekst kontekst, MediaSessionCompat mediaSession) MediaControllerCompat controller = mediaSession.getController (); MediaMetadataCompat mediaMetadata = controller.getMetadata (); MediaDescriptionCompat description = mediaMetadata.getDescription (); NotificationCompat.Builder builder = ny NotificationCompat.Builder (kontekst); byggeren .setContentTitle (description.getTitle ()) .setContentText (description.getSubtitle ()) .setSubText (description.getDescription ()) .setLargeIcon (description.getIconBitmap ()) .setContentIntent (controller.getSessionActivity ()) .setDeleteIntent (MediaButtonReceiver .buildMediaButtonPendingIntent (kontekst, PlaybackStateCompat.ACTION_STOP)) .setVisibility (NotificationCompat.VISIBILITY_PUBLIC); returbygger;
Når det er opprettet, fortsett og lukk filen. Vi vil fokusere på bakgrunnslyttjenesten i neste avsnitt.
Nå er det på tide å grave inn i kjernen i å lage medieappen din. Det er noen medlemsvariabler som du vil deklarere først for denne prøveappen: a Mediaspiller
for den faktiske avspillingen, og a MediaSessionCompat
objekt som vil administrere metadata og avspillingskontroller / tilstander.
privat MediaPlayer mMediaPlayer; privat MediaSessionCompat mMediaSessionCompat;
I tillegg trenger du en BroadcastReceiver
som lytter etter endringer i hodetelefontilstanden. For å holde det enkelt, vil denne mottakeren sette pause på Mediaspiller
, hvis det spiller.
privat BroadcastReceiver mNoisyReceiver = ny BroadcastReceiver () @Override public void onReceive (Kontekst kontekst, Intent intent) if (mMediaPlayer! = null && mMediaPlayer.isPlaying ()) mMediaPlayer.pause (); ;
For den endelige medlemsvariabelen vil du opprette en MediaSessionCompat.Callback
objekt, som brukes til å håndtere avspillingsstatus når mediesesjon handler.
privat MediaSessionCompat.Callback mMediaSessionCallback = ny MediaSessionCompat.Callback () @Override public void onPlay () super.onPlay (); @Override public void onPause () super.onPause (); @Override public void onPlayFromMediaId (String mediaId, Bundle extras) super.onPlayFromMediaId (mediaId, extras); ;
Vi vil revidere hver av metodene senere i denne opplæringen, da de vil bli brukt til å drive operasjoner i vår medieapp.
Det er to metoder som vi også må deklarere, selv om de ikke trenger å gjøre noe i forbindelse med denne opplæringen: onGetRoot ()
og onLoadChildren ()
. Du kan bruke følgende kode for standardinnstillingene dine.
@Nullable @Override public BrowserRoot onGetRoot (@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) if (TextUtils.equals (clientPackageName, getPackageName ())) returner ny BrowserRoot (getString (R.string.app_name) null ); returnere null; // Ikke viktig for generell lydtjeneste, kreves for klassen @Override public void onLoadChildren (@NonNull String parentId, @NonNull Result> resultat) result.sendResult (null);
Til slutt vil du vil overstyre onStartCommand ()
metode, som er inngangspunktet i din Service
. Denne metoden vil ta Intent som sendes til Service
og send det til MediaButtonReceiver
klasse.
@Override public int onStartCommand (Intent intent, int flagg, int startId) MediaButtonReceiver.handleIntent (mMediaSessionCompat, hensikt); returnere super.onStartCommand (hensikt, flagg, startId);
Nå som du har opprettet basiselementvariablene, er det på tide å initialisere alt. Vi gjør dette ved å ringe til ulike hjelpemetoder i onCreate ()
.
@Override public void onCreate () super.onCreate (); initMediaPlayer (); initMediaSession (); initNoisyReceiver ();
Den første metoden, initMediaPlayer ()
, vil initialisere Mediaspiller
objekt som vi opprettet øverst i klassen, be om partial wake lock (som er grunnen til at vi krevde tillatelsen i AndroidManifest.xml), og sett spillerens volum.
privat tomt initMediaPlayer () mMediaPlayer = nytt MediaPlayer (); mMediaPlayer.setWakeMode (getApplicationContext (), PowerManager.PARTIAL_WAKE_LOCK); mMediaPlayer.setAudioStreamType (AudioManager.STREAM_MUSIC); mMediaPlayer.setVolume (1.0f, 1.0f);
Den neste metoden, initMediaSession ()
, er hvor vi initialiserer MediaSessionCompat
objekt og led den til medieknappene og kontrollmetoder som tillater oss å håndtere avspilling og brukerinngang. Denne metoden starter ved å opprette en ComponentName
objekt som peker på Android support biblioteket MediaButtonReceiver
klassen, og bruker det til å opprette en ny MediaSessionCompat
. Vi passerer da MediaSession.Callback
objekt som vi opprettet tidligere til det, og angi flaggene som er nødvendige for å motta medieknappinnganger og styresignaler. Deretter oppretter vi en ny Intent
for å håndtere medieknappinnganger på pre-Lollipop-enheter, og angi mediesessionstoken for vår tjeneste.
private void initMediaSession () ComponentName mediaButtonReceiver = nytt ComponentName (getApplicationContext (), MediaButtonReceiver.class); mMediaSessionCompat = ny MediaSessionCompat (getApplicationContext (), "Tag", mediaButtonReceiver, null); mMediaSessionCompat.setCallback (mMediaSessionCallback); mMediaSessionCompat.setFlags (MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); Intent mediaButtonIntent = ny Intent (Intent.ACTION_MEDIA_BUTTON); mediaButtonIntent.setClass (dette, MediaButtonReceiver.class); PendingIntent pendingIntent = PendingIntent.getBroadcast (dette, 0, mediaButtonIntent, 0); mMediaSessionCompat.setMediaButtonReceiver (pendingIntent); setSessionToken (mMediaSessionCompat.getSessionToken ());
Endelig registrerer vi BroadcastReceiver
som vi opprettet på toppen av klassen slik at vi kan lytte til hodetelefonbytterhendelser.
Private void initNoisyReceiver () // Håndtak hodetelefoner kommer unplugged. kan ikke gjøres via en åpen mottaker IntentFilter filter = ny IntentFilter (AudioManager.ACTION_AUDIO_BECOMING_NOISY); registerReceiver (mEoisyReceiver, filter);
Nå som du er ferdig med å initialisere BroadcastReceiver
, MediaSessionCompat
og Mediaspiller
Objekter, det er på tide å se på lydfokus.
Selv om vi kanskje tror at våre egne lydapps er de viktigste for øyeblikket, vil andre apper på enheten konkurrere om å lage egne lyder, for eksempel et e-postvarsel eller mobilspill. For å kunne jobbe med disse ulike situasjonene bruker Android-systemet lydfokus for å bestemme hvordan lyd skal håndteres.
Det første tilfellet vi vil håndtere, begynner avspilling og forsøker å motta enhetens fokus. I din MediaSessionCompat.Callback
objekt, gå inn i onPlay ()
metode og legg til følgende tilstandskontroll.
@Override public void onPlay () super.onPlay (); hvis (! successfullyRetrievedAudioFocus ()) return;
Ovennevnte kode vil ringe en hjelpemetode som forsøker å hente fokus, og hvis det ikke kan, vil det bare returnere. I en ekte app, vil du ønske å håndtere mislykket lydavspilling mer grasiøst. successfullyRetrievedAudioFocus ()
vil få en referanse til systemet Lydbehandling
, og forsøk å be om lydfokus for streaming av musikk. Det vil da returnere a boolean
som representerer om forespørselen lyktes eller ikke.
privat boolsk vellykketRetrievedAudioFocus () AudioManager audioManager = (AudioManager) getSystemService (Context.AUDIO_SERVICE); int resultat = audioManager.requestAudioFocus (dette, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); returresultat == AudioManager.AUDIOFOCUS_GAIN;
Du vil legge merke til at vi også går forbi dette
inn i det requestAudioFocus ()
metode, som forbinder OnAudioFocusChangeListener
med vår tjeneste. Det er noen forskjellige stater du vil lytte etter for å være en "god medborger" i enhetens app-økosystem.
AudioManager.AUDIOFOCUS_LOSS
: Dette skjer når en annen app har bedt om lydfokus. Når dette skjer, bør du stoppe lydavspilling i appen din.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
: Denne tilstanden er angitt når en annen app ønsker å spille av lyd, men det forventer bare å trenge fokus for en kort stund. Du kan bruke denne tilstanden til å pause lydavspillingen.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
: Når lydfokus blir bedt om, men kaster en "can duck" -status, betyr det at du kan fortsette avspillingen, men bør ta volumet litt ned. Dette kan oppstå når en varslingslyd spilles av enheten.AudioManager.AUDIOFOCUS_GAIN
: Den endelige staten vi skal diskutere er AUDIOFOCUS_GAIN
. Dette er staten når en avspillbar lydavspilling har fullført, og appen din kan gjenoppta på sine tidligere nivåer.En forenklet onAudioFocusChange ()
tilbakeringing kan se slik ut:
@Override public void onAudioFocusChange (int fokusChange) switch (focusChange) tilfelle AudioManager.AUDIOFOCUS_LOSS: if (mMediaPlayer.isPlaying ()) mMediaPlayer.stop (); gå i stykker; tilfelle AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: mMediaPlayer.pause (); gå i stykker; tilfelle AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: if (mMediaPlayer! = null) mMediaPlayer.setVolume (0.3f, 0.3f); gå i stykker; tilfelle AudioManager.AUDIOFOCUS_GAIN: if (mMediaPlayer! = null) if (! mMediaPlayer.isPlaying ()) mMediaPlayer.start (); mMediaPlayer.setVolume (1.0f, 1.0f); gå i stykker;
Nå som du har en generell struktur sammen for din Service
, det er på tide å dykke inn i MediaSessionCompat.Callback
. I den siste delen har du lagt litt til onPlay ()
for å sjekke om lydfokus ble gitt. Under betinget utsagn vil du sette inn MediaSessionCompat
protester mot aktiv, gi den en tilstand av STATE_PLAYING
, og tilordne de riktige tiltakene som er nødvendige for å opprette pause-knapper på kontrollpanelene for Lollipop Lock-skjerm, telefon og Android Wear.
@Override public void onPlay () super.onPlay (); hvis (! successfullyRetrievedAudioFocus ()) return; mMediaSessionCompat.setActive (true); setMediaPlaybackState (PlaybackStateCompat.STATE_PLAYING); ...
De setMediaPlaybackState ()
Metoden ovenfor er en hjelpemetode som lager en PlaybackStateCompat.Builder
objekt og gir den de riktige handlinger og tilstander, og deretter bygger og forbinder a PlaybackStateCompat
med din MediaSessionCompat
gjenstand.
privat tomt settMediaPlaybackState (int state) PlaybackStateCompat.Builder playbackstateBuilder = ny PlaybackStateCompat.Builder (); hvis (state == PlaybackStateCompat.STATE_PLAYING) playbackstateBuilder.setActions (PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PAUSE); ellers playbackstateBuilder.setActions (PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY); playbackstateBuilder.setState (state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 0); mMediaSessionCompat.setPlaybackState (playbackstateBuilder.build ());
Det er viktig å merke seg at du trenger både ACTION_PLAY_PAUSE
og heller ikke ACTION_PAUSE
eller ACTION_PLAY
flagg i handlingene dine for å få ordentlig kontroll på Android Wear.
Tilbake i onPlay ()
, du vil vise et spillvarsel som er knyttet til din MediaSessionCompat
objekt ved å bruke MediaStyleHelper
klasse som vi definerte tidligere, og deretter vise det varselet.
privat tomt showPlayingNotification () NotificationCompat.Builder builder = MediaStyleHelper.from (BackgroundAudioService.this, mMediaSessionCompat); hvis (builder == null) return; builder.addAction (new NotificationCompat.Action (android.R.drawable.ic_media_pause, "Pause", MediaButtonReceiver.buildMediaButtonPendingIntent (dette, PlaybackStateCompat.ACTION_PLAY_PAUSE))); builder.setStyle (new NotificationCompat.MediaStyle (). setShowActionsInCompactView (0) .setMediaSession (mMediaSessionCompat.getSessionToken ())); builder.setSmallIcon (R.mipmap.ic_launcher); NotificationManagerCompat.from (BackgroundAudioService.this) .notify (1, builder.build ());
Til slutt begynner du Mediaspiller
ved slutten av onPlay ()
.
@Override public void onPlay () super.onPlay (); ... showPlayingNotification (); mMediaPlayer.start ();
Når tilbakeringingen mottar en pause-kommando, onPause ()
vil bli kalt. Her vil du sette pause på Mediaspiller
, sett staten til STATE_PAUSED
, og vis en midlertidig melding.
@Override public void onPause () super.onPause (); hvis (mMediaPlayer.isPlaying ()) mMediaPlayer.pause (); setMediaPlaybackState (PlaybackStateCompat.STATE_PAUSED); showPausedNotification ();
Våre showPausedNotification ()
hjelpemetoden vil se ut som showPlayNotification ()
metode.
privat tomt showPausedNotification () NotificationCompat.Builder builder = MediaStyleHelper.from (dette, mMediaSessionCompat); hvis (builder == null) return; builder.addAction (new NotificationCompat.Action (android.R.drawable.ic_media_play, "Play", MediaButtonReceiver.buildMediaButtonPendingIntent (dette, PlaybackStateCompat.ACTION_PLAY_PAUSE))); builder.setStyle (new NotificationCompat.MediaStyle (). setShowActionsInCompactView (0) .setMediaSession (mMediaSessionCompat.getSessionToken ())); builder.setSmallIcon (R.mipmap.ic_launcher); NotificationManagerCompat.from (this) .notify (1, builder.build ());
Den neste metoden i tilbakekallingen som vi skal diskutere, onPlayFromMediaId ()
, tar a string
og a Bunt
som parametere. Dette er tilbakeringingsmetoden du kan bruke til å endre lydspor / innhold i appen din.
For denne opplæringen vil vi ganske enkelt godta en ressurs-ID og forsøke å spille det, og deretter gjenopprette sessions metadata. Som du har lov til å passere en Bunt
inn i denne metoden kan du bruke den til å tilpasse andre aspekter av medieavspillingen, for eksempel å sette opp en tilpasset bakgrunnslyd for et spor.
@Override public void onPlayFromMediaId (String mediaId, Bundle extras) super.onPlayFromMediaId (mediaId, extras); prøv AssetFileDescriptor afd = getResources (). openRawResourceFd (Integer.valueOf (mediaId)); hvis (afd == null) return; prøv mMediaPlayer.setDataSource (afd.getFileDescriptor (), afd.getStartOffset (), afd.getLength ()); fangst (IllegalStateException e) mMediaPlayer.release (); initMediaPlayer (); mMediaPlayer.setDataSource (afd.getFileDescriptor (), afd.getStartOffset (), afd.getLength ()); afd.close (); initMediaSessionMetadata (); fangst (IOException e) return; prøv mMediaPlayer.prepare (); fangst (IOException e) // Arbeid med statister her hvis du vil
Nå som vi har diskutert de to hovedmetodene i denne tilbakeringingen du vil bruke i appene dine, er det viktig å vite at det finnes andre valgfrie metoder du kan bruke til å tilpasse tjenesten. Noen metoder inkluderer onSeekTo ()
, som lar deg endre avspillingsposisjonen til innholdet ditt, og på bestilling ()
, som vil akseptere en string
betegner typen kommando, a Bunt
for ekstra informasjon om kommandoen, og a ResultReceiver
tilbakeringing, som vil tillate deg å sende egendefinerte kommandoer til din Service
.
@Override public void onCommand (String command, Bundle extras, ResultReceiver cb) super.onCommand (command, extras, cb); hvis (COMMAND_EXAMPLE.equalsIgnoreCase (kommando)) // Custom command here @Override public void onSeekTo (long pos) super.onSeekTo (pos);
Når lydfilen vår er ferdig, vil vi ønske å bestemme hva vår neste tiltak vil være. Mens du kanskje vil spille det neste sporet i appen din, holder vi det enkelt og slipper Mediaspiller
.
@Override public void onCompletion (MediaPlayer mediaPlayer) if (mMediaPlayer! = Null) mMediaPlayer.release ();
Til slutt vil vi gjøre noen ting i onDestroy ()
metode for vår Service
. Først, få en referanse til systemtjenesten Lydbehandling
, og ring abandonAudioFocus ()
med vår AudioFocusChangeListener
som en parameter, som vil varsle andre apper på enheten som du gir opp lydfokus. Deretter avregistreres BroadcastReceiver
Det ble satt opp for å lytte til hodetelefonendringer, og slippe ut MediaSessionCompat
gjenstand. Til slutt vil du avbryte avspillingskontrollvarselet.
@Override public void onDestroy () super.onDestroy (); AudioManager audioManager = (AudioManager) getSystemService (Context.AUDIO_SERVICE); audioManager.abandonAudioFocus (this); unregisterReceiver (mNoisyReceiver); mMediaSessionCompat.release (); NotificationManagerCompat.from (dette) .cancel (1);
På dette tidspunktet bør du ha en fungerende grunnleggende bakgrunnslyd Service
ved hjelp av MediaSessionCompat
for avspillingskontroll over enheter. Selv om det allerede har vært mye involvert i å skape tjenesten, bør du kunne styre avspilling fra appen din, et varsel, låseskjermkontroller på pre-Lollipop-enheter (Lollipop og over vil bruke varselet på låseskjermen), og fra eksterne enheter, for eksempel Android Wear, en gang på Service
har blitt startet.
Mens de fleste kontroller blir automatiske, vil du fortsatt ha litt arbeid for å starte og kontrollere en mediesesjon fra kontrollene dine i appen. I det minste vil du ha en MediaBrowserCompat.ConnectionCallback
, MediaControllerCompat.Callback
, MediaBrowserCompat
, og MediaControllerCompat
objekter opprettet i appen din.
MediaControllerCompat.Callback
vil ha en metode kalt onPlaybackStateChanged ()
som mottar endringer i avspillingsstatus, og kan brukes til å holde brukergrensesnittet synkronisert.
privat MediaControllerCompat.Callback mMediaControllerCompatCallback = ny MediaControllerCompat.Callback () @Override public void onPlaybackStateChanged (PlaybackStateCompat state) super.onPlaybackStateChanged (state); hvis (tilstand == null) return; bytte (state.getState ()) tilfelle PlaybackStateCompat.STATE_PLAYING: mCurrentState = STATE_PLAYING; gå i stykker; tilfelle PlaybackStateCompat.STATE_PAUSED: mCurrentState = STATE_PAUSED; gå i stykker; ;
MediaBrowserCompat.ConnectionCallback
har en onConnected ()
metode som vil bli kalt når en ny MediaBrowserCompat
objekt er opprettet og tilkoblet. Du kan bruke dette til å initialisere din MediaControllerCompat
objekt, knytt det til din MediaControllerCompat.Callback
, og knytte den sammen med MediaSessionCompat
fra din Service
. Når det er ferdig, kan du starte lydavspilling fra denne metoden.
privat MediaBrowserCompat.ConnectionCallback mMediaBrowserCompatConnectionCallback = ny MediaBrowserCompat.ConnectionCallback () @Override public void onConnected () super.onConnected (); prøv mMediaControllerCompat = ny MediaControllerCompat (MainActivity.this, mMediaBrowserCompat.getSessionToken ()); mMediaControllerCompat.registerCallback (mMediaControllerCompatCallback); setSupportMediaController (mMediaControllerCompat); getSupportMediaController (). getTransportControls (). playFromMediaId (String.valueOf (R.raw.warner_tautz_off_broadway), null); fangst (RemoteException e) ;
Du vil merke at kodestykket ovenfor bruker getSupportMediaController (). getTransportControls ()
å kommunisere med mediesesjonen. Med samme teknikk kan du ringe onPlay ()
og onPause ()
i lydtjenesten din MediaSessionCompat.Callback
gjenstand.
hvis (mCurrentState == STATE_PAUSED) getSupportMediaController (). getTransportControls (). spill (); mCurrentState = STATE_PLAYING; andre hvis (getSupportMediaController (). getPlaybackState (). getState () == PlaybackStateCompat.STATE_PLAYING) getSupportMediaController (). getTransportControls (). pause (); mCurrentState = STATE_PAUSED;
Når du er ferdig med lydavspillingen, kan du stoppe lydtjenesten og koble fra MediaBrowserCompat
objekt, som vi vil gjøre i denne opplæringen når dette Aktivitet
er ødelagt.
@Override protected void onDestroy () super.onDestroy (); hvis (getSupportMediaController (). getPlaybackState (). getState () == PlaybackStateCompat.STATE_PLAYING) getSupportMediaController (). getTransportControls (). pause (); mMediaBrowserCompat.disconnect ();
Puh! Som du ser, er det mange bevegelige stykker involvert i å skape og bruke en bakgrunnslydtjeneste på riktig måte.
I denne opplæringen har du opprettet en tjeneste som spiller en enkel lydfil, lytter etter endringer i lydfokus og koblinger til MediaSessionCompat
å gi universell avspillingskontroll på Android-enheter, inkludert telefoner og Android Wear. Hvis du kjører i veibeskyttelse mens du arbeider gjennom denne opplæringen, anbefaler jeg sterkt at du sjekker ut den tilhørende Android-prosjektkoden på Envato Tuts + s GitHub.
Og sjekk ut noen av våre andre Android-kurs og opplæringsprogrammer her på Envato Tuts+!