Saturday, October 14, 2017

3.0 Work in Progress 9: Shuffle and Notifications

The last update I showed off the new queue ui which did involve a complete rewrite of the underlying queue backend code.  Due to that and switching to the new data model (Room-based), I ended up taking the plunge and rewriting the MusicService.  It seemed like a waste to try to hack it up to use all the new stuff I added when I eventually planned on rewriting it anyway.  Due to that I dont have too much to show UI wise, but I did make some queue improvements and redid how shuffle works.

One of the largest complaints I've gotten over the years was the inability to skip back/forward while shuffled and get a consistent track order.  Once you skipped back, the next track would be chosen at random.  My response was always that I understood the frustration but without rewriting shuffle / music service entirely there was not much i could do.  Since I am rewriting the music service and queue, this seemed like the perfect opportunity to fix this issue.

In the video below, it shows off the new shuffle queue.  It essentially works like the "randomize" option in the queue.  Turning it on will put your current playing song at position 1 and shuffle the order of the rest.  Pressing next track will simply just go to the next track in the queue.  This allows you to go back and forth and maintain the same order.  This also greatly simplifies the code on the backend.  Next track doesnt care if its in shuffle or not.. it just goes to the next track listed in the queue.  When shuffle is turned off it will return to the previous order.  Any new tracks added while in shuffle will just be placed at the end when shuffle is turned off.  Note: For this video and testing i added a shuffle button to the min player at the bottom.  This was just so i could keep the queue up and change the mode back and forth.



Notifications were managed by the MusicService, so I gave them a rewrite as well.  Google has added a lot API since I originally created these notifications, so this time around I will just leverage what they provide instead of doing a custom view for it.  These newer notification apis will also allow album art to play on your lockscreen on devices that it didnt work before (most phones would already show the album art on the lockscreen but a select few would not). 

Plans for the notifications will be 5 customizable button slots (most likely just the common actions like next / prev / play/pause / exit / toggle shuffle) and 2 (maybe 3?) metadata lines that are customizable like the other metadata views I've shown in previous updates.  The images below just show off the standard next play/pause and prev buttons.

Lineage OS 7.1.2
Lineage OS 7.1.2
Galaxy S5 running 6.0.1

Friday, September 22, 2017

3.0 Work in Progress 8: Queue

Its been awhile since I've posted a new update but I spent a few weeks working on 2.2.4/2.2.5 and now I am back working on 3.0.  This next update focuses on the queue.  Its not finished but it is working enough that I can show it off.

The queue involved a bunch more work than the previous views since I had to rewrite the actual backing queue data structure.  Previously the Queue class would retain its own copy of the tracks and it was in charge of persisting all changes into the database.  This time around I made it much simpler by just keeping everything straight in the database, so the queue and db no longer have to stay in sync.

The queue features drag and drop like the previous version of gmmp, however it is done a bit differently (at least for now pending feedback).  Instead of having the "handle" on the left on each list item that you press to start dragging, dragging is now started via a long press.  The reasoning behind the behavior.. honestly.. is the drag and drop helper class that google provides works this way https://developer.android.com/reference/android/support/v7/widget/helper/ItemTouchHelper.html .  I didnt see any reason to reinvent the wheel.  Also deleting can be done via swiping left.


Due to the swipe left to delete, I have chosen to not make the Queue available to be placed as one of the library tabs.  However, the queue will be accessible straight from now playing via the FAB (this is shown in the video), and via the side navigation drawer.  I originally had the queue in as a tab and it was too unreliable trying to delete.  Half the time it would just scroll over to the next tab.

Just like all the other views, the metadata displayed in the queue will be customizable.  I also improved the metadata views quite a bit by adding the ability to define how much space each "slot/alignment" takes up (see WIP 1 for details).   This allows for something like the image above.  The song title and artist will take up 80% of the view and the track duration only takes up 20%.  Below is the code that represents the queue list item:

 var metadataLinesModel = MetadataLinesModel(GMThemeEngine.getInstance(context).currentTheme).apply {  
       addMetadataLine(arrayOf("<weight=0.8><align=left><typeface=sans-serif><size=16>%ps%. %tr%", "<weight=0.2><align=right><typeface=sans-serif><size=16>%du%" ))  
       addMetadataLine("<color=2><align=left><typeface=sans-serif><size=14>%ar%")  
     }  


Here is the video of the queue in action


Wednesday, August 30, 2017

2.2.4

I was finally able to work out some of the sdcard write issues.  I could only reproduce issues when creating playlists, so hopefully that was the only problem.  I also tested with android O (the emulator) and didnt experience any issues, so GMMP should be good to go for 8.0

2.2.4 (08/29/2017):
-Updated google services to 11.0.4
-Updated support libraries to 26.0.0
-Updated compile sdk to Android O
-Fixed bug where keyboard would not show when using a gesture to jump to search
-Play next/prev album now orders by album name instead of by artist (use next/prev album by artist for other behavior)
-Reset anything set to next/prev album to next/prev album by artist
-Notification will now properly update state when queue completes with on queue completion set to default or stop
-Audio focus is now only abandoned when the music service is destroyed
-Fixed audioengine startup race condition deadlock
-Fixed creating new files on sdcard (playlists / downloaded album art)
-Fixed issue where some m4a files would play at the incorrect sample rate

2.2.3 (05/02/2017):
-Update google services to 10.2.4
-Fixed some audioengine crashes
-Fixed other various crashes and bugs

Monday, August 14, 2017

3.0 Work in Progress 7: Fast Scroll & Gestures

The last 2 weeks I added in fast scroll (scroll through long lists fast by dragging down the bar on the right with the letter / number indicator to show where you are in the list) and gestures on now playing and the mini player.

The first up is fast scroll.  This rendition of fast scroll SHOULD be a bit more consistent than the old one in 2.0.  There was a random bug that was very difficult to reproduce that would cause the letters to not match what was showing on the screen.  The fast scroll code is much simpler this time around so i dont expect that bug to be an issue.  The video is a big laggy and the fast scroll in general is not as smooth as i'd like it to be, but that will be worked on once 3.0 gets to beta.



The second feature is the addition of gestures.  All the previous gestures have returned (swipe left right up down / tap / double tap / long press) plus I added in a few more.  New gestures are 2 finger tap/double tap/long press, and a 2 finger pan.  The pan is considered a "continuous" gesture, so there will only be a few actions that you can bind to it.  The video shows it bound to adjusting the volume, but the other options would be adjusting tempo or seeking the track.  If you decide to not use the pan, you will be able to use 2 finger swipes.  Right now GMMP is using the stock version of the gesture library and it works fairly decent for the most part.   If users find some gestures hard to use I will try to tweak the library to improve that.



My next 3.0 post on here will most likely not be for another 3-4 weeks since I have a vacation coming up and I plan on doing a 2.x patch for android O plus some other bug reports i've gotten over the past few months.

Saturday, July 29, 2017

3.0 Work in Progress 6: Mini Player

This update mainly focuses on the addition of the mini player to the bottom of the library view (and will also be on other views where it makes sense) but I did also make some improvements in the play / pause animation.



Above shows the new mini player.  Just like every other view so far, the metadata portion will be customizable and you will be able to choose how many buttons are shown.   Right now you can choose between prev track, play/pause, and next track.  I may open it up to some of the other media actions like shuffle / repeat but i havent decided on that yet.

In the WIP4 post I had shown off a little chunk of the code that added a bunch of "behaviors" to a view.  The behaviors contain all the common code for a particular behavior of a UI view.  This allows me to rewrite the same (or very similar) chunks of code for different UIs.  GMMP 1/2.x was loaded with repeated code despite my best efforts to refactor.  Anyway, since the mini player shares a lot of the same abilities as now playing, I created 2 new behaviors: PlayingInfoBehavior and MediaButtonBehavior.

So here are the behaviors for the Library view:


view?.let {
            addBehavior(LifecycleBehavior::class, StatusBarBehavior(context, it))
            addBehavior(LifecycleBehavior::class, ToolbarBehavior(this@LibraryPagerPresenter, it))
            addBehavior(LifecycleBehavior::class, PlayingInfoBehavior(context, this@LibraryPagerPresenter, it, state))
            addBehavior(LifecycleBehavior::class, MediaButtonBehavior(context, this@LibraryPagerPresenter, it, state))
            addBehavior(FragmentContainerBehavior::class, FragmentContainerBehavior(it, state))
            addBehavior(MenuBehavior::class, HasMenuBehavior())
        }


I now set up the state of the library view for the above screenshot:


state.metadataLinesModel = MetadataLinesModel(theme).apply {
            addMetadataLine("<align=left><typeface=sans-serif><size=16>%tr%")
            addMetadataLine("<align=left><typeface=sans-serif><size=12><color=secondary>%ar%")
        }

state.buttonDefinitions = mapOf(R.id.miniPlayerPlayPause to
                                                MediaButtonDefinition(-1, R.drawable.ic_gm_pause_to_play, 0, MusicControlEvent.PLAY_PAUSE, theme.iconColorNormal),
                                        R.id.miniPlayerButton1 to
                                                MediaButtonDefinition(0, R.drawable.ic_gm_skip_previous, 0, MusicControlEvent.PREVIOUS_TRACK, theme.iconColorNormal, View.GONE),
                                        R.id.miniPlayerButton3 to
                                                MediaButtonDefinition(1, R.drawable.ic_gm_skip_next, 0, MusicControlEvent.NEXT_TRACK, theme.iconColorNormal, View.GONE))

The first part defines how the metadata is going to display and the second part defines the media buttons.  Tells it what icon to use, the size, the action that will happen on press, the color, and finally the visibility.  This is all "hardcoded" right now but will eventually hooked up to the preferences making it really easy to customize.


Small change in the code here to get the 3 buttons:


state.metadataLinesModel = MetadataLinesModel(theme).apply {
            addMetadataLine("<align=left><typeface=sans-serif><size=16>%tr%")
            addMetadataLine("<align=left><typeface=sans-serif><size=12><color=secondary>%ar%")
        }

state.buttonDefinitions = mapOf(R.id.miniPlayerPlayPause to
                                                MediaButtonDefinition(-1, R.drawable.ic_gm_pause_to_play, 0, MusicControlEvent.PLAY_PAUSE, theme.iconColorNormal, View.VISIBLE),
                                        R.id.miniPlayerButton1 to
                                                MediaButtonDefinition(0, R.drawable.ic_gm_skip_previous, 0, MusicControlEvent.PREVIOUS_TRACK, theme.iconColorNormal, View.VISIBLE),
                                        R.id.miniPlayerButton3 to
                                                MediaButtonDefinition(1, R.drawable.ic_gm_skip_next, 0, MusicControlEvent.NEXT_TRACK, theme.iconColorNormal, View.VISIBLE))


Now playing is now configured the same way:


override fun onViewAttached()
    {
        super.onViewAttached()

        view?.let {
            addBehavior(LifecycleBehavior::class, ToolbarBehavior(this@NowPlayingPresenter, it, false))
            addBehavior(LifecycleBehavior::class, PlayingInfoBehavior(context, this@NowPlayingPresenter, it, state))
            addBehavior(LifecycleBehavior::class, MediaButtonBehavior(context, this@NowPlayingPresenter, it, state))
        }
    }

    private fun createState()
    {
        //Read state
        val theme = GMThemeEngine.getInstance(context).currentTheme
        state.metadataLinesModel = MetadataLinesModel(theme).apply {
            addMetadataLine("<align=center><typeface=sans-serif-medium><size=24>%tr%")
            addMetadataLine("<align=center><typeface=sans-serif-medium><size=20>%ar%")
            addMetadataLine("<align=center><typeface=sans-serif-medium><size=20>%al%")
        }

        state.buttonDefinitions = mapOf(R.id.npPlayPause to MediaButtonDefinition(-1, R.drawable.ic_gm_pause_to_play_circle_outline, 0, MusicControlEvent.PLAY_PAUSE, theme.accentColor),
                                        R.id.npMediaBtn1 to MediaButtonDefinition(0, R.drawable.ic_gm_repeat, 0, MusicControlEvent.TOGGLE_REPEAT, theme.iconColorNormal),
                                        R.id.npMediaBtn2 to MediaButtonDefinition(1, R.drawable.ic_gm_skip_previous, 1, MusicControlEvent.PREVIOUS_TRACK, theme.iconColorNormal),
                                        R.id.npMediaBtn3 to MediaButtonDefinition(2, R.drawable.ic_gm_skip_next, 1, MusicControlEvent.NEXT_TRACK, theme.iconColorNormal),
                                        R.id.npMediaBtn4 to MediaButtonDefinition(3, R.drawable.ic_gm_shuffle, 0, MusicControlEvent.TOGGLE_SHUFFLE, theme.iconColorNormal))

    }

Here is a screenshot of the play pause in now playing using the icon from the mini player.  I plan on also adding the other play icon inside the filled circle (in addition to the current play with the circle outline.


Finally here is a video of everything in action.  The animations / transitions didnt seem to record as smooth as it appears on the device, so just keep that in mind.  I do most of my testing on a galaxy s3 running 7.1.2 and an original moto x running 5.1, so if things look smooth on those old devices they should be great on the newer ones.  This video also shows a bug with the 3 dot menu getting shifted to the left a bit when coming back to the library view.  I havent really looked into that much but it'll get fixed.. no worries.


I also want to note that when 3.0 is finally released I do think I will add 2 other options for how now playing looks.  One being the colored toolbar on top, with the album art underneath it taking up all the space between the toolbar and the metadata.  The other being smaller album art centered around a blurred background generated from the album art.  Some other players use that look and i really like it.  Plus it will be a compromise for current users of the holo themes with the blurred background

Sunday, July 16, 2017

3.0 Work in Progress 5: Library view

In this update I added a library view similar to the one in the old UI.  I also added in an artist list, genre list, and track list tabs.


The big difference with this one is the toolbar will collapse when you scroll down and if you scroll up it will become visible again


Here is a video of it in action


The plan with the library view is to allow the user to choose which views to add to the library (others will end up in the side navigation view if they are not in the library), the title of those views, and their order.  Below is the chunk of code responsible for building the library, its "hardcoded" currently but will be fairly simple to hook up to the preferences.

v.setupAdapter(listOf(BaseFragmentEntry("Artists", ArtistListFragment::class)
                                  , BaseFragmentEntry("Albums", AlbumListFragment::class)
                                  , BaseFragmentEntry("Genres", GenreListFragment::class)
                                  , BaseFragmentEntry("Songs", TrackListFragment::class)))

I am sure many of you are anxious to try out the new UI, but it is still a ways off.  Some of the views are not too straight forward to code like the equalizer and the browser / folder view, so I expect they will take some time to code.  My plan is to have a working alpha by this fall.  The alpha however will not really contain much customization.  Its really easy to add customization to the code, however actually creating UIs to configure everything is going to take a long time.  Because of this, the alpha will be through a separate group in google+ that users will have to join.  I didnt want to push something so incomplete to the beta channel

Thursday, June 29, 2017

3.0 Work in Progress 4: MultiSelect, Overflow Menu, and Sort

For those who havent noticed, I am trying to do post progess updates every 2 weeks.  This iteration doesnt have too much to actually show off, however a lot of work was done under the hood.  Everything has been wired for easily adding multi select (and its action menu), the overflow menu, and sort.   By "wired" I mean that the majority of the code was written and for each new view I only have to write a few lines of code to add the capabilities and then write the code to handle the actual selected actions.

Example:

This is in the album list view's presenter code (couldnt figure out a way to post code so the formatting is a bit weird).  As you can see its not much code at all to add a lot of functionality

override fun onViewAttached()
{
    super.onViewAttached()

    view?.let {        addBehavior(MenuBehavior::class
, ViewModeMenuBehavior(R.menu.menu_gm_shared_view_mode, state))
        addBehavior(MenuBehavior::class
, GridSizeMenuBehavior(R.menu.menu_gm_shared_gridsize, state))
        addBehavior(MenuBehavior::class
, SortMenuBehavior(R.menu.menu_gm_sort_album_list, state))
        addBehavior(SelectionBehavior::class
, AlbumSelectionBehavior())
        addBehavior(ActionModeBehavior::class
, AlbumActionModeBehavior(it))
    }}



Anyway here are some screenshots