/* * Copyright (C) 2014 Fastboot Mobile, LLC. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See * the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program; * if not, see <http://www.gnu.org/licenses>. */ package com.fastbootmobile.encore.voice; import android.app.SearchManager; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.Message; import android.os.RemoteException; import android.util.Log; import com.fastbootmobile.encore.app.BuildConfig; import com.fastbootmobile.encore.utils.Utils; import com.fastbootmobile.encore.framework.PlaybackProxy; import com.fastbootmobile.encore.framework.PluginsLookup; import com.fastbootmobile.encore.framework.Suggestor; import com.fastbootmobile.encore.model.Album; import com.fastbootmobile.encore.model.Artist; import com.fastbootmobile.encore.model.Playlist; import com.fastbootmobile.encore.model.SearchResult; import com.fastbootmobile.encore.model.Song; import com.fastbootmobile.encore.providers.IMusicProvider; import com.fastbootmobile.encore.providers.ProviderAggregator; import com.fastbootmobile.encore.providers.ProviderConnection; import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.List; /** * */ public class VoiceActionHelper implements VoiceCommander.ResultListener { private static final String TAG = "VoiceActionHelper"; private static final boolean DEBUG = BuildConfig.DEBUG; private static final int MSG_START_PLAY = 1; private static final int MSG_START_PLAY_LIST = 3; private int mPendingAction; private int mPendingExtra; private String[] mPendingParams; private Handler mHandler; private Message mPendingMessage; private Context mContext; private List<SearchResult> mPreviousSearchResults; private static class VoiceActionHandler extends Handler { private WeakReference<VoiceActionHelper> mParent; public VoiceActionHandler(WeakReference<VoiceActionHelper> parent) { mParent = parent; } @Override public void handleMessage(Message msg) { mParent.get().resetPendingValues(); if (msg.what == MSG_START_PLAY) { if (DEBUG) Log.d(TAG, "Final handled action: START_PLAY"); Song song = (Song) msg.obj; PlaybackProxy.playSong(song); } else if (msg.what == MSG_START_PLAY_LIST) { List<Song> songs = (List<Song>) msg.obj; if (DEBUG) Log.d(TAG, "Final handled action: START_PLAY_LIST (" + songs.size() + " songs)"); PlaybackProxy.clearQueue(); for (Song song : songs) { PlaybackProxy.queueSong(song, false); } PlaybackProxy.playAtIndex(0); } } } public VoiceActionHelper(Context context) { mHandler = new VoiceActionHandler(new WeakReference<>(this)); mContext = context; } private void resetPendingValues() { mPendingMessage = null; mPendingAction = 0; mPendingExtra = 0; mPendingParams = null; } @Override public void onResult(int action, int extra, String[] params) { mPendingAction = action; mPendingExtra = extra; mPendingParams = params; if (DEBUG) Log.d(TAG, "onResult: action=" + action + " extra=" + extra + " params=" + Arrays.toString(params)); switch (action) { case VoiceCommander.ACTION_PLAY_ALBUM: case VoiceCommander.ACTION_PLAY_ARTIST: case VoiceCommander.ACTION_PLAY_PLAYLIST: case VoiceCommander.ACTION_PLAY_TRACK: if (extra == VoiceCommander.EXTRA_SOURCE) { handlePlay(params[0], params[1]); } else { handlePlay(params[0], null); } break; case VoiceCommander.ACTION_PAUSE: handlePause(); break; case VoiceCommander.ACTION_NEXT: if (extra == VoiceCommander.EXTRA_TIME_SECS) { handleNext(0, Integer.parseInt(params[0])); } else if (extra == VoiceCommander.EXTRA_TIME_MINS) { handleNext(Integer.parseInt(params[0]), 0); } else { handleNext(0, 0); } break; case VoiceCommander.ACTION_PREVIOUS: if (extra == VoiceCommander.EXTRA_TIME_SECS) { handlePrevious(0, Integer.parseInt(params[0])); } else if (extra == VoiceCommander.EXTRA_TIME_MINS) { handlePrevious(Integer.parseInt(params[0]), 0); } else { handlePrevious(0, 0); } break; case VoiceCommander.ACTION_JUMP: handleJump(Integer.parseInt(params[0])); break; case VoiceCommander.ACTION_GOOGLE: handleGoogle(mContext, params[0]); break; default: Log.e(TAG, "Unknown result action " + action); break; } } private void handlePlay(String request, String source) { if (source != null) { // The user specified the source: Only search for a result on this provider ProviderConnection conn = PluginsLookup.getDefault().getProviderByName(source); if (conn != null) { IMusicProvider provider = conn.getBinder(); if (provider != null) { try { if (DEBUG) Log.d(TAG, "Started searching '" + request + "' on " + source); provider.startSearch(request); } catch (RemoteException e) { Log.e(TAG, "Cannot start search on " + conn.getProviderName()); } } } else { Log.e(TAG, "Cannot start search: provider " + source + " not found"); } } else { // No source specified: Search globally ProviderAggregator.getDefault().startSearch(request); } } private void handlePause() { PlaybackProxy.pause(); } private void handleNext(int mins, int secs) { if (mins == 0 && secs == 0) { PlaybackProxy.next(); } else if (mins > 0) { PlaybackProxy.seek(PlaybackProxy.getCurrentTrackPosition() + mins * 60 * 1000); } else if (secs > 0) { PlaybackProxy.seek(PlaybackProxy.getCurrentTrackPosition() + secs * 1000); } } private void handlePrevious(int mins, int secs) { if (mins == 0 && secs == 0) { PlaybackProxy.previous(); } else if (mins > 0) { PlaybackProxy.seek(PlaybackProxy.getCurrentTrackPosition() - mins * 60 * 1000); } else if (secs > 0) { PlaybackProxy.seek(PlaybackProxy.getCurrentTrackPosition() - secs * 1000); } } private void handleJump(int index) { PlaybackProxy.playAtIndex(index - 1); } private void handleGoogle(Context context, String query) { Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); intent.putExtra(SearchManager.QUERY, query); context.startActivity(intent); } public void onSearchResult(List<SearchResult> results) { mPreviousSearchResults = results; // Match the result to one or multiple songs. We first try to look for an exact match, // and if we have multiple, we'll prefer the following order: Playlist, Artist, Album // and then Song. // TODO: We don't handle cases where entities might not be loaded ProviderAggregator aggr = ProviderAggregator.getDefault(); Message msg = null; String request = mPendingParams[0]; float bestMatchPercentage = -1; for (SearchResult result : results) { if (mPendingAction == VoiceCommander.ACTION_PLAY_PLAYLIST) { // Playlist List<String> playlists = result.getPlaylistList(); for (String ref : playlists) { Playlist playlist = aggr.retrievePlaylist(ref, result.getIdentifier()); if (playlist == null) continue; if (request.equalsIgnoreCase(playlist.getName())) { if (DEBUG) Log.d(TAG, "Got an exact playlist match"); msg = mHandler.obtainMessage(MSG_START_PLAY_LIST, Utils.refListToSongList(playlist.songsList(), playlist.getProvider())); bestMatchPercentage = 1; break; } else { float percent = Utils.distancePercentage(request, playlist.getName()); if (percent > bestMatchPercentage) { if (DEBUG) Log.d(TAG, "Matched playlist " + playlist.getName() + " (" + percent + ")"); bestMatchPercentage = percent; msg = mHandler.obtainMessage(MSG_START_PLAY_LIST, Utils.refListToSongList(playlist.songsList(), playlist.getProvider())); } } } } else if (mPendingAction == VoiceCommander.ACTION_PLAY_ARTIST) { // Artist List<String> artists = result.getArtistList(); for (String ref : artists) { Artist artist = aggr.retrieveArtist(ref, result.getIdentifier()); if (artist == null) continue; if (request.equalsIgnoreCase(artist.getName())) { if (DEBUG) Log.d(TAG, "Got an exact artist match"); List<Song> radio = Suggestor.getInstance().buildArtistRadio(artist); bestMatchPercentage = 1; // Radio songs might be null or empty if artist albums aren't loaded, we wait // for artist update in callback then. if (radio != null && radio.size() > 0) { msg = mHandler.obtainMessage(MSG_START_PLAY_LIST, radio); break; } else { Log.w(TAG, "Matched exact artist, but artist radio unavailable"); // Ensure album contents are fetched ProviderConnection pc = PluginsLookup.getDefault() .getProvider(artist.getProvider()); if (pc != null) { IMusicProvider binder = pc.getBinder(); try { if (binder != null) { List<String> albums = artist.getAlbums(); for (String albumRef : albums) { binder.fetchAlbumTracks(albumRef); } } } catch (RemoteException e) { // ignore } } } } else if (artist.getName() != null) { float percent = Utils.distancePercentage(request, artist.getName()); if (percent > bestMatchPercentage) { List<Song> radio = Suggestor.getInstance().buildArtistRadio(artist); bestMatchPercentage = percent; if (DEBUG) Log.d(TAG, "Matched artist " + artist.getName() + " (" + percent + ")"); // Radio songs might be null or empty if artist albums aren't loaded, we wait // for artist update in callback then. if (radio != null && radio.size() > 0) { msg = mHandler.obtainMessage(MSG_START_PLAY_LIST, radio); } else { Log.w(TAG, "Matched average artist, but artist radio unavailable"); // Ensure album contents are fetched ProviderConnection pc = PluginsLookup.getDefault() .getProvider(artist.getProvider()); if (pc != null) { IMusicProvider binder = pc.getBinder(); try { if (binder != null) { List<String> albums = artist.getAlbums(); for (String albumRef : albums) { binder.fetchAlbumTracks(albumRef); } } } catch (RemoteException e) { // ignore } } } } } } } else if (mPendingAction == VoiceCommander.ACTION_PLAY_ALBUM) { // Album List<String> albums = result.getAlbumsList(); for (String ref : albums) { Album album = aggr.retrieveAlbum(ref, result.getIdentifier()); if (album == null) continue; if (request.equalsIgnoreCase(album.getName())) { if (DEBUG) Log.d(TAG, "Got an exact album match"); msg = mHandler.obtainMessage(MSG_START_PLAY_LIST, Utils.refIteratorToSongList(album.songs(), album.getProvider())); bestMatchPercentage = 1; break; } else { float percent = Utils.distancePercentage(request, album.getName()); if (percent > bestMatchPercentage) { if (DEBUG) Log.d(TAG, "Matched album " + album.getName() + " (" + percent + ")"); bestMatchPercentage = percent; msg = mHandler.obtainMessage(MSG_START_PLAY_LIST, Utils.refIteratorToSongList(album.songs(), album.getProvider())); } } } } else if (mPendingAction == VoiceCommander.ACTION_PLAY_TRACK) { // Song List<String> songs = result.getSongsList(); for (String ref : songs) { Song song = aggr.retrieveSong(ref, result.getIdentifier()); if (song == null) continue; if (request.equalsIgnoreCase(song.getTitle())) { if (DEBUG) Log.d(TAG, "Got an exact song title match"); msg = mHandler.obtainMessage(MSG_START_PLAY, song); bestMatchPercentage = 1; break; } else { float percent = Utils.distancePercentage(request, song.getTitle()); if (percent > bestMatchPercentage) { if (DEBUG) Log.d(TAG, "Matched song " + song.getTitle() + " (" + percent + ")"); bestMatchPercentage = percent; msg = mHandler.obtainMessage(MSG_START_PLAY, song); } } } } } // If we found a match, go ahead. If we requested a source, post the message // immediately. If no exact source was specified, post it 1 second later to allow // eventual updates. If we have no exact match, either wait for results in update, or // don't do anything. if (msg != null) { if (mPendingExtra == VoiceCommander.EXTRA_SOURCE) { // We're only expecting one source, so run immediately msg.sendToTarget(); } else { // Other sources might replay, wait a second and kick the message if (mPendingMessage != null) { mHandler.removeMessages(mPendingMessage.what, mPendingMessage.obj); } mPendingMessage = msg; mHandler.sendMessageDelayed(msg, 1000); } } } public void onArtistUpdate(List<Artist> artists) { if (mPendingAction == VoiceCommander.ACTION_PLAY_ARTIST) { if (mPreviousSearchResults != null) { if (DEBUG) Log.d(TAG, "Got updated artist information"); onSearchResult(mPreviousSearchResults); } else { if (DEBUG) Log.d(TAG, "Got artist update but no pending search results"); } } } public void onAlbumUpdate(List<Album> albums) { if (mPendingAction == VoiceCommander.ACTION_PLAY_ALBUM) { if (mPreviousSearchResults != null) { if (DEBUG) Log.d(TAG, "Got updated album information"); onSearchResult(mPreviousSearchResults); } else { if (DEBUG) Log.d(TAG, "Got album update but no pending search results"); } } } public void onSongUpdate(List<Song> songs) { if (mPendingAction == VoiceCommander.ACTION_PLAY_TRACK) { if (mPreviousSearchResults != null) { if (DEBUG) Log.d(TAG, "Got updated song information"); onSearchResult(mPreviousSearchResults); } else { if (DEBUG) Log.d(TAG, "Got song update but no pending search results"); } } } }