Hurra! “Kolekcjonerska” edycja mojej app-ki w Xamarin.Android uzyskała właśnie taką funkcjonalność jak oryginał w Javie po odcinku 9 z DSP. Sprawdzajcie na githubie. To większość kodu, do przeportowania pozostał jedynie uzupełniający materiał z odcinka 10.
Ponieważ portuję swój własny kod sprzed paru miesięcy, nie chciałbym się powtarzać, jeśli chodzi o sprawy związane ściśle z samym Androidem, które już opisywałem podczas DSP. Zajmiemy się więc bardziej wysublimowanymi sprawami związanymi z samym Xamarinem lub stykiem Android-Xamarin.
#1 Target API Level
Rzecz niby banalna, a trochę dała mi w kość w Visual Studio 2015. Przy ustawieniu docelowego targetu na API 23 z Android 6 dostawałem najpierw komunikat z ostrzeżeniami o wersji Javy. Postąpiłem zgodnie ze stackoverflow i doinstalowałem Javę 8.0 SDK (x86). Potem w ustawieniach ogólnych Xamarin (Tools –> Options –> Xamarin) w Visual Studio zmieniłem ścieżkę na tę wersję. Wtedy dostałem ostrzeżenie, że wersja z manifestu Android jest starsza niż TargetFramework!!! i że do kompilacji i tak zostanie użyta najnowsza wersja API, czyli 24 z nadchodzącego Android 7 (może dlatego wcześniej krzyczało o nowszą Javę niż 7 instalowana domyślnie z VS 2015 U3). A wszystko miałem ustawione jak należy. W końcu wpadłem na pomysł, by ręcznie przedytować plik .csproj i parametr AndroidUseLatestPlatformSdk ustawiłem na False (pozostawał ciągle na True w XML mimo wybrania z comba w VS konkretnej wersji SDK). Pomogło!
#2 Klasy zagnieżdżone z akcjami
Obserwując różne sample Xamarin wyczaiłem lepszy niż ostatnio pattern do portowania klas callbacków z Javy (czy broadcast receiverów). Otóż aby nie przekazywać jawnie referencji do obiektu klasy rodzica wzbogacamy callback o akcje wywoływane przez jego metody. Natomiast kod akcji ustawiamy w klasie rodzica, przeważnie w jej konstruktorze lub metodzie tworzącej np. OnCreate.
    public abstract class BaseActivity: AppCompatActivity, …   
    {           
        class Callback : MediaControllerCompat.Callback   
        {    
            public Action<PlaybackStateCompat> OnPlaybackStateChangedImpl { get; set; }    
            public Action<MediaMetadataCompat> OnMetadataChangedImpl { get; set; }
            public override void OnPlaybackStateChanged(PlaybackStateCompat state)   
            {    
                OnPlaybackStateChangedImpl(state);    
            }
            public override void OnMetadataChanged(MediaMetadataCompat metadata)   
            {    
                OnMetadataChangedImpl(metadata);    
            }    
        }
readonly Callback mediaControllerCallback = new Callback();
…
   
        protected override void OnCreate(Bundle savedInstanceState)    
        {    
            base.OnCreate(savedInstanceState);
…
            mediaControllerCallback.OnPlaybackStateChangedImpl = (state) =>   
            {    
                if (ShouldShowControls)    
                {    
                    ShowPlaybackControls();    
                }    
                else    
                {    
                    LogHelper.Debug(Tag, "mediaControllerCallback.onPlaybackStateChanged: " +    
                            "hiding controls because state is ", state.State);    
                    HidePlaybackControls();    
                }    
            };
            mediaControllerCallback.OnMetadataChangedImpl = (metadata) =>   
            {    
                if (ShouldShowControls)    
                {    
                    ShowPlaybackControls();    
                }    
                else    
                {    
                    LogHelper.Debug(Tag, "mediaControllerCallback.onMetadataChanged: " +    
                        "hiding controls because metadata is null");    
                    HidePlaybackControls();    
                }    
            };
        }                                 
                
    }
#3 Asynchroniczne wywołania
Marzy mi się API Android z async-ami we wszystkich trwających dłużej metodach, tak jak mamy w WinRT API w Windows 8.x/10. Póki co na pewno async możemy stosować przy kliencie Http z .NET. Przykładem niech będzie metoda z klasy BitmapHelper z przykładu MediaBrowserService (będącego portem z oryginału w Javie z Android SDK):
        public async static Task<Bitmap> FetchAndRescaleBitmap(string uri, int width, int height)   
        {    
            try    
            {    
                using (var client = new HttpClient())    
                {    
                    Stream stream = await client.GetStreamAsync(uri);    
                    int scaleFactor = FindScaleFactor(width, height, stream);    
                    LogHelper.Debug(Tag, string.Format("Scaling bitmap {0} by factor {1} to support {3}x{4} requested dimension.",    
                        uri, scaleFactor, width, height));    
                    return Scale(scaleFactor, stream);    
                }    
            }    
            catch (IOException e)    
            {    
                throw e;    
            }    
        }
Później jednak autor płynnie przechodzi z kodu z async na kod bez async:
public void Fetch(string artUrl, FetchListener listener)   
        {    
           …
            Task.Run(async () => {   
                Bitmap[] bitmaps;    
                try    
                {    
                    Bitmap bitmap = await BitmapHelper.FetchAndRescaleBitmap(artUrl, MaxArtWidth, MaxArtHeight);    
                    Bitmap icon = BitmapHelper.Scale(bitmap, MaxArtWidthIcon, MaxArtHeightIcon);    
                    bitmaps = new[] { bitmap, icon };    
                    cache.Put(artUrl, bitmaps);    
                }    
                catch (IOException)    
                {    
                    return null;    
                }    
                LogHelper.Debug(Tag, "doInBackground: putting bitmap in cache. cache size=" + cache.Size());    
                return bitmaps;    
            }).ContinueWith((antecedent) => {    
                var bitmaps = antecedent.Result;    
                if (bitmaps == null)    
                    listener.OnError(artUrl, new ArgumentException("got null bitmaps"));    
                else    
                    listener.OnFetched(artUrl, bitmaps[BigBitmapIndex], bitmaps[IconBitmapIndex]);    
            }, TaskContinuationOptions.OnlyOnRanToCompletion);    
        }
Ja z kolei odczyt utworów z Music Library w klasie MusicProvider umieściłem w innym wątku:
public void RetrieveMediaAsync(Context context, Action<bool> callback)   
        {    
            LogHelper.Debug(Tag, "retrieveMediaAsync called");    
            if (currentState == State.Initialized)    
            {    
                callback?.Invoke(true);    
                return;    
            }
            Task.Run(() =>   
            {    
                try    
                {    
                    if (currentState == State.NonInitialized)    
                    {    
                        currentState = State.Initializing;
                        var projection = new string[]   
                        {    
                            MediaStore.Audio.Media.InterfaceConsts.Id,    
                            MediaStore.Audio.Media.InterfaceConsts.Artist,    
                            MediaStore.Audio.Media.InterfaceConsts.Title,    
                            MediaStore.Audio.Media.InterfaceConsts.Duration,    
                            MediaStore.Audio.Media.InterfaceConsts.Data,    
                            MediaStore.Audio.Media.InterfaceConsts.MimeType    
                        };
                        var selection = MediaStore.Audio.Media.InterfaceConsts.IsMusic + "!= 0";   
                        var sortOrder = MediaStore.Audio.Media.InterfaceConsts.DateAdded + " DESC";
var cursor = context.ContentResolver.Query(MediaStore.Audio.Media.ExternalContentUri, projection, selection, null, sortOrder);
                        if (cursor != null && cursor.MoveToFirst())   
                        {    
                            do    
                            {    
                                int idColumn = cursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.Id);    
                                int artistColumn = cursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.Artist);    
                                int titleColumn = cursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.Title);    
                                int durationColumn = cursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.Duration);    
                                int filePathIndex = cursor.GetColumnIndexOrThrow(MediaStore.Audio.Media.InterfaceConsts.Data);
var id = cursor.GetLong(idColumn).ToString();
                                var item = new MediaMetadataCompat.Builder()   
                                        .PutString(MediaMetadataCompat.MetadataKeyMediaId, id)    
                                        .PutString(CustomMetadataTrackSource, cursor.GetString(filePathIndex))    
                                        .PutString(MediaMetadataCompat.MetadataKeyArtist, cursor.GetString(artistColumn))    
                                        .PutString(MediaMetadataCompat.MetadataKeyTitle, cursor.GetString(titleColumn))    
                                        .PutLong(MediaMetadataCompat.MetadataKeyDuration, cursor.GetInt(durationColumn))    
                                        .Build();
musicListById.Add(id, new MutableMediaMetadata(id, item));
                            } while (cursor.MoveToNext());   
                        }
                        currentState = State.Initialized;   
                    }    
                }    
                finally    
                {    
                    if (currentState != State.Initialized)    
                    {    
                         currentState = State.NonInitialized;    
                    }    
                }    
            }).ContinueWith((antecedent) => {    
                callback?.Invoke(currentState == State.Initialized);    
            }, TaskContinuationOptions.OnlyOnRanToCompletion);            
        }  
Brakuje według mnie dobrego wykładu, kiedy stosować async w Xamarin, kiedy wybrać Task.Run, a kiedy coś innego… Chyba że jeszcze nie dotarłem do takich materiałów, w każdym razie w podstawowej dokumentacji głównie jest tłumaczone czym w ogóle async w C# jest. Pamiętam, że gdzieś ktoś opisał, jak async został zaimplementowany w Xamarin… Przydałoby się to odszukać i przeprowadzić małe dochodzenie…
#4 Synchronizowane kolekcje
Synchronizowane kolekcje, kolejny stosunkowo mało nagłaśniany temat. W Javie ktoś napisze Collections.synchronizedList(…) i jak to przetłumaczyć na C#? Co prawda mamy w .NET trochę synchronizowanych kolekcji, zwłaszcza od .NET 4.0 pojawiły się dość nowoczesne bardziej wydajne byty, nie bazujące tylko na lock-ach. Ale jeśli potrzebujemy indeksowanego dostępu jak w liście, to akurat tego … nie mamy. Tłumacząć klasę QueueManager użyłem w końcu kodu z klasy SynchronizedList ze źródeł Xamarin.Forms… Ale to jest klasa internal, w dodatku jej implementacja bazuje na lock-ach. Czemu w .NET nie ma takiej gotowej klasy, albo w Xamarin?
#5 Elementy Javy i Java-C#
Brakuje mi jakiejś klasy czy konstrukcji z Javy. Co robię? Tworzę odpowiedniki w C# lub … wywołuję elementy Javy z dodatkowych przestrzeni nazw…. Tłumacząc LightOrganProcessor w pewnym momencie potrzebowałem wywołać System.currentTimeMillis(). Mógłbym to zrobić pisząc Java.Lang.JavaSystem.CurrentTimeMillis(), ale zauważyłem że ktoś w jednym z przykładów dla Xamarin napisał ręcznie w czystym C# odpowiednik:
var beginningOfTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
long systemTimeStartSec = (long)(DateTime.UtcNow - beginningOfTime).TotalMilliseconds;
Nie zauważyłem różnic pomiędzy jednym a drugim sposobem, ale może w C# powinniśmy używać jak najwięcej C# by unikać narzutu komunikacji między dwiema maszynami wirtualnymi…
A teraz rzecz, na której straciłem kilka godzin, zanim odkryłem co nie działało. Serwis Androidowy nie dawał mi znaku życia i żadnych informacji o błędach czemu nie wstawał. Okazało się, że nie tworzyłem w BaseActivity obiektu klasy ComponentName w należyty sposób. Co prawda podawałem nazwę ala Java ale w stringu, może nie był do końca dobry, albo ktoś tego inaczej nie przewidział. W każdym razie wzorowany na jednym samplu poprawny sposób na ComponentName wymagający obiektu class Javy:
mediaBrowser = new MediaBrowserCompat(this,   
                new ComponentName(this, Java.Lang.Class.FromType(typeof(MusicService))), connectionCallback, null);
#6 Fragmenty i własne widoki
O ile w Javie jak jest pakiet, to wszystko w nim jest, o tyle w projekcie w Xamarin jest pewna dowolność. Nazwa pakietu nie musi zgadzać się z .NET-ową przestrzenią nazw w klasach aplikacji (w generowanym przez Xamarin manifeście - jak podejrzałem - są wstawiane dość długie unikalne ciągi znaków przed nazwami klas). W moim przypadku przy wstawianiu fragmentu do XML musiałem zmienić jego nazwę, by pasowała do faktycznego namespace w C# (miałem wyjątek przy otwieraniu widoku aplikacji):
<fragment   
            android:name="LightOrganApp.Droid.UI.PlaybackControlsFragment"    
            android:id="@+id/fragment_playback_controls"    
            android:layout_width="match_parent"    
            android:layout_height="wrap_content"    
            android:layout_alignParentBottom="true"    
            tools:layout="@layout/fragment_playback_controls" />
Podobnie było z własnym widokiem:
<LightOrganApp.Droid.UI.CircleView    
       android:id="@+id/bass_light"    
       android:layout_width="match_parent"    
       android:layout_height="0dp"    
       android:layout_weight="1"    
       app:circleColor="#d50000"/>    
#7 Wyjątki
Zauważyłem po samplach, że można łapać wyjątki systemowe z Javy, ale tam, gdzie da się łapać wyjątek w .NET to taki jest wtedy łapany.
#8 Stylowanie CardView
Potrzebowałem zrobić sobie zielony dolny pasek. U mnie jest to fragment hostowany w CardView. Tło było widoczne z CardView, to więc ostylowałem. Jak? Dodałem definicję stylu w values/styles.xml:
<style name="AppTheme.CardView" parent="Theme.AppCompat">    
       <item name="cardBackgroundColor">#ff1b5e20</item>    
   </style>
Po czym zaaplikowałem ten styl w miejscach użycia CardView:
<android.support.v7.widget.CardView   
           android:id="@+id/controls_container"    
           android:layout_width="match_parent"    
           android:layout_height="wrap_content"    
           android:layout_gravity="bottom"    
           app:cardElevation="8dp"    
           style="@style/AppTheme.CardView">    
           <fragment    
               android:name="LightOrganApp.Droid.UI.PlaybackControlsFragment"    
               android:id="@+id/fragment_playback_controls"    
               android:layout_width="match_parent"    
               android:layout_height="wrap_content"    
               android:layout_alignParentBottom="true"    
               tools:layout="@layout/fragment_playback_controls" />    
       </android.support.v7.widget.CardView>
Uwaga jeszcze do designera plików .axml w Visual Studio 2015. Czemu po każdym załadowaniu widoku do designera plik XML ulega edycji, mimo że nic w nim jeszcze nie zmieniłem?
#9 Zagadka z wizualizacji
Największa zagadka wszech czasów przede mną do rozwiązania. Czemu ten sam z pozoru kod w Javie powoduje ładne błyski świateł w rytm muzy, natomiast w Xamarinie… błyska trochę co najwyżej pierwsze światło, a reszta w zasadzie nie mruga? Porównywałem już parę razy kod oryginału z portowanym i póki co nie wiem. Powstaje pytanie czy to nie jest objaw mniejszej wydajności, chyba że wrapper Xamarina do Visualizer’a jest w jakiś sposób niedopracowany. Trzeba zbadać, czy w obu przypadkach z API dostaję przy tej samej piosence takie same tablice z bajtami dla szybkiej transformaty Fouriera… Ustaliłem że dwa pozostałe światła dostają ciągle współczynnik 1, więc nic dziwnego, że nie mrugają.
#10 Rozmiar i linkowanie
Oryginalna aplikacja w Javie zajmuje na telefonie 15,42 MB. Jej nie w pełni funkcjonalny odpowiednik w Xamarin… 30,97 MB, czyli póki co dwa razy tyle. Linkowanie w Xamarin ma za zadanie wyrzucać nieużywany kod. Domyślna opcja to brak, druga to linkowanie tylko dla SDK, ostatnia - najmniej bezpieczna - linkowanie dla SDK i aplikacji. Po zastosowaniu stosunkowo bezpiecznej opcji drugiej aplikacja na telefonie zajęła… 30,82 MB. Czyli praktycznie niewiele to wniosło. Opcji trzeciej póki co nie próbowałem, bo wymaga pewnych zabezpieczeń w kodzie aplikacji, by nie zostało z niego wycięte za dużo… Przydałby się jakiś dobry przewodnik do tego, informacje w podstawowej dokumentacji wskazują raczej na żmudną pracę z możliwością wpadki.
To tyle na dziś. Punkt #9 wymaga rozwiązania. Jak ktoś to czyta i ma jakiś pomysł względem tej zagadki, zapraszam do dyskusji. Poza tym w najbliższym kroku będę portował resztę kodu z Javy, by dorównać funkcjonalnością oryginałowi.