/** * Copyright (c) 2013 Nokia Corporation. All rights reserved. Nokia and Nokia * Connecting People are registered trademarks of Nokia Corporation. Oracle and * Java are trademarks or registered trademarks of Oracle and/or its affiliates. * Other product and company names mentioned herein may be trademarks or trade * names of their respective owners. See LICENSE.TXT for license information. */ package com.nokia.example.musicexplorer.ui; import java.util.Timer; import java.util.TimerTask; import java.util.Vector; import javax.microedition.lcdui.Command; import javax.microedition.lcdui.CommandListener; import javax.microedition.lcdui.Displayable; import javax.microedition.lcdui.Form; import javax.microedition.lcdui.Item; import javax.microedition.lcdui.ItemCommandListener; import javax.microedition.lcdui.TextField; import javax.microedition.lcdui.ItemStateListener; import org.json.me.JSONObject; import org.json.me.JSONArray; import org.json.me.JSONException; import org.tantalum.Task; import org.tantalum.util.L; import com.nokia.example.musicexplorer.data.QueryPager; import com.nokia.example.musicexplorer.data.ApiCache; import com.nokia.example.musicexplorer.data.model.GenericProductModel; import com.nokia.example.musicexplorer.data.model.AlbumModel; import com.nokia.example.musicexplorer.data.model.ArtistModel; import com.nokia.example.musicexplorer.data.model.CategoryModel; import com.nokia.example.musicexplorer.data.model.TrackModel; /** * Search view. The search textfield waits an amount of time before making an * API call. */ public class SearchView extends Form implements CommandListener, ItemStateListener, ItemCommandListener, InitializableView { public static final String VIEW_TITLE = "Search artists"; public static final String PATH_TO_ICON = "/search_icon.png"; private static final int MAX_QUERY_LENGTH_CHARS = 100; private static final int QUERY_THROTTLE_MILLISECONDS = 1500; private static final int SEARCH_QUERY_MIN_LENGTH = 1; private static final int ITEMS_PER_PAGE = 20; private final ViewManager viewManager; private final Command backCommand; private Vector viewModel; private QueryPager queryPager; private TextField searchField; private String searchQuery; private Timer throttle; private LoadMoreButton loadMoreButton; /** * Constructor which sets the view title, adds a back command to it and adds * the dummy text content to it. * * @param viewTitle Title shown in the title bar of this view * @param viewManager View manager which will handle view switching */ public SearchView(ViewManager viewManager) { super(VIEW_TITLE); this.viewManager = viewManager; this.backCommand = new Command("Back", Command.BACK, 1); this.searchField = new TextField(null, null, MAX_QUERY_LENGTH_CHARS, TextField.ANY); // Initialize the query pager. this.queryPager = new QueryPager(); this.queryPager.setItemsPerPage(ITEMS_PER_PAGE); this.loadMoreButton = new LoadMoreButton(this); append(this.searchField); addCommand(backCommand); setCommandListener(this); setItemStateListener(this); } /** * @see com.nokia.example.musicexplorer.ui.InitializableView#initialize() */ public void initialize() { } /** * @see javax.microedition.lcdui.CommandListener#commandAction( * javax.microedition.lcdui.Command, javax.microedition.lcdui.Displayable) */ public void commandAction(Command command, Displayable displayable) { if (backCommand.equals(command)) { // Hardware back button was pressed viewManager.goBack(); } } /** * @see javax.microedition.lcdui.ItemCommandListener#commandAction( * javax.microedition.lcdui.Command, javax.microedition.lcdui.Item) */ public void commandAction(Command c, Item item) { if (c == loadMoreButton.getCommand()) { // Load more triggered performSearch(false, true); // Don't clear results. Load a new page. } } /** * Whenever the search query is changed, the state change of the textfield * is registered here. * * @see javax.microedition.ItemStateListener#itemStateChanged(Item) */ public void itemStateChanged(Item item) { if (item instanceof TextField) { handleSearchField((TextField) item); } } /** * Handles the search TextField actions. * * @param searchField */ private void handleSearchField(TextField searchField) { searchQuery = searchField.getString(); if (searchQuery.length() > SEARCH_QUERY_MIN_LENGTH) { throttleSearch(); } } /** * Delays the search and cancels a possible pending search. */ private void throttleSearch() { if (throttle != null) { // Cancel previous timer task. throttle.cancel(); } throttle = new Timer(); throttle.schedule(new TimerTask() { public void run() { performSearch(true, false); // Clear previous results. No paging. cancel(); } }, QUERY_THROTTLE_MILLISECONDS); } /** * Performs a search API call. The method can as well handle clearing any * previous search results (in the case of new query) and query the next * page of a paged query. * * @param clearResults * @param nextPage */ private void performSearch(boolean clearResults, boolean nextPage) { String pagingQueryString; if (nextPage) { pagingQueryString = queryPager.getQueryStringForNextPage(); } else { queryPager.reset(); pagingQueryString = queryPager.getCurrentQueryString(); } if (clearResults) { clearSearchResults(); } ApiCache.search( this.searchQuery, new SearchResultHandlerTask(), pagingQueryString); } private int getCategoryEnum(String category) { category = category.toLowerCase(); if (category.indexOf("track") > -1) { return CategoryModel.TRACK; } else if (category.indexOf("artist") > -1) { return CategoryModel.ARTIST; } else if (category.indexOf("album") > -1) { return CategoryModel.ALBUM; } else if (category.indexOf("single") > -1) { return CategoryModel.SINGLE; } return 0; } /** * Parses JSON response to ListItem objects that can be appended to view. * * @param results */ private void parseResultsToViewModel(JSONArray results) { if (viewModel != null) { viewModel.removeAllElements(); } else { viewModel = new Vector(); } /* * Response parsing * * Get "items" JSONArray and iterate over the JSONObjects. 1. Check * objects "category" JSONObject 2. Decide which model to instantiate * based on the category 3. Save models (search results) to viewModel * vector. */ int loopMax = results.length(); if(loopMax > 0) { for (int i = 0; i < loopMax; i++) { JSONObject obj; GenericProductModel model = null; try { obj = (JSONObject) results.get(i); String category = obj.getJSONObject("category").getString("id"); switch (getCategoryEnum(category.toLowerCase())) { case CategoryModel.SINGLE: model = new AlbumModel(obj); break; case CategoryModel.ALBUM: model = new AlbumModel(obj); break; case CategoryModel.ARTIST: model = new ArtistModel(obj); break; case CategoryModel.TRACK: model = new TrackModel(obj); break; default: L.i("Category type not detected.", ""); break; } if (model != null) { append(new ListItem(viewManager, model)); } } catch (JSONException e) { L.e("Failed to convert item of index " + Integer.toString(i), "", e); } } } else { append("No results."); } } /** * Clears the view. */ private void clearSearchResults() { loadMoreButton.remove(); deleteAll(); append(this.searchField); } /** * Appends search results to the view. If the results are paged, appends a * load more button as well. */ private void appendToView() { if (viewModel != null) { // Avoid leaving the button between the paged results. loadMoreButton.remove(); int loopMax = viewModel.size(); for (int i = 0; i < loopMax; i++) { append((ListItem) viewModel.elementAt(i)); } // Append load more button if there is paging available. if (queryPager.hasMorePages()) { loadMoreButton.append(); } } } private class SearchResultHandlerTask extends Task { public SearchResultHandlerTask() { super(Task.NORMAL_PRIORITY); } public Object exec(Object response) { if (response instanceof JSONObject) { JSONArray resultsArray; JSONObject paging; try { resultsArray = ((JSONObject) response).getJSONArray("items"); paging = ((JSONObject) response).getJSONObject("paging"); // Set query pager and pass results to parser. queryPager.setPaging(paging); parseResultsToViewModel(resultsArray); appendToView(); } catch (JSONException ex) { L.e("Could not parse JSON", "", ex); } } return response; } } }