com.concavenp.artistrymuse.fragments.SearchResultFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.concavenp.artistrymuse.fragments.SearchResultFragment.java

Source

/*
 * ArtistryMuse is an application that allows artist to share projects
 * they have created along with the inspirations behind them for others to
 * discover and enjoy.
 * Copyright (C) 2017  David A. Todd
 *
 * 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.concavenp.artistrymuse.fragments;

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ViewFlipper;

import com.concavenp.artistrymuse.R;
import com.concavenp.artistrymuse.StorageDataType;
import com.concavenp.artistrymuse.fragments.adapter.SearchFragmentPagerAdapter;
import com.concavenp.artistrymuse.fragments.adapter.SearchResultAdapter;
import com.concavenp.artistrymuse.fragments.viewholder.ProjectResponseViewHolder;
import com.concavenp.artistrymuse.fragments.viewholder.UserResponseViewHolder;
import com.concavenp.artistrymuse.model.Project;
import com.concavenp.artistrymuse.model.ProjectResponse;
import com.concavenp.artistrymuse.model.ProjectResponseHit;
import com.concavenp.artistrymuse.model.Request;
import com.concavenp.artistrymuse.model.User;
import com.concavenp.artistrymuse.model.UserResponse;
import com.concavenp.artistrymuse.model.UserResponseHit;
import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.Query;
import com.google.firebase.database.ValueEventListener;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;
import java.util.UUID;

/**
 * A simple {@link Fragment} subclass.
 * Use the {@link SearchResultFragment#newInstance} factory method to
 * create an instance of this fragment.
 */
public class SearchResultFragment extends BaseFragment
        implements SearchFragmentPagerAdapter.OnSearchInteractionListener {

    /**
     * The logging tag string to be associated with log data for this class
     */
    @SuppressWarnings("unused")
    private static final String TAG = SearchResultFragment.class.getSimpleName();

    // Parameter arguments, these are names that match the fragment initialization parameters
    private static final String TYPE_PARAM = "type";

    // Types of parameters
    private StorageDataType mType;

    private SearchResultAdapter<UserResponseHit, UserResponseViewHolder> mUsersAdapter;
    private SearchResultAdapter<ProjectResponseHit, ProjectResponseViewHolder> mProjectsAdapter;

    private EndlessRecyclerOnScrollListener mScrollListener;

    private ChildEventListener mChildEventListener;
    private DataSnapshot mDataSnapshot;

    private String mSearchText;

    /**
     * The Shared Preferences key lookup value for identifying the last used tab position.
     */
    private static final String SEARCH_STRING = "SEARCH_STRING_";
    private static final String FLIP_ACTIVE_SEARCH_STRING = "FLIP_ACTIVE_SEARCH_STRING";

    // This flipper allows the content of the fragment to show the user either the list search
    // results or a informative message stating that a search needs to be performed to find
    // results.
    private ViewFlipper mFlipper;

    /**
     * Use this factory method to create a new instance of
     * this fragment using the provided parameters.
     *
     * @param type - The type of search result
     * @return A new instance of fragment SearchResultFragment.
     */
    public static SearchResultFragment newInstance(StorageDataType type) {

        SearchResultFragment fragment = new SearchResultFragment();

        Bundle args = new Bundle();
        args.putInt(TYPE_PARAM, type.ordinal());
        fragment.setArguments(args);

        return fragment;

    }

    /**
     * Required empty public constructor
     */
    public SearchResultFragment() {

        // Do nothing

    }

    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        if (getArguments() != null) {

            mType = StorageDataType.values()[getArguments().getInt(TYPE_PARAM)];

        }

    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        // Inflate the layout for this fragment
        View mainView = inflater.inflate(R.layout.fragment_search_result, container, false);

        // Save off the flipper for use in deciding which view to show
        mFlipper = mainView.findViewById(R.id.fragment_search_ViewFlipper);

        // The widgets that will "view" the search result data contained within their corresponding adapters
        RecyclerView mRecycler = mainView.findViewById(R.id.search_recycler_view);

        // Use this setting to improve performance if you know that changes in content do not change the layout size of the RecyclerView
        mRecycler.setHasFixedSize(true);

        // Set up Layout
        int columnCount = getResources().getInteger(R.integer.list_column_count);
        StaggeredGridLayoutManager mLayoutManager = new StaggeredGridLayoutManager(columnCount,
                StaggeredGridLayoutManager.VERTICAL);
        mRecycler.setLayoutManager(mLayoutManager);

        // Create the adapter that will be used to hold and paginate through the resulting search data
        switch (mType) {
        case USERS: {
            mUsersAdapter = new SearchResultAdapter<>(UserResponseViewHolder.class, mInteractionListener,
                    R.layout.item_user);
            mUsersAdapter.clearData();
            mRecycler.setAdapter(mUsersAdapter);
            break;
        }
        case PROJECTS: {
            mProjectsAdapter = new SearchResultAdapter<>(ProjectResponseViewHolder.class, mInteractionListener,
                    R.layout.item_project);
            mProjectsAdapter.clearData();
            mRecycler.setAdapter(mProjectsAdapter);
            break;
        }
        }

        // Setup the endless scrolling
        mScrollListener = new EndlessRecyclerOnScrollListener(mLayoutManager) {
            @Override
            public void onLoadMore(int currentPage) {

                // Clear the child listener from the previous search if not already done
                if (mChildEventListener != null) {
                    mDatabase.removeEventListener(mChildEventListener);
                }

                // Clear any data that saved from the last search
                mDataSnapshot = null;

                // Get the data
                search(currentPage);

            }
        };
        mScrollListener.initValues();
        mRecycler.addOnScrollListener(mScrollListener);

        return mainView;

    }

    private String getDatabaseNameFromType() {

        switch (mType) {
        case USERS: {
            return User.USER;
        }
        case PROJECTS: {
            return Project.PROJECT;
        }
        default: {
            return null;
        }
        }

    }

    /**
     * Performs the work of re-querying the cloud services for data to be displayed.  An adapter
     * is used to translate the data retrieved into the populated displayed view.
     */
    private void search(final int dataPosition) {

        UUID requestId = UUID.randomUUID();

        // Build the query to be used
        final Query responseQuery = getResponseQuery(mDatabase, requestId);

        // Create the JSON request object that will be placed into the database
        Request request = new Request(Request.FIREBASE, mSearchText, getDatabaseNameFromType(), dataPosition * 10);

        Log.d(TAG, "Here is the query that will be used: " + responseQuery);

        // It took a while to get this right.  So, I'll put in some words for what's going on here.
        // When performing a search this App will create a new DB entry within the "search" table.
        // A Heroku hosted Node application is monitoring this "search" table for the entries
        // created here.  The entries and processed with the results placed in the same table using
        // the the request UID as the lookup key.  This application is simply waiting for the
        // result entry node within the DB to be created.  This can take some time given several
        // different variables to the situation.  So, when the DB node is detected it will then
        // query for the Value of the node.  Turns out just listening for the Value of the node
        // has bugs within the Firebase codebase.  I know this because Google has contacted me
        // regarding the bugs I've exposed.  (YEH ME!)  The bug in question appears to be the result
        // of not waiting until the writing of data within the DB in complete before issued a message
        // out the listeners of the data (this App) that it is complete.  The result is that this
        // App would get errors back the DB query because it was only ever partially finished.
        mChildEventListener = responseQuery.addChildEventListener(new ChildEventListener() {

            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String childNode) {

                // This should be null - meaning that that the child has been created and it is time to query for the data
                if (childNode == null) {

                    // We no longer need this child listener
                    mDatabase.removeEventListener(mChildEventListener);

                    // Listen for the result.
                    //
                    // NOTE: this cannot be done as a one off due to the unpredictable time nature of
                    // the processed response becoming available.
                    //mProjectsValueEventListener = responseQuery.addValueEventListener(new ValueEventListener() {
                    responseQuery.addListenerForSingleValueEvent(new ValueEventListener() {

                        @Override
                        public void onDataChange(DataSnapshot dataSnapshot) {

                            // Check to see if the data is there yet
                            if (dataSnapshot.exists()) {

                                // Save the data
                                mDataSnapshot = dataSnapshot;

                                // Verify there is an object to work with
                                Object objectResponse = dataSnapshot.getValue();
                                if (objectResponse != null) {

                                    // Add the new data given the type
                                    //
                                    // NOTE: this should be done via generics used within this class,
                                    // however my fist attempt landing in a too complicated solution.
                                    //
                                    switch (mType) {
                                    case USERS: {

                                        // Convert the JSON to Object
                                        UserResponse response = mDataSnapshot.getValue(UserResponse.class);

                                        // Add the search results to the adapter
                                        if ((response.getHits().getTotal() > 0)
                                                && (response.getHits().getHits() != null)) {
                                            mUsersAdapter.add(response.getHits().getHits());

                                            // We are now performing a search, flip control to the individual fragments of the TabLayout
                                            mFlipper.setDisplayedChild(mFlipper.indexOfChild(
                                                    mFlipper.findViewById(R.id.search_recycler_view)));
                                        } else {
                                            Log.d(TAG,
                                                    "There does not appear to be any results from the search query");

                                            // We are now performing a search, flip control to the individual fragments of the TabLayout
                                            mFlipper.setDisplayedChild(mFlipper.indexOfChild(mFlipper
                                                    .findViewById(R.id.fragment_search_no_results_Flipper)));
                                        }

                                        break;
                                    }
                                    case PROJECTS: {
                                        // Convert the JSON to Object
                                        ProjectResponse response = mDataSnapshot.getValue(ProjectResponse.class);

                                        // Add the search results to the adapter
                                        if ((response.getHits().getTotal() > 0)
                                                && (response.getHits().getHits() != null)) {
                                            mProjectsAdapter.add(response.getHits().getHits());

                                            // We are now performing a search, flip control to the individual fragments of the TabLayout
                                            mFlipper.setDisplayedChild(mFlipper.indexOfChild(
                                                    mFlipper.findViewById(R.id.search_recycler_view)));
                                        } else {
                                            Log.d(TAG,
                                                    "There does not appear to be any results from the search query");

                                            // We are now performing a search, flip control to the individual fragments of the TabLayout
                                            mFlipper.setDisplayedChild(mFlipper.indexOfChild(mFlipper
                                                    .findViewById(R.id.fragment_search_no_results_Flipper)));
                                        }
                                        break;
                                    }

                                    }

                                } else {

                                    Log.e(TAG, "Expected response from search query was null");

                                    // We are now performing a search, flip control to the individual fragments of the TabLayout
                                    mFlipper.setDisplayedChild(mFlipper.indexOfChild(
                                            mFlipper.findViewById(R.id.fragment_search_error_Flipper)));

                                }

                            } else {

                                Log.e(TAG, "There is no data in the snapshot");

                                // We are now performing a search, flip control to the individual fragments of the TabLayout
                                mFlipper.setDisplayedChild(mFlipper
                                        .indexOfChild(mFlipper.findViewById(R.id.fragment_search_no_data_Flipper)));

                            }

                        }

                        @Override
                        public void onCancelled(DatabaseError databaseError) {

                            // I have not yet seen this occur yet
                            Log.e(TAG, "The Value query for the search results has encountered an error: "
                                    + databaseError);

                        }

                    });

                } else {

                    // Skip, we only care about the creation of the node which will result in
                    // a null value, so do nothing.

                }

            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String s) {

                // Do nothing

            }

            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {

                // Do nothing

            }

            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String s) {

                // Do nothing

            }

            @Override
            public void onCancelled(DatabaseError databaseError) {

                // Do nothing

            }

        });

        // Add the search request to the database.  The Flashlight (this is a Heroku hosted
        // Node.js Application along with an instance of Elasticsearch) service will see this and
        // consume the request and generate a response containing the results of the Elasticsearch.
        mDatabase.child(Request.SEARCH).child(Request.REQUEST).child(requestId.toString()).setValue(request);

    }

    private Query getResponseQuery(DatabaseReference databaseReference, UUID uuid) {

        Query myTopPostsQuery = databaseReference.child(Request.SEARCH).child(Request.RESPONSE)
                .child(uuid.toString());

        return myTopPostsQuery;

    }

    /**
     * Save off the User entered search string.
     *
     * @param outState - The bundle that will be presented to this fragment upon re-creation
     */
    @Override
    public void onSaveInstanceState(Bundle outState) {

        super.onSaveInstanceState(outState);

        outState.putInt(FLIP_ACTIVE_SEARCH_STRING + getDatabaseNameFromType(), mFlipper.getDisplayedChild());

        try {
            switch (mType) {
            case USERS: {
                List<UserResponseHit> list = mUsersAdapter.getData();
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(baos);
                oos.writeObject(list);
                outState.putByteArray(SEARCH_STRING + getDatabaseNameFromType(), baos.toByteArray());
                break;
            }
            case PROJECTS: {
                List<ProjectResponseHit> list = mProjectsAdapter.getData();
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(baos);
                oos.writeObject(list);
                outState.putByteArray(SEARCH_STRING + getDatabaseNameFromType(), baos.toByteArray());
                break;
            }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * Restore the instance data saved.  This includes the User entered search string.
     *
     * @param savedInstanceState - The bundle of instance data saved
     */
    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {

        super.onActivityCreated(savedInstanceState);

        if (savedInstanceState != null) {

            mFlipper.setDisplayedChild(
                    savedInstanceState.getInt(FLIP_ACTIVE_SEARCH_STRING + getDatabaseNameFromType(), 0));

            try {
                switch (mType) {
                case USERS: {
                    ByteArrayInputStream bais = new ByteArrayInputStream(
                            savedInstanceState.getByteArray(SEARCH_STRING + getDatabaseNameFromType()));
                    ObjectInputStream ois = new ObjectInputStream(bais);
                    List<UserResponseHit> list = (List<UserResponseHit>) ois.readObject();
                    mUsersAdapter.clearData();
                    mUsersAdapter.add(list);
                    break;
                }
                case PROJECTS: {
                    ByteArrayInputStream bais = new ByteArrayInputStream(
                            savedInstanceState.getByteArray(SEARCH_STRING + getDatabaseNameFromType()));
                    ObjectInputStream ois = new ObjectInputStream(bais);
                    List<ProjectResponseHit> list = (List<ProjectResponseHit>) ois.readObject();
                    mProjectsAdapter.clearData();
                    mProjectsAdapter.add(list);
                    break;
                }
                }
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }

        }

    }

    @Override
    public void onSearchInteraction(String searchString) {

        mSearchText = searchString;

        // We are now performing a search, flip control to the individual fragments of the TabLayout
        mFlipper.setDisplayedChild(
                mFlipper.indexOfChild(mFlipper.findViewById(R.id.fragment_search_searching_Flipper)));

        // Clear any results that are being stored within the adapter scroll listener
        switch (mType) {
        case USERS: {
            mUsersAdapter.clearData();
            break;
        }
        case PROJECTS: {
            mProjectsAdapter.clearData();
            break;
        }
        }

        // Init the values for this endless scroller
        mScrollListener.initValues();

        // Perform a search and display the data for the first page of the pagination (aka zero)
        search(0);

    }

}