diff --git a/ParseLoginSampleBasic/build.gradle b/ParseLoginSampleBasic/build.gradle index 42f48eb..7aa1c05 100644 --- a/ParseLoginSampleBasic/build.gradle +++ b/ParseLoginSampleBasic/build.gradle @@ -7,6 +7,7 @@ dependencies { compile rootProject.ext.androidSupport compile files(rootProject.ext.parsePath) compile files(rootProject.ext.parseFacebookUtilsPath) + compile files(rootProject.ext.parseTwitterUtilsPath) } android { diff --git a/ParseLoginSampleCodeCustomization/build.gradle b/ParseLoginSampleCodeCustomization/build.gradle index 42f48eb..7aa1c05 100644 --- a/ParseLoginSampleCodeCustomization/build.gradle +++ b/ParseLoginSampleCodeCustomization/build.gradle @@ -7,6 +7,7 @@ dependencies { compile rootProject.ext.androidSupport compile files(rootProject.ext.parsePath) compile files(rootProject.ext.parseFacebookUtilsPath) + compile files(rootProject.ext.parseTwitterUtilsPath) } android { diff --git a/ParseLoginSampleLayoutOverride/build.gradle b/ParseLoginSampleLayoutOverride/build.gradle index 42f48eb..7aa1c05 100644 --- a/ParseLoginSampleLayoutOverride/build.gradle +++ b/ParseLoginSampleLayoutOverride/build.gradle @@ -7,6 +7,7 @@ dependencies { compile rootProject.ext.androidSupport compile files(rootProject.ext.parsePath) compile files(rootProject.ext.parseFacebookUtilsPath) + compile files(rootProject.ext.parseTwitterUtilsPath) } android { diff --git a/ParseLoginSampleWithDispatchActivity/build.gradle b/ParseLoginSampleWithDispatchActivity/build.gradle index 42f48eb..7aa1c05 100644 --- a/ParseLoginSampleWithDispatchActivity/build.gradle +++ b/ParseLoginSampleWithDispatchActivity/build.gradle @@ -7,6 +7,7 @@ dependencies { compile rootProject.ext.androidSupport compile files(rootProject.ext.parsePath) compile files(rootProject.ext.parseFacebookUtilsPath) + compile files(rootProject.ext.parseTwitterUtilsPath) } android { diff --git a/ParseLoginUI/build.gradle b/ParseLoginUI/build.gradle index 00108d2..f6c81e2 100644 --- a/ParseLoginUI/build.gradle +++ b/ParseLoginUI/build.gradle @@ -13,8 +13,9 @@ dependencies { // // Since the dependency below is "provided" instead of "compile", your project's build.gradle // does not have to refer to the same Parse SDK instance that's in the ParseLoginUI/libs folder. - provided files("$rootProject.projectDir/ParseLoginUI/libs/Parse-1.9.4.jar") - provided files("$rootProject.projectDir/ParseLoginUI/libs/ParseFacebookUtilsV4-1.9.4.jar") + provided files("$rootProject.projectDir/ParseLoginUI/libs/Parse-1.10.0.jar") + provided files("$rootProject.projectDir/ParseLoginUI/libs/ParseFacebookUtilsV4-1.10.0.jar") + provided files("$rootProject.projectDir/ParseLoginUI/libs/ParseTwitterUtils-1.10.0.jar") } android { diff --git a/ParseLoginUI/libs/Parse-1.10.0.jar b/ParseLoginUI/libs/Parse-1.10.0.jar new file mode 100644 index 0000000..08b0d7b Binary files /dev/null and b/ParseLoginUI/libs/Parse-1.10.0.jar differ diff --git a/ParseLoginUI/libs/Parse-1.9.4.jar b/ParseLoginUI/libs/Parse-1.9.4.jar deleted file mode 100644 index 8d1bc70..0000000 Binary files a/ParseLoginUI/libs/Parse-1.9.4.jar and /dev/null differ diff --git a/ParseLoginUI/libs/ParseFacebookUtilsV4-1.9.4.jar b/ParseLoginUI/libs/ParseFacebookUtilsV4-1.10.0.jar similarity index 86% rename from ParseLoginUI/libs/ParseFacebookUtilsV4-1.9.4.jar rename to ParseLoginUI/libs/ParseFacebookUtilsV4-1.10.0.jar index 1e336b5..3e6eadb 100644 Binary files a/ParseLoginUI/libs/ParseFacebookUtilsV4-1.9.4.jar and b/ParseLoginUI/libs/ParseFacebookUtilsV4-1.10.0.jar differ diff --git a/ParseLoginUI/libs/ParseTwitterUtils-1.10.0.jar b/ParseLoginUI/libs/ParseTwitterUtils-1.10.0.jar new file mode 100644 index 0000000..b3e929c Binary files /dev/null and b/ParseLoginUI/libs/ParseTwitterUtils-1.10.0.jar differ diff --git a/ParseLoginUI/src/com/parse/ParseImageView.java b/ParseLoginUI/src/com/parse/ParseImageView.java new file mode 100644 index 0000000..d8a4823 --- /dev/null +++ b/ParseLoginUI/src/com/parse/ParseImageView.java @@ -0,0 +1,186 @@ +/* + * 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; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.ImageView; + +import bolts.Continuation; +import bolts.Task; + +/** + * A specialized {@code ImageView} that downloads and displays remote images stored on Parse's + * servers. + *

+ * Given a {@code ParseFile} storing an image, a {@code ParseImageView} works seamlessly to fetch + * the file data and display it in the background. See below for an example: + * + *

+ * ParseImageView imageView = (ParseImageView) findViewById(android.R.id.icon);
+ * // The placeholder will be used before and during the fetch, to be replaced by the fetched image
+ * // data.
+ * imageView.setPlaceholder(getResources().getDrawable(R.drawable.placeholder));
+ * imageView.setParseFile(file);
+ * imageView.loadInBackground(new GetDataCallback() {
+ *   @Override
+ *   public void done(byte[] data, ParseException e) {
+ *     Log.i("ParseImageView",
+ *         "Fetched! Data length: " + data.length + ", or exception: " + e.getMessage());
+ *   }
+ * });
+ * 
+ */ +public class ParseImageView extends ImageView { + private ParseFile file; + private Drawable placeholder; + private boolean isLoaded = false; + + /** + * Simple constructor to use when creating a {@code ParseImageView} from code. + * + * @param context + * Context for this View + */ + public ParseImageView(Context context) { + super(context); + } + + /** + * Constructor that is called when inflating a {@code ParseImageView} from XML. + * + * @param context + * Context for this View + * @param attributeSet + * AttributeSet defined for this View in XML + */ + public ParseImageView(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } + + /** + * Perform inflation from XML and apply a class-specific base style. + * + * @param context + * Context for this View + * @param attributeSet + * AttributeSet defined for this View in XML + * @param defStyle + * Class-specific base style. + */ + public ParseImageView(Context context, AttributeSet attributeSet, int defStyle) { + super(context, attributeSet, defStyle); + } + + @Override + protected void onDetachedFromWindow() { + // AdapterViews tend to try and avoid calling this, instead preferring to recycle the Views + + // subviews. This is, however, called when the AdapterView itself is detached, or the Activity + // is destroyed. + if (this.file != null) { + this.file.cancel(); + } + } + + @Override + public void setImageBitmap(Bitmap bitmap) { + super.setImageBitmap(bitmap); + this.isLoaded = true; + } + + /** + * Sets the placeholder to be used while waiting for an image to be loaded. + * + * @param placeholder + * A {@code Drawable} to be displayed while the remote image data is being fetched. This + * value can be null, and this {@code ImageView} will simply be blank while data is + * fetched. + */ + public void setPlaceholder(Drawable placeholder) { + this.placeholder = placeholder; + if (!this.isLoaded) { + this.setImageDrawable(this.placeholder); + } + } + + /** + * Sets the remote file on Parse's server that stores the image. + * + * @param file + * The remote file on Parse's server. + */ + public void setParseFile(ParseFile file) { + if (this.file != null) { + this.file.cancel(); + } + this.isLoaded = false; + this.file = file; + this.setImageDrawable(this.placeholder); + } + + /** + * Kick off downloading of remote image. When the download is finished, the image data will be + * displayed. + * + * @return A Task that is resolved when the image data is fetched and this View displays the image. + */ + public Task loadInBackground() { + if (file == null) { + return Task.forResult(null); + } + + final ParseFile loadingFile = file; + return file.getDataInBackground().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + byte[] data = task.getResult(); + if (file != loadingFile) { + // This prevents the very slim chance of the file's download finishing and the callback + // triggering just before this ImageView is reused for another ParseObject. + return Task.cancelled(); + } + if (data != null) { + Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); + if (bitmap != null) { + setImageBitmap(bitmap); + } + } + return task; + } + }, Task.UI_THREAD_EXECUTOR); + } + + /** + * Kick off downloading of remote image. When the download is finished, the image data will be + * displayed and the {@code completionCallback} will be triggered. + * + * @param completionCallback + * A custom {@code GetDataCallback} to be called after the image data is fetched and this + * {@code ImageView} displays the image. + */ + public void loadInBackground(final GetDataCallback completionCallback) { + ParseTaskUtils.callbackOnMainThreadAsync(loadInBackground(), completionCallback, true); + } +} \ No newline at end of file diff --git a/ParseLoginUI/src/com/parse/ParseQueryAdapter.java b/ParseLoginUI/src/com/parse/ParseQueryAdapter.java new file mode 100644 index 0000000..82f57ab --- /dev/null +++ b/ParseLoginUI/src/com/parse/ParseQueryAdapter.java @@ -0,0 +1,708 @@ +/* + * 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; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.database.DataSetObserver; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.Adapter; +import android.widget.BaseAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.parse.ParseQuery.CachePolicy; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.WeakHashMap; + +import bolts.Capture; + +/** + * A {@code ParseQueryAdapter} handles the fetching of objects by page, and displaying objects as + * views in a {@link android.widget.ListView}. + *

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

+ * final ParseQueryAdapter adapter = new ParseQueryAdapter(this, "TestObject");
+ * adapter.setTextKey("name");
+ *
+ * ListView listView = (ListView) findViewById(R.id.listview);
+ * listView.setAdapter(adapter);
+ * 
+ *

+ * 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.
+ * ParseQueryAdapter.QueryFactory<ParseObject> factory =
+ *     new ParseQueryAdapter.QueryFactory<ParseObject>() {
+ *       public ParseQuery create() {
+ *         ParseQuery query = new ParseQuery("Customer");
+ *         query.whereEqualTo("activated", true);
+ *         query.orderByDescending("moneySpent");
+ *         return query;
+ *       }
+ *     };
+ *
+ * // 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.
+ * 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
+ *   }
+ * });
+ *
+ * // Attach it to your ListView, as in the example above
+ * ListView listView = (ListView) findViewById(R.id.listview);
+ * listView.setAdapter(adapter);
+ * 
+ */ +public class ParseQueryAdapter extends BaseAdapter { + + /** + * Implement to construct your own custom {@link ParseQuery} for fetching objects. + */ + public static interface QueryFactory { + public ParseQuery create(); + } + + /** + * Implement with logic that is called before and after objects are fetched from Parse by the + * adapter. + */ + public static interface OnQueryLoadListener { + public void onLoading(); + + public void onLoaded(List objects, Exception e); + } + + // 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 by this PQA. + // 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<>(); + + // 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 List> onQueryLoadListeners = + new ArrayList<>(); + + private static final int VIEW_TYPE_ITEM = 0; + private static final int VIEW_TYPE_NEXT_PAGE = 1; + + /** + * 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. + * + * @param context + * The activity utilizing this adapter. + * @param clazz + * The {@link ParseObject} subclass type to fetch and display. + */ + public ParseQueryAdapter(Context context, Class clazz) { + this(context, ParseObject.getClassName(clazz)); + } + + /** + * 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. + * + * @param context + * The activity utilizing this adapter. + * @param className + * The name of the Parse class of {@link ParseObject}s to display. + */ + public ParseQueryAdapter(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 ParseQueryAdapter}. 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 ParseQueryAdapter(Context context, Class clazz, + int itemViewResource) { + this(context, ParseObject.getClassName(clazz), itemViewResource); + } + + /** + * 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. + * + * @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 ParseQueryAdapter(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 ParseQueryAdapter"); + } + } + /** + * 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 + * The activity utilizing this adapter. + * @param queryFactory + * A {@link QueryFactory} to build a {@link ParseQuery} for fetching objects. + */ + public ParseQueryAdapter(Context context, QueryFactory queryFactory) { + this(context, queryFactory, null); + } + + /** + * 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 + * 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 ParseQueryAdapter(Context context, QueryFactory queryFactory, int itemViewResource) { + this(context, queryFactory, Integer.valueOf(itemViewResource)); + } + + private ParseQueryAdapter(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; + } + + /** {@inheritDoc} **/ + @Override + public T getItem(int index) { + if (index == this.getPaginationCellRow()) { + return null; + } + return this.objects.get(index); + } + + /** {@inheritDoc} **/ + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getItemViewType(int position) { + if (position == this.getPaginationCellRow()) { + return VIEW_TYPE_NEXT_PAGE; + } + return VIEW_TYPE_ITEM; + } + + @Override + public int getViewTypeCount() { + return 2; + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + super.registerDataSetObserver(observer); + this.dataSetObservers.put(observer, null); + if (this.autoload) { + this.loadObjects(); + } + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + super.unregisterDataSetObserver(observer); + this.dataSetObservers.remove(observer); + } + + /** + * Remove all elements from the list. + */ + public void clear() { + this.objectPages.clear(); + syncObjectsWithPages(); + this.notifyDataSetChanged(); + this.currentPage = 0; + } + + /** + * 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() { + this.loadObjects(0, true); + } + + private void loadObjects(final int page, final boolean shouldClear) { + final ParseQuery query = this.queryFactory.create(); + + if (this.objectsPerPage > 0 && this.paginationEnabled) { + this.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); + + query.findInBackground(new FindCallback() { + @SuppressLint("ShowToast") + @Override + public void done(List foundObjects, ParseException e) { + if ((!Parse.isLocalDatastoreEnabled() && + query.getCachePolicy() == 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()) { + 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); + } + }); + } + + /** + * 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); + } + } + + /** + * Loads the next page of objects, appends to table, and notifies the UI that the model has + * changed. + */ + public void loadNextPage() { + this.loadObjects(currentPage + 1, false); + } + + /** + * Overrides {@link Adapter#getCount()} method to return the number of cells to + * display. If pagination is turned on, this count will include an extra +1 count for the + * pagination cell row. + * + * @return The number of cells to be displayed by the {@link android.widget.ListView}. + */ + @Override + public int getCount() { + int count = this.objects.size(); + + if (this.shouldShowPaginationCell()) { + count++; + } + + return count; + } + + /** + * Override this method to customize each cell given a {@link ParseObject}. + *

+ * If a view is not provided, a default view will be created based upon + * {@code android.R.layout.activity_list_item}. + *

+ * This method expects a {@code TextView} with id {@code android.R.id.text1} in your object views. + * If {@link #setImageKey(String)} was used, this method also expects an {@code ImageView} with id + * {@code android.R.id.icon}. + *

+ * This method displays the text value specified by the text key (set via + * {@link #setTextKey(String)}) and an image (described by a {@link ParseFile}, under the key set + * via {@link #setImageKey(String)}) if applicable. If the text key is not set, the value for + * {@link ParseObject#getObjectId()} will be displayed instead. + * + * @param object + * The {@link ParseObject} associated with this item. + * @param v + * The {@code View} associated with this row. This view, if non-null, is being recycled + * and intended to be used for displaying this item. + * @param parent + * The parent that this view will eventually be attached to + * @return The customized view displaying the {@link ParseObject}'s information. + */ + public View getItemView(T object, View v, ViewGroup parent) { + if (v == null) { + v = this.getDefaultView(this.context); + } + + TextView textView; + try { + textView = (TextView) v.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); + } + + if (textView != null) { + if (this.textKey == null) { + textView.setText(object.getObjectId()); + } else if (object.get(this.textKey) != null) { + textView.setText(object.get(this.textKey).toString()); + } else { + textView.setText(null); + } + } + + if (this.imageKey != null) { + ParseImageView imageView; + try { + imageView = (ParseImageView) v.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); + } + 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"); + } + if (!this.imageViewSet.containsKey(imageView)) { + this.imageViewSet.put(imageView, null); + } + imageView.setPlaceholder(this.placeholder); + imageView.setParseFile((ParseFile) object.get(this.imageKey)); + imageView.loadInBackground(); + } + + return v; + } + + /** + * 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}. + * + * @param v + * The view object associated with this row + type (a "Next Page" view, instead of an + * "Item" view). + * @param parent + * The parent that this view will eventually be attached to + * @return The view object that allows the user to paginate. + */ + public View getNextPageView(View v, ViewGroup parent) { + if (v == null) { + v = this.getDefaultView(this.context); + } + TextView textView = (TextView) v.findViewById(android.R.id.text1); + textView.setText("Load more..."); + return v; + } + + /** + * The base class, {@code Adapter}, defines a {@code getView} method intended to display data at + * the specified position in the data set. We override it here in order to toggle between + * {@link #getNextPageView(View, ViewGroup)} and + * {@link #getItemView(ParseObject, View, ViewGroup)} depending on the value of + * {@link #getItemViewType(int)}. + */ + @Override + public final View getView(int position, View convertView, ViewGroup parent) { + if (this.getItemViewType(position) == VIEW_TYPE_NEXT_PAGE) { + View nextPageView = this.getNextPageView(convertView, parent); + nextPageView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + loadNextPage(); + } + }); + return nextPageView; + } + return this.getItemView(this.getItem(position), convertView, parent); + } + + /** + * 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); + } + } + } + + /** + * 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); + } + + private View getDefaultView(Context context) { + if (this.itemResourceId != null) { + return View.inflate(context, this.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; + } + + private int getPaginationCellRow() { + return this.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); + } + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index e6ff26b..3ef1a06 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ ext { facebookSDK = 'com.facebook.android:facebook-android-sdk:4.0.1' androidSupport = 'com.android.support:support-v4:22.0.0' bolts = 'com.parse.bolts:bolts-android:1.2.0' - parsePath = "$rootProject.projectDir/ParseLoginUI/libs/Parse-1.9.4.jar" - parseFacebookUtilsPath = "$rootProject.projectDir/ParseLoginUI/libs/ParseFacebookUtilsV4-1.9.4.jar" + parsePath = "$rootProject.projectDir/ParseLoginUI/libs/Parse-1.10.0.jar" + parseFacebookUtilsPath = "$rootProject.projectDir/ParseLoginUI/libs/ParseFacebookUtilsV4-1.10.0.jar" + parseTwitterUtilsPath = "$rootProject.projectDir/ParseLoginUI/libs/ParseTwitterUtils-1.10.0.jar" }