package com.nuscomputing.ivle; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import com.nuscomputing.ivle.providers.ModulesContract; import com.nuscomputing.ivlelapi.FailedLoginException; import com.nuscomputing.ivlelapi.IVLE; import com.nuscomputing.ivlelapi.JSONParserException; import com.nuscomputing.ivlelapi.Module; import com.nuscomputing.ivlelapi.NetworkErrorException; import android.accounts.Account; import android.annotation.TargetApi; import android.app.SearchManager; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.support.v4.app.LoaderManager; import android.support.v4.app.ListFragment; import android.support.v4.content.AsyncTaskLoader; import android.support.v4.content.Loader; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; /** * Searchable fragment to display search results. * @author yjwong */ public class SearchableFragment extends ListFragment { // {{{ properties /** TAG for logging */ public static final String TAG = "SearchableFragment"; /** A reference to the layout inflater */ private LayoutInflater mLayoutInflater; /** The search results adapter */ private SearchResultAdapter mAdapter; // }}} // {{{ methods @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.searchable_fragment, null); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // Obtain the search query. Bundle args = getArguments(); // Get the layout inflater. mLayoutInflater = getActivity().getLayoutInflater(); // Create the adapter with no results first, we will populate it as // we search. List<SearchResult> results = new ArrayList<SearchResult>(); mAdapter = new SearchResultAdapter(getActivity(), results); setListAdapter(mAdapter); // Perform the search query. getLoaderManager().initLoader(DataLoader.LOADER_SEARCHABLE_FRAGMENT, args, new SearchResultLoaderCallbacks()); // Get the list view. ListView listview = getListView(); listview.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { // Get the search result. SearchResult result = (SearchResult) parent.getItemAtPosition(position); Bundle resultDetails = result.getResultDetails(); int resultType = result.getResultType(); // Act based on the result type. switch (resultType) { case SearchResult.RESULT_TYPE_HEADING: return; case SearchResult.RESULT_TYPE_MODULE: // Get the module ID. long moduleId = resultDetails.getLong(SearchResult.KEY_MODULE_ID); String moduleCourseName = resultDetails.getString(SearchResult.KEY_MODULE_NAME); // Start the module activity. Intent intent = new Intent(); intent.putExtra("moduleId", moduleId); intent.putExtra("moduleCourseName", moduleCourseName); intent.setClass(getActivity(), ModuleActivity.class); startActivity(intent); return; case SearchResult.RESULT_TYPE_MODULE_ONLINE: // Get the IVLE ID. String moduleIvleId = resultDetails.getString(SearchResult.KEY_MODULE_IVLE_ID); moduleCourseName = resultDetails.getString(SearchResult.KEY_MODULE_NAME); // Start the online module activity. intent = new Intent(); intent.putExtra("moduleIvleId", moduleIvleId); intent.putExtra("moduleCourseName", moduleCourseName); intent.setClass(getActivity(), com.nuscomputing.ivle.online.ModuleActivity.class); startActivity(intent); return; default: // Ignore. } } }); } // }}} // {{{ classes /** * The loader callbacks for search results. * @author yjwong */ class SearchResultLoaderCallbacks implements LoaderManager.LoaderCallbacks<List<SearchResult>> { // {{{ methods @Override public Loader<List<SearchResult>> onCreateLoader(int id, Bundle args) { // Create a new loader. SearchResultLoader loader = new SearchResultLoader(getActivity(), args); loader.setAdapter(mAdapter); return loader; } @TargetApi(11) @Override public void onLoadFinished(Loader<List<SearchResult>> loader, List<SearchResult> results) { // Check if there are no results. if (results.size() == 0) { TextView tvNoSearchResults = (TextView) getActivity().findViewById(R.id.searchable_fragment_no_search_results); tvNoSearchResults.setVisibility(View.VISIBLE); } else { // We finished searching, notify that our data set is changed. mAdapter.clear(); mAdapter.addAll(results); mAdapter.notifyDataSetChanged(); } // Hide the loading progress. TextView tvLoading = (TextView) getActivity().findViewById(R.id.searchable_fragment_loading); tvLoading.setVisibility(View.GONE); } @Override public void onLoaderReset(Loader<List<SearchResult>> loader) { // Do nothing. } // }}} } /** * The loader for search results. * @author yjwong */ static class SearchResultLoader extends AsyncTaskLoader<List<SearchResult>> { // {{{ properties /** Arguments to this loader */ private Bundle mArgs; /** The context */ private Context mContext; /** The search results */ private List<SearchResult> mSearchResults; /** The adapter */ private SearchResultAdapter mAdapter; /** Handler to run stuff on UI thread */ private Handler mHandler = new Handler(); /** Runnable to update results */ private Runnable mResultUpdater = new Runnable() { @TargetApi(11) @Override public void run() { mAdapter.clear(); mAdapter.addAll(mSearchResults); mAdapter.notifyDataSetChanged(); } }; // }}} // {{{ methods SearchResultLoader(Context context, Bundle args) { super(context); mContext = context; mArgs = args; } void setAdapter(SearchResultAdapter adapter) { mAdapter = adapter; } @Override public void onStartLoading() { if (mSearchResults != null) { deliverResult(mSearchResults); } if (takeContentChanged() || mSearchResults == null) { forceLoad(); } } @Override public List<SearchResult> loadInBackground() { // Get the search query. Bundle resultDetails; SearchResult result; String query = mArgs.getString(SearchManager.QUERY); // List of results. mSearchResults = new ArrayList<SearchResult>(); // Get the active account. Account account = AccountUtils.getActiveAccount(mContext, false); // Get an IVLE instance. IVLE ivle = IVLEUtils.getIVLEInstance(mContext); // Get the content provider. ContentResolver resolver = mContext.getContentResolver(); ContentProviderClient provider = resolver.acquireContentProviderClient(Constants.PROVIDER_AUTHORITY); // Acquire modules first. try { Cursor cursor = provider.query(ModulesContract.CONTENT_URI, new String[] { ModulesContract.ID, ModulesContract.COURSE_NAME, ModulesContract.COURSE_CODE }, "(" + ModulesContract.COURSE_CODE + " LIKE ? OR " + ModulesContract.COURSE_NAME + " LIKE ? " + ") AND " + DatabaseHelper.MODULES_TABLE_NAME + "." + ModulesContract.ACCOUNT + " = ?", new String[] { query, "%" + query + "%", account.name }, null); // Have we found any results in modules? if (cursor.getCount() > 0) { // Add the heading. resultDetails = new Bundle(); resultDetails.putString(SearchResult.KEY_HEADING_TITLE, mContext.getString(R.string.searchable_fragment_heading_title_modules)); result = new SearchResult(SearchResult.RESULT_TYPE_HEADING, resultDetails); mSearchResults.add(result); // Add the results. while (cursor.moveToNext()) { resultDetails = new Bundle(); resultDetails.putLong(SearchResult.KEY_MODULE_ID, cursor.getLong(cursor.getColumnIndex(ModulesContract.ID))); resultDetails.putString(SearchResult.KEY_MODULE_NAME, cursor.getString(cursor.getColumnIndex(ModulesContract.COURSE_NAME))); resultDetails.putString(SearchResult.KEY_MODULE_CODE, cursor.getString(cursor.getColumnIndex(ModulesContract.COURSE_CODE))); result = new SearchResult(SearchResult.RESULT_TYPE_MODULE, resultDetails); mSearchResults.add(result); } } // Update results. mHandler.post(mResultUpdater); } catch (RemoteException e) { Log.e(TAG, "RemoteException encountered while searching"); } // Acquire online modules. try { // Obtain the modules via the API. Map<String, String> criterion = new HashMap<String, String>(); //criterion.put("ModuleCode", query); criterion.put("ModuleTitle", URLEncoder.encode(query, "UTF-8")); Module[] modules = ivle.searchModules(criterion); // Filter the modules. List<Module> modulesFiltered = new ArrayList<Module>(); for (Module module : modules) { // Remove inactive modules. if (!module.isActive.equals("N")) { modulesFiltered.add(module); } } // Have we found any results? if (modulesFiltered.size() > 0) { // Add the heading. resultDetails = new Bundle(); resultDetails.putString(SearchResult.KEY_HEADING_TITLE, mContext.getString(R.string.searchable_fragment_heading_title_modules_online)); result = new SearchResult(SearchResult.RESULT_TYPE_HEADING, resultDetails); mSearchResults.add(result); // Add the results. for (Module module : modulesFiltered) { resultDetails = new Bundle(); resultDetails.putString(SearchResult.KEY_MODULE_IVLE_ID, module.ID); resultDetails.putString(SearchResult.KEY_MODULE_NAME, module.courseName); resultDetails.putString(SearchResult.KEY_MODULE_CODE, module.courseCode); result = new SearchResult(SearchResult.RESULT_TYPE_MODULE_ONLINE, resultDetails); mSearchResults.add(result); } } // Update results. mHandler.post(mResultUpdater); } catch (NetworkErrorException e) { Log.e(TAG, "NetworkErrorException encountered in searchModules"); } catch (FailedLoginException e) { Log.e(TAG, "FailedLoginException encountered in searchModules"); } catch (JSONParserException e) { Log.e(TAG, "JSONParserException encountered in searchModules"); } catch (UnsupportedEncodingException e) { Log.e(TAG, "UnsupportedEncodingException encountered in searchModules"); } return mSearchResults; } // }}} } /** * Search result adapter. * @author yjwong */ class SearchResultAdapter extends ArrayAdapter<SearchResult> { // {{{ properties /** The view types */ public static final int VIEW_TYPE_HEADING = 1; public static final int VIEW_TYPE_MODULE = 2; public static final int VIEW_TYPE_MODULE_ONLINE = 3; // }}} // {{{ methods public SearchResultAdapter(Context context, List<SearchResult> results) { super(context, android.R.id.text1, results); } @TargetApi(11) @Override public void addAll(Collection<? extends SearchResult> collection) { // Wrapper for addAll in case it's not available on <= HONEYCOMB. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { super.addAll(collection); } else { for (SearchResult result : collection) { super.add(result); } } } @Override public int getItemViewType(int position) { // Get the item. SearchResult item = getItem(position); return item.getResultType(); } @Override public int getViewTypeCount() { return SearchResult.RESULT_TYPE_MAX; } @Override public View getView(int position, View convertView, ViewGroup parent) { // Get the view type. int viewType = getItemViewType(position); // Get the item. SearchResult item = getItem(position); Bundle resultDetails = item.getResultDetails(); // Setup the appropriate view based on the view type. switch (viewType) { case VIEW_TYPE_HEADING: convertView = mLayoutInflater.inflate(R.layout.searchable_fragment_list_heading, null); convertView.setClickable(true); TextView tvHeadingTitle = (TextView) convertView.findViewById(R.id.searchable_fragment_list_heading_title); tvHeadingTitle.setText(resultDetails.getString(SearchResult.KEY_HEADING_TITLE)); return convertView; case VIEW_TYPE_MODULE: case VIEW_TYPE_MODULE_ONLINE: convertView = mLayoutInflater.inflate(R.layout.searchable_fragment_list_item_module, null); convertView.setTag(item); TextView tvModuleTitle = (TextView) convertView.findViewById(R.id.searchable_fragment_list_module_name); tvModuleTitle.setText(resultDetails.getString(SearchResult.KEY_MODULE_NAME)); TextView tvModuleCode = (TextView) convertView.findViewById(R.id.searchable_fragment_list_module_code); tvModuleCode.setText(resultDetails.getString(SearchResult.KEY_MODULE_CODE)); return convertView; default: throw new IllegalArgumentException("invalid view type"); } } // }}} } /** * Represents a search result. * @author yjwong */ static class SearchResult { // {{{ properties /** The types of search result */ public static final int RESULT_TYPE_HEADING = 1; public static final int RESULT_TYPE_MODULE = 2; public static final int RESULT_TYPE_MODULE_ONLINE = 3; public static final int RESULT_TYPE_MAX = 4; /** Keys for various result items */ public static final String KEY_HEADING_TITLE = "heading_title"; public static final String KEY_MODULE_ID = "module_id"; public static final String KEY_MODULE_IVLE_ID = "module_ivle_id"; public static final String KEY_MODULE_NAME = "module_name"; public static final String KEY_MODULE_CODE = "module_code"; /** Bundle containing the result details */ private final Bundle mResultDetails; /** The type of this search result */ private int mResultType; // }}} // {{{ methods SearchResult(int type, Bundle details) { // Check the type. if (type >= RESULT_TYPE_MAX) { throw new IllegalArgumentException("type is invalid"); } mResultType = type; mResultDetails = details; } /** * Method: getResultType * <p> * Gets the result type for the search query. */ public int getResultType() { return mResultType; } /** * Method: getResultDetails * <p> * Gets the result details. */ public Bundle getResultDetails() { return mResultDetails; } // }}} } // }}} }