From ad09631a0a1d14504ce6fbb477dd258574133bdf Mon Sep 17 00:00:00 2001 From: Lukas Koebis Date: Tue, 1 Sep 2015 17:42:18 -0700 Subject: [PATCH 01/10] Added RecyclerView.Adapter adapter; sample project --- ParseLoginUI/build.gradle | 2 + .../parse/ParseQueryRecyclerViewAdapter.java | 644 ++++++++++++++++++ ParseQueryAdapterExample/build.gradle | 18 + .../src/main/AndroidManifest.xml | 58 ++ .../queryadaptersample/SampleActivity.java | 57 ++ .../queryadaptersample/SampleApplication.java | 37 + .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 75968 bytes .../src/main/res/layout/activity.xml | 16 + .../src/main/res/values-v11/themes.xml | 11 + .../src/main/res/values/strings.xml | 9 + .../src/main/res/values/themes.xml | 20 + settings.gradle | 1 + 12 files changed, 873 insertions(+) create mode 100644 ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java create mode 100644 ParseQueryAdapterExample/build.gradle create mode 100644 ParseQueryAdapterExample/src/main/AndroidManifest.xml create mode 100644 ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleActivity.java create mode 100644 ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleApplication.java create mode 100644 ParseQueryAdapterExample/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 ParseQueryAdapterExample/src/main/res/layout/activity.xml create mode 100644 ParseQueryAdapterExample/src/main/res/values-v11/themes.xml create mode 100644 ParseQueryAdapterExample/src/main/res/values/strings.xml create mode 100644 ParseQueryAdapterExample/src/main/res/values/themes.xml diff --git a/ParseLoginUI/build.gradle b/ParseLoginUI/build.gradle index 8bc41c0..7136397 100644 --- a/ParseLoginUI/build.gradle +++ b/ParseLoginUI/build.gradle @@ -3,6 +3,8 @@ apply plugin: 'android-library' dependencies { compile 'com.parse.bolts:bolts-android:1.2.1' compile 'com.android.support:support-v4:22.0.0' + compile 'com.android.support:appcompat-v7:22.0.0' + compile 'com.android.support:recyclerview-v7:22.0.0' compile 'com.parse:parse-android:1.10.1' provided 'com.facebook.android:facebook-android-sdk:4.0.1' diff --git a/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java b/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java new file mode 100644 index 0000000..91e907e --- /dev/null +++ b/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java @@ -0,0 +1,644 @@ +package com.parse; + + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; + +import bolts.Capture; + +/** + * Created by lukask on 9/1/15. + */ +public class ParseQueryRecyclerViewAdapter extends RecyclerView.Adapter { + + private static final int DEFAULT_TYPE = 0; + private static final int PAGINATION_CELL_ROW_TYPE = 1; + + + /** + * Implement to construct your own custom {@link ParseQuery} for fetching objects. + */ + public interface QueryFactory { + ParseQuery create(); + } + + /** + * Implement with logic that is called before and after objects are fetched from Parse by the + * adapter. + */ + public interface OnQueryLoadListener { + void onLoading(); + + void onLoaded(List objects, Exception e); + } + + /** + * OnClickListener. + */ + public interface OnClickListener { + void onClick(T item, int position); + } + + /** + * ViewHolder. + */ + public static class ViewHolder extends RecyclerView.ViewHolder { + TextView textView; + ParseImageView imageView; + + public ViewHolder(View itemView) { + super(itemView); + + try { + textView = (TextView) itemView.findViewById(android.R.id.text1); + } catch (ClassCastException ex) { + throw new IllegalStateException( + "Your object views must have a TextView whose id attribute is 'android.R.id.text1'", ex); + } + try { + imageView = (ParseImageView) itemView.findViewById(android.R.id.icon); + } catch (ClassCastException ex) { + throw new IllegalStateException( + "Your object views must have a ParseImageView whose id attribute is 'android.R.id.icon'", ex); + } + } + } + + // The key to use to display on the cell text label. + private String textKey; + + // The key to use to fetch an image for display in the cell's image view. + private String imageKey; + + // The number of objects to show per page (default: 25) + private int objectsPerPage = 25; + + // Whether the table should use the built-in pagination feature (default: + // true) + private boolean paginationEnabled = true; + + // A Drawable placeholder, to be set on ParseImageViews while images are loading. Can be null. + private Drawable placeholder; + + // A WeakHashMap, holding references to ParseImageViews that have been configured. + // Accessed and iterated over if setPlaceholder(Drawable) is called after some set of + // ParseImageViews have already been instantiated and configured. + private WeakHashMap imageViewSet = new WeakHashMap<>(); + + // A WeakHashMap, keeping track of the DataSetObservers on this class + private WeakHashMap dataSetObservers = new WeakHashMap<>(); + + // Whether the adapter should trigger loadObjects() on registerDataSetObserver(); Defaults to + // true. + private boolean autoload = true; + + private Context context; + + private List objects = new ArrayList<>(); + + private Set runningQueries = Collections.newSetFromMap(new ConcurrentHashMap()); + + // Used to keep track of the pages of objects when using CACHE_THEN_NETWORK. When using this, + // the data will be flattened and put into the objects list. + private List> objectPages = new ArrayList<>(); + + private int currentPage = 0; + + private Integer itemResourceId; + + private boolean hasNextPage = true; + + private QueryFactory queryFactory; + + private OnClickListener onClickListener; + + private List> onQueryLoadListeners = new ArrayList<>(); + + /** + * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, + * this adapter will fetch and display all {@link ParseObject}s of the specified class, + * ordered by creation time. + * + * @param context + * The activity utilizing this adapter. + * @param clazz + * The {@link ParseObject} subclass type to fetch and display. + */ + public ParseQueryRecyclerViewAdapter(Context context, Class clazz) { + this(context, ParseObject.getClassName(clazz)); + } + + /** + * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, + * this adapter will fetch and display all {@link ParseObject}s of the specified class, ordered + * by creation time. + * + * @param context + * The activity utilizing this adapter. + * @param className + * The name of the Parse class of {@link ParseObject}s to display. + */ + public ParseQueryRecyclerViewAdapter(Context context, final String className) { + this(context, new QueryFactory() { + @Override + public ParseQuery create() { + ParseQuery query = ParseQuery.getQuery(className); + query.orderByDescending("createdAt"); + + return query; + } + }); + + if (className == null) { + throw new RuntimeException("You need to specify a className for the ParseQueryAdapter"); + } + } + + /** + * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, + * this adapter will fetch and display all {@link ParseObject}s of the specified class, ordered + * by creation time. + * + * @param context + * The activity utilizing this adapter. + * @param clazz + * The {@link ParseObject} subclass type to fetch and display. + * @param itemViewResource + * A resource id that represents the layout for an item in the AdapterView. + */ + public ParseQueryRecyclerViewAdapter(Context context, Class clazz, + int itemViewResource) { + this(context, ParseObject.getClassName(clazz), itemViewResource); + } + + /** + * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, + * this adapter will fetch and display all {@link ParseObject}s of the specified class, ordered + * by creation time. + * + * @param context + * The activity utilizing this adapter. + * @param className + * The name of the Parse class of {@link ParseObject}s to display. + * @param itemViewResource + * A resource id that represents the layout for an item in the AdapterView. + */ + public ParseQueryRecyclerViewAdapter(Context context, final String className, int itemViewResource) { + this(context, new QueryFactory() { + @Override + public ParseQuery create() { + ParseQuery query = ParseQuery.getQuery(className); + query.orderByDescending("createdAt"); + + return query; + } + }, itemViewResource); + + if (className == null) { + throw new RuntimeException( + "You need to specify a className for the ParseQueryRecyclerViewAdapter"); + } + } + /** + * Constructs a {@code ParseQueryRecyclerViewAdapter}. Allows the caller to define further + * constraints on the {@link ParseQuery} to be used when fetching items from Parse. + * + * @param context + * The activity utilizing this adapter. + * @param queryFactory + * A {@link QueryFactory} to build a {@link ParseQuery} for fetching objects. + */ + public ParseQueryRecyclerViewAdapter(Context context, QueryFactory queryFactory) { + this(context, queryFactory, null); + } + + /** + * Constructs a {@code ParseQueryRecyclerViewAdapter}. Allows the caller to define further + * constraints on the {@link ParseQuery} to be used when fetching items from Parse. + * + * @param context + * The activity utilizing this adapter. + * @param queryFactory + * A {@link QueryFactory} to build a {@link ParseQuery} for fetching objects. + * @param itemViewResource + * A resource id that represents the layout for an item in the AdapterView. + */ + public ParseQueryRecyclerViewAdapter(Context context, QueryFactory queryFactory, int itemViewResource) { + this(context, queryFactory, Integer.valueOf(itemViewResource)); + } + + private ParseQueryRecyclerViewAdapter(Context context, QueryFactory queryFactory, Integer itemViewResource) { + super(); + this.context = context; + this.queryFactory = queryFactory; + this.itemResourceId = itemViewResource; + } + + /** + * Return the context provided by the {@code Activity} utilizing this {@code ParseQueryAdapter}. + * + * @return The activity utilizing this adapter. + */ + public Context getContext() { + return this.context; + } + + public void clear() { + this.objectPages.clear(); + cancelAllQueries(); + syncObjectsWithPages(); + this.notifyDataSetChanged(); + this.currentPage = 0; + } + + private void cancelAllQueries() { + for (ParseQuery q : runningQueries) { + q.cancel(); + } + runningQueries.clear(); + } + + /** + * Clears the table and loads the first page of objects asynchronously. This method is called + * automatically when this {@code Adapter} is attached to an {@code AdapterView}. + *

+ * {@code loadObjects()} should only need to be called if {@link #setAutoload(boolean)} is set to + * {@code false}. + */ + public void loadObjects() { + loadObjects(0, true); + } + + private void loadObjects(final int page, final boolean shouldClear) { + final ParseQuery query = queryFactory.create(); + + if (objectsPerPage > 0 && paginationEnabled) { + setPageOnQuery(page, query); + } + + this.notifyOnLoadingListeners(); + + // Create a new page + if (page >= objectPages.size()) { + objectPages.add(page, new ArrayList()); + } + + // In the case of CACHE_THEN_NETWORK, two callbacks will be called. Using this flag to keep track, + final Capture firstCallBack = new Capture<>(true); + + runningQueries.add(query); + + // TODO convert to Tasks and CancellationTokens + // (depends on https://github.com/ParsePlatform/Parse-SDK-Android/issues/6) + query.findInBackground(new FindCallback() { + @Override + public void done(List foundObjects, ParseException e) { + if (!runningQueries.contains(query)) { + return; + } + // In the case of CACHE_THEN_NETWORK, two callbacks will be called. We can only remove the + // query after the second callback. + if (query.getCachePolicy() != ParseQuery.CachePolicy.CACHE_THEN_NETWORK || + (query.getCachePolicy() == ParseQuery.CachePolicy.CACHE_THEN_NETWORK && !firstCallBack.get())) { + runningQueries.remove(query); + } + if ((!Parse.isLocalDatastoreEnabled() && + query.getCachePolicy() == ParseQuery.CachePolicy.CACHE_ONLY) + && (e != null) && e.getCode() == ParseException.CACHE_MISS) { + // no-op on cache miss + return; + } + + if ((e != null) && ((e.getCode() == ParseException.CONNECTION_FAILED) || (e.getCode() != ParseException.CACHE_MISS))) { + hasNextPage = true; + } else if (foundObjects != null) { + if (shouldClear && firstCallBack.get()) { + runningQueries.remove(query); + cancelAllQueries(); + runningQueries.add(query); // allow 2nd callback + objectPages.clear(); + objectPages.add(new ArrayList()); + currentPage = page; + firstCallBack.set(false); + } + + // Only advance the page, this prevents second call back from CACHE_THEN_NETWORK to + // reset the page. + if (page >= currentPage) { + currentPage = page; + + // since we set limit == objectsPerPage + 1 + hasNextPage = (foundObjects.size() > objectsPerPage); + } + + if (paginationEnabled && foundObjects.size() > objectsPerPage) { + // Remove the last object, fetched in order to tell us whether there was a "next page" + foundObjects.remove(objectsPerPage); + } + + List currentPage = objectPages.get(page); + currentPage.clear(); + currentPage.addAll(foundObjects); + + syncObjectsWithPages(); + + // executes on the UI thread + notifyDataSetChanged(); + } + + notifyOnLoadedListeners(foundObjects, e); + } + }); + } + + /** + * Loads the next page of objects, appends to table, and notifies the UI that the model has + * changed. + */ + public void loadNextPage() { + if (objects.size() == 0 && runningQueries.size() == 0) { + loadObjects(0, false); + } + else { + loadObjects(currentPage + 1, false); + } + } + + @Override + public int getItemViewType(int position) { + if (position == getPaginationCellRow()) { + return PAGINATION_CELL_ROW_TYPE; + } + return DEFAULT_TYPE; + } + + private View getDefaultView() { + if (this.itemResourceId != null) { + return View.inflate(context, itemResourceId, null); + } + LinearLayout view = new LinearLayout(context); + view.setPadding(8, 4, 8, 4); + + ParseImageView imageView = new ParseImageView(context); + imageView.setId(android.R.id.icon); + imageView.setLayoutParams(new LinearLayout.LayoutParams(50, 50)); + view.addView(imageView); + + TextView textView = new TextView(context); + textView.setId(android.R.id.text1); + textView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + textView.setPadding(8, 0, 0, 0); + view.addView(textView); + + return view; + } + + /** + * Override this method to customize the "Load Next Page" cell, visible when pagination is turned + * on and there may be more results to display. + *

+ * This method expects a {@code TextView} with id {@code android.R.id.text1}. + * + * @return The view object that allows the user to paginate. + */ + public View getNextPageView() { + View v = getDefaultView(); + TextView textView = (TextView) v.findViewById(android.R.id.text1); + textView.setText("Load more..."); + return v; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { + switch (viewType) { + case DEFAULT_TYPE: + return new ViewHolder(getDefaultView()); + case PAGINATION_CELL_ROW_TYPE: + return new ViewHolder(getNextPageView()); + } + return null; + } + + @Override + public void onBindViewHolder(ViewHolder viewHolder, final int position) { + if (position < objects.size()) { + final T object = objects.get(position); + + if (this.textKey == null) { + viewHolder.textView.setText(object.getObjectId()); + } else if (object.get(this.textKey) != null) { + viewHolder.textView.setText(object.get(this.textKey).toString()); + } else { + viewHolder.textView.setText(null); + } + + if (imageKey != null) { + if (viewHolder.imageView == null) { + throw new IllegalStateException( + "Your object views must have a ParseImageView whose id attribute is 'android.R.id.icon' if an imageKey is specified"); + } + if (!this.imageViewSet.containsKey(viewHolder.imageView)) { + this.imageViewSet.put(viewHolder.imageView, null); + } + viewHolder.imageView.setPlaceholder(placeholder); + viewHolder.imageView.setParseFile((ParseFile) object.get(imageKey)); + viewHolder.imageView.loadInBackground(); + } + viewHolder.textView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (onClickListener != null) { + onClickListener.onClick(object, position); + } + } + }); + } + else if (position == objects.size()) { + viewHolder.textView.setText("Load more..."); + if (viewHolder.imageView != null) { + viewHolder.imageView.setVisibility(View.GONE); + } + viewHolder.textView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + loadNextPage(); + } + }); + } + else { + throw new RuntimeException(); + } + } + + @Override + public int getItemCount() { + int count = this.objects.size(); + + if (this.shouldShowPaginationCell()) { + count++; + } + + return count; + } + + @Override + public void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { + super.registerAdapterDataObserver(observer); + this.dataSetObservers.put(observer, null); + if (this.autoload) { + this.loadObjects(); + } + } + + @Override + public void unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { + super.unregisterAdapterDataObserver(observer); + this.dataSetObservers.remove(observer); + } + + + /** + * This is a helper function to sync the objects with objectPages. This is only used with the + * CACHE_THEN_NETWORK option. + */ + private void syncObjectsWithPages() { + objects.clear(); + for (List pageOfObjects : objectPages) { + objects.addAll(pageOfObjects); + } + } + + private int getPaginationCellRow() { + return objects.size(); + } + + private boolean shouldShowPaginationCell() { + return this.paginationEnabled && this.objects.size() > 0 && this.hasNextPage; + } + + private void notifyOnLoadingListeners() { + for (OnQueryLoadListener listener : this.onQueryLoadListeners) { + listener.onLoading(); + } + } + + private void notifyOnLoadedListeners(List objects, Exception e) { + for (OnQueryLoadListener listener : this.onQueryLoadListeners) { + listener.onLoaded(objects, e); + } + } + + /** + * Override this method to manually paginate the provided {@code ParseQuery}. By default, this + * method will set the {@code limit} value to {@link #getObjectsPerPage()} and the {@code skip} + * value to {@link #getObjectsPerPage()} * {@code page}. + *

+ * Overriding this method will not be necessary, in most cases. + * + * @param page + * the page number of results to fetch from Parse. + * @param query + * the {@link ParseQuery} used to fetch items from Parse. This query will be mutated and + * used in its mutated form. + */ + protected void setPageOnQuery(int page, ParseQuery query) { + query.setLimit(this.objectsPerPage + 1); + query.setSkip(page * this.objectsPerPage); + } + + public void setTextKey(String textKey) { + this.textKey = textKey; + } + + public void setImageKey(String imageKey) { + this.imageKey = imageKey; + } + + public void setObjectsPerPage(int objectsPerPage) { + this.objectsPerPage = objectsPerPage; + } + + public int getObjectsPerPage() { + return this.objectsPerPage; + } + + /** + * Enable or disable pagination of results. Defaults to true. + * + * @param paginationEnabled + * Defaults to true. + */ + public void setPaginationEnabled(boolean paginationEnabled) { + this.paginationEnabled = paginationEnabled; + } + + /** + * Sets a placeholder image to be used when fetching data for each item in the {@code AdapterView} + * . Will not be used if {@link #setImageKey(String)} was not used to define which images to + * display. + * + * @param placeholder + * A {@code Drawable} to be displayed while the remote image data is being fetched. This + * value can be null, and {@code ImageView}s in this AdapterView will simply be blank + * while data is being fetched. + */ + public void setPlaceholder(Drawable placeholder) { + if (this.placeholder == placeholder) { + return; + } + this.placeholder = placeholder; + Iterator iter = this.imageViewSet.keySet().iterator(); + ParseImageView imageView; + while (iter.hasNext()) { + imageView = iter.next(); + if (imageView != null) { + imageView.setPlaceholder(this.placeholder); + } + } + } + + public void setOnClickListener(OnClickListener onClickListener) { + this.onClickListener = onClickListener; + } + + /** + * Enable or disable the automatic loading of results upon attachment to an {@code AdapterView}. + * Defaults to true. + * + * @param autoload + * Defaults to true. + */ + public void setAutoload(boolean autoload) { + if (this.autoload == autoload) { + // An extra precaution to prevent an overzealous setAutoload(true) after assignment to an + // AdapterView from triggering an unnecessary additional loadObjects(). + return; + } + this.autoload = autoload; + if (this.autoload && !this.dataSetObservers.isEmpty() && this.objects.isEmpty()) { + this.loadObjects(); + } + } + + public void addOnQueryLoadListener(OnQueryLoadListener listener) { + this.onQueryLoadListeners.add(listener); + } + + public void removeOnQueryLoadListener(OnQueryLoadListener listener) { + this.onQueryLoadListeners.remove(listener); + } +} diff --git a/ParseQueryAdapterExample/build.gradle b/ParseQueryAdapterExample/build.gradle new file mode 100644 index 0000000..06bf48d --- /dev/null +++ b/ParseQueryAdapterExample/build.gradle @@ -0,0 +1,18 @@ +apply plugin: 'android' + +dependencies { + // rootProject.ext.* variables are defined in project gradle file, you can also use path here. + compile project(':ParseLoginUI') + compile rootProject.ext.androidSupport + compile rootProject.ext.parse +} + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + } +} diff --git a/ParseQueryAdapterExample/src/main/AndroidManifest.xml b/ParseQueryAdapterExample/src/main/AndroidManifest.xml new file mode 100644 index 0000000..cc05984 --- /dev/null +++ b/ParseQueryAdapterExample/src/main/AndroidManifest.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleActivity.java b/ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleActivity.java new file mode 100644 index 0000000..47a87e4 --- /dev/null +++ b/ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleActivity.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2014, Parse, LLC. All rights reserved. + * + * You are hereby granted a non-exclusive, worldwide, royalty-free license to use, + * copy, modify, and distribute this software in source code or binary form for use + * in connection with the web services and APIs provided by Parse. + * + * As with any software that integrates with the Parse platform, your use of + * this software is subject to the Parse Terms of Service + * [https://www.parse.com/about/terms]. This copyright notice shall be + * included in all copies or substantial portions of the software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +package com.parse.queryadaptersample; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; + +import com.parse.ParseQueryRecyclerViewAdapter; +import com.parse.ParseUser; +import com.parse.queryadaptersample.R; + +/** + * Shows the user profile. This simple activity can function regardless of whether the user + * is currently logged in. + */ +public class SampleActivity extends Activity { + + private RecyclerView recyclerView; + + private ParseUser currentUser; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity); + + recyclerView = (RecyclerView) findViewById(R.id.recycler_view); + + ParseQueryRecyclerViewAdapter adapter = new ParseQueryRecyclerViewAdapter(this, "TestClass"); + adapter.loadObjects(); + + recyclerView.setAdapter(adapter); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + } +} diff --git a/ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleApplication.java b/ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleApplication.java new file mode 100644 index 0000000..7dd408f --- /dev/null +++ b/ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleApplication.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2014, Parse, LLC. All rights reserved. + * + * You are hereby granted a non-exclusive, worldwide, royalty-free license to use, + * copy, modify, and distribute this software in source code or binary form for use + * in connection with the web services and APIs provided by Parse. + * + * As with any software that integrates with the Parse platform, your use of + * this software is subject to the Parse Terms of Service + * [https://www.parse.com/about/terms]. This copyright notice shall be + * included in all copies or substantial portions of the software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +package com.parse.queryadaptersample; + +import android.app.Application; + +import com.parse.Parse; + +public class SampleApplication extends Application { + @Override + public void onCreate() { + super.onCreate(); + // Required - Initialize the Parse SDK + Parse.initialize(this); + + Parse.setLogLevel(Parse.LOG_LEVEL_DEBUG); + } +} diff --git a/ParseQueryAdapterExample/src/main/res/drawable-xxhdpi/ic_launcher.png b/ParseQueryAdapterExample/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..0eee02d6423df0659ebdb0549a32e5583b183f57 GIT binary patch literal 75968 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 F{{{7IbC>`C literal 0 HcmV?d00001 diff --git a/ParseQueryAdapterExample/src/main/res/layout/activity.xml b/ParseQueryAdapterExample/src/main/res/layout/activity.xml new file mode 100644 index 0000000..d28e207 --- /dev/null +++ b/ParseQueryAdapterExample/src/main/res/layout/activity.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/ParseQueryAdapterExample/src/main/res/values-v11/themes.xml b/ParseQueryAdapterExample/src/main/res/values-v11/themes.xml new file mode 100644 index 0000000..323816b --- /dev/null +++ b/ParseQueryAdapterExample/src/main/res/values-v11/themes.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/ParseQueryAdapterExample/src/main/res/values/strings.xml b/ParseQueryAdapterExample/src/main/res/values/strings.xml new file mode 100644 index 0000000..7e60729 --- /dev/null +++ b/ParseQueryAdapterExample/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + + + + 4R4qtxUJIYGzoDrJJfNKki1xpFMtWtOwZIeDTGjJ + lcTXrnkl9A7dnUlVlKMnnoKMo8Be9z5G8QJ3qINq + + Parse QueryAdapter Sample + diff --git a/ParseQueryAdapterExample/src/main/res/values/themes.xml b/ParseQueryAdapterExample/src/main/res/values/themes.xml new file mode 100644 index 0000000..4084ba0 --- /dev/null +++ b/ParseQueryAdapterExample/src/main/res/values/themes.xml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/settings.gradle b/settings.gradle index 036e858..ab0228a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,3 +8,4 @@ include ':ParseLoginSampleBasic' include ':ParseLoginSampleWithDispatchActivity' include ':ParseLoginSampleCodeCustomization' include ':ParseLoginSampleLayoutOverride' +include ':ParseQueryAdapterExample' \ No newline at end of file From 2167b5bb12dbfd952aa5a1458222fdecdaaa1af0 Mon Sep 17 00:00:00 2001 From: Lukas Koebis Date: Thu, 3 Sep 2015 17:27:10 -0700 Subject: [PATCH 02/10] removed view types --- .../parse/ParseQueryRecyclerViewAdapter.java | 35 +------------------ 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java b/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java index 91e907e..2b72355 100644 --- a/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java +++ b/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java @@ -24,10 +24,6 @@ */ public class ParseQueryRecyclerViewAdapter extends RecyclerView.Adapter { - private static final int DEFAULT_TYPE = 0; - private static final int PAGINATION_CELL_ROW_TYPE = 1; - - /** * Implement to construct your own custom {@link ParseQuery} for fetching objects. */ @@ -377,14 +373,6 @@ public void loadNextPage() { } } - @Override - public int getItemViewType(int position) { - if (position == getPaginationCellRow()) { - return PAGINATION_CELL_ROW_TYPE; - } - return DEFAULT_TYPE; - } - private View getDefaultView() { if (this.itemResourceId != null) { return View.inflate(context, itemResourceId, null); @@ -407,30 +395,9 @@ private View getDefaultView() { return view; } - /** - * Override this method to customize the "Load Next Page" cell, visible when pagination is turned - * on and there may be more results to display. - *

- * This method expects a {@code TextView} with id {@code android.R.id.text1}. - * - * @return The view object that allows the user to paginate. - */ - public View getNextPageView() { - View v = getDefaultView(); - TextView textView = (TextView) v.findViewById(android.R.id.text1); - textView.setText("Load more..."); - return v; - } - @Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { - switch (viewType) { - case DEFAULT_TYPE: - return new ViewHolder(getDefaultView()); - case PAGINATION_CELL_ROW_TYPE: - return new ViewHolder(getNextPageView()); - } - return null; + return new ViewHolder(getDefaultView()); } @Override From 82a2510395624439a29becff9316f63c2a8240b8 Mon Sep 17 00:00:00 2001 From: Lukas Koebis Date: Thu, 3 Sep 2015 17:31:09 -0700 Subject: [PATCH 03/10] removed some 'this.' --- .../parse/ParseQueryRecyclerViewAdapter.java | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java b/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java index 2b72355..0ca74be 100644 --- a/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java +++ b/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java @@ -253,11 +253,11 @@ public Context getContext() { } public void clear() { - this.objectPages.clear(); + objectPages.clear(); cancelAllQueries(); syncObjectsWithPages(); - this.notifyDataSetChanged(); - this.currentPage = 0; + notifyDataSetChanged(); + currentPage = 0; } private void cancelAllQueries() { @@ -285,7 +285,7 @@ private void loadObjects(final int page, final boolean shouldClear) { setPageOnQuery(page, query); } - this.notifyOnLoadingListeners(); + notifyOnLoadingListeners(); // Create a new page if (page >= objectPages.size()) { @@ -405,10 +405,10 @@ public void onBindViewHolder(ViewHolder viewHolder, final int position) { if (position < objects.size()) { final T object = objects.get(position); - if (this.textKey == null) { + if (textKey == null) { viewHolder.textView.setText(object.getObjectId()); - } else if (object.get(this.textKey) != null) { - viewHolder.textView.setText(object.get(this.textKey).toString()); + } else if (object.get(textKey) != null) { + viewHolder.textView.setText(object.get(textKey).toString()); } else { viewHolder.textView.setText(null); } @@ -418,8 +418,8 @@ public void onBindViewHolder(ViewHolder viewHolder, final int position) { throw new IllegalStateException( "Your object views must have a ParseImageView whose id attribute is 'android.R.id.icon' if an imageKey is specified"); } - if (!this.imageViewSet.containsKey(viewHolder.imageView)) { - this.imageViewSet.put(viewHolder.imageView, null); + if (!imageViewSet.containsKey(viewHolder.imageView)) { + imageViewSet.put(viewHolder.imageView, null); } viewHolder.imageView.setPlaceholder(placeholder); viewHolder.imageView.setParseFile((ParseFile) object.get(imageKey)); @@ -453,9 +453,9 @@ public void onClick(View v) { @Override public int getItemCount() { - int count = this.objects.size(); + int count = objects.size(); - if (this.shouldShowPaginationCell()) { + if (shouldShowPaginationCell()) { count++; } @@ -465,9 +465,9 @@ public int getItemCount() { @Override public void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { super.registerAdapterDataObserver(observer); - this.dataSetObservers.put(observer, null); - if (this.autoload) { - this.loadObjects(); + dataSetObservers.put(observer, null); + if (autoload) { + loadObjects(); } } @@ -494,17 +494,17 @@ private int getPaginationCellRow() { } private boolean shouldShowPaginationCell() { - return this.paginationEnabled && this.objects.size() > 0 && this.hasNextPage; + return paginationEnabled && objects.size() > 0 && hasNextPage; } private void notifyOnLoadingListeners() { - for (OnQueryLoadListener listener : this.onQueryLoadListeners) { + for (OnQueryLoadListener listener : onQueryLoadListeners) { listener.onLoading(); } } private void notifyOnLoadedListeners(List objects, Exception e) { - for (OnQueryLoadListener listener : this.onQueryLoadListeners) { + for (OnQueryLoadListener listener : onQueryLoadListeners) { listener.onLoaded(objects, e); } } @@ -523,8 +523,8 @@ private void notifyOnLoadedListeners(List objects, Exception e) { * used in its mutated form. */ protected void setPageOnQuery(int page, ParseQuery query) { - query.setLimit(this.objectsPerPage + 1); - query.setSkip(page * this.objectsPerPage); + query.setLimit(objectsPerPage + 1); + query.setSkip(page * objectsPerPage); } public void setTextKey(String textKey) { @@ -540,7 +540,7 @@ public void setObjectsPerPage(int objectsPerPage) { } public int getObjectsPerPage() { - return this.objectsPerPage; + return objectsPerPage; } /** @@ -568,7 +568,7 @@ public void setPlaceholder(Drawable placeholder) { return; } this.placeholder = placeholder; - Iterator iter = this.imageViewSet.keySet().iterator(); + Iterator iter = imageViewSet.keySet().iterator(); ParseImageView imageView; while (iter.hasNext()) { imageView = iter.next(); @@ -590,22 +590,22 @@ public void setOnClickListener(OnClickListener onClickListener) { * Defaults to true. */ public void setAutoload(boolean autoload) { - if (this.autoload == autoload) { + if (autoload == autoload) { // An extra precaution to prevent an overzealous setAutoload(true) after assignment to an // AdapterView from triggering an unnecessary additional loadObjects(). return; } this.autoload = autoload; - if (this.autoload && !this.dataSetObservers.isEmpty() && this.objects.isEmpty()) { - this.loadObjects(); + if (autoload && !dataSetObservers.isEmpty() && objects.isEmpty()) { + loadObjects(); } } public void addOnQueryLoadListener(OnQueryLoadListener listener) { - this.onQueryLoadListeners.add(listener); + onQueryLoadListeners.add(listener); } public void removeOnQueryLoadListener(OnQueryLoadListener listener) { - this.onQueryLoadListeners.remove(listener); + onQueryLoadListeners.remove(listener); } } From 088742271a391a51ce6335194afaedc5668e387a Mon Sep 17 00:00:00 2001 From: Lukas Koebis Date: Tue, 8 Sep 2015 20:59:43 -0700 Subject: [PATCH 04/10] cleaned up example --- .../{drawable-xxhdpi => drawable}/ic_launcher.png | Bin .../src/main/res/values-v11/themes.xml | 11 ----------- .../src/main/res/values/strings.xml | 1 + settings.gradle | 2 +- 4 files changed, 2 insertions(+), 12 deletions(-) rename ParseQueryAdapterExample/src/main/res/{drawable-xxhdpi => drawable}/ic_launcher.png (100%) delete mode 100644 ParseQueryAdapterExample/src/main/res/values-v11/themes.xml diff --git a/ParseQueryAdapterExample/src/main/res/drawable-xxhdpi/ic_launcher.png b/ParseQueryAdapterExample/src/main/res/drawable/ic_launcher.png similarity index 100% rename from ParseQueryAdapterExample/src/main/res/drawable-xxhdpi/ic_launcher.png rename to ParseQueryAdapterExample/src/main/res/drawable/ic_launcher.png diff --git a/ParseQueryAdapterExample/src/main/res/values-v11/themes.xml b/ParseQueryAdapterExample/src/main/res/values-v11/themes.xml deleted file mode 100644 index 323816b..0000000 --- a/ParseQueryAdapterExample/src/main/res/values-v11/themes.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/ParseQueryAdapterExample/src/main/res/values/strings.xml b/ParseQueryAdapterExample/src/main/res/values/strings.xml index 7e60729..7f8a974 100644 --- a/ParseQueryAdapterExample/src/main/res/values/strings.xml +++ b/ParseQueryAdapterExample/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ + 4R4qtxUJIYGzoDrJJfNKki1xpFMtWtOwZIeDTGjJ lcTXrnkl9A7dnUlVlKMnnoKMo8Be9z5G8QJ3qINq diff --git a/settings.gradle b/settings.gradle index ab0228a..faa9065 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,4 +8,4 @@ include ':ParseLoginSampleBasic' include ':ParseLoginSampleWithDispatchActivity' include ':ParseLoginSampleCodeCustomization' include ':ParseLoginSampleLayoutOverride' -include ':ParseQueryAdapterExample' \ No newline at end of file +include ':ParseQueryAdapterExample' From ebb8a8b0a5f1d675cff12c377916871d31fcdf18 Mon Sep 17 00:00:00 2001 From: Lukas Koebis Date: Wed, 9 Sep 2015 16:20:18 -0700 Subject: [PATCH 05/10] - updated appcompat dependencies (gradle) - added test file - added copyright / doc - moved binding to ViewHolder - use 2 spaces for indentation --- ParseLoginUI/build.gradle | 4 +- .../ParseQueryRecyclerViewAdapterTest.java | 104 ++ .../parse/ParseQueryRecyclerViewAdapter.java | 1211 +++++++++-------- 3 files changed, 748 insertions(+), 571 deletions(-) create mode 100644 ParseLoginUI/src/androidTest/java/com/parse/ParseQueryRecyclerViewAdapterTest.java diff --git a/ParseLoginUI/build.gradle b/ParseLoginUI/build.gradle index 7136397..ba6eee0 100644 --- a/ParseLoginUI/build.gradle +++ b/ParseLoginUI/build.gradle @@ -3,10 +3,10 @@ apply plugin: 'android-library' dependencies { compile 'com.parse.bolts:bolts-android:1.2.1' compile 'com.android.support:support-v4:22.0.0' - compile 'com.android.support:appcompat-v7:22.0.0' - compile 'com.android.support:recyclerview-v7:22.0.0' compile 'com.parse:parse-android:1.10.1' + provided 'com.android.support:appcompat-v7:22.0.0' + provided 'com.android.support:recyclerview-v7:22.0.0' provided 'com.facebook.android:facebook-android-sdk:4.0.1' provided files("$rootProject.projectDir/ParseLoginUI/libs/ParseFacebookUtilsV4-1.10.1.jar") provided files("$rootProject.projectDir/ParseLoginUI/libs/ParseTwitterUtils-1.10.1.jar") diff --git a/ParseLoginUI/src/androidTest/java/com/parse/ParseQueryRecyclerViewAdapterTest.java b/ParseLoginUI/src/androidTest/java/com/parse/ParseQueryRecyclerViewAdapterTest.java new file mode 100644 index 0000000..aa123d8 --- /dev/null +++ b/ParseLoginUI/src/androidTest/java/com/parse/ParseQueryRecyclerViewAdapterTest.java @@ -0,0 +1,104 @@ +package com.parse; + +import com.parse.ui.TestActivity; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.ArrayList; +import java.util.List; + +import bolts.Task; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Created by lukask on 9/9/15. + */ +public class ParseQueryRecyclerViewAdapterTest extends BaseActivityInstrumentationTestCase2 { + + @ParseClassName("Thing") + public static class Thing extends ParseObject { + public Thing() { + } + } + + public ParseQueryRecyclerViewAdapterTest() { + super(TestActivity.class); + } + + private int TOTAL_THINGS = 10; + private List savedThings = new ArrayList(); + + @Override + public void setUp() throws Exception { + super.setUp(); + + // Register a mock cachedQueryController, the controller maintain a cache list and return + // results based on query state's CachePolicy + ParseQueryController queryController = mock(ParseQueryController.class); + Answer>> queryAnswer = new Answer>>() { + private List cachedThings = new ArrayList<>(); + + @Override + public Task> answer(InvocationOnMock invocation) throws Throwable { + ParseQuery.State state = (ParseQuery.State) invocation.getArguments()[0]; + int start = state.skip(); + // The default value of limit in ParseQuery is -1. + int end = state.limit() > 0 ? + Math.min(state.skip() + state.limit(), TOTAL_THINGS) : TOTAL_THINGS; + List things; + if (state.cachePolicy() == ParseQuery.CachePolicy.CACHE_ONLY) { + try { + things = new ArrayList<>(cachedThings.subList(start, end)); + } catch (IndexOutOfBoundsException e) { + // Cache miss, throw exception + return Task.forError( + new ParseException(ParseException.CACHE_MISS, "results not cached")); + } + } else { + things = new ArrayList<>(savedThings.subList(start, end)); + // Update cache + for (int i = start; i < end; i++) { + if (i < cachedThings.size()) { + cachedThings.set(i, savedThings.get(i)); + } else { + cachedThings.add(i, savedThings.get(i)); + } + } + } + return Task.forResult(things); + } + }; + when(queryController.findAsync(any(ParseQuery.State.class), any(ParseUser.class), any(Task.class))) + .thenAnswer(queryAnswer); + ParseCorePlugins.getInstance().registerQueryController(queryController); + + // Register a mock currentUserController to make getSessionToken work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync()).thenReturn(Task.forResult(mock(ParseUser.class))); + when(currentUserController.getCurrentSessionTokenAsync()) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseObject.registerSubclass(Thing.class); + // Make test data set + for (int i = 0; i < TOTAL_THINGS; i++) { + ParseObject thing = ParseObject.create("Thing"); + thing.put("aValue", i * 10); + thing.put("name", "Thing " + i); + thing.setObjectId(String.valueOf(i)); + savedThings.add(thing); + } + } + + @Override + public void tearDown() throws Exception { + savedThings = null; + ParseCorePlugins.getInstance().reset(); + ParseObject.unregisterSubclass("Thing"); + super.tearDown(); + } +} diff --git a/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java b/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java index 0ca74be..db1012c 100644 --- a/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java +++ b/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java @@ -1,5 +1,25 @@ -package com.parse; +/* + * Copyright (c) 2015, Parse, LLC. All rights reserved. + * + * You are hereby granted a non-exclusive, worldwide, royalty-free license to use, + * copy, modify, and distribute this software in source code or binary form for use + * in connection with the web services and APIs provided by Parse. + * + * As with any software that integrates with the Parse platform, your use of + * this software is subject to the Parse Terms of Service + * [https://www.parse.com/about/terms]. This copyright notice shall be + * included in all copies or substantial portions of the software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.parse; import android.content.Context; import android.graphics.drawable.Drawable; @@ -20,592 +40,645 @@ import bolts.Capture; /** - * Created by lukask on 9/1/15. + * A {@code ParseQueryRecyclerViewAdapter} handles the fetching of objects by page, and displaying + * objects as views in a {@link android.support.v7.widget.RecyclerView}. + *

+ * This class is highly configurable, but also intended to be easy to get started with. See below + * for an example of using a {@code ParseQueryRecyclerViewAdapter} inside an + * {@link android.app.Activity}'s {@code onCreate}: + *

+ * final ParseQueryRecyclerViewAdapter adapter
+ *         = new ParseQueryRecyclerViewAdapter(this, "TestObject");
+ * adapter.setTextKey("name");
+ *
+ * RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
+ * recyclerView.setAdapter(adapter);
+ * recyclerView.setLayoutManager(new LinearLayoutManager(this));
+ *
+ * adapter.loadObjects();
+ * 
+ *

+ * Below, an example showing off the level of configuration available with this class: + *

+ * // Instantiate a QueryFactory to define the ParseQuery to be used for fetching items in this
+ * // Adapter.
+ * ParseQueryRecyclerViewAdapter.QueryFactory<ParseObject> factory =
+ *     new ParseQueryRecyclerViewAdapter.QueryFactory<ParseObject>() {
+ *       public ParseQuery create() {
+ *         ParseQuery query = new ParseQuery("Customer");
+ *         query.whereEqualTo("activated", true);
+ *         query.orderByDescending("moneySpent");
+ *         return query;
+ *       }
+ *     };
+ *
+ * // Pass the factory into the ParseQueryRecyclerViewAdapter's constructor.
+ * ParseQueryRecyclerViewAdapter<ParseObject> adapter
+ *         = new ParseQueryRecyclerViewAdapter<ParseObject>(this, factory);
+ * adapter.setTextKey("name");
+ *
+ * // Perhaps set a callback to be fired upon successful loading of a new set of ParseObjects.
+ * adapter.addOnQueryLoadListener(new OnQueryLoadListener<ParseObject>() {
+ *   public void onLoading() {
+ *     // Trigger any "loading" UI
+ *   }
+ *
+ *   public void onLoaded(List<ParseObject> objects, ParseException e) {
+ *     // Execute any post-loading logic, hide "loading" UI
+ *   }
+ * });
+ *
+ * 
*/ public class ParseQueryRecyclerViewAdapter extends RecyclerView.Adapter { - /** - * Implement to construct your own custom {@link ParseQuery} for fetching objects. - */ - public interface QueryFactory { - ParseQuery create(); - } - - /** - * Implement with logic that is called before and after objects are fetched from Parse by the - * adapter. - */ - public interface OnQueryLoadListener { - void onLoading(); - - void onLoaded(List objects, Exception e); - } - - /** - * OnClickListener. - */ - public interface OnClickListener { - void onClick(T item, int position); - } - - /** - * ViewHolder. - */ - public static class ViewHolder extends RecyclerView.ViewHolder { - TextView textView; - ParseImageView imageView; - - public ViewHolder(View itemView) { - super(itemView); - - try { - textView = (TextView) itemView.findViewById(android.R.id.text1); - } catch (ClassCastException ex) { - throw new IllegalStateException( - "Your object views must have a TextView whose id attribute is 'android.R.id.text1'", ex); - } - try { - imageView = (ParseImageView) itemView.findViewById(android.R.id.icon); - } catch (ClassCastException ex) { - throw new IllegalStateException( - "Your object views must have a ParseImageView whose id attribute is 'android.R.id.icon'", ex); - } + /** + * Implement to construct your own custom {@link ParseQuery} for fetching objects. + */ + public interface QueryFactory { + ParseQuery create(); + } + + /** + * Implement with logic that is called before and after objects are fetched from Parse by the + * adapter. + */ + public interface OnQueryLoadListener { + void onLoading(); + + void onLoaded(List objects, Exception e); + } + + /** + * OnClickListener. + */ + public interface OnClickListener { + void onClick(T item, int position); + } + + /** + * ViewHolder. + */ + public class ViewHolder extends RecyclerView.ViewHolder { + private TextView textView; + private ParseImageView imageView; + + public ViewHolder(View itemView) { + super(itemView); + + try { + textView = (TextView) itemView.findViewById(android.R.id.text1); + } catch (ClassCastException ex) { + throw new IllegalStateException( + "Your object views must have a TextView whose id attribute is 'android.R.id.text1'", ex); + } + try { + imageView = (ParseImageView) itemView.findViewById(android.R.id.icon); + } catch (ClassCastException ex) { + throw new IllegalStateException( + "Your object views must have a ParseImageView whose id attribute is 'android.R.id.icon'", ex); + } + } + + public void bind(final T object, final int position) { + if (textKey == null) { + textView.setText(object.getObjectId()); + } else if (object.get(textKey) != null) { + textView.setText(object.get(textKey).toString()); + } else { + textView.setText(null); + } + + if (imageKey != null) { + if (imageView == null) { + throw new IllegalStateException( + "Your object views must have a ParseImageView whose id attribute is 'android.R.id.icon' if an imageKey is specified"); } - } - - // The key to use to display on the cell text label. - private String textKey; - - // The key to use to fetch an image for display in the cell's image view. - private String imageKey; - - // The number of objects to show per page (default: 25) - private int objectsPerPage = 25; - - // Whether the table should use the built-in pagination feature (default: - // true) - private boolean paginationEnabled = true; - - // A Drawable placeholder, to be set on ParseImageViews while images are loading. Can be null. - private Drawable placeholder; - - // A WeakHashMap, holding references to ParseImageViews that have been configured. - // Accessed and iterated over if setPlaceholder(Drawable) is called after some set of - // ParseImageViews have already been instantiated and configured. - private WeakHashMap imageViewSet = new WeakHashMap<>(); - - // A WeakHashMap, keeping track of the DataSetObservers on this class - private WeakHashMap dataSetObservers = new WeakHashMap<>(); - - // Whether the adapter should trigger loadObjects() on registerDataSetObserver(); Defaults to - // true. - private boolean autoload = true; - - private Context context; - - private List objects = new ArrayList<>(); - - private Set runningQueries = Collections.newSetFromMap(new ConcurrentHashMap()); - - // Used to keep track of the pages of objects when using CACHE_THEN_NETWORK. When using this, - // the data will be flattened and put into the objects list. - private List> objectPages = new ArrayList<>(); - - private int currentPage = 0; - - private Integer itemResourceId; - - private boolean hasNextPage = true; - - private QueryFactory queryFactory; - - private OnClickListener onClickListener; - - private List> onQueryLoadListeners = new ArrayList<>(); - - /** - * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, - * this adapter will fetch and display all {@link ParseObject}s of the specified class, - * ordered by creation time. - * - * @param context - * The activity utilizing this adapter. - * @param clazz - * The {@link ParseObject} subclass type to fetch and display. - */ - public ParseQueryRecyclerViewAdapter(Context context, Class clazz) { - this(context, ParseObject.getClassName(clazz)); - } - - /** - * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, - * this adapter will fetch and display all {@link ParseObject}s of the specified class, ordered - * by creation time. - * - * @param context - * The activity utilizing this adapter. - * @param className - * The name of the Parse class of {@link ParseObject}s to display. - */ - public ParseQueryRecyclerViewAdapter(Context context, final String className) { - this(context, new QueryFactory() { - @Override - public ParseQuery create() { - ParseQuery query = ParseQuery.getQuery(className); - query.orderByDescending("createdAt"); - - return query; - } - }); - - if (className == null) { - throw new RuntimeException("You need to specify a className for the ParseQueryAdapter"); + if (!imageViewSet.containsKey(imageView)) { + imageViewSet.put(imageView, null); } - } - - /** - * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, - * this adapter will fetch and display all {@link ParseObject}s of the specified class, ordered - * by creation time. - * - * @param context - * The activity utilizing this adapter. - * @param clazz - * The {@link ParseObject} subclass type to fetch and display. - * @param itemViewResource - * A resource id that represents the layout for an item in the AdapterView. - */ - public ParseQueryRecyclerViewAdapter(Context context, Class clazz, - int itemViewResource) { - this(context, ParseObject.getClassName(clazz), itemViewResource); - } - - /** - * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, - * this adapter will fetch and display all {@link ParseObject}s of the specified class, ordered - * by creation time. - * - * @param context - * The activity utilizing this adapter. - * @param className - * The name of the Parse class of {@link ParseObject}s to display. - * @param itemViewResource - * A resource id that represents the layout for an item in the AdapterView. - */ - public ParseQueryRecyclerViewAdapter(Context context, final String className, int itemViewResource) { - this(context, new QueryFactory() { - @Override - public ParseQuery create() { - ParseQuery query = ParseQuery.getQuery(className); - query.orderByDescending("createdAt"); - - return query; - } - }, itemViewResource); - - if (className == null) { - throw new RuntimeException( - "You need to specify a className for the ParseQueryRecyclerViewAdapter"); + imageView.setPlaceholder(placeholder); + imageView.setParseFile((ParseFile) object.get(imageKey)); + imageView.loadInBackground(); + } + textView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (onClickListener != null) { + onClickListener.onClick(object, position); + } } - } - /** - * Constructs a {@code ParseQueryRecyclerViewAdapter}. Allows the caller to define further - * constraints on the {@link ParseQuery} to be used when fetching items from Parse. - * - * @param context - * The activity utilizing this adapter. - * @param queryFactory - * A {@link QueryFactory} to build a {@link ParseQuery} for fetching objects. - */ - public ParseQueryRecyclerViewAdapter(Context context, QueryFactory queryFactory) { - this(context, queryFactory, null); - } - - /** - * Constructs a {@code ParseQueryRecyclerViewAdapter}. Allows the caller to define further - * constraints on the {@link ParseQuery} to be used when fetching items from Parse. - * - * @param context - * The activity utilizing this adapter. - * @param queryFactory - * A {@link QueryFactory} to build a {@link ParseQuery} for fetching objects. - * @param itemViewResource - * A resource id that represents the layout for an item in the AdapterView. - */ - public ParseQueryRecyclerViewAdapter(Context context, QueryFactory queryFactory, int itemViewResource) { - this(context, queryFactory, Integer.valueOf(itemViewResource)); - } - - private ParseQueryRecyclerViewAdapter(Context context, QueryFactory queryFactory, Integer itemViewResource) { - super(); - this.context = context; - this.queryFactory = queryFactory; - this.itemResourceId = itemViewResource; - } - - /** - * Return the context provided by the {@code Activity} utilizing this {@code ParseQueryAdapter}. - * - * @return The activity utilizing this adapter. - */ - public Context getContext() { - return this.context; - } - - public void clear() { - objectPages.clear(); - cancelAllQueries(); - syncObjectsWithPages(); - notifyDataSetChanged(); - currentPage = 0; - } - - private void cancelAllQueries() { - for (ParseQuery q : runningQueries) { - q.cancel(); + }); + } + + public void setNextView() { + textView.setText("Load more..."); + if (imageView != null) { + imageView.setVisibility(View.GONE); + } + textView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + loadNextPage(); } - runningQueries.clear(); - } - - /** - * Clears the table and loads the first page of objects asynchronously. This method is called - * automatically when this {@code Adapter} is attached to an {@code AdapterView}. - *

- * {@code loadObjects()} should only need to be called if {@link #setAutoload(boolean)} is set to - * {@code false}. - */ - public void loadObjects() { - loadObjects(0, true); + }); } - - private void loadObjects(final int page, final boolean shouldClear) { - final ParseQuery query = queryFactory.create(); - - if (objectsPerPage > 0 && paginationEnabled) { - setPageOnQuery(page, query); + } + + // The key to use to display on the cell text label. + private String textKey; + + // The key to use to fetch an image for display in the cell's image view. + private String imageKey; + + // The number of objects to show per page (default: 25) + private int objectsPerPage = 25; + + // Whether the table should use the built-in pagination feature (default: + // true) + private boolean paginationEnabled = true; + + // A Drawable placeholder, to be set on ParseImageViews while images are loading. Can be null. + private Drawable placeholder; + + // A WeakHashMap, holding references to ParseImageViews that have been configured. + // Accessed and iterated over if setPlaceholder(Drawable) is called after some set of + // ParseImageViews have already been instantiated and configured. + private WeakHashMap imageViewSet = new WeakHashMap<>(); + + // A WeakHashMap, keeping track of the DataSetObservers on this class + private WeakHashMap dataSetObservers = new WeakHashMap<>(); + + // Whether the adapter should trigger loadObjects() on registerDataSetObserver(); Defaults to + // true. + private boolean autoload = true; + + private Context context; + + private List objects = new ArrayList<>(); + + private Set runningQueries = Collections.newSetFromMap(new ConcurrentHashMap()); + + // Used to keep track of the pages of objects when using CACHE_THEN_NETWORK. When using this, + // the data will be flattened and put into the objects list. + private List> objectPages = new ArrayList<>(); + + private int currentPage = 0; + + private int itemResourceId; + + private boolean hasNextPage = true; + + private QueryFactory queryFactory; + + private OnClickListener onClickListener; + + private List> onQueryLoadListeners = new ArrayList<>(); + + /** + * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, + * this adapter will fetch and display all {@link ParseObject}s of the specified class, + * ordered by creation time. + * + * @param context + * The activity utilizing this adapter. + * @param clazz + * The {@link ParseObject} subclass type to fetch and display. + */ + public ParseQueryRecyclerViewAdapter(Context context, Class clazz) { + this(context, ParseObject.getClassName(clazz)); + } + + /** + * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, + * this adapter will fetch and display all {@link ParseObject}s of the specified class, ordered + * by creation time. + * + * @param context + * The activity utilizing this adapter. + * @param className + * The name of the Parse class of {@link ParseObject}s to display. + */ + public ParseQueryRecyclerViewAdapter(Context context, final String className) { + this(context, new QueryFactory() { + @Override + public ParseQuery create() { + ParseQuery query = ParseQuery.getQuery(className); + query.orderByDescending("createdAt"); + + return query; + } + }); + + if (className == null) { + throw new RuntimeException("You need to specify a className for the ParseQueryAdapter"); + } + } + + /** + * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, + * this adapter will fetch and display all {@link ParseObject}s of the specified class, ordered + * by creation time. + * + * @param context + * The activity utilizing this adapter. + * @param clazz + * The {@link ParseObject} subclass type to fetch and display. + * @param itemViewResource + * A resource id that represents the layout for an item in the AdapterView. + */ + public ParseQueryRecyclerViewAdapter( + Context context, Class clazz, int itemViewResource) { + this(context, ParseObject.getClassName(clazz), itemViewResource); + } + + /** + * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, + * this adapter will fetch and display all {@link ParseObject}s of the specified class, ordered + * by creation time. + * + * @param context + * The activity utilizing this adapter. + * @param className + * The name of the Parse class of {@link ParseObject}s to display. + * @param itemViewResource + * A resource id that represents the layout for an item in the AdapterView. + */ + public ParseQueryRecyclerViewAdapter( + Context context, final String className, int itemViewResource) { + this(context, new QueryFactory() { + @Override + public ParseQuery create() { + ParseQuery query = ParseQuery.getQuery(className); + query.orderByDescending("createdAt"); + + return query; + } + }, itemViewResource); + + if (className == null) { + throw new RuntimeException( + "You need to specify a className for the ParseQueryRecyclerViewAdapter"); + } + } + /** + * Constructs a {@code ParseQueryRecyclerViewAdapter}. Allows the caller to define further + * constraints on the {@link ParseQuery} to be used when fetching items from Parse. + * + * @param context + * The activity utilizing this adapter. + * @param queryFactory + * A {@link QueryFactory} to build a {@link ParseQuery} for fetching objects. + */ + public ParseQueryRecyclerViewAdapter(Context context, QueryFactory queryFactory) { + this(context, queryFactory, -1); + } + + /** + * Constructs a {@code ParseQueryRecyclerViewAdapter}. Allows the caller to define further + * constraints on the {@link ParseQuery} to be used when fetching items from Parse. + * + * @param context + * The activity utilizing this adapter. + * @param queryFactory + * A {@link QueryFactory} to build a {@link ParseQuery} for fetching objects. + * @param itemViewResource + * A resource id (>0) that represents the layout for an item in the AdapterView. + */ + public ParseQueryRecyclerViewAdapter( + Context context, QueryFactory queryFactory, int itemViewResource) { + super(); + this.context = context; + this.queryFactory = queryFactory; + this.itemResourceId = itemViewResource; + } + + /** + * Return the context provided by the {@code Activity} utilizing this {@code ParseQueryAdapter}. + * + * @return The activity utilizing this adapter. + */ + public Context getContext() { + return this.context; + } + + public void clear() { + objectPages.clear(); + cancelAllQueries(); + syncObjectsWithPages(); + notifyDataSetChanged(); + currentPage = 0; + } + + private void cancelAllQueries() { + for (ParseQuery q : runningQueries) { + q.cancel(); + } + runningQueries.clear(); + } + + /** + * Clears the table and loads the first page of objects asynchronously. This method is called + * automatically when this {@code Adapter} is attached to an {@code AdapterView}. + *

+ * {@code loadObjects()} should only need to be called if {@link #setAutoload(boolean)} is set to + * {@code false}. + */ + public void loadObjects() { + loadObjects(0, true); + } + + private void loadObjects(final int page, final boolean shouldClear) { + final ParseQuery query = queryFactory.create(); + + if (objectsPerPage > 0 && paginationEnabled) { + setPageOnQuery(page, query); + } + + notifyOnLoadingListeners(); + + // Create a new page + if (page >= objectPages.size()) { + objectPages.add(page, new ArrayList()); + } + + // In the case of CACHE_THEN_NETWORK, two callbacks will be called. Using this flag to keep track, + final Capture firstCallBack = new Capture<>(true); + + runningQueries.add(query); + + // TODO convert to Tasks and CancellationTokens + // (depends on https://github.com/ParsePlatform/Parse-SDK-Android/issues/6) + query.findInBackground(new FindCallback() { + @Override + public void done(List foundObjects, ParseException e) { + if (!runningQueries.contains(query)) { + return; } - - notifyOnLoadingListeners(); - - // Create a new page - if (page >= objectPages.size()) { - objectPages.add(page, new ArrayList()); + // In the case of CACHE_THEN_NETWORK, two callbacks will be called. We can only remove the + // query after the second callback. + if (query.getCachePolicy() != ParseQuery.CachePolicy.CACHE_THEN_NETWORK || + (query.getCachePolicy() == ParseQuery.CachePolicy.CACHE_THEN_NETWORK && !firstCallBack.get())) { + runningQueries.remove(query); } - - // In the case of CACHE_THEN_NETWORK, two callbacks will be called. Using this flag to keep track, - final Capture firstCallBack = new Capture<>(true); - - runningQueries.add(query); - - // TODO convert to Tasks and CancellationTokens - // (depends on https://github.com/ParsePlatform/Parse-SDK-Android/issues/6) - query.findInBackground(new FindCallback() { - @Override - public void done(List foundObjects, ParseException e) { - if (!runningQueries.contains(query)) { - return; - } - // In the case of CACHE_THEN_NETWORK, two callbacks will be called. We can only remove the - // query after the second callback. - if (query.getCachePolicy() != ParseQuery.CachePolicy.CACHE_THEN_NETWORK || - (query.getCachePolicy() == ParseQuery.CachePolicy.CACHE_THEN_NETWORK && !firstCallBack.get())) { - runningQueries.remove(query); - } - if ((!Parse.isLocalDatastoreEnabled() && - query.getCachePolicy() == ParseQuery.CachePolicy.CACHE_ONLY) - && (e != null) && e.getCode() == ParseException.CACHE_MISS) { - // no-op on cache miss - return; - } - - if ((e != null) && ((e.getCode() == ParseException.CONNECTION_FAILED) || (e.getCode() != ParseException.CACHE_MISS))) { - hasNextPage = true; - } else if (foundObjects != null) { - if (shouldClear && firstCallBack.get()) { - runningQueries.remove(query); - cancelAllQueries(); - runningQueries.add(query); // allow 2nd callback - objectPages.clear(); - objectPages.add(new ArrayList()); - currentPage = page; - firstCallBack.set(false); - } - - // Only advance the page, this prevents second call back from CACHE_THEN_NETWORK to - // reset the page. - if (page >= currentPage) { - currentPage = page; - - // since we set limit == objectsPerPage + 1 - hasNextPage = (foundObjects.size() > objectsPerPage); - } - - if (paginationEnabled && foundObjects.size() > objectsPerPage) { - // Remove the last object, fetched in order to tell us whether there was a "next page" - foundObjects.remove(objectsPerPage); - } - - List currentPage = objectPages.get(page); - currentPage.clear(); - currentPage.addAll(foundObjects); - - syncObjectsWithPages(); - - // executes on the UI thread - notifyDataSetChanged(); - } - - notifyOnLoadedListeners(foundObjects, e); - } - }); - } - - /** - * Loads the next page of objects, appends to table, and notifies the UI that the model has - * changed. - */ - public void loadNextPage() { - if (objects.size() == 0 && runningQueries.size() == 0) { - loadObjects(0, false); - } - else { - loadObjects(currentPage + 1, false); - } - } - - private View getDefaultView() { - if (this.itemResourceId != null) { - return View.inflate(context, itemResourceId, null); - } - LinearLayout view = new LinearLayout(context); - view.setPadding(8, 4, 8, 4); - - ParseImageView imageView = new ParseImageView(context); - imageView.setId(android.R.id.icon); - imageView.setLayoutParams(new LinearLayout.LayoutParams(50, 50)); - view.addView(imageView); - - TextView textView = new TextView(context); - textView.setId(android.R.id.text1); - textView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); - textView.setPadding(8, 0, 0, 0); - view.addView(textView); - - return view; - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { - return new ViewHolder(getDefaultView()); - } - - @Override - public void onBindViewHolder(ViewHolder viewHolder, final int position) { - if (position < objects.size()) { - final T object = objects.get(position); - - if (textKey == null) { - viewHolder.textView.setText(object.getObjectId()); - } else if (object.get(textKey) != null) { - viewHolder.textView.setText(object.get(textKey).toString()); - } else { - viewHolder.textView.setText(null); - } - - if (imageKey != null) { - if (viewHolder.imageView == null) { - throw new IllegalStateException( - "Your object views must have a ParseImageView whose id attribute is 'android.R.id.icon' if an imageKey is specified"); - } - if (!imageViewSet.containsKey(viewHolder.imageView)) { - imageViewSet.put(viewHolder.imageView, null); - } - viewHolder.imageView.setPlaceholder(placeholder); - viewHolder.imageView.setParseFile((ParseFile) object.get(imageKey)); - viewHolder.imageView.loadInBackground(); - } - viewHolder.textView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (onClickListener != null) { - onClickListener.onClick(object, position); - } - } - }); - } - else if (position == objects.size()) { - viewHolder.textView.setText("Load more..."); - if (viewHolder.imageView != null) { - viewHolder.imageView.setVisibility(View.GONE); - } - viewHolder.textView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - loadNextPage(); - } - }); - } - else { - throw new RuntimeException(); - } - } - - @Override - public int getItemCount() { - int count = objects.size(); - - if (shouldShowPaginationCell()) { - count++; - } - - return count; - } - - @Override - public void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { - super.registerAdapterDataObserver(observer); - dataSetObservers.put(observer, null); - if (autoload) { - loadObjects(); - } - } - - @Override - public void unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { - super.unregisterAdapterDataObserver(observer); - this.dataSetObservers.remove(observer); - } - - - /** - * This is a helper function to sync the objects with objectPages. This is only used with the - * CACHE_THEN_NETWORK option. - */ - private void syncObjectsWithPages() { - objects.clear(); - for (List pageOfObjects : objectPages) { - objects.addAll(pageOfObjects); - } - } - - private int getPaginationCellRow() { - return objects.size(); - } - - private boolean shouldShowPaginationCell() { - return paginationEnabled && objects.size() > 0 && hasNextPage; - } - - private void notifyOnLoadingListeners() { - for (OnQueryLoadListener listener : onQueryLoadListeners) { - listener.onLoading(); + if ((!Parse.isLocalDatastoreEnabled() && + query.getCachePolicy() == ParseQuery.CachePolicy.CACHE_ONLY) + && (e != null) && e.getCode() == ParseException.CACHE_MISS) { + // no-op on cache miss + return; } - } - private void notifyOnLoadedListeners(List objects, Exception e) { - for (OnQueryLoadListener listener : onQueryLoadListeners) { - listener.onLoaded(objects, e); + if ((e != null) && ((e.getCode() == ParseException.CONNECTION_FAILED) || (e.getCode() != ParseException.CACHE_MISS))) { + hasNextPage = true; + } else if (foundObjects != null) { + if (shouldClear && firstCallBack.get()) { + runningQueries.remove(query); + cancelAllQueries(); + runningQueries.add(query); // allow 2nd callback + objectPages.clear(); + objectPages.add(new ArrayList()); + currentPage = page; + firstCallBack.set(false); + } + + // Only advance the page, this prevents second call back from CACHE_THEN_NETWORK to + // reset the page. + if (page >= currentPage) { + currentPage = page; + + // since we set limit == objectsPerPage + 1 + hasNextPage = (foundObjects.size() > objectsPerPage); + } + + if (paginationEnabled && foundObjects.size() > objectsPerPage) { + // Remove the last object, fetched in order to tell us whether there was a "next page" + foundObjects.remove(objectsPerPage); + } + + List currentPage = objectPages.get(page); + currentPage.clear(); + currentPage.addAll(foundObjects); + + syncObjectsWithPages(); + + // executes on the UI thread + notifyDataSetChanged(); } - } - - /** - * Override this method to manually paginate the provided {@code ParseQuery}. By default, this - * method will set the {@code limit} value to {@link #getObjectsPerPage()} and the {@code skip} - * value to {@link #getObjectsPerPage()} * {@code page}. - *

- * Overriding this method will not be necessary, in most cases. - * - * @param page - * the page number of results to fetch from Parse. - * @param query - * the {@link ParseQuery} used to fetch items from Parse. This query will be mutated and - * used in its mutated form. - */ - protected void setPageOnQuery(int page, ParseQuery query) { - query.setLimit(objectsPerPage + 1); - query.setSkip(page * objectsPerPage); - } - public void setTextKey(String textKey) { - this.textKey = textKey; - } + notifyOnLoadedListeners(foundObjects, e); + } + }); + } - public void setImageKey(String imageKey) { - this.imageKey = imageKey; + /** + * Loads the next page of objects, appends to table, and notifies the UI that the model has + * changed. + */ + public void loadNextPage() { + if (objects.size() == 0 && runningQueries.size() == 0) { + loadObjects(0, false); } - - public void setObjectsPerPage(int objectsPerPage) { - this.objectsPerPage = objectsPerPage; - } - - public int getObjectsPerPage() { - return objectsPerPage; + else { + loadObjects(currentPage + 1, false); } + } - /** - * Enable or disable pagination of results. Defaults to true. - * - * @param paginationEnabled - * Defaults to true. - */ - public void setPaginationEnabled(boolean paginationEnabled) { - this.paginationEnabled = paginationEnabled; + private View getDefaultView() { + if (this.itemResourceId != -1) { + return View.inflate(context, itemResourceId, null); } + LinearLayout view = new LinearLayout(context); + view.setPadding(8, 4, 8, 4); - /** - * Sets a placeholder image to be used when fetching data for each item in the {@code AdapterView} - * . Will not be used if {@link #setImageKey(String)} was not used to define which images to - * display. - * - * @param placeholder - * A {@code Drawable} to be displayed while the remote image data is being fetched. This - * value can be null, and {@code ImageView}s in this AdapterView will simply be blank - * while data is being fetched. - */ - public void setPlaceholder(Drawable placeholder) { - if (this.placeholder == placeholder) { - return; - } - this.placeholder = placeholder; - Iterator iter = imageViewSet.keySet().iterator(); - ParseImageView imageView; - while (iter.hasNext()) { - imageView = iter.next(); - if (imageView != null) { - imageView.setPlaceholder(this.placeholder); - } - } - } - - public void setOnClickListener(OnClickListener onClickListener) { - this.onClickListener = onClickListener; - } - - /** - * Enable or disable the automatic loading of results upon attachment to an {@code AdapterView}. - * Defaults to true. - * - * @param autoload - * Defaults to true. - */ - public void setAutoload(boolean autoload) { - if (autoload == autoload) { - // An extra precaution to prevent an overzealous setAutoload(true) after assignment to an - // AdapterView from triggering an unnecessary additional loadObjects(). - return; - } - this.autoload = autoload; - if (autoload && !dataSetObservers.isEmpty() && objects.isEmpty()) { - loadObjects(); - } - } - - public void addOnQueryLoadListener(OnQueryLoadListener listener) { - onQueryLoadListeners.add(listener); - } - - public void removeOnQueryLoadListener(OnQueryLoadListener listener) { - onQueryLoadListeners.remove(listener); - } + ParseImageView imageView = new ParseImageView(context); + imageView.setId(android.R.id.icon); + imageView.setLayoutParams(new LinearLayout.LayoutParams(50, 50)); + view.addView(imageView); + + TextView textView = new TextView(context); + textView.setId(android.R.id.text1); + textView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + textView.setPadding(8, 0, 0, 0); + view.addView(textView); + + return view; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { + return new ViewHolder(getDefaultView()); + } + + @Override + public void onBindViewHolder(ViewHolder viewHolder, final int position) { + if (position < objects.size()) { + final T object = objects.get(position); + viewHolder.bind(object, position); + } + else if (position == objects.size()) { + viewHolder.setNextView(); + } + else { + throw new RuntimeException(); + } + } + + @Override + public int getItemCount() { + int count = objects.size(); + + if (shouldShowPaginationCell()) { + count++; + } + + return count; + } + + @Override + public void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { + super.registerAdapterDataObserver(observer); + dataSetObservers.put(observer, null); + if (autoload) { + loadObjects(); + } + } + + @Override + public void unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { + super.unregisterAdapterDataObserver(observer); + this.dataSetObservers.remove(observer); + } + + + /** + * This is a helper function to sync the objects with objectPages. This is only used with the + * CACHE_THEN_NETWORK option. + */ + private void syncObjectsWithPages() { + objects.clear(); + for (List pageOfObjects : objectPages) { + objects.addAll(pageOfObjects); + } + } + + private int getPaginationCellRow() { + return objects.size(); + } + + private boolean shouldShowPaginationCell() { + return paginationEnabled && objects.size() > 0 && hasNextPage; + } + + private void notifyOnLoadingListeners() { + for (OnQueryLoadListener listener : onQueryLoadListeners) { + listener.onLoading(); + } + } + + private void notifyOnLoadedListeners(List objects, Exception e) { + for (OnQueryLoadListener listener : onQueryLoadListeners) { + listener.onLoaded(objects, e); + } + } + + /** + * Override this method to manually paginate the provided {@code ParseQuery}. By default, this + * method will set the {@code limit} value to {@link #getObjectsPerPage()} and the {@code skip} + * value to {@link #getObjectsPerPage()} * {@code page}. + *

+ * Overriding this method will not be necessary, in most cases. + * + * @param page + * the page number of results to fetch from Parse. + * @param query + * the {@link ParseQuery} used to fetch items from Parse. This query will be mutated and + * used in its mutated form. + */ + protected void setPageOnQuery(int page, ParseQuery query) { + query.setLimit(objectsPerPage + 1); + query.setSkip(page * objectsPerPage); + } + + public void setTextKey(String textKey) { + this.textKey = textKey; + } + + public void setImageKey(String imageKey) { + this.imageKey = imageKey; + } + + public void setObjectsPerPage(int objectsPerPage) { + this.objectsPerPage = objectsPerPage; + } + + public int getObjectsPerPage() { + return objectsPerPage; + } + + /** + * Enable or disable pagination of results. Defaults to true. + * + * @param paginationEnabled + * Defaults to true. + */ + public void setPaginationEnabled(boolean paginationEnabled) { + this.paginationEnabled = paginationEnabled; + } + + /** + * Sets a placeholder image to be used when fetching data for each item in the {@code AdapterView} + * . Will not be used if {@link #setImageKey(String)} was not used to define which images to + * display. + * + * @param placeholder + * A {@code Drawable} to be displayed while the remote image data is being fetched. This + * value can be null, and {@code ImageView}s in this AdapterView will simply be blank + * while data is being fetched. + */ + public void setPlaceholder(Drawable placeholder) { + if (this.placeholder == placeholder) { + return; + } + this.placeholder = placeholder; + Iterator iter = imageViewSet.keySet().iterator(); + ParseImageView imageView; + while (iter.hasNext()) { + imageView = iter.next(); + if (imageView != null) { + imageView.setPlaceholder(this.placeholder); + } + } + } + + public void setOnClickListener(OnClickListener onClickListener) { + this.onClickListener = onClickListener; + } + + /** + * Enable or disable the automatic loading of results upon attachment to an {@code AdapterView}. + * Defaults to true. + * + * @param autoload + * Defaults to true. + */ + public void setAutoload(boolean autoload) { + if (autoload == autoload) { + // An extra precaution to prevent an overzealous setAutoload(true) after assignment to an + // AdapterView from triggering an unnecessary additional loadObjects(). + return; + } + this.autoload = autoload; + if (autoload && !dataSetObservers.isEmpty() && objects.isEmpty()) { + loadObjects(); + } + } + + public void addOnQueryLoadListener(OnQueryLoadListener listener) { + onQueryLoadListeners.add(listener); + } + + public void removeOnQueryLoadListener(OnQueryLoadListener listener) { + onQueryLoadListeners.remove(listener); + } } From cb4c4942fcedd6104406cd2010a201de09e85f26 Mon Sep 17 00:00:00 2001 From: Lukas Koebis Date: Wed, 9 Sep 2015 17:28:58 -0700 Subject: [PATCH 06/10] fixed build --- ParseLoginUI/build.gradle | 5 +++-- .../main/java/com/parse/ParseQueryRecyclerViewAdapter.java | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ParseLoginUI/build.gradle b/ParseLoginUI/build.gradle index ba6eee0..cdad84f 100644 --- a/ParseLoginUI/build.gradle +++ b/ParseLoginUI/build.gradle @@ -5,8 +5,9 @@ dependencies { compile 'com.android.support:support-v4:22.0.0' compile 'com.parse:parse-android:1.10.1' - provided 'com.android.support:appcompat-v7:22.0.0' - provided 'com.android.support:recyclerview-v7:22.0.0' + compile 'com.android.support:appcompat-v7:22.0.0' + compile 'com.android.support:recyclerview-v7:22.0.0' + provided 'com.facebook.android:facebook-android-sdk:4.0.1' provided files("$rootProject.projectDir/ParseLoginUI/libs/ParseFacebookUtilsV4-1.10.1.jar") provided files("$rootProject.projectDir/ParseLoginUI/libs/ParseTwitterUtils-1.10.1.jar") diff --git a/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java b/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java index db1012c..b60dddb 100644 --- a/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java +++ b/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java @@ -511,7 +511,7 @@ public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { } @Override - public void onBindViewHolder(ViewHolder viewHolder, final int position) { + public void onBindViewHolder(ParseQueryRecyclerViewAdapter.ViewHolder viewHolder, final int position) { if (position < objects.size()) { final T object = objects.get(position); viewHolder.bind(object, position); From 84571e4cce566f439a2442f6188aa864fe1348c2 Mon Sep 17 00:00:00 2001 From: Lukas Koebis Date: Thu, 10 Sep 2015 20:41:29 -0700 Subject: [PATCH 07/10] changed package and renamed class - use exception instead of package-private method - copyed private static method --- .../ParseQueryAdapter.java} | 90 ++++++++++++------- .../queryadaptersample/SampleActivity.java | 5 +- 2 files changed, 60 insertions(+), 35 deletions(-) rename ParseLoginUI/src/main/java/com/parse/{ParseQueryRecyclerViewAdapter.java => widget/ParseQueryAdapter.java} (88%) diff --git a/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java b/ParseLoginUI/src/main/java/com/parse/widget/ParseQueryAdapter.java similarity index 88% rename from ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java rename to ParseLoginUI/src/main/java/com/parse/widget/ParseQueryAdapter.java index b60dddb..221d3b6 100644 --- a/ParseLoginUI/src/main/java/com/parse/ParseQueryRecyclerViewAdapter.java +++ b/ParseLoginUI/src/main/java/com/parse/widget/ParseQueryAdapter.java @@ -19,7 +19,7 @@ * */ -package com.parse; +package com.parse.widget; import android.content.Context; import android.graphics.drawable.Drawable; @@ -29,6 +29,14 @@ import android.widget.LinearLayout; import android.widget.TextView; +import com.parse.FindCallback; +import com.parse.ParseClassName; +import com.parse.ParseException; +import com.parse.ParseFile; +import com.parse.ParseImageView; +import com.parse.ParseObject; +import com.parse.ParseQuery; + import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -40,15 +48,15 @@ import bolts.Capture; /** - * A {@code ParseQueryRecyclerViewAdapter} handles the fetching of objects by page, and displaying + * A {@code ParseQueryAdapter} handles the fetching of objects by page, and displaying * objects as views in a {@link android.support.v7.widget.RecyclerView}. *

* This class is highly configurable, but also intended to be easy to get started with. See below - * for an example of using a {@code ParseQueryRecyclerViewAdapter} inside an + * for an example of using a {@code ParseQueryAdapter} inside an * {@link android.app.Activity}'s {@code onCreate}: *

- * final ParseQueryRecyclerViewAdapter adapter
- *         = new ParseQueryRecyclerViewAdapter(this, "TestObject");
+ * final ParseQueryAdapter adapter
+ *         = new ParseQueryAdapter(this, "TestObject");
  * adapter.setTextKey("name");
  *
  * RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
@@ -62,8 +70,8 @@
  * 
  * // Instantiate a QueryFactory to define the ParseQuery to be used for fetching items in this
  * // Adapter.
- * ParseQueryRecyclerViewAdapter.QueryFactory<ParseObject> factory =
- *     new ParseQueryRecyclerViewAdapter.QueryFactory<ParseObject>() {
+ * ParseQueryAdapter.QueryFactory<ParseObject> factory =
+ *     new ParseQueryAdapter.QueryFactory<ParseObject>() {
  *       public ParseQuery create() {
  *         ParseQuery query = new ParseQuery("Customer");
  *         query.whereEqualTo("activated", true);
@@ -72,9 +80,9 @@
  *       }
  *     };
  *
- * // Pass the factory into the ParseQueryRecyclerViewAdapter's constructor.
- * ParseQueryRecyclerViewAdapter<ParseObject> adapter
- *         = new ParseQueryRecyclerViewAdapter<ParseObject>(this, factory);
+ * // Pass the factory into the ParseQueryAdapter's constructor.
+ * ParseQueryAdapter<ParseObject> adapter
+ *         = new ParseQueryAdapter<ParseObject>(this, factory);
  * adapter.setTextKey("name");
  *
  * // Perhaps set a callback to be fired upon successful loading of a new set of ParseObjects.
@@ -90,7 +98,7 @@
  *
  * 
*/ -public class ParseQueryRecyclerViewAdapter extends RecyclerView.Adapter { +public class ParseQueryAdapter extends RecyclerView.Adapter { /** * Implement to construct your own custom {@link ParseQuery} for fetching objects. @@ -236,7 +244,7 @@ public void onClick(View v) { private List> onQueryLoadListeners = new ArrayList<>(); /** - * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, + * Constructs a {@code ParseQueryAdapter}. Given a {@link ParseObject} subclass, * this adapter will fetch and display all {@link ParseObject}s of the specified class, * ordered by creation time. * @@ -245,12 +253,12 @@ public void onClick(View v) { * @param clazz * The {@link ParseObject} subclass type to fetch and display. */ - public ParseQueryRecyclerViewAdapter(Context context, Class clazz) { - this(context, ParseObject.getClassName(clazz)); + public ParseQueryAdapter(Context context, Class clazz) { + this(context, getClassName(clazz)); } /** - * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, + * Constructs a {@code ParseQueryAdapter}. Given a {@link ParseObject} subclass, * this adapter will fetch and display all {@link ParseObject}s of the specified class, ordered * by creation time. * @@ -259,7 +267,7 @@ public ParseQueryRecyclerViewAdapter(Context context, Class() { @Override public ParseQuery create() { @@ -276,7 +284,7 @@ public ParseQuery create() { } /** - * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, + * Constructs a {@code ParseQueryAdapter}. Given a {@link ParseObject} subclass, * this adapter will fetch and display all {@link ParseObject}s of the specified class, ordered * by creation time. * @@ -287,13 +295,13 @@ public ParseQuery create() { * @param itemViewResource * A resource id that represents the layout for an item in the AdapterView. */ - public ParseQueryRecyclerViewAdapter( + public ParseQueryAdapter( Context context, Class clazz, int itemViewResource) { - this(context, ParseObject.getClassName(clazz), itemViewResource); + this(context, getClassName(clazz), itemViewResource); } /** - * Constructs a {@code ParseQueryRecyclerViewAdapter}. Given a {@link ParseObject} subclass, + * Constructs a {@code ParseQueryAdapter}. Given a {@link ParseObject} subclass, * this adapter will fetch and display all {@link ParseObject}s of the specified class, ordered * by creation time. * @@ -304,7 +312,7 @@ public ParseQueryRecyclerViewAdapter( * @param itemViewResource * A resource id that represents the layout for an item in the AdapterView. */ - public ParseQueryRecyclerViewAdapter( + public ParseQueryAdapter( Context context, final String className, int itemViewResource) { this(context, new QueryFactory() { @Override @@ -318,11 +326,11 @@ public ParseQuery create() { if (className == null) { throw new RuntimeException( - "You need to specify a className for the ParseQueryRecyclerViewAdapter"); + "You need to specify a className for the ParseQueryAdapter"); } } /** - * Constructs a {@code ParseQueryRecyclerViewAdapter}. Allows the caller to define further + * Constructs a {@code ParseQueryAdapter}. Allows the caller to define further * constraints on the {@link ParseQuery} to be used when fetching items from Parse. * * @param context @@ -330,12 +338,12 @@ public ParseQuery create() { * @param queryFactory * A {@link QueryFactory} to build a {@link ParseQuery} for fetching objects. */ - public ParseQueryRecyclerViewAdapter(Context context, QueryFactory queryFactory) { + public ParseQueryAdapter(Context context, QueryFactory queryFactory) { this(context, queryFactory, -1); } /** - * Constructs a {@code ParseQueryRecyclerViewAdapter}. Allows the caller to define further + * Constructs a {@code ParseQueryAdapter}. Allows the caller to define further * constraints on the {@link ParseQuery} to be used when fetching items from Parse. * * @param context @@ -345,7 +353,7 @@ public ParseQueryRecyclerViewAdapter(Context context, QueryFactory queryFacto * @param itemViewResource * A resource id (>0) that represents the layout for an item in the AdapterView. */ - public ParseQueryRecyclerViewAdapter( + public ParseQueryAdapter( Context context, QueryFactory queryFactory, int itemViewResource) { super(); this.context = context; @@ -421,11 +429,14 @@ public void done(List foundObjects, ParseException e) { (query.getCachePolicy() == ParseQuery.CachePolicy.CACHE_THEN_NETWORK && !firstCallBack.get())) { runningQueries.remove(query); } - if ((!Parse.isLocalDatastoreEnabled() && - query.getCachePolicy() == ParseQuery.CachePolicy.CACHE_ONLY) - && (e != null) && e.getCode() == ParseException.CACHE_MISS) { - // no-op on cache miss - return; + try { + if (query.getCachePolicy() == ParseQuery.CachePolicy.CACHE_ONLY + && e != null && e.getCode() == ParseException.CACHE_MISS) { + // no-op on cache miss + return; + } + } catch (IllegalStateException ignore) { + // LocaleDatastore disabled } if ((e != null) && ((e.getCode() == ParseException.CONNECTION_FAILED) || (e.getCode() != ParseException.CACHE_MISS))) { @@ -511,7 +522,7 @@ public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { } @Override - public void onBindViewHolder(ParseQueryRecyclerViewAdapter.ViewHolder viewHolder, final int position) { + public void onBindViewHolder(ParseQueryAdapter.ViewHolder viewHolder, final int position) { if (position < objects.size()) { final T object = objects.get(position); viewHolder.bind(object, position); @@ -681,4 +692,19 @@ public void addOnQueryLoadListener(OnQueryLoadListener listener) { public void removeOnQueryLoadListener(OnQueryLoadListener listener) { onQueryLoadListeners.remove(listener); } + + /** + * Gets the class name based on the {@link ParseClassName} annotation associated with a class. + * + * @param clazz + * The class to inspect. + * @return The name of the Parse class, if one is provided. Otherwise, {@code null}. + */ + private static String getClassName(Class clazz) { + ParseClassName info = clazz.getAnnotation(ParseClassName.class); + if (info == null) { + return null; + } + return info.value(); + } } diff --git a/ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleActivity.java b/ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleActivity.java index 47a87e4..f883610 100644 --- a/ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleActivity.java +++ b/ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleActivity.java @@ -26,9 +26,8 @@ import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; -import com.parse.ParseQueryRecyclerViewAdapter; import com.parse.ParseUser; -import com.parse.queryadaptersample.R; +import com.parse.widget.ParseQueryAdapter; /** * Shows the user profile. This simple activity can function regardless of whether the user @@ -48,7 +47,7 @@ protected void onCreate(Bundle savedInstanceState) { recyclerView = (RecyclerView) findViewById(R.id.recycler_view); - ParseQueryRecyclerViewAdapter adapter = new ParseQueryRecyclerViewAdapter(this, "TestClass"); + ParseQueryAdapter adapter = new ParseQueryAdapter(this, "TestClass"); adapter.loadObjects(); recyclerView.setAdapter(adapter); From e8ebe8749588ec7e6da27d2a3c4e249466e1cb2d Mon Sep 17 00:00:00 2001 From: Lukas Koebis Date: Fri, 11 Sep 2015 17:41:28 -0700 Subject: [PATCH 08/10] added tests --- .../ParseQueryRecyclerViewAdapterTest.java | 514 +++++++++++++++++- .../com/parse/widget/ParseQueryAdapter.java | 10 +- 2 files changed, 520 insertions(+), 4 deletions(-) diff --git a/ParseLoginUI/src/androidTest/java/com/parse/ParseQueryRecyclerViewAdapterTest.java b/ParseLoginUI/src/androidTest/java/com/parse/ParseQueryRecyclerViewAdapterTest.java index aa123d8..ac39478 100644 --- a/ParseLoginUI/src/androidTest/java/com/parse/ParseQueryRecyclerViewAdapterTest.java +++ b/ParseLoginUI/src/androidTest/java/com/parse/ParseQueryRecyclerViewAdapterTest.java @@ -1,22 +1,51 @@ +/* + * Copyright (c) 2015, Parse, LLC. All rights reserved. + * + * You are hereby granted a non-exclusive, worldwide, royalty-free license to use, + * copy, modify, and distribute this software in source code or binary form for use + * in connection with the web services and APIs provided by Parse. + * + * As with any software that integrates with the Parse platform, your use of + * this software is subject to the Parse Terms of Service + * [https://www.parse.com/about/terms]. This copyright notice shall be + * included in all copies or substantial portions of the software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + package com.parse; +import android.database.DataSetObserver; +import android.os.SystemClock; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + import com.parse.ui.TestActivity; +import com.parse.widget.ParseQueryAdapter; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import bolts.Capture; import bolts.Task; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -/** - * Created by lukask on 9/9/15. - */ public class ParseQueryRecyclerViewAdapterTest extends BaseActivityInstrumentationTestCase2 { @ParseClassName("Thing") @@ -101,4 +130,483 @@ public void tearDown() throws Exception { ParseObject.unregisterSubclass("Thing"); super.tearDown(); } + + public void testLoadObjects() throws Exception { + final ParseQueryAdapter adapter = new ParseQueryAdapter<>(activity, Thing.class); + final Semaphore done = new Semaphore(0); + adapter.addOnQueryLoadListener(new ParseQueryAdapter.OnQueryLoadListener() { + @Override + public void onLoading() { + } + + @Override + public void onLoaded(List objects, Exception e) { + assertNull(e); + assertEquals(TOTAL_THINGS, objects.size()); + done.release(); + } + }); + + adapter.loadObjects(); + + // Make sure we assert in callback is executed + assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); + } + + public void testLoadObjectsWithGenericParseObjects() throws Exception { + final ParseQueryAdapter adapter = + new ParseQueryAdapter<>(activity, Thing.class); + final Semaphore done = new Semaphore(0); + adapter.addOnQueryLoadListener(new ParseQueryAdapter.OnQueryLoadListener() { + @Override + public void onLoading() { + } + + @Override + public void onLoaded(List objects, Exception e) { + assertNull(e); + assertEquals(TOTAL_THINGS, objects.size()); + done.release(); + } + }); + + adapter.loadObjects(); + + // Make sure we assert in callback is executed + assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); + } + + public void testGetItemViewWithTextKey() throws Exception { + final ParseQueryAdapter adapter = + new ParseQueryAdapter<>(activity, Thing.class); + adapter.setTextKey("name"); + + final Semaphore done = new Semaphore(0); + adapter.addOnQueryLoadListener(new ParseQueryAdapter.OnQueryLoadListener() { + @Override + public void onLoading() { + + } + + @Override + public void onLoaded(List objects, Exception e) { + ParseQueryAdapter.ViewHolder viewHolder = adapter.onCreateViewHolder(null, 0); + adapter.onBindViewHolder(viewHolder, 0); + assertEquals("Thing 0", viewHolder.getTextView().getText().toString()); + done.release(); + } + }); + + adapter.loadObjects(); + + // Make sure we assert in callback is executed + assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); + } + + public void testGetItemViewWithNoTextKey() throws Exception { + final ParseQueryAdapter adapter = + new ParseQueryAdapter<>(activity, Thing.class); + + final Semaphore done = new Semaphore(0); + adapter.addOnQueryLoadListener(new ParseQueryAdapter.OnQueryLoadListener() { + @Override + public void onLoading() { + + } + + @Override + public void onLoaded(List objects, Exception e) { + ParseQueryAdapter.ViewHolder viewHolder = adapter.onCreateViewHolder(null, 0); + adapter.onBindViewHolder(viewHolder, 0); + assertEquals(savedThings.get(0).getObjectId(), viewHolder.getTextView().getText()); + done.release(); + } + }); + + adapter.loadObjects(); + + // Make sure we assert in callback is executed + assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); + } + + public void testLoadObjectsWithLimitsObjectsPerPage() throws Exception { + final ParseQueryAdapter adapter = new ParseQueryAdapter<>(activity, Thing.class); + final int pageSize = 4; + adapter.setObjectsPerPage(pageSize); + final Capture timesThrough = new Capture<>(0); + final Semaphore done = new Semaphore(0); + final ParseQueryAdapter.OnQueryLoadListener listener = new ParseQueryAdapter.OnQueryLoadListener() { + @Override + public void onLoading() { + } + + @Override + public void onLoaded(List objects, Exception e) { + if (e != null) { + return; + } + + switch (timesThrough.get()) { + case 0: + // first time through, should have one page of results + "Load more" + assertEquals(pageSize, objects.size()); + assertEquals(pageSize + 1, adapter.getItemCount()); + adapter.loadNextPage(); + break; + case 1: + // second time through, should have two pages of results + "Load more" + assertEquals(pageSize, objects.size()); + assertEquals(2 * pageSize + 1, adapter.getItemCount()); + adapter.loadNextPage(); + break; + case 2: + // last time through, no "Load more" necessary. + assertEquals(TOTAL_THINGS - 2 * pageSize, objects.size()); + assertEquals(TOTAL_THINGS, adapter.getItemCount()); + done.release(); + } + timesThrough.set(timesThrough.get() + 1); + } + }; + adapter.addOnQueryLoadListener(listener); + + adapter.loadObjects(); + + // Make sure we assert in callback is executed + assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); + } + + public void testLoadObjectsWithLimitsObjectsPerPageAndNoRemainder() throws Exception { + final ParseQueryAdapter adapter = new ParseQueryAdapter<>(activity, Thing.class); + final int pageSize = 5; + adapter.setObjectsPerPage(pageSize); + final Capture timesThrough = new Capture<>(0); + final Semaphore done = new Semaphore(0); + final ParseQueryAdapter.OnQueryLoadListener listener = new ParseQueryAdapter.OnQueryLoadListener() { + @Override + public void onLoading() { + } + + @Override + public void onLoaded(List objects, Exception e) { + if (e != null) { + return; + } + + switch (timesThrough.get()) { + case 0: + // first time through, should have one page of results + "Load more" cell + assertEquals(pageSize, objects.size()); + assertEquals(pageSize + 1, adapter.getItemCount()); + adapter.loadNextPage(); + break; + case 1: + // second time through, should have two pages' worth of results. It should realize that an + // additional "Load more" link isn't necessary, since this second page covers all of the + // results. + assertEquals(TOTAL_THINGS - pageSize, objects.size()); + assertEquals(TOTAL_THINGS, adapter.getItemCount()); + done.release(); + } + timesThrough.set(timesThrough.get() + 1); + } + }; + adapter.addOnQueryLoadListener(listener); + + adapter.loadObjects(); + + // Make sure we assert in callback is executed + assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); + } + + public void testLoadObjectsWithNoPagination() throws Exception { + final int additional = 16; + for (int i = 0; i < additional; i++) { + ParseObject thing = ParseObject.create(Thing.class); + thing.put("name", "Additional Thing " + i); + savedThings.add(thing); + } + TOTAL_THINGS += additional; + + final ParseQueryAdapter adapter = new ParseQueryAdapter<>(activity, Thing.class); + adapter.setPaginationEnabled(false); + final Semaphore done = new Semaphore(0); + adapter.addOnQueryLoadListener(new ParseQueryAdapter.OnQueryLoadListener() { + @Override + public void onLoading() { + } + + @Override + public void onLoaded(List objects, Exception e) { + assertNull(e); + assertEquals(TOTAL_THINGS, objects.size()); + assertEquals(TOTAL_THINGS, adapter.getItemCount()); + done.release(); + } + }); + + adapter.loadObjects(); + + // Make sure we assert in callback is executed + assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); + } + + public void testClear() throws Exception { + final ParseQueryAdapter adapter = new ParseQueryAdapter<>(activity, Thing.class); + final Semaphore done = new Semaphore(0); + final Capture counter = new Capture<>(0); + adapter.addOnQueryLoadListener(new ParseQueryAdapter.OnQueryLoadListener() { + @Override + public void onLoading() { + } + + @Override + public void onLoaded(List objects, Exception e) { + if (e != null) { + return; + } + switch (counter.get()) { + case 0: + assertEquals(TOTAL_THINGS, objects.size()); + assertEquals(TOTAL_THINGS, adapter.getItemCount()); + adapter.clear(); + assertEquals(0, adapter.getItemCount()); + adapter.loadObjects(); + break; + default: + assertEquals(TOTAL_THINGS, objects.size()); + assertEquals(TOTAL_THINGS, adapter.getItemCount()); + done.release(); + } + counter.set(counter.get() + 1); + } + }); + + adapter.loadObjects(); + + // Make sure we assert in callback is executed + assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); + } + + public void testLoadObjectsWithCacheThenNetworkQueryAndPagination() throws Exception { + ParseQueryAdapter.QueryFactory factory = new ParseQueryAdapter.QueryFactory() { + @Override + public ParseQuery create() { + ParseQuery query = new ParseQuery(Thing.class); + query.setCachePolicy(ParseQuery.CachePolicy.CACHE_THEN_NETWORK); + return query; + } + }; + + final ParseQueryAdapter adapter = new ParseQueryAdapter<>(activity, factory); + final int pageSize = 5; + adapter.setObjectsPerPage(pageSize); + adapter.setPaginationEnabled(true); + final Capture timesThrough = new Capture<>(0); + final Semaphore done = new Semaphore(0); + adapter.addOnQueryLoadListener(new ParseQueryAdapter.OnQueryLoadListener() { + @Override + public void onLoading() { + } + + @Override + public void onLoaded(List objects, Exception e) { + if (e != null) { + return; + } + + switch (timesThrough.get()) { + case 0: + // Network callback for first page + assertEquals(pageSize, objects.size()); + assertEquals(pageSize + 1, adapter.getItemCount()); + adapter.loadNextPage(); + break; + case 1: + // Network callback for second page + assertEquals(TOTAL_THINGS - pageSize, objects.size()); + assertEquals(TOTAL_THINGS, adapter.getItemCount()); + adapter.loadObjects(); + break; + case 2: + // Cache callback for first page + assertEquals(pageSize, objects.size()); + assertEquals(pageSize + 1, adapter.getItemCount()); + break; + case 3: + // Network callback for first page + assertEquals(pageSize, objects.size()); + assertEquals(pageSize + 1, adapter.getItemCount()); + adapter.loadNextPage(); + break; + case 4: + // Cache callback for second page + assertEquals(TOTAL_THINGS - pageSize, objects.size()); + assertEquals(TOTAL_THINGS, adapter.getItemCount()); + break; + case 5: + // Network callback for second page + assertEquals(TOTAL_THINGS - pageSize, objects.size()); + assertEquals(TOTAL_THINGS, adapter.getItemCount()); + done.release(); + break; + } + timesThrough.set(timesThrough.get() + 1); + } + }); + + adapter.loadObjects(); + + // Make sure we assert in callback is executed + assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); + } + + public void testLoadObjectsWithOnLoadingAndOnLoadedCallback() throws Exception { + final ParseQueryAdapter adapter = new ParseQueryAdapter<>(activity, Thing.class); + adapter.setObjectsPerPage(5); + final Capture flag = new Capture<>(false); + final Semaphore done = new Semaphore(0); + + adapter.addOnQueryLoadListener(new ParseQueryAdapter.OnQueryLoadListener() { + @Override + public void onLoading() { + assertFalse(flag.get()); + flag.set(true); + assertEquals(0, adapter.getItemCount()); + } + + @Override + public void onLoaded(List objects, Exception e) { + assertTrue(flag.get()); + assertEquals(5, objects.size()); + done.release(); + } + }); + + adapter.loadObjects(); + + // Make sure we assert in callback is executed + assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); + } + + public void testLoadNextPageBeforeLoadObjects() throws Exception { + final ParseQueryAdapter adapter = new ParseQueryAdapter<>(activity, Thing.class); + final Semaphore done = new Semaphore(0); + adapter.addOnQueryLoadListener(new ParseQueryAdapter.OnQueryLoadListener() { + @Override + public void onLoading() { + } + + @Override + public void onLoaded(List objects, Exception e) { + assertNull(e); + assertEquals(TOTAL_THINGS, objects.size()); + done.release(); + } + }); + + adapter.loadNextPage(); + + // Make sure we assert in callback is executed + assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); + } + + public void testIncomingQueryResultAfterClearing() throws Exception { + final ParseQueryAdapter adapter = new ParseQueryAdapter<>(activity, Thing.class); + final int pageSize = 4; + adapter.setObjectsPerPage(pageSize); + final Semaphore done = new Semaphore(0); + final Capture timesThrough = new Capture<>(0); + adapter.addOnQueryLoadListener(new ParseQueryAdapter.OnQueryLoadListener() { + @Override + public void onLoading() {} + + @Override + public void onLoaded(List objects, Exception e) { + switch (timesThrough.get()) { + case 0: + adapter.loadNextPage(); + adapter.clear(); + case 1: + done.release(); + } + timesThrough.set(timesThrough.get()+1); + } + }); + + adapter.loadObjects(); + + // Make sure we assert in callback is executed + assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); + } + + public void testLoadObjectsWithOverrideSetPageOnQuery() throws Exception { + final int arbitraryLimit = 3; + final ParseQueryAdapter adapter = + new ParseQueryAdapter(activity, Thing.class) { + @Override + public void setPageOnQuery(int page, ParseQuery query) { + // Make sure that this method is being used + respected. + query.setLimit(arbitraryLimit); + } + }; + final Semaphore done = new Semaphore(0); + adapter.addOnQueryLoadListener(new ParseQueryAdapter.OnQueryLoadListener() { + @Override + public void onLoading() { + }; + + @Override + public void onLoaded(List objects, Exception e) { + assertEquals(arbitraryLimit, objects.size()); + done.release(); + } + }); + + adapter.loadObjects(); + + // Make sure we assert in callback is executed + assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); + } + + public void testLoadObjectsWithtAutoload() throws Exception { + final ParseQueryAdapter adapter = new ParseQueryAdapter<>(activity, Thing.class); + final Capture flag = new Capture<>(false); + // Make sure that the Adapter doesn't start trying to load objects until AFTER we set this flag + // to true (= triggered by calling setAutoload, NOT registerDataSetObserver, if autoload is + // false). + adapter.setAutoload(false); + final Semaphore done = new Semaphore(0); + adapter.addOnQueryLoadListener(new ParseQueryAdapter.OnQueryLoadListener() { + @Override + public void onLoading() { + assertEquals(0, adapter.getItemCount()); + assertTrue(flag.get()); + } + + @Override + public void onLoaded(List objects, Exception e) { + assertEquals(TOTAL_THINGS, adapter.getItemCount()); + done.release(); + } + }); + RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() { }; + adapter.registerAdapterDataObserver(observer); + flag.set(true); + adapter.setAutoload(true); + + // Make sure we assert in callback is executed + assertTrue(done.tryAcquire(10, TimeUnit.SECONDS)); + } + + private LinearLayout buildReusableListCell() { + LinearLayout view = new LinearLayout(activity); + TextView textView = new TextView(activity); + textView.setId(android.R.id.text1); + view.addView(textView); + ParseImageView imageView = new ParseImageView(activity); + imageView.setId(android.R.id.icon); + view.addView(imageView); + return view; + } } diff --git a/ParseLoginUI/src/main/java/com/parse/widget/ParseQueryAdapter.java b/ParseLoginUI/src/main/java/com/parse/widget/ParseQueryAdapter.java index 221d3b6..e8b20bf 100644 --- a/ParseLoginUI/src/main/java/com/parse/widget/ParseQueryAdapter.java +++ b/ParseLoginUI/src/main/java/com/parse/widget/ParseQueryAdapter.java @@ -191,6 +191,14 @@ public void onClick(View v) { } }); } + + public TextView getTextView() { + return textView; + } + + public ParseImageView getImageView() { + return imageView; + } } // The key to use to display on the cell text label. @@ -674,7 +682,7 @@ public void setOnClickListener(OnClickListener onClickListener) { * Defaults to true. */ public void setAutoload(boolean autoload) { - if (autoload == autoload) { + if (this.autoload == autoload) { // An extra precaution to prevent an overzealous setAutoload(true) after assignment to an // AdapterView from triggering an unnecessary additional loadObjects(). return; From 8ff28880cb1253de8c88639d62e168a88154c1b7 Mon Sep 17 00:00:00 2001 From: Lukas Koebis Date: Mon, 14 Sep 2015 17:10:29 -0700 Subject: [PATCH 09/10] updated sample app --- .../main/java/com/parse/widget/ParseQueryAdapter.java | 2 +- .../com/parse/queryadaptersample/SampleActivity.java | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/ParseLoginUI/src/main/java/com/parse/widget/ParseQueryAdapter.java b/ParseLoginUI/src/main/java/com/parse/widget/ParseQueryAdapter.java index e8b20bf..4d287df 100644 --- a/ParseLoginUI/src/main/java/com/parse/widget/ParseQueryAdapter.java +++ b/ParseLoginUI/src/main/java/com/parse/widget/ParseQueryAdapter.java @@ -182,7 +182,7 @@ public void onClick(View v) { public void setNextView() { textView.setText("Load more..."); if (imageView != null) { - imageView.setVisibility(View.GONE); + imageView.setVisibility(View.INVISIBLE); } textView.setOnClickListener(new View.OnClickListener() { @Override diff --git a/ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleActivity.java b/ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleActivity.java index f883610..90173b0 100644 --- a/ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleActivity.java +++ b/ParseQueryAdapterExample/src/main/java/com/parse/queryadaptersample/SampleActivity.java @@ -30,8 +30,8 @@ import com.parse.widget.ParseQueryAdapter; /** - * Shows the user profile. This simple activity can function regardless of whether the user - * is currently logged in. + * Shows a list of "testString"s in the class "TestClass" using a + * {@link android.support.v7.widget.RecyclerView} and {@code com.parse.widget.ParseQueryAdapter}. */ public class SampleActivity extends Activity { @@ -48,6 +48,11 @@ protected void onCreate(Bundle savedInstanceState) { recyclerView = (RecyclerView) findViewById(R.id.recycler_view); ParseQueryAdapter adapter = new ParseQueryAdapter(this, "TestClass"); + + // Configure adapter. + adapter.setObjectsPerPage(3); + adapter.setTextKey("testString"); + adapter.loadObjects(); recyclerView.setAdapter(adapter); From 9d0576bfd8a312d502f72e5660446d8b2a411b03 Mon Sep 17 00:00:00 2001 From: Lukas Koebis Date: Mon, 14 Sep 2015 17:49:27 -0700 Subject: [PATCH 10/10] removed parse keys --- ParseQueryAdapterExample/src/main/res/values/strings.xml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ParseQueryAdapterExample/src/main/res/values/strings.xml b/ParseQueryAdapterExample/src/main/res/values/strings.xml index 7f8a974..8a6e893 100644 --- a/ParseQueryAdapterExample/src/main/res/values/strings.xml +++ b/ParseQueryAdapterExample/src/main/res/values/strings.xml @@ -2,9 +2,8 @@ - - 4R4qtxUJIYGzoDrJJfNKki1xpFMtWtOwZIeDTGjJ - lcTXrnkl9A7dnUlVlKMnnoKMo8Be9z5G8QJ3qINq + YOUR_PARSE_APP_ID + YOUR_PARSE_CLIENT_KEY Parse QueryAdapter Sample