Xamarin.Forms kontra Xamarin, witam serdecznie. Przed nami kolejny odcinek z luksusowej różowej edycji. Dziś będzie dość zwięźle i krótko. Generalnie odpowiemy na dwa pytania. Pierwsze: jak pobrać listę utworów muzycznych, by dało się ją wyświetlić na liście w Xamarin.Forms? I drugie: jak uporać się z uprawnieniami, do których nie ma uniwersalnej infrastruktury? No to zaczynamy!
Na obecny moment app-ka z LightOrgan/Xamarin.Forms ładuje listę kawałków muzycznych na Android i iOS:
Ktoś powie, że to może niewielki przyrost od ostatniego razu, ale ostatnio miałem nieco mniej czasu, a poza tym obsługa uprawnień w Xamarin.Forms wymagała chwilki zastanowienia (czemu nie ma jakiegoś uniwersalnego API?). Koniec końców natrafiłem na świetny post Simplified iOS & Android Runtime Permissions with Plugins!. Przedstawione tam rozwiązanie zostało zamknięte w pakiecie nuget Plugin.Permissions ze źródłami na https://github.com/jamesmontemagno/PermissionsPlugin, do których dopisałem sobie obsługę Media Library od iOS 9.3. Do enum-a Permission dodałem Media i obsłużyłem to jedynie w iOS wzorując się na obsłudze uprawnienia Photos. W klasie PermissionImplementation sprowadziło się to głównie do dodania takich elementów:
PermissionStatus MediaPermissionStatus
{
get
{
if (UIDevice.CurrentDevice.CheckSystemVersion(9, 3))
{
var status = MPMediaLibrary.AuthorizationStatus;
switch (status)
{
case MPMediaLibraryAuthorizationStatus.Authorized:
return PermissionStatus.Granted;
case MPMediaLibraryAuthorizationStatus.Denied:
return PermissionStatus.Denied;
case MPMediaLibraryAuthorizationStatus.Restricted:
return PermissionStatus.Restricted;
default:
return PermissionStatus.Unknown;
}
}
else
{
return PermissionStatus.Granted;
}
}
}
Task<PermissionStatus> RequestMediaPermission()
{
if (UIDevice.CurrentDevice.CheckSystemVersion(9, 3))
{
if (MediaPermissionStatus != PermissionStatus.Unknown)
return Task.FromResult(MediaPermissionStatus);
var tcs = new TaskCompletionSource<PermissionStatus>();
MPMediaLibrary.RequestAuthorization(status =>
{
switch (status)
{
case MPMediaLibraryAuthorizationStatus.Authorized:
tcs.SetResult(PermissionStatus.Granted);
break;
case MPMediaLibraryAuthorizationStatus.Denied:
tcs.SetResult(PermissionStatus.Denied);
break;
case MPMediaLibraryAuthorizationStatus.Restricted:
tcs.SetResult(PermissionStatus.Restricted);
break;
default:
tcs.SetResult(PermissionStatus.Unknown);
break;
}
});
return tcs.Task;
}
else
{
return Task.FromResult(PermissionStatus.Granted);
}
}
Uprawnienia sprawdzam w aplikacji Xamarin.Forms podczas jej uruchamiania i wznawiania. W klasie App wygląda to tak:
protected override async void OnStart()
{
await CheckPermissions();
}
…
protected override async void OnResume()
{
await CheckPermissions();
}
private async Task CheckPermissions()
{
try
{
var permissionsList = new List<Permission>();
if (Device.OS == TargetPlatform.Android)
{
var status = await CrossPermissions.Current.CheckPermissionStatusAsync(Permission.Storage);
if (status != PermissionStatus.Granted)
permissionsList.Add(Permission.Storage);
status = await CrossPermissions.Current.CheckPermissionStatusAsync(Permission.Microphone);
if (status != PermissionStatus.Granted)
permissionsList.Add(Permission.Microphone);
}
else if (Device.OS == TargetPlatform.iOS)
{
var status = await CrossPermissions.Current.CheckPermissionStatusAsync(Permission.Media);
if (status != PermissionStatus.Granted)
permissionsList.Add(Permission.Media);
}
if (permissionsList.Count > 0)
{
var results = await CrossPermissions.Current.RequestPermissionsAsync(permissionsList.ToArray());
if (results.Any(p => p.Value != PermissionStatus.Granted))
await MainPage.DisplayAlert(AppResources.Permissions, AppResources.NotAllPermissionsMsg, "OK");
}
}
catch (Exception ex)
{
}
}
Dodatkowo uprawnienia sprawdzam też na stronie FileListPage. Mankamentem stron w Xamarin.Forms jest fakt, że nie mają zdarzenia czy metody wywoływanej tuż po załadowaniu bądź wyświetleniu strony. Skorzystałem z metody OnAppearing, która - jak nazwa wskazuje - wywołuje się tuż przed wyświetleniem strony:
protected override async void OnAppearing()
{
base.OnAppearing();
var hasPermission = await CheckPermission();
if (hasPermission)
{
allMediaItems = await DependencyService.Get<IMusicService>().GetItemsAsync();
}
else
{
allMediaItems = new List<MediaItem>();
await DisplayAlert(AppResources.Permissions, string.Format(AppResources.PermissionDeniedMsg, GetPermissionName()), "OK");
}
SearchFiles();
}
private async Task<bool> CheckPermission()
{
var status = PermissionStatus.Denied;
if (Device.OS == TargetPlatform.Android)
status = await CrossPermissions.Current.CheckPermissionStatusAsync(Permission.Storage);
else if (Device.OS == TargetPlatform.iOS)
status = await CrossPermissions.Current.CheckPermissionStatusAsync(Permission.Media);
return status == PermissionStatus.Granted;
}
private string GetPermissionName()
{
if (Device.OS == TargetPlatform.Android)
return "READ_EXTERNAL_STORAGE";
else if (Device.OS == TargetPlatform.iOS)
return "Media library access";
return string.Empty;
}
Widzimy tutaj wywołanie tzw. dependency service, czyli serwisu w Xamarin.Forms, który ma różną implementację na każdej z platform (u mnie Android i iOS). Jego interfejs składa się póki co z jednej metody:
public interface IMusicService
{
Task<List<MediaItem>> GetItemsAsync();
}
Implementacja na Android przedstawia się następująco:
[assembly: Xamarin.Forms.Dependency(typeof(AndroidMusicService))]
namespace LightOrganApp.Droid
{
public class AndroidMusicService : IMusicService
{
public async Task<List<MediaItem>> GetItemsAsync()
{
return await Task.Run(() =>
{
var items = new List<MediaItem>();
try
{
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 = Forms.Context.ContentResolver.Query(MediaStore.Audio.Media.ExternalContentUri, projection, selection, null, sortOrder);
if (cursor != null && cursor.MoveToFirst())
{
do
{
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);
var item = new MediaItem(cursor.GetString(titleColumn), cursor.GetString(artistColumn), DateUtils.FormatElapsedTime(cursor.GetInt(durationColumn) / 1000));
items.Add(item);
} while (cursor.MoveToNext());
}
}
catch
{
}
return items;
});
}
}
}
a na iOS tak:
[assembly: Xamarin.Forms.Dependency(typeof(iOSMusicService))]
namespace LightOrganApp.iOS
{
public class iOSMusicService : IMusicService
{
public async Task<List<MediaItem>> GetItemsAsync()
{
return await Task.Run(() =>
{
var query = new MPMediaQuery();
var mediaTypeNumber = NSNumber.FromInt32((int)MPMediaType.Music);
var predicate = MPMediaPropertyPredicate.PredicateWithValue(mediaTypeNumber, MPMediaItem.MediaTypeProperty);
query.AddFilterPredicate(predicate);
var unknownArtist = NSBundle.MainBundle.LocalizedString("unknownArtist", "Unknown Artist");
return query.Items.Select(item => new MediaItem(item.Title, (item.Artist != null) ? item.Artist : unknownArtist, GetDisplayTime((int)item.PlaybackDuration))).ToList();
});
}
private string GetDisplayTime(int seconds)
{
var h = seconds / 3600;
var m = seconds / 60 - h * 60;
var s = seconds - h * 3600 - m * 60;
var str = "";
if (h > 0)
str += $"{h}:";
str += $"{m:00}:{s:00}";
return str;
}
}
}
Podsumowując udało się nam współdzielić maksymalnie wszystko, co się dało w aplikacji Xamarin.Forms, rozdzielając jedynie szczegóły implementacyjne tam, gdzie było to niezbędne. To oczywiście dopiero wstęp do podpinania kodu opartego na natywnym API. W kolejnym kroku pasuje podpiąć odtwarzanie muzyki wraz z odpowiednimi notyfikacjami, a także z jawnym serwisem w tle w przypadku Android ( iOS - jak dotąd w moich projektach - używa systemowego odtwarzacza, który robi to w sposób niejawny). Ewentualnie może od razu podepnie się analizę FFT opracowaną jak dotąd w Android do sterowania światłami. Co z tego wyjdzie? O tym w następnym odcinku. Stay tuned!