public class LoadingIndicator {

    private static WeakReference<View> loadingIndicatorRef;

    private static Handler mainHandler;

    /**
     * Sets the loading indicator view to show and hide on command. This widget must be attached to the
     * activity's layout and must be at least on the same hierarchy level as overlapping views (it may be
     * hidden under sibling views).
     *
     * @param loadingIndicator
     *         the view to show and hide on command
     * @param mainHandler
     *         a handler associated with the main thread
     */
    public static void set(View loadingIndicator, Handler mainHandler) {
        loadingIndicatorRef = new WeakReference<View>(loadingIndicator);
        LoadingIndicator.mainHandler = mainHandler;
    }

    /**
     * Brings the loading indicator to the front and makes it {@link View#VISIBLE}.
     */
    public static void show() {
        final View loadingIndicator = getLoadingIndicator();
        if (loadingIndicator == null) {
            return;
        }
        if (ThreadUtil.isMainThread()) {
            loadingIndicator.bringToFront();
            loadingIndicator.setVisibility(View.VISIBLE);
        } else {
            mainHandler.post(new Runnable() {
                @Override
                public void run() {
                    loadingIndicator.bringToFront();
                    loadingIndicator.setVisibility(View.VISIBLE);
                }
            });
        }
    }

    /**
     * Sets the loading indicator's visibility to {@link View#GONE}.
     */
    public static void hide() {
        final View loadingIndicator = getLoadingIndicator();
        if (loadingIndicator == null) {
            return;
        }
        if (ThreadUtil.isMainThread()) {
            loadingIndicator.setVisibility(View.GONE);
        } else {
            mainHandler.post(new Runnable() {
                @Override
                public void run() {
                    loadingIndicator.setVisibility(View.GONE);
                }
            });
        }
    }

    /**
     * @return the loading indicator view or null if none set
     */
    private static View getLoadingIndicator() {
        return loadingIndicatorRef == null ? null : loadingIndicatorRef.get();
    }
} public class DataChangeListener {

    private static final int GC_EACH_CNT_CALL = 100;

    private static final Collection<WeakReference<OnProductCreateListener>> onProductCreateListeners = new
            ArrayList<WeakReference<OnProductCreateListener>>();
    private static final Collection<WeakReference<OnProductUpdateListener>> onProductUpdateListeners = new
            ArrayList<WeakReference<OnProductUpdateListener>>();
    private static final Collection<WeakReference<OnOpinionCreateListener>> onOpinionCreateListeners = new
            ArrayList<WeakReference<OnOpinionCreateListener>>();
    private static final Collection<WeakReference<OnOpinionUpdateListener>> onOpinionUpdateListeners = new
            ArrayList<WeakReference<OnOpinionUpdateListener>>();
    private static final Collection<WeakReference<OnImageCreateListener>> onImageCreateListeners = new
            ArrayList<WeakReference<OnImageCreateListener>>();
    private static final Collection<WeakReference<OnImageUpdateListener>> onImageUpdateListeners = new
            ArrayList<WeakReference<OnImageUpdateListener>>();

    private static int cntCallForGc = 0;

    /**
     * Adds a listener to be notified of product creations.
     *
     * Only a weak reference to the listener is kept - maintain a strong reference to avoid garbage collection
     * of the listener.
     *
     * @param onProductCreateListener
     *         the listener to add
     */
    public static void addOnProductCreateListener(OnProductCreateListener onProductCreateListener) {
        checkGc();
        synchronized (onProductCreateListeners) {
            onProductCreateListeners.add(new WeakReference<OnProductCreateListener>(onProductCreateListener));
        }
    }

    /**
     * Removes the specified listener.
     *
     * @param onProductCreateListener
     *         the listener to remove
     */
    public static void removeOnProductCreateListener(OnProductCreateListener onProductCreateListener) {
        synchronized (onProductCreateListeners) {
            removeFromWeakReferences(onProductCreateListeners, onProductCreateListener);
        }
    }

    /**
     * Adds a listener to be notified of product updates.
     *
     * Only a weak reference to the listener is kept - maintain a strong reference to avoid garbage collection
     * of the listener.
     *
     * @param onProductUpdateListener
     *         the listener to add
     */
    public static void addOnProductUpdateListener(OnProductUpdateListener onProductUpdateListener) {
        checkGc();
        synchronized (onProductUpdateListeners) {
            onProductUpdateListeners.add(new WeakReference<OnProductUpdateListener>(onProductUpdateListener));
        }
    }

    /**
     * Removes the specified listener.
     *
     * @param onProductUpdateListener
     *         the listener to remove
     */
    public static void removeOnProductUpdateListener(OnProductUpdateListener onProductUpdateListener) {
        synchronized (onProductUpdateListeners) {
            removeFromWeakReferences(onProductUpdateListeners, onProductUpdateListener);
        }
    }

    /**
     * Adds a listener to be notified of opinion creations.
     *
     * Only a weak reference to the listener is kept - maintain a strong reference to avoid garbage collection
     * of the listener.
     *
     * @param onOpinionCreateListener
     *         the listener to add
     */
    public static void addOnOpinionCreateListener(OnOpinionCreateListener onOpinionCreateListener) {
        checkGc();
        synchronized (onOpinionCreateListeners) {
            onOpinionCreateListeners.add(new WeakReference<OnOpinionCreateListener>(onOpinionCreateListener));
        }
    }

    /**
     * Removes the specified listener.
     *
     * @param onOpinionCreateListener
     *         the listener to remove
     */
    public static void removeOnOpinionCreateListener(OnOpinionCreateListener onOpinionCreateListener) {
        synchronized (onOpinionCreateListeners) {
            removeFromWeakReferences(onOpinionCreateListeners, onOpinionCreateListener);
        }
    }

    /**
     * Adds a listener to be notified of opinion updates.
     *
     * Only a weak reference to the listener is kept - maintain a strong reference to avoid garbage collection
     * of the listener.
     *
     * @param onOpinionUpdateListener
     *         the listener to add
     */
    public static void addOnOpinionUpdateListener(OnOpinionUpdateListener onOpinionUpdateListener) {
        checkGc();
        synchronized (onOpinionUpdateListeners) {
            onOpinionUpdateListeners.add(new WeakReference<OnOpinionUpdateListener>(onOpinionUpdateListener));
        }
    }

    /**
     * Removes the specified listener.
     *
     * @param onOpinionUpdateListener
     *         the listener to remove
     */
    public static void removeOnOpinionUpdateListener(OnOpinionUpdateListener onOpinionUpdateListener) {
        synchronized (onOpinionUpdateListeners) {
            removeFromWeakReferences(onOpinionUpdateListeners, onOpinionUpdateListener);
        }
    }

    /**
     * Adds a listener to be notified of image creations.
     *
     * Only a weak reference to the listener is kept - maintain a strong reference to avoid garbage collection
     * of the listener.
     *
     * @param onImageCreateListener
     *         the listener to add
     */
    public static void addOnImageCreateListener(OnImageCreateListener onImageCreateListener) {
        checkGc();
        synchronized (onImageCreateListeners) {
            onImageCreateListeners.add(new WeakReference<OnImageCreateListener>(onImageCreateListener));
        }
    }

    /**
     * Removes the specified listener.
     *
     * @param onImageCreateListener
     *         the listener to remove
     */
    public static void removeOnImageCreateListener(OnImageCreateListener onImageCreateListener) {
        synchronized (onImageCreateListeners) {
            removeFromWeakReferences(onImageCreateListeners, onImageCreateListener);
        }
    }

    /**
     * Adds a listener to be notified of image updates.
     *
     * Only a weak reference to the listener is kept - maintain a strong reference to avoid garbage collection
     * of the listener.
     *
     * @param onImageUpdateListener
     *         the listener to add
     */
    public static void addOnImageUpdateListener(OnImageUpdateListener onImageUpdateListener) {
        checkGc();
        synchronized (onImageUpdateListeners) {
            onImageUpdateListeners.add(new WeakReference<OnImageUpdateListener>(onImageUpdateListener));
        }
    }

    /**
     * Removes the specified listener.
     *
     * @param onImageUpdateListener
     *         the listener to remove
     */
    public static void removeOnImageUpdateListener(OnImageUpdateListener onImageUpdateListener) {
        synchronized (onImageUpdateListeners) {
            removeFromWeakReferences(onImageUpdateListeners, onImageUpdateListener);
        }
    }

    /**
     * Notifies all registered {@code OnProductCreateListener}s of a new product.
     *
     * @param product
     *         the product that has been created
     */
    public static void productCreate(Product product) {
        synchronized (onProductCreateListeners) {
            for (WeakReference<OnProductCreateListener> listenerRef : onProductCreateListeners) {
                OnProductCreateListener listener = listenerRef.get();
                if (listener != null && !listenerRef.isEnqueued()) {
                    listener.onProductCreate(product);
                }
            }
        }
    }

    /**
     * Notifies all registered {@code OnProductUpdateListener}s of an updated product.
     *
     * @param product
     *         the product that has been updated (in its updated state)
     */
    public static void productUpdate(Product product) {
        synchronized (onProductUpdateListeners) {
            for (WeakReference<OnProductUpdateListener> listenerRef : onProductUpdateListeners) {
                OnProductUpdateListener listener = listenerRef.get();
                if (listener != null && !listenerRef.isEnqueued()) {
                    listener.onProductUpdate(product);
                }
            }
        }
    }

    /**
     * Notifies all registered {@code OnOpinionCreateListener}s of a new opinion.
     *
     * @param opinion
     *         the opinion that has been created
     */
    public static void opinionCreate(Opine opinion) {
        synchronized (onOpinionCreateListeners) {
            for (WeakReference<OnOpinionCreateListener> listenerRef : onOpinionCreateListeners) {
                OnOpinionCreateListener listener = listenerRef.get();
                if (listener != null && !listenerRef.isEnqueued()) {
                    listener.onOpinionCreate(opinion);
                }
            }
        }
    }

    /**
     * Notifies all registered {@code OnOpinionUpdateListener}s of an updated opinion.
     *
     * @param opinion
     *         the opinion that has been updated (in its updated state)
     */
    public static void opinionUpdate(Opine opinion) {
        synchronized (onOpinionUpdateListeners) {
            for (WeakReference<OnOpinionUpdateListener> listenerRef : onOpinionUpdateListeners) {
                OnOpinionUpdateListener listener = listenerRef.get();
                if (listener != null && !listenerRef.isEnqueued()) {
                    listener.onOpinionUpdate(opinion);
                }
            }
        }
    }

    /**
     * Notifies all registered {@code OnImageCreateListener}s of a new image.
     *
     * @param image
     *         the image that has been created
     */
    public static void imageCreate(ProductImage image) {
        synchronized (onImageCreateListeners) {
            for (WeakReference<OnImageCreateListener> listenerRef : onImageCreateListeners) {
                OnImageCreateListener listener = listenerRef.get();
                if (listener != null && !listenerRef.isEnqueued()) {
                    listener.onImageCreate(image);
                }
            }
        }
    }

    /**
     * Notifies all registered {@code OnImageUpdateListener}s of an updated image.
     *
     * @param image
     *         the image that has been updated (in its updated state)
     */
    public static void imageUpdate(ProductImage image) {
        synchronized (onImageUpdateListeners) {
            for (WeakReference<OnImageUpdateListener> listenerRef : onImageUpdateListeners) {
                OnImageUpdateListener listener = listenerRef.get();
                if (listener != null && !listenerRef.isEnqueued()) {
                    listener.onImageUpdate(image);
                }
            }
        }
    }

    /**
     * Removes an object from a list of objects contained in weak references.
     *
     * @param references
     *         the list to remove the object encapsulated in a weak reference from
     * @param object
     *         the object to search the weak references for
     * @param <T>
     *         the type of object
     */
    public static <T> void removeFromWeakReferences(Iterable<WeakReference<T>> references, T object) {
        for (Iterator<WeakReference<T>> iterator = references.iterator(); iterator.hasNext(); ) {
            WeakReference<T> reference = iterator.next();
            if (reference.get() == object) {
                iterator.remove();
            }
        }
    }

    /**
     * Increases the count of calls and, once it reaches {@link #GC_EACH_CNT_CALL}, removes from all lists of
     * listeners those that have been garbage collected or are enqueued.
     */
    private static void checkGc() {
        cntCallForGc++;
        if (cntCallForGc >= GC_EACH_CNT_CALL) {
            cntCallForGc = 0;
            Log.v(DataChangeListener.class.getSimpleName(), "Starting garbage collection of dead listeners");
            synchronized (onProductCreateListeners) {
                gcDeadReferences(onProductCreateListeners);
            }
            synchronized (onProductUpdateListeners) {
                gcDeadReferences(onProductUpdateListeners);
            }
            synchronized (onOpinionCreateListeners) {
                gcDeadReferences(onOpinionCreateListeners);
            }
            synchronized (onOpinionUpdateListeners) {
                gcDeadReferences(onOpinionUpdateListeners);
            }
            synchronized (onImageCreateListeners) {
                gcDeadReferences(onImageCreateListeners);
            }
            synchronized (onImageUpdateListeners) {
                gcDeadReferences(onImageUpdateListeners);
            }
        }
    }

    /**
     * Removes from a list of weak references those that have been garbage collected or are enqueued.
     *
     * @param references
     *         the list of weak references to go through
     */
    private static <T> void gcDeadReferences(Iterable<WeakReference<T>> references) {
        for (Iterator<WeakReference<T>> iterator = references.iterator(); iterator.hasNext(); ) {
            WeakReference reference = iterator.next();
            if (reference.get() == null || reference.isEnqueued()) {
                iterator.remove();
            }
        }
    }

    /**
     * Listens for new products.
     */
    public interface OnProductCreateListener {
        void onProductCreate(Product product);
    }

    /**
     * Listens for updated products.
     */
    public interface OnProductUpdateListener {
        void onProductUpdate(Product product);
    }

    /**
     * Listens for new opinions.
     */
    public interface OnOpinionCreateListener {
        void onOpinionCreate(Opine opinion);
    }

    /**
     * Listens for updated opinions.
     */
    public interface OnOpinionUpdateListener {
        void onOpinionUpdate(Opine opinion);
    }

    /**
     * Listens for new images.
     */
    public interface OnImageCreateListener {
        void onImageCreate(ProductImage image);
    }

    /**
     * Listens for updated images.
     */
    public interface OnImageUpdateListener {
        void onImageUpdate(ProductImage image);
    }
} public interface NavigationHandler {

    /**
     * Shows the dialog to sign in or up.
     *
     * @param queryOnSuccess
     *         a query to run on successful login (this is used for example if a query fails due to
     *         insufficient credentials and needs to be repeated once logged in) or null
     * @param queryOnSuccessCompletion
     *         a completion callback to go hand in hand with {@code queryOnSuccess} or null
     * @param queryError
     *         the error resulting from {@code queryOnSuccess} if it was run before
     */
    void showSignInDialog(PLYAndroid.Query queryOnSuccess, PLYCompletion queryOnSuccessCompletion,
            PLYAndroid.QueryError queryError);

    /**
     * Shows the dialog to sign in using username and password.
     *
     * @param userName
     *         an optional value to prefill the user name field with
     * @param queryOnSuccess
     *         a query to run on successful login (this is used for example if a query fails due to
     *         insufficient credentials and needs to be repeated once logged in) or null
     * @param queryOnSuccessCompletion
     *         a completion callback to go hand in hand with {@code queryOnSuccess} or null
     * @param queryError
     *         the error resulting from {@code queryOnSuccess} if it was run before
     */
    void showSignInExistingUserDialog(CharSequence userName, PLYAndroid.Query queryOnSuccess, PLYCompletion
            queryOnSuccessCompletion, PLYAndroid.QueryError queryError);

    /**
     * Shows the dialog to sign up as a new user.
     *
     * @param emailAddress
     *         the email address to preset (null for none)
     * @param userName
     *         the user name to preset (null for none)
     * @param queryOnSuccess
     *         a query to run on successful signup (this is used for example if a query fails due to
     *         insufficient credentials and needs to be repeated once logged in) or null
     * @param queryOnSuccessCompletion
     *         a completion callback to go hand in hand with {@code queryOnSuccess} or null
     * @param queryError
     *         the error resulting from {@code queryOnSuccess} if it was run before
     */
    void showSignUpDialog(CharSequence emailAddress, CharSequence userName, PLYAndroid.Query
            queryOnSuccess, PLYCompletion queryOnSuccessCompletion, PLYAndroid.QueryError queryError);

    /**
     * Opens the global timeline.
     */
    void openTimeline();

    /**
     * Looks up the product with the specified GTIN in the ProductLayer database in an async fashion, opening
     * the product page on success or prompting the user to enter details of the new product.
     *
     * @param gtin
     *         the GTIN to look up
     */
    void lookUpProduct(String gtin);

    /**
     * Opens a page enabling the user to enter details and take photos of a new product.
     *
     * @param gtin
     *         the GTIN of the new product
     */
    void openProductCreatePage(String gtin);

    /**
     * Opens a page enabling the user to change details of an existing product.
     *
     * @param product
     *         the product to edit
     */
    void openProductEditPage(Product product);

    /**
     * Opens the product page.
     *
     * @param product
     *         the product to show
     */
    void openProductPage(Product product);

    /**
     * Opens the opinion page.
     *
     * @param product
     *         the product to write an opinion about
     * @param parentOpinion
     *         the opinion to reply to or null if not a reply
     */
    void openOpinionPage(Product product, Opine parentOpinion);

    /**
     * Opens the user profile page.
     *
     * @param user
     *         the user to show
     * @param selectedTab
     *         the index of the tab on the profile page to switch to
     */
    void openProfilePage(User user, int selectedTab);

    /**
     * Opens the user settings page.
     *
     * @param user
     *         the user to edit (= the currently logged in user)
     */
    void openProfileEditPage(User user);

    /**
     * Sends a search query and displays the results.
     *
     * @param query
     *         the query to send
     */
    void search(String query);

} public interface UserHandler {

    /**
     * @return the current user
     */
    User getUser();

    /**
     * Updates the currently stored user object if its retrieval time is newer than the current one.
     *
     * @param user
     *         the new user object
     * @param userRetrievalTime
     *         the retrieval time of the user object or null for now
     */
    void setUser(User user, Long userRetrievalTime);

    /**
     * Checks whether the specified user is the currently logged in one.
     *
     * @param user
     *         the user to compare with the current one
     * @return true if this user is currently logged in, false else
     */
    boolean isCurrentUser(User user);

    /**
     * @param key
     *         the social provider key
     * @return true if the current user is connected with the specified social network, false else
     */
    boolean isSocialNetworkConnected(String key);

    /**
     * @return the nanotime of the last remote user retrieval
     */
    long getUserRetrievalTime();

    /**
     * @return the current user's friends (followed users)
     */
    ArrayList<User> getFriends();

    /**
     * Sets the currently stored user friends.
     *
     * @param friends
     *         the current user's followed users
     */
    void setFriends(ArrayList<User> friends);

    /**
     * Tests whether the specified user is a friend of the currently logged in user.
     *
     * @param user
     *         the possible friend
     * @return true if the specified user is in the stored friends list (or if the specified user is the
     * current user), false else
     */
    boolean isFriend(User user);

    /**
     * Tests whether the specified user is a friend of the currently logged in user.
     *
     * @param user
     *         the possible friend
     * @return true if the specified user is in the stored friends list (or if the specified user is the
     * current user), false else
     */
    boolean isFriend(SimpleUserInfo user);

    /**
     * Adds a user to the current user's friend list.
     *
     * @param user
     *         the user to add
     */
    void addFriend(User user);

    /**
     * Removes a user from the current user's friend list.
     *
     * @param user
     *         the user to remove
     */
    void removeFriend(User user);

    /**
     * Sets a user's score.
     *
     * @param points
     *         the new score
     */
    void setPoints(long points);

    /**
     * Updates a user's list of unlocked achievements.
     *
     * @param keys
     *         the keys of the unlocked achievements
     */
    void newAchievements(List<String> keys);

    /**
     * @return whether the user handler is currently restoring a saved state (non-blocking)
     */
    boolean isLoadingState();

    /**
     * Adds a listener to be notified of friends list updates.
     *
     * Only a weak reference to the listener is kept - maintain a strong reference to avoid garbage collection
     * of the listener.
     *
     * @param onFriendsUpdateListener
     *         the listener to add
     */
    void addOnFriendsUpdateListener(OnFriendsUpdateListener onFriendsUpdateListener);

    /**
     * Removes the specified listener.
     *
     * @param onFriendsUpdateListener
     *         the listener to remove
     */
    void removeOnFriendsUpdateListener(OnFriendsUpdateListener onFriendsUpdateListener);

    /**
     * Adds a listener to be notified of points updates.
     *
     * Only a weak reference to the listener is kept - maintain a strong reference to avoid garbage collection
     * of the listener.
     *
     * @param onPointsUpdateListener
     *         the listener to add
     */
    void addOnPointsUpdateListener(OnPointsUpdateListener onPointsUpdateListener);

    /**
     * Removes the specified listener.
     *
     * @param onPointsUpdateListener
     *         the listener to remove
     */
    void removeOnPointsUpdateListener(OnPointsUpdateListener onPointsUpdateListener);

    /**
     * Adds a listener to be notified of new unlocked achievements.
     *
     * Only a weak reference to the listener is kept - maintain a strong reference to avoid garbage collection
     * of the listener.
     *
     * @param onAchievementsUnlockedListener
     *         the listener to add
     */
    void addOnAchievementsUnlockedListener(OnAchievementsUnlockedListener onAchievementsUnlockedListener);

    /**
     * Removes the specified listener.
     *
     * @param onAchievementsUnlockedListener
     *         the listener to remove
     */
    void removeOnAchievementsUnlockedListener(OnAchievementsUnlockedListener onAchievementsUnlockedListener);

    /**
     * Listens for updates to the friends list.
     */
    interface OnFriendsUpdateListener {

        /**
         * Called when the user's list of friends has been updated.
         */
        void onFriendsUpdate();

    }

    /**
     * Listens for updates to the user's score.
     */
    interface OnPointsUpdateListener {

        /**
         * Called when the user's score has been updated.
         *
         * @param points
         *         the new score
         */
        void onPointsUpdate(long points);

    }

    /**
     * Listens for new achievements unlocked by the user.
     */
    interface OnAchievementsUnlockedListener {

        /**
         * Called when one or more achievements have been unlocked by the user.
         *
         * @param keys
         *         the keys of the achievements that have been unlocked
         */
        void onAchievementsUnlocked(List<String> keys);

    }

} public class ImagePreview extends CardView {

    private final int cardBackgroundColor;
    private final int friendBackgroundColor;

    private Context context;

    private AuthorView authorView;
    private ImageView productImage;
    private Target productImageTarget = new ProductImageTarget();
    private int imagePlaceholderColor;
    private View scrim;

    private ProductImage image;
    private User currentUser;
    private ArrayList<User> friends;

    /**
     * Creates the cardview setting the dimensions of the product and author image placeholder.
     *
     * @param context
     *         the application context
     * @param navigationHandler
     *         the activity's navigation handler to open new screens
     * @param imageWidthPx
     *         the width of the product image
     * @param maxImageHeightPx
     *         the maximum height of the product image
     * @param avatarSizePx
     *         the width/height of the author's avatar
     */
    public ImagePreview(final Context context, final NavigationHandler navigationHandler, int imageWidthPx,
            int maxImageHeightPx, int avatarSizePx) {
        this(context, null);
        ViewGroup.LayoutParams productImageLayoutParams = productImage.getLayoutParams();
        productImageLayoutParams.width = imageWidthPx;
        productImageLayoutParams.height = maxImageHeightPx;
        ViewGroup.LayoutParams lpForScrim = scrim.getLayoutParams();
        lpForScrim.height = ProductPreview.calcScrimSize(maxImageHeightPx);
        authorView.setAvatarSize(avatarSizePx);
        authorView.setOnClickListeners(navigationHandler);
        productImage.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (image != null) {
                    Intent intent = new Intent(getContext(), ImageActivity.class);
                    intent.putExtra(ImageActivity.EXTRA_IMAGE, image);
                    intent.putExtra(ImageActivity.EXTRA_USER, currentUser);
                    intent.putExtra(ImageActivity.EXTRA_FRIENDS, friends);
                    context.startActivity(intent);
                }
            }
        });
    }

    /**
     * Creates the cardview. Is called by {@link #ImagePreview(Context, NavigationHandler, int, int, int)} and
     * if inflated from XML.
     *
     * @param context
     *         the application context
     */
    public ImagePreview(Context context) {
        this(context, null);
    }

    /**
     * Creates the cardview. Is called by {@link #ImagePreview(Context, NavigationHandler, int, int, int)} and
     * if inflated from XML.
     *
     * @param context
     *         the application context
     * @param attrs
     *         any attributes set via XML
     */
    public ImagePreview(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    /**
     * Creates the cardview. Is called by {@link #ImagePreview(Context, NavigationHandler, int, int, int)} and
     * if inflated from XML.
     *
     * @param context
     *         the application context
     * @param attrs
     *         any attributes set via XML
     * @param defStyleAttr
     *         any style set via XML
     */
    public ImagePreview(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.compound_image_preview, this, true);
        MarginLayoutParams marginLayoutParams = new MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
        int feedItemMarginPx = Math.round(getResources().getDimension(R.dimen.feed_item_margin));
        marginLayoutParams.setMargins(feedItemMarginPx, feedItemMarginPx, feedItemMarginPx, feedItemMarginPx);
        setLayoutParams(marginLayoutParams);
        setRadius(getResources().getDimension(R.dimen.feed_item_radius));
        Integer cardBackgroundColor = ThemeUtil.getIntegerValue(context, R.attr.cardBackground);
        this.cardBackgroundColor = cardBackgroundColor == null ? Color.WHITE : cardBackgroundColor;
        setCardBackgroundColor(this.cardBackgroundColor);
        Integer friendBackgroundColor = ThemeUtil.getIntegerValue(context, R.attr.friendBackground);
        this.friendBackgroundColor = friendBackgroundColor == null ? Color.WHITE : friendBackgroundColor;
        authorView = (AuthorView) findViewById(R.id.author);
        productImage = (ImageView) findViewById(R.id.product_image);
        scrim = findViewById(R.id.scrim);
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            setPreventCornerOverlap(false);
            Integer imagePlaceholderColor = ThemeUtil.getIntegerValue(context, R.attr.imagePlaceholder);
            this.imagePlaceholderColor = imagePlaceholderColor == null ? Color.TRANSPARENT :
                    imagePlaceholderColor;
            productImage.setBackgroundColor(this.imagePlaceholderColor);
        }
    }

    /**
     * Sets a new image to be displayed in the cardview.
     *
     * @param image
     *         the image to show
     * @param productImageUrl
     *         the URL of the product image to load
     * @param productDominantColor
     *         the dominant color (rgb) of the product image to use as background while the image is being
     *         loaded
     * @param author
     *         the author to show
     * @param authorImageUrl
     *         the URL of the author's avatar image to load
     * @param userHandler
     *         the handler to retrieve user info from for friend and voting info
     */
    public void setImage(ProductImage image, String productImageUrl, int[] productDominantColor,
            SimpleUserInfo author, String authorImageUrl, UserHandler userHandler) {
        currentUser = userHandler.getUser();
        friends = userHandler.getFriends();
        if (this.image != null && image.equals(this.image)) {
            return;
        }
        // display new product image
        this.image = image;
        Picasso.with(context).cancelRequest(productImageTarget);
        productImage.setImageBitmap(null);
        boolean isFriend = userHandler.isFriend(author);
        // colorize cardview if this entry was created by oneself or a friend
        setCardBackgroundColor(isFriend ? friendBackgroundColor : cardBackgroundColor);
        // display the image's dominant color as background while it is being loaded
        if (productDominantColor != null) {
            productImage.setBackgroundColor(Color.rgb(productDominantColor[0], productDominantColor[1],
                    productDominantColor[2]));
        } else {
            productImage.setBackgroundColor(imagePlaceholderColor);
        }
        // load product image
        if (productImageUrl != null) {
            productImage.setVisibility(VISIBLE);
            Picasso.with(context).load(productImageUrl).into(productImageTarget);
        } else {
            productImage.setVisibility(GONE);
            // TODO hide view or failed loading picture placeholder
        }
        // display new author
        authorView.setAuthor(author, authorImageUrl, isFriend, image.getProduct(), image.getCreated());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;

        ImagePreview that = (ImagePreview) o;

        if (image != null ? !image.equals(that.image) : that.image != null)
            return false;
        return !(currentUser != null ? !currentUser.equals(that.currentUser) : that.currentUser != null);

    }

    @Override
    public int hashCode() {
        int result = image != null ? image.hashCode() : 0;
        result = 31 * result + (currentUser != null ? currentUser.hashCode() : 0);
        return result;
    }

    private class ProductImageTarget implements Target {
        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
            ViewGroup.LayoutParams productImageLayoutParams = productImage.getLayoutParams();
            int bitmapWidth = bitmap.getWidth();
            int bitmapHeight = bitmap.getHeight();
            if (productImageLayoutParams.height != bitmapHeight) {
                productImageLayoutParams.height = bitmapHeight;
                ViewGroup.LayoutParams lpForScrim = scrim.getLayoutParams();
                lpForScrim.height = ProductPreview.calcScrimSize(bitmapHeight);
            }
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                productImage.setBackgroundColor(Color.TRANSPARENT);
            }
            productImage.setImageBitmap(bitmap);
            Log.d(getClass().getSimpleName(), bitmapWidth + "x" + bitmapHeight + " image from " +
                    from.name() + " loaded into " + productImageLayoutParams.width + "x" +
                    productImageLayoutParams.height + " view");
        }

        @Override
        public void onBitmapFailed(Drawable errorDrawable) {
            Log.e(getClass().getSimpleName(), "Error loading image for product " + image.getProduct()
                    .getName());
            productImage.setVisibility(GONE);
            // TODO hide view or failed loading picture placeholder
        }

        @Override
        public void onPrepareLoad(Drawable placeHolderDrawable) {
        }
    }
} public class OpinionView extends CardView {

    private final int cardBackgroundColor;
    private final int friendBackgroundColor;

    private AuthorView authorView;
    private View speechBubble;
    private TextView opinionView;
    private LikeView likeView;

    private int widthPx;

    private Opine opinion;

    /**
     * Creates the cardview setting the dimensions of the author image placeholder.
     *
     * @param context
     *         the application context
     * @param navigationHandler
     *         the activity's navigation handler to open new screens
     * @param widthPx
     *         the width of the cardview in px
     * @param avatarSizePx
     *         the width/height of the author's avatar
     */
    public OpinionView(Context context, final NavigationHandler navigationHandler, int widthPx, int
            avatarSizePx) {
        this(context, null);
        this.widthPx = widthPx;
        authorView.setAvatarSize(avatarSizePx);
        authorView.showBubbleTriangle();
        authorView.setOnClickListeners(navigationHandler);
        OnClickListener writeOpinionClickListener = new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (opinion != null) {
                    Product product = opinion.getProduct();
                    if (product != null) {
                        navigationHandler.openOpinionPage(product, opinion);
                    }
                }
            }
        };
        speechBubble.setOnClickListener(writeOpinionClickListener);
    }

    /**
     * Creates the cardview. Is called by {@link #OpinionView(Context, NavigationHandler, int, int)} and if
     * inflated from XML.
     *
     * @param context
     *         the application context
     */
    public OpinionView(Context context) {
        this(context, null);
    }

    /**
     * Creates the cardview. Is called by {@link #OpinionView(Context, NavigationHandler, int, int)} and if
     * inflated from XML.
     *
     * @param context
     *         the application context
     * @param attrs
     *         any attributes set via XML
     */
    public OpinionView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    /**
     * Creates the cardview. Is called by {@link #OpinionView(Context, NavigationHandler, int, int)} and if
     * inflated from XML.
     *
     * @param context
     *         the application context
     * @param attrs
     *         any attributes set via XML
     * @param defStyleAttr
     *         any style set via XML
     */
    public OpinionView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.compound_opinion, this, true);
        MarginLayoutParams marginLayoutParams = new MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
        int feedItemMarginPx = Math.round(getResources().getDimension(R.dimen.feed_item_margin));
        marginLayoutParams.setMargins(feedItemMarginPx, feedItemMarginPx, feedItemMarginPx, feedItemMarginPx);
        setLayoutParams(marginLayoutParams);
        setRadius(getResources().getDimension(R.dimen.feed_item_radius));
        Integer cardBackgroundColor = ThemeUtil.getIntegerValue(context, R.attr.cardBackground);
        this.cardBackgroundColor = cardBackgroundColor == null ? Color.WHITE : cardBackgroundColor;
        setCardBackgroundColor(this.cardBackgroundColor);
        Integer friendBackgroundColor = ThemeUtil.getIntegerValue(context, R.attr.friendBackground);
        this.friendBackgroundColor = friendBackgroundColor == null ? Color.WHITE : friendBackgroundColor;
        authorView = (AuthorView) findViewById(R.id.author);
        speechBubble = findViewById(R.id.speech_bubble);
        opinionView = (TextView) findViewById(R.id.opinion);
        likeView = (LikeView) findViewById(R.id.like_view);
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            setPreventCornerOverlap(false);
        }
    }

    /**
     * Sets a new opinion to be displayed in the cardview.
     *
     * @param opinion
     *         the opine to show
     * @param author
     *         the author to show
     * @param authorImageUrl
     *         the URL of the author's avatar image to load
     * @param userHandler
     *         the handler to retrieve user info from for friend and voting info
     * @param client
     *         the PLYAndroid client to use for voting
     */
    public void setOpinion(Opine opinion, SimpleUserInfo author, String authorImageUrl, UserHandler
            userHandler, PLYAndroid client) {
        if (this.opinion != null && opinion.equals(this.opinion)) {
            return;
        }
        // display new opinion
        this.opinion = opinion;
        opinionView.setText(opinion.getText());
        boolean isFriend = userHandler.isFriend(author);
        // colorize cardview if this entry was created by oneself or a friend
        setCardBackgroundColor(isFriend ? friendBackgroundColor : cardBackgroundColor);
        // display new author
        authorView.setAuthor(author, authorImageUrl, isFriend, opinion.getProduct(), opinion.getCreated());
        // update like view
        likeView.setObjectId(opinion.getId());
        likeView.setClient(client);
        User currentUser = userHandler.getUser();
        String userId = currentUser == null ? null : currentUser.getId();
        likeView.setFromVoters(opinion.getUpVoters(), opinion.getDownVoters(), userId, userHandler
                .getFriends(), widthPx);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;

        OpinionView that = (OpinionView) o;

        return !(opinion != null ? !opinion.equals(that.opinion) : that.opinion != null);

    }

    @Override
    public int hashCode() {
        return opinion != null ? opinion.hashCode() : 0;
    }
} public class ProductPreview extends CardView {

    private final int cardBackgroundColor;
    private final int friendBackgroundColor;

    private Context context;

    private AuthorView authorView;
    private ImageView productImage;
    private Target productImageTarget = new ProductImageTarget();
    private int imagePlaceholderColor;
    private View scrim;
    private LikeView likeView;
    private ImageView writeOpinionImage;

    private int widthPx;

    private Product product;

    /**
     * Creates the cardview setting the dimensions of the product and author image placeholder.
     *
     * @param context
     *         the application context
     * @param navigationHandler
     *         the activity's navigation handler to open new screens
     * @param imageWidthPx
     *         the width of the product image
     * @param maxImageHeightPx
     *         the maximum height of the product image
     * @param avatarSizePx
     *         the width/height of the author's avatar
     */
    public ProductPreview(Context context, final NavigationHandler navigationHandler, int imageWidthPx, int
            maxImageHeightPx, int avatarSizePx) {
        this(context, null);
        widthPx = imageWidthPx;
        ViewGroup.LayoutParams productImageLayoutParams = productImage.getLayoutParams();
        productImageLayoutParams.width = imageWidthPx;
        productImageLayoutParams.height = maxImageHeightPx;
        ViewGroup.LayoutParams lpForScrim = scrim.getLayoutParams();
        lpForScrim.height = calcScrimSize(maxImageHeightPx);
        authorView.setAvatarSize(avatarSizePx);
        authorView.setOnClickListeners(navigationHandler);
        OnClickListener productClickListener = new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (product != null) {
                    navigationHandler.openProductPage(product);
                }
            }
        };
        productImage.setOnClickListener(productClickListener);
        writeOpinionImage.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (product != null) {
                    navigationHandler.openOpinionPage(product, null);
                }
            }
        });
    }

    /**
     * Creates the cardview. Is called by {@link #ProductPreview(Context, NavigationHandler, int, int, int)}
     * and if inflated from XML.
     *
     * @param context
     *         the application context
     */
    public ProductPreview(Context context) {
        this(context, null);
    }

    /**
     * Creates the cardview. Is called by {@link #ProductPreview(Context, NavigationHandler, int, int, int)}
     * and if inflated from XML.
     *
     * @param context
     *         the application context
     * @param attrs
     *         any attributes set via XML
     */
    public ProductPreview(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    /**
     * Creates the cardview. Is called by {@link #ProductPreview(Context, NavigationHandler, int, int, int)}
     * and if inflated from XML.
     *
     * @param context
     *         the application context
     * @param attrs
     *         any attributes set via XML
     * @param defStyleAttr
     *         any style set via XML
     */
    public ProductPreview(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.compound_product_preview, this, true);
        MarginLayoutParams marginLayoutParams = new MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
        int feedItemMarginPx = Math.round(getResources().getDimension(R.dimen.feed_item_margin));
        marginLayoutParams.setMargins(feedItemMarginPx, feedItemMarginPx, feedItemMarginPx, feedItemMarginPx);
        setLayoutParams(marginLayoutParams);
        setRadius(getResources().getDimension(R.dimen.feed_item_radius));
        Integer cardBackgroundColor = ThemeUtil.getIntegerValue(context, R.attr.cardBackground);
        this.cardBackgroundColor = cardBackgroundColor == null ? Color.WHITE : cardBackgroundColor;
        setCardBackgroundColor(this.cardBackgroundColor);
        Integer friendBackgroundColor = ThemeUtil.getIntegerValue(context, R.attr.friendBackground);
        this.friendBackgroundColor = friendBackgroundColor == null ? Color.WHITE : friendBackgroundColor;
        authorView = (AuthorView) findViewById(R.id.author);
        productImage = (ImageView) findViewById(R.id.product_image);
        likeView = (LikeView) findViewById(R.id.like_view);
        writeOpinionImage = (ImageView) findViewById(R.id.write_opinion_image);
        scrim = findViewById(R.id.scrim);
        Integer imagePlaceholderColor = ThemeUtil.getIntegerValue(context, R.attr.imagePlaceholder);
        this.imagePlaceholderColor = imagePlaceholderColor == null ? Color.TRANSPARENT :
                imagePlaceholderColor;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            setPreventCornerOverlap(false);
            productImage.setBackgroundColor(this.imagePlaceholderColor);
        }
    }

    /**
     * Calculates the ideal scrim height.
     *
     * @param maxHeight
     *         the height of the image it is on top of
     * @return the ideal scrim height
     */
    public static int calcScrimSize(int maxHeight) {
        int minHeight = MetricsUtil.inPx(80);
        int idealHeight = maxHeight / 2;
        return idealHeight > minHeight ? idealHeight : minHeight;
    }

    /**
     * Sets a new product to be displayed in the cardview.
     *
     * @param product
     *         the product to show
     * @param productImageUrl
     *         the URL of the product image to load
     * @param productDominantColor
     *         the dominant color (rgb) of the product image to use as background while the image is being
     *         loaded
     * @param author
     *         the author to show
     * @param authorImageUrl
     *         the URL of the author's avatar image to load
     * @param userHandler
     *         the handler to retrieve user info from for friend and voting info
     * @param client
     *         the PLYAndroid client to use for voting
     */
    public void setProduct(Product product, String productImageUrl, int[] productDominantColor,
            SimpleUserInfo author, String authorImageUrl, UserHandler userHandler, PLYAndroid client) {
        if (this.product != null && product.equals(this.product)) {
            return;
        }
        // display new product image
        this.product = product;
        Picasso.with(context).cancelRequest(productImageTarget);
        productImage.setImageBitmap(null);
        boolean isFriend = userHandler.isFriend(author);
        // colorize cardview if this entry was created by oneself or a friend
        setCardBackgroundColor(isFriend ? friendBackgroundColor : cardBackgroundColor);
        // display the image's dominant color as background while it is being loaded
        if (productDominantColor != null) {
            productImage.setBackgroundColor(Color.rgb(productDominantColor[0], productDominantColor[1],
                    productDominantColor[2]));
        } else {
            productImage.setBackgroundColor(imagePlaceholderColor);
        }
        // load product image
        if (productImageUrl != null) {
            productImage.setVisibility(VISIBLE);
            Picasso.with(context).load(productImageUrl).into(productImageTarget);
            //likeView.setVisibility(VISIBLE);
        } else {
            productImage.setVisibility(GONE);
            //likeView.setVisibility(GONE);
            // TODO product placeholder image?
        }
        // display new author
        authorView.setAuthor(author, authorImageUrl, isFriend, product, product.getCreated());
        // update like view
        likeView.setObjectId(product.getId());
        likeView.setClient(client);
        User currentUser = userHandler.getUser();
        String userId = currentUser == null ? null : currentUser.getId();
        likeView.setFromVoters(product.getUpVoters(), product.getDownVoters(), userId, userHandler
                .getFriends(), widthPx);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;

        ProductPreview that = (ProductPreview) o;

        return !(product != null ? !product.equals(that.product) : that.product != null);

    }

    @Override
    public int hashCode() {
        return product != null ? product.hashCode() : 0;
    }

    private class ProductImageTarget implements Target {
        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
            ViewGroup.LayoutParams productImageLayoutParams = productImage.getLayoutParams();
            int bitmapWidth = bitmap.getWidth();
            int bitmapHeight = bitmap.getHeight();
            if (productImageLayoutParams.height != bitmapHeight) {
                productImageLayoutParams.height = bitmapHeight;
                ViewGroup.LayoutParams lpForScrim = scrim.getLayoutParams();
                lpForScrim.height = calcScrimSize(bitmapHeight);
            }
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                productImage.setBackgroundColor(Color.TRANSPARENT);
            }
            productImage.setImageBitmap(bitmap);
            Log.d(getClass().getSimpleName(), bitmapWidth + "x" + bitmapHeight + " image from " +
                    from.name() + " loaded into " + productImageLayoutParams.width + "x" +
                    productImageLayoutParams.height + " view");
        }

        @Override
        public void onBitmapFailed(Drawable errorDrawable) {
            Log.e(getClass().getSimpleName(), "Error loading image for product " + product.getName());
            productImage.setVisibility(GONE);
            // TODO product placeholder image?
        }

        @Override
        public void onPrepareLoad(Drawable placeHolderDrawable) {
        }
    }
} public class PLYAndroid {

    private static final String PREFS_NAME = "PLYAndroidPrefs";
    private static final String STATE_API_SCHEMA = "stateApiSchema";
    private static final String STATE_API_HOST = "stateApiHost";
    private static final String STATE_API_PORT = "stateApiPort";
    private static final String STATE_API_VERSION = "stateApiVersion";
    private static final String STATE_API_KEY = "stateApiKey";
    private static final String STATE_PROXY_ENABLED = "stateProxyEnabled";
    private static final String STATE_PROXY_HOST = "stateProxyHost";
    private static final String STATE_PROXY_PORT = "stateProxyPort";
    private static final String STATE_CONFIG = "stateConfig";
    private static final String STATE_USERNAME = "stateUsername";
    private static final String STATE_SESSION = "stateSession";
    private static final String STATE_TOKEN = "stateToken";
    private static final String STATE_USER_AGENT = "stateUserAgent";
    private static final String STATE_PREFERRED_LANGUAGE = "statePreferredLanguage";
    private static final String STATE_ADDITIONAL_LANGUAGES = "stateAdditionalLanguages";
    private static final String STATE_ADDITIONAL_HEADERS = "stateAdditionalHeaders";

    private final ExecutorService threadPool;

    private PLYRestClient client;
    private PLYQueryListener queryListener;
    private PLYUserProgressListener userProgressListener;
    private String language;

    private Handler mainHandler = new Handler(Looper.getMainLooper());

    private volatile Thread stateLoader;
    private volatile boolean stateLoaded;

    /**
     * Initializes a thread pool and the PLYRestClient to handle communication.
     *
     * {@code UserService.login} must be used to log in before using services requiring authentication.
     *
     * @param config
     *         the server connection, API version, any proxy information
     */
    public PLYAndroid(PLYRestClientConfig config) {
        stateLoaded = true;
        threadPool = Executors.newCachedThreadPool();
        initRestClient(config);
    }

    /**
     * Initializes a thread pool and uses the specified PLYRestClient to handle communication.
     *
     * @param client
     *         the REST client
     * @param orderedExecution
     *         true to activate just a single thread to handle service calls in order, false to initialize a
     *         thread pool handling several calls at the same time in an unspecified order
     */
    private PLYAndroid(PLYRestClient client, boolean orderedExecution) {
        stateLoaded = true;
        threadPool = orderedExecution ? Executors.newSingleThreadExecutor() : Executors.newCachedThreadPool();
        this.client = client;
    }

    /**
     * Initializes the REST client using an OkHttp RestTemplate and the provided configuration.
     *
     * @param config
     *         the server connection, API version, any proxy information
     */
    private void initRestClient(PLYRestClientConfig config) {
        client = new PLYRestClient(config);
        client.setRestTemplate(getOkHttpRestTemplate(config));
    }

    /**
     * Builds an HTTP client for Spring using OkHttp.
     *
     * @param config
     *         any proxy configuration
     * @return an OkHttp HTTP client for Spring
     */
    private RestTemplate getOkHttpRestTemplate(PLYRestClientConfig config) {
        OkHttpClient okHttpClient = new OkHttpClient();
        if (config.proxyEnabled) {
            Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(config.proxyHost, config
                    .proxyPort));
            okHttpClient.setProxy(proxy);
        }
        ClientHttpRequestFactory requestFactory = new OkHttpClientHttpRequestFactory(okHttpClient);
        RestTemplate restTemplate = new RestTemplate(requestFactory);
        PLYRestClient.enableConverters(restTemplate);
        return restTemplate;
    }

    /**
     * Creates a PLYAndroid instance executing any service calls strictly in the order they are called with
     * only one being active at a time. Shares the REST client and any headers with the original PLYAndroid
     * client - this is not a deep copy.
     *
     * @return a copied PLYAndroid instance with a new thread pool that executes service calls in order
     */
    public PLYAndroid copyForOrderedThreadExecution() {
        waitForStateLoaded();
        PLYAndroid sequentialClient = new PLYAndroid(client, true);
        sequentialClient.setQueryListener(queryListener);
        sequentialClient.setUserProgressListener(userProgressListener);
        sequentialClient.setLanguage(language);
        return sequentialClient;
    }

    /**
     * Creates a new {@link PLYTask} consisting of a REST client query and a completion object and submits it
     * to the thread pool.
     *
     * @param query
     *         the REST client query to execute
     * @param completion
     *         tasks to do on success and on error
     * @param <T>
     *         the type returned by the query
     * @return a Future object to optionally wait for the results or to cancel the query
     */
    public <T> Future<T> submit(Query<T> query, PLYCompletion<T> completion) {
        return threadPool.submit(new PLYTask<>(query, completion));
    }

    /**
     * @return the PLYRestClient handling synchronous communication with the server
     */
    public PLYRestClient getRestClient() {
        waitForStateLoaded();
        return client;
    }

    /**
     * Sets a callback for queries that complete with an error due to failed authentication.
     *
     * @param queryListener
     *         the query listener to install
     */
    public void setQueryListener(PLYQueryListener queryListener) {
        this.queryListener = queryListener;
    }

    /**
     * Sets a callback for queries that result in user progress (i.e. earned points, achievements).
     *
     * @param userProgressListener
     *         the user progress listener to install
     */
    public void setUserProgressListener(PLYUserProgressListener userProgressListener) {
        this.userProgressListener = userProgressListener;
    }

    /**
     * @return the preferred language in ISO 639-1 format
     */
    public String getLanguage() {
        waitForStateLoaded();
        return language;
    }

    /**
     * Sets the preferred language (sent in Accept-Language header).
     *
     * @param language
     *         the preferred language in ISO 639-1 format
     */
    public void setLanguage(String language) {
        this.language = language;
        client.setPreferredLanguage(language);
    }

    /**
     * Clears any headers the REST client is sending with requests for authorization purposes (client-side
     * logout).
     *
     * @see com.productlayer.android.sdk.services.UserService#logout
     * @see com.productlayer.android.sdk.services.UserService#login
     */
    public void resetAuth() {
        waitForStateLoaded();
        client.setUsername(null);
        client.setPassword(null);
        client.setSession(null);
        client.setToken(null);
    }

    /**
     * @return the currently stored authorization token without any "key=" prefix (or null if none is set), to
     * be used with {@link com.productlayer.android.sdk.services.UserService#login(PLYAndroid, String,
     * Boolean, PLYCompletion)}
     */
    public String getAuthToken() {
        waitForStateLoaded();
        String tokenHeader = client.getToken();
        if (tokenHeader == null) {
            return null;
        }
        return tokenHeader.replace(PLYRestClient.COOKIE_AUTH_TOKEN + "=", "");
    }

    /**
     * @return whether an authorization token is sent with each request (does not guarantee a successful
     * login)
     * @see com.productlayer.android.sdk.services.UserService#isSignedIn(PLYAndroid, PLYCompletion)
     * @see com.productlayer.android.sdk.services.UserService#login(PLYAndroid, String, Boolean,
     * PLYCompletion)
     */
    public boolean isAuthTokenSet() {
        String authToken = getAuthToken();
        return authToken != null && !authToken.isEmpty();
    }

    /**
     * Waits for the state loading thread to finish if it is currently running.
     */
    private void waitForStateLoaded() {
        if (stateLoaded) {
            return;
        }
        if (stateLoader != null) {
            try {
                stateLoader.join(1000);
            } catch (Exception ignored) {
            } finally {
                stateLoaded = true;
                stateLoader = null;
            }
        }
    }

    /**
     * Call this in your activity's {@link Activity#onStop} to save the currently used PLYAndroid instance
     * (i.e. config, auth) to persistent SharedPreferences storage.
     *
     * @param context
     *         the context to retrieve shared preferences with
     * @param saveConfig
     *         true to save static configuration such as API and any proxy information
     * @param saveDynamic
     *         true to save dynamic state such as the authentication token
     */
    public void onStop(Context context, boolean saveConfig, boolean saveDynamic) {
        if (!saveConfig && !saveDynamic) {
            return;
        }
        SharedPreferences settings = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = settings.edit();
        if (saveConfig) {
            // save client config in shared preferences
            editor.putString(STATE_API_SCHEMA, client.getConfigApiSchema());
            editor.putString(STATE_API_HOST, client.getConfigApiHost());
            editor.putInt(STATE_API_PORT, client.getConfigApiPort());
            editor.putString(STATE_API_VERSION, client.getConfigApiVersion());
            editor.putString(STATE_API_KEY, client.getConfigApiKey());
            editor.putBoolean(STATE_PROXY_ENABLED, client.isConfigProxyEnabled());
            editor.putString(STATE_PROXY_HOST, client.getConfigProxyHost());
            editor.putInt(STATE_PROXY_PORT, client.getConfigProxyPort());
        }
        if (saveDynamic) {
            // save auth session data in shared preferences
            editor.putString(STATE_USERNAME, client.getUsername());
            editor.putString(STATE_SESSION, client.getSession());
            editor.putString(STATE_TOKEN, client.getToken());
        }
        editor.apply();
    }

    /**
     * Call this in your activity's {@link Activity#onSaveInstanceState} to save the currently used PLYAndroid
     * instance (i.e. config, auth, locale, other headers) in non-persistent {@code Bundle} storage - to be
     * retrieved when/if the activity instance is restored.
     *
     * @param outState
     *         Bundle in which to store the information
     * @param saveConfig
     *         true to save static configuration such as API and any proxy information
     * @param saveDynamic
     *         true to save dynamic state such as the authentication token, language and other headers
     */
    public void onSaveInstanceState(Bundle outState, boolean saveConfig, boolean saveDynamic) {
        if (!saveConfig && !saveDynamic) {
            return;
        }
        if (saveConfig) {
            // save client config in bundle
            Serializable config = new PLYRestClientConfig(client.getConfigApiSchema(), client
                    .getConfigApiHost(), client.getConfigApiPort(), client.getConfigApiVersion(), client
                    .getConfigApiKey(), client.isConfigProxyEnabled(), client.getConfigProxyHost(), client
                    .getConfigProxyPort());
            outState.putSerializable(STATE_CONFIG, config);
        }
        if (saveDynamic) {
            // save auth session data, locale and other headers in bundle
            outState.putString(STATE_USERNAME, client.getUsername());
            outState.putString(STATE_SESSION, client.getSession());
            outState.putString(STATE_TOKEN, client.getToken());
            outState.putString(STATE_USER_AGENT, client.getUserAgent());
            outState.putString(STATE_PREFERRED_LANGUAGE, language);
            outState.putStringArray(STATE_ADDITIONAL_LANGUAGES, client.getAdditionalLanguages());
            outState.putSerializable(STATE_ADDITIONAL_HEADERS, (Serializable) client.getAdditionalHeaders());
        }
    }

    /**
     * Call this in your activity's {@link Activity#onCreate} to restore the previously used PLYAndroid
     * instance (i.e. config, auth) from persistent SharedPreferences storage. This method will not do
     * anything if the configuration is also saved in the supplied {@code Bundle}, in favor of {@link
     * #onRestoreInstanceState} restoring the data from non-persistent storage.
     *
     * In addition make sure to re-set any handlers.
     *
     * @param context
     *         the context to retrieve shared preferences with
     * @param savedInstanceState
     *         Bundle to retrieve previously stored information from
     * @param restoreConfig
     *         true to restore static configuration such as API and any proxy information
     * @param restoreDynamic
     *         true to restore dynamic state such as the authentication token
     */
    public void onCreate(final Context context, Bundle savedInstanceState, final boolean restoreConfig,
            final boolean restoreDynamic) {
        stateLoaded = false;
        if ((!restoreConfig && !restoreDynamic) || (savedInstanceState != null && savedInstanceState
                .getSerializable(STATE_CONFIG) != null)) {
            stateLoaded = true;
            // state will be restored in onRestoreInstanceState
            return;
        }
        // retrieve from shared preferences (in the bg due to disk access, i.e. performance reasons)
        Runnable stateLoaderRunnable = new Runnable() {
            @Override
            public void run() {
                SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
                if (restoreConfig) {
                    // restore client config from shared preferences
                    String apiSchema = prefs.getString(STATE_API_SCHEMA, null);
                    if (apiSchema != null) {
                        String apiHost = prefs.getString(STATE_API_HOST, null);
                        int apiPort = prefs.getInt(STATE_API_PORT, 0);
                        String apiVersion = prefs.getString(STATE_API_VERSION, null);
                        String apiKey = prefs.getString(STATE_API_KEY, null);
                        boolean proxyEnabled = prefs.getBoolean(STATE_PROXY_ENABLED, false);
                        String proxyHost = prefs.getString(STATE_PROXY_HOST, null);
                        int proxyPort = prefs.getInt(STATE_PROXY_PORT, 0);
                        initRestClient(new PLYRestClientConfig(apiSchema, apiHost, apiPort, apiVersion,
                                apiKey, proxyEnabled, proxyHost, proxyPort));
                        setLanguage(language);
                    }
                }
                if (restoreDynamic) {
                    // restore auth session data from shared preferences
                    String token = prefs.getString(STATE_TOKEN, null);
                    if (token != null) {
                        client.setToken(token);
                        client.setUsername(prefs.getString(STATE_USERNAME, null));
                        client.setSession(prefs.getString(STATE_SESSION, null));
                    }
                }
                stateLoaded = true;
                stateLoader = null;
            }
        };
        stateLoader = new Thread(stateLoaderRunnable);
        stateLoader.start();
    }

    /**
     * Call this in your activity's {@link Activity#onRestoreInstanceState} to restore the previously used
     * PLYAndroid instance (i.e. config, auth, locale, other headers) from non-persistent {@code Bundle}
     * storage.
     *
     * In addition make sure to re-set any handlers.
     *
     * @param savedInstanceState
     *         Bundle to retrieve previously stored information from
     * @param restoreConfig
     *         true to restore static configuration such as API and any proxy information
     * @param restoreDynamic
     *         true to restore dynamic state such as the authentication token, language and other headers
     */
    public void onRestoreInstanceState(Bundle savedInstanceState, boolean restoreConfig, boolean
            restoreDynamic) {
        stateLoaded = false;
        if ((!restoreConfig && !restoreDynamic) || savedInstanceState == null) {
            stateLoaded = true;
            return;
        }
        // retrieve stored information from bundle
        if (restoreConfig) {
            // restore client config from bundle
            PLYRestClientConfig config = (PLYRestClientConfig) savedInstanceState.getSerializable
                    (STATE_CONFIG);
            if (config != null) {
                initRestClient(config);
            }
        }
        if (restoreDynamic) {
            // restore auth session data, locale and other headers from bundle
            String token = savedInstanceState.getString(STATE_TOKEN);
            if (token != null) {
                client.setToken(token);
                client.setUsername(savedInstanceState.getString(STATE_USERNAME));
                client.setSession(savedInstanceState.getString(STATE_SESSION));
                client.setUserAgent(savedInstanceState.getString(STATE_USER_AGENT));
                client.setAdditionalLanguages(savedInstanceState.getStringArray(STATE_ADDITIONAL_LANGUAGES));
                //noinspection unchecked
                client.setAdditionalHeaders((Map<String, String>) savedInstanceState.getSerializable
                        (STATE_ADDITIONAL_HEADERS));
            }
            String language = savedInstanceState.getString(STATE_PREFERRED_LANGUAGE);
            if (language != null) {
                this.language = language;
            }
        }
        setLanguage(language);
        stateLoaded = true;
    }

    /**
     * Contains a single method returning an object of generic type.
     *
     * Used by the Android SDK service classes to encapsulate calls to the synchronous Java SDK.
     *
     * @param <T>
     *         the type used for the return value of its single execute method
     */
    public interface Query<T> {

        /**
         * @return the result of the implementing class' query
         * @throws PLYHttpException
         *         on any HTTP status code indicating failure
         * @throws RestClientException
         *         on any client-side HTTP error
         */
        T execute() throws PLYHttpException, RestClientException;

    }

    /**
     * Wraps an exception that may be thrown by the REST client, offering convenience methods to determine the
     * type of failure and to query a HTTP status code if present.
     */
    public class QueryError {

        private final Exception exception;

        /**
         * Creates a new query error caused by the given exception.
         *
         * @param exception
         *         the exception wrapped by this query error
         */
        public QueryError(Exception exception) {
            this.exception = exception;
        }

        /**
         * @return true if a HTTP status code that indicated an error (4xx, 5xx) was received, false if a
         * client-side error occurred
         */
        public boolean isHttpStatusError() {
            return exception instanceof PLYHttpException;
        }

        /**
         * @return the HTTP status code if this error was due to an erronous HTTP status code (4xx, 5xx), -1
         * else
         */
        public int getHttpStatusCode() {
            if (!isHttpStatusError()) {
                return -1;
            }
            return ((PLYHttpException) exception).getHttpStatus();
        }

        /**
         * @return any messages and/or HTTP status code in the wrapped exception
         */
        public String getMessage() {
            StringBuilder sb = new StringBuilder();
            if (isHttpStatusError()) {
                PLYHttpException e = (PLYHttpException) exception;
                int httpStatus = e.getHttpStatus();
                if (httpStatus > 0) {
                    sb.append("HTTP Status Code ").append(e.getHttpStatus()).append("\n");
                }
                List<ErrorMessage> errorMessages = e.getErrors();
                if (errorMessages != null) {
                    for (ErrorMessage errorMessage : errorMessages) {
                        sb.append(errorMessage.getMessage()).append(" (").append(errorMessage.getCode())
                                .append(")\n");
                    }
                }
            } else {
                String message = exception.getMessage();
                if (message != null) {
                    sb.append(message);
                }
            }
            return sb.toString();
        }

        /**
         * Looks through the attached internal {@link PLYStatusCodes} error codes to find the specified error
         * code.
         *
         * @param errorCode
         *         the error code to look for
         * @return true if {@code errorCode} was found, false else
         */
        public boolean hasInternalErrorCode(int errorCode) {
            if (!isHttpStatusError()) {
                return false;
            }
            PLYHttpException e = (PLYHttpException) exception;
            List<ErrorMessage> errorMessages = e.getErrors();
            if (errorMessages != null) {
                for (ErrorMessage errorMessage : errorMessages) {
                    if (errorMessage.getCode() == errorCode) {
                        return true;
                    }
                }
            }
            return false;
        }

        /**
         * @return the exception that caused this error (of type PLYHttpException on HTTP status codes
         * indicating failure, RestClientException on client-side HTTP errors)
         */
        public Exception getException() {
            return exception;
        }

    }

    /**
     * Executes a {@link Query} to forward its result to the {@link PLYCompletion} methods.
     *
     * Implements {@link Callable} - designed to be used by another thread.
     *
     * @param <T>
     *         the type that is returned by the {@link Query}
     */
    private class PLYTask<T> implements Callable<T> {

        private final Query<T> query;

        private final PLYCompletion<T> completion;

        /**
         * Stores the parameters to be used during {@link #call()}.
         *
         * @param query
         *         the query to execute
         * @param completion
         *         actions to set on success or on error once the query has finished
         */
        public PLYTask(Query<T> query, PLYCompletion<T> completion) {
            this.query = query;
            this.completion = completion;
        }

        /**
         * Executes the query and forwards its result to {@link PLYCompletion#onSuccess (Object)} or to {@link
         * PLYCompletion#onError(QueryError)} on any exception.
         *
         * @return the result of the query
         */
        @Override
        public T call() {
            final T result;
            HttpHeaders headers;
            try {
                result = query.execute();
                headers = client.getLastHttpHeaders();
            } catch (RuntimeException e) {
                final QueryError queryError = new QueryError(e);
                if (!queryError.isHttpStatusError() || queryError.getHttpStatusCode() != PLYStatusCodes
                        .HTTP_STATUS_FORBIDDEN_CODE || queryListener == null || !queryListener.onFailedAuth
                        (query, completion, queryError)) {
                    completion.onError(queryError);
                    mainHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            completion.onPostError(queryError);
                        }
                    });
                }
                throw e;
            }
            completion.onSuccess(result);
            mainHandler.post(new Runnable() {
                @Override
                public void run() {
                    completion.onPostSuccess(result);
                }
            });
            checkResponseHeaders(headers);
            return result;
        }

        /**
         * Checks the specified response headers for those indicating user progress (i.e. advancement in
         * points or achievements) and, if found, calls the installed {@link PLYUserProgressListener}.
         *
         * @param headers
         *         the HTTP headers to check
         */
        private void checkResponseHeaders(@SuppressWarnings("TypeMayBeWeakened") HttpHeaders headers) {
            if (headers == null) {
                return;
            }
            if (userProgressListener == null) {
                return;
            }
            String pointsStr = headers.getFirst(PLYRestClient.HEADER_USER_POINTS);
            String pointsChangeStr = headers.getFirst(PLYRestClient.HEADER_USER_POINTS_CHANGE);
            if (pointsStr != null && pointsChangeStr != null) {
                try {
                    final long points = Long.valueOf(pointsStr);
                    final int pointsChange = Integer.valueOf(pointsChangeStr);
                    mainHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            userProgressListener.earnedPoints(points, pointsChange);
                        }
                    });
                } catch (NumberFormatException ignored) {
                }
            }
            String achievements = headers.getFirst(PLYRestClient.HEADER_USER_NEW_ACHIEVEMENTS);
            if (achievements != null) {
                try {
                    final List<String> keys = new ArrayList<>();
                    JSONArray jsonArray = new JSONArray(achievements);
                    int jsonArraySize = jsonArray.length();
                    for (int i = 0; i < jsonArraySize; i++) {
                        keys.add(jsonArray.getString(i));
                    }
                    mainHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            userProgressListener.earnedAchievements(keys);
                        }
                    });
                } catch (JSONException ignored) {
                }
            }
        }

    }

} public abstract class PLYCompletion<T> {

    /**
     * Actions to set after a successful query. Do not modify the UI in this method.
     *
     * @param result
     *         the return value of the query
     */
    public abstract void onSuccess(T result);

    /**
     * Actions to set after a failed query. Do not directly modify the UI in this method.
     *
     * @param error
     *         contains the exception that was generated as a result of the failed query, can be used to
     *         determine the type of failure
     */
    public abstract void onError(PLYAndroid.QueryError error);

    /**
     * UI changes to display after {@link #onSuccess}. Runs on the UI thread.
     *
     * @param result
     *         the return value of the query
     */
    @SuppressWarnings("NoopMethodInAbstractClass")
    public void onPostSuccess(T result) {
    }

    /**
     * UI changes to display after {@link #onError}. Runs on the UI thread.
     *
     * @param error
     *         contains the exception that was generated as a result of the failed query, can be used to
     *         determine the type of failure
     */
    @SuppressWarnings("NoopMethodInAbstractClass")
    public void onPostError(PLYAndroid.QueryError error) {
    }

    /**
     * @return true if an authorization failure should trigger a login prompt, false else
     */
    public boolean promptForLogin() {
        return true;
    }

} public class ImageService {

    /**
     * Deletes a specific product image. Only the owner or an admin can delete the image. If the user earns
     * points for this operation 'X-ProductLayer-User-Points' and 'X-ProductLayer-User-Points-Changed' will be
     * present in the response header.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param imageID
     *         The identifier of the image
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         metadata of any deleted images
     * @return a Future object to optionally wait for the {@code ProductImage[]} result or to cancel the query
     */
    public static Future<ProductImage[]> deleteProductImage(final PLYAndroid client, final String imageID,
            PLYCompletion<ProductImage[]> completion) {
        return client.submit(new PLYAndroid.Query<ProductImage[]>() {
            @Override
            public ProductImage[] execute() {
                return com.productlayer.rest.client.services.ImageService.deleteProductImage(client
                        .getRestClient(), imageID);
            }
        }, completion);
    }

    /**
     * Deletes the avatar image of a user.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param userID
     *         The identifier of the user
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         metadata of the deleted user avatar image
     * @return a Future object to optionally wait for the {@code UserAvatarImage} result or to cancel the
     * query
     */
    public static Future<UserAvatarImage> deleteUserAvatar(final PLYAndroid client, final String userID,
            PLYCompletion<UserAvatarImage> completion) {
        return client.submit(new PLYAndroid.Query<UserAvatarImage>() {
            @Override
            public UserAvatarImage execute() {
                return com.productlayer.rest.client.services.ImageService.deleteUserAvatar(client
                        .getRestClient(), userID);
            }
        }, completion);
    }

    /**
     * Downvotes a specific product image. If the user already up voted the image the up-vote will be
     * removed.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param imageID
     *         The identifier of the metadata object or the identifier of the file ({image_id}.jpg)
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The image
     *         metadata with the new vote score
     * @return a Future object to optionally wait for the {@code ProductImage} result or to cancel the query
     */
    public static Future<ProductImage> downVoteProductImage(final PLYAndroid client, final String imageID,
            PLYCompletion<ProductImage> completion) {
        return client.submit(new PLYAndroid.Query<ProductImage>() {
            @Override
            public ProductImage execute() {
                return com.productlayer.rest.client.services.ImageService.downVoteProductImage(client
                        .getRestClient(), imageID);
            }
        }, completion);
    }

    /**
     * Gets the default image (highest voted image) of a product. For some browsers you need to add the .jpg
     * file extension (/product/{gtin}/default_image.jpg) to the url.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param gtin
     *         The GTIN (barcode) of the product
     * @param maxWidth
     *         [Optional] The preferred maximum width
     * @param maxHeight
     *         [Optional] The preferred maximum height
     * @param crop
     *         [Optional] Whether the image should be cropped
     * @param quality
     *         [Optional] The quality of the image between 20 and 100
     * @return the URL to the requested data
     */
    public static String getDefaultProductImageForSizeURL(final PLYAndroid client, final String gtin, final
    Integer maxWidth, final Integer maxHeight, final Boolean crop, final Integer quality) {
        return com.productlayer.rest.client.services.ImageService.getDefaultProductImageForSizeURL(client
                .getRestClient(), gtin, maxWidth, maxHeight, crop, quality);
    }

    /**
     * Gets the metadata of the default image (highest voted image) of a specific product.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param gtin
     *         The GTIN (barcode) of the product
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The image
     *         metadata
     * @return a Future object to optionally wait for the {@code ProductImage} result or to cancel the query
     */
    public static Future<ProductImage> getDefaultProductImageMeta(final PLYAndroid client, final String
            gtin, PLYCompletion<ProductImage> completion) {
        return client.submit(new PLYAndroid.Query<ProductImage>() {
            @Override
            public ProductImage execute() {
                return com.productlayer.rest.client.services.ImageService.getDefaultProductImageMeta(client
                        .getRestClient(), gtin);
            }
        }, completion);
    }

    /**
     * Gets a specific image. For some browsers you need to add the .jpg file extension
     * (/image/{image_id}.jpg) to the url.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param imageID
     *         The identifier of the image
     * @param maxWidth
     *         [Optional] The preferred maximum width
     * @param maxHeight
     *         [Optional] The preferred maximum height
     * @param crop
     *         [Optional] Whether the image should be cropped
     * @param quality
     *         [Optional] The quality of the image between 20 and 100
     * @return the URL to the requested data
     */
    public static String getImageForSizeURL(final PLYAndroid client, final String imageID, final Integer
            maxWidth, final Integer maxHeight, final Boolean crop, final Integer quality) {
        return com.productlayer.rest.client.services.ImageService.getImageForSizeURL(client.getRestClient()
                , imageID, maxWidth, maxHeight, crop, quality);
    }

    /**
     * Gets a specific image meta information.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param imageID
     *         The identifier of the image
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The image
     *         metadata
     * @return a Future object to optionally wait for the {@code ProductImage} result or to cancel the query
     */
    public static Future<ProductImage> getImageMeta(final PLYAndroid client, final String imageID,
            PLYCompletion<ProductImage> completion) {
        return client.submit(new PLYAndroid.Query<ProductImage>() {
            @Override
            public ProductImage execute() {
                return com.productlayer.rest.client.services.ImageService.getImageMeta(client.getRestClient
                        (), imageID);
            }
        }, completion);
    }

    /**
     * Gets all image metadata of a specific product. Use this to get all image URLs of a product.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param gtin
     *         The GTIN (barcode) of the product
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: Any found
     *         product image metadata
     * @return a Future object to optionally wait for the {@code ProductImage[]} result or to cancel the query
     */
    public static Future<ProductImage[]> getProductImages(final PLYAndroid client, final String gtin,
            PLYCompletion<ProductImage[]> completion) {
        return client.submit(new PLYAndroid.Query<ProductImage[]>() {
            @Override
            public ProductImage[] execute() {
                return com.productlayer.rest.client.services.ImageService.getProductImages(client
                        .getRestClient(), gtin);
            }
        }, completion);
    }

    /**
     * Gets the avatar of a specific user.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param userID
     *         The identifier of the user
     * @param size
     *         [Optional] The size of the avatar image in pixel. The avatar image is always a square image and
     *         the maximum size is 512 pixel.
     * @return the URL to the requested data
     */
    public static String getUserAvatarURL(final PLYAndroid client, final String userID, final Integer size) {
        return com.productlayer.rest.client.services.ImageService.getUserAvatarURL(client.getRestClient(),
                userID, size);
    }

    /**
     * Sends a report about copyright infringements or any other problems with the image.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param imageID
     *         The identifier of the image
     * @param report
     *         The report
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         problem report object
     * @return a Future object to optionally wait for the {@code ProblemReport} result or to cancel the query
     */
    public static Future<ProblemReport> reportImage(final PLYAndroid client, final String imageID, final
    ProblemReport report, PLYCompletion<ProblemReport> completion) {
        return client.submit(new PLYAndroid.Query<ProblemReport>() {
            @Override
            public ProblemReport execute() {
                return com.productlayer.rest.client.services.ImageService.reportImage(client.getRestClient
                        (), imageID, report);
            }
        }, completion);
    }

    /**
     * Rotates the image clockwise.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param imageID
     *         The identifier of the image
     * @param degrees
     *         [Optional] The degrees to rotate the image clockwise
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The image
     *         metadata after the rotation
     * @return a Future object to optionally wait for the {@code ProductImage} result or to cancel the query
     */
    public static Future<ProductImage> rotateImage(final PLYAndroid client, final String imageID, final
    Integer degrees, PLYCompletion<ProductImage> completion) {
        return client.submit(new PLYAndroid.Query<ProductImage>() {
            @Override
            public ProductImage execute() {
                return com.productlayer.rest.client.services.ImageService.rotateImage(client.getRestClient
                        (), imageID, degrees);
            }
        }, completion);
    }

    /**
     * Upvotes a specific product image. If the user already down voted the image the down-vote will be
     * removed.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param imageID
     *         The identifier of the metadata object or the identifier of the file ({image_id}.jpg)
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The image
     *         metadata with the new vote score
     * @return a Future object to optionally wait for the {@code ProductImage} result or to cancel the query
     */
    public static Future<ProductImage> upVoteProductImage(final PLYAndroid client, final String imageID,
            PLYCompletion<ProductImage> completion) {
        return client.submit(new PLYAndroid.Query<ProductImage>() {
            @Override
            public ProductImage execute() {
                return com.productlayer.rest.client.services.ImageService.upVoteProductImage(client
                        .getRestClient(), imageID);
            }
        }, completion);
    }

    /**
     * Updates the avatar image of a user.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param userID
     *         The identifier of the user
     * @param filePath
     *         the path to the file to upload
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         metadata of the updated user avatar image
     * @return a Future object to optionally wait for the {@code UserAvatarImage} result or to cancel the
     * query
     */
    public static Future<UserAvatarImage> updateUserAvatar(final PLYAndroid client, final String userID,
            final String filePath, PLYCompletion<UserAvatarImage> completion) {
        return client.submit(new PLYAndroid.Query<UserAvatarImage>() {
            @Override
            public UserAvatarImage execute() {
                return com.productlayer.rest.client.services.ImageService.updateUserAvatar(client
                        .getRestClient(), userID, filePath);
            }
        }, completion);
    }

    /**
     * Uploads an image for an opine. If the user earns points for this operation 'X-ProductLayer-User-Points'
     * and 'X-ProductLayer-User-Points-Changed' will be present in the response header.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param opineID
     *         The identifier of the opine
     * @param filePath
     *         the path to the file to upload
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         metadata of the uploaded image
     * @return a Future object to optionally wait for the {@code ProductImage} result or to cancel the query
     */
    public static Future<ProductImage> uploadOpineImage(final PLYAndroid client, final String opineID,
            final String filePath, PLYCompletion<ProductImage> completion) {
        return client.submit(new PLYAndroid.Query<ProductImage>() {
            @Override
            public ProductImage execute() {
                return com.productlayer.rest.client.services.ImageService.uploadOpineImage(client
                        .getRestClient(), opineID, filePath);
            }
        }, completion);
    }

    /**
     * Uploads a product image. If the user earns points for this operation 'X-ProductLayer-User-Points' and
     * 'X-ProductLayer-User-Points-Changed' will be present in the response header.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param gtin
     *         The GTIN (barcode) of the product
     * @param filePath
     *         the path to the file to upload
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The image
     *         metadata
     * @return a Future object to optionally wait for the {@code ProductImage} result or to cancel the query
     */
    public static Future<ProductImage> uploadProductImage(final PLYAndroid client, final String gtin, final
    String filePath, PLYCompletion<ProductImage> completion) {
        return client.submit(new PLYAndroid.Query<ProductImage>() {
            @Override
            public ProductImage execute() {
                return com.productlayer.rest.client.services.ImageService.uploadProductImage(client
                        .getRestClient(), gtin, filePath);
            }
        }, completion);
    }
} public class ProductService {

    /**
     * Creates a new product. If the user earns points for this operation 'X-ProductLayer-User-Points' and
     * 'X-ProductLayer-User-Points-Changed' will be present in the response header.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param product
     *         The product
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The newly
     *         created product
     * @return a Future object to optionally wait for the {@code Product} result or to cancel the query
     */
    public static Future<Product> createProduct(final PLYAndroid client, final Product product,
            PLYCompletion<Product> completion) {
        return client.submit(new PLYAndroid.Query<Product>() {
            @Override
            public Product execute() {
                return com.productlayer.rest.client.services.ProductService.createProduct(client
                        .getRestClient(), product);
            }
        }, completion);
    }

    /**
     * Downvotes a specific product. By using the product ID instead of the GTIN it's possible to vote for a
     * specific localized product.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param productID
     *         The identifier of the product (identifies a specific localized product, e.g.: Apple iPhone 5S
     *         (en))
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         product with the new vote score
     * @return a Future object to optionally wait for the {@code Product} result or to cancel the query
     */
    public static Future<Product> downVoteProduct(final PLYAndroid client, final String productID,
            PLYCompletion<Product> completion) {
        return client.submit(new PLYAndroid.Query<Product>() {
            @Override
            public Product execute() {
                return com.productlayer.rest.client.services.ProductService.downVoteProduct(client
                        .getRestClient(), productID);
            }
        }, completion);
    }

    /**
     * Gets suggestions of brand owners of a GTIN.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param gtin
     *         The GTIN (barcode) of the product
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: Any
     *         suggested brand owners for a GTIN
     * @return a Future object to optionally wait for the {@code BrandOwner[]} result or to cancel the query
     */
    public static Future<BrandOwner[]> getBrandOwnerSuggestions(final PLYAndroid client, final String gtin,
            PLYCompletion<BrandOwner[]> completion) {
        return client.submit(new PLYAndroid.Query<BrandOwner[]>() {
            @Override
            public BrandOwner[] execute() {
                return com.productlayer.rest.client.services.ProductService.getBrandOwnerSuggestions(client
                        .getRestClient(), gtin);
            }
        }, completion);
    }

    /**
     * Gets known brand owner names.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: All brand
     *         owner names
     * @return a Future object to optionally wait for the {@code String[]} result or to cancel the query
     */
    public static Future<String[]> getBrandOwners(final PLYAndroid client, PLYCompletion<String[]>
            completion) {
        return client.submit(new PLYAndroid.Query<String[]>() {
            @Override
            public String[] execute() {
                return com.productlayer.rest.client.services.ProductService.getBrandOwners(client
                        .getRestClient());
            }
        }, completion);
    }

    /**
     * Gets known brand names.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: All brand
     *         names
     * @return a Future object to optionally wait for the {@code String[]} result or to cancel the query
     */
    public static Future<String[]> getBrands(final PLYAndroid client, PLYCompletion<String[]> completion) {
        return client.submit(new PLYAndroid.Query<String[]>() {
            @Override
            public String[] execute() {
                return com.productlayer.rest.client.services.ProductService.getBrands(client.getRestClient());
            }
        }, completion);
    }

    /**
     * Gets suggestions of categories of a GTIN.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param gtin
     *         The GTIN (barcode) of the product
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: Any
     *         suggested categories for a GTIN
     * @return a Future object to optionally wait for the {@code Category[]} result or to cancel the query
     */
    public static Future<Category[]> getCategorySuggestions(final PLYAndroid client, final String gtin,
            PLYCompletion<Category[]> completion) {
        return client.submit(new PLYAndroid.Query<Category[]>() {
            @Override
            public Category[] execute() {
                return com.productlayer.rest.client.services.ProductService.getCategorySuggestions(client
                        .getRestClient(), gtin);
            }
        }, completion);
    }

    /**
     * Gets the scoring history within a range from a product.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param from_date
     *         [Optional] Start date, format: yyyy-MM-dd HH:mm:ss
     * @param to_date
     *         [Optional] End date, format: yyyy-MM-dd HH:mm:ss
     * @param count
     *         [Optional] The amount of results to be returned, default: '200'
     * @param language
     *         [Optional] The preferred language (e.g.: 'en' or 'de'), default: 'en'
     * @param showOpines
     *         [Optional] Display opines, default: 'true'
     * @param showReviews
     *         [Optional] Display reviews, default: 'true'
     * @param showPictures
     *         [Optional] Display uploaded images, default: 'true'
     * @param showProducts
     *         [Optional] Display created/updated products, default: 'true'
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         hottest products contained within a ranking results object
     * @return a Future object to optionally wait for the {@code RankingResults<Product>} result or to cancel
     * the query
     */
    public static Future<RankingResults<Product>> getHottestProducts(final PLYAndroid client, final Date
            from_date, final Date to_date, final Integer count, final String language, final Boolean
            showOpines, final Boolean showReviews, final Boolean showPictures, final Boolean showProducts,
            PLYCompletion<RankingResults<Product>> completion) {
        return client.submit(new PLYAndroid.Query<RankingResults<Product>>() {
            @Override
            public RankingResults<Product> execute() {
                return com.productlayer.rest.client.services.ProductService.getHottestProducts(client
                        .getRestClient(), from_date, to_date, count, language, showOpines, showReviews,
                        showPictures, showProducts);
            }
        }, completion);
    }

    /**
     * Gets localized category keys.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param language
     *         [Optional] The preferred language (e.g.: 'en' or 'de'), default: 'en'
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: All
     *         category keys and as their value the translation to the
     * @return a Future object to optionally wait for the {@code Map<String, String>} result or to cancel the
     * query preferred language
     */
    public static Future<Map<String, String>> getLocalizedCategories(final PLYAndroid client, final String
            language, PLYCompletion<Map<String, String>> completion) {
        return client.submit(new PLYAndroid.Query<Map<String, String>>() {
            @Override
            public Map<String, String> execute() {
                return com.productlayer.rest.client.services.ProductService.getLocalizedCategories(client
                        .getRestClient(), language);
            }
        }, completion);
    }

    /**
     * Gets localized characteristics keys.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param language
     *         [Optional] The preferred language (e.g.: 'en' or 'de'), default: 'en'
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: All
     *         characteristics keys and as their value the translation to
     * @return a Future object to optionally wait for the {@code Map<String, String>} result or to cancel the
     * query the preferred language
     */
    public static Future<Map<String, String>> getLocalizedCharacteristics(final PLYAndroid client, final
    String language, PLYCompletion<Map<String, String>> completion) {
        return client.submit(new PLYAndroid.Query<Map<String, String>>() {
            @Override
            public Map<String, String> execute() {
                return com.productlayer.rest.client.services.ProductService.getLocalizedCharacteristics
                        (client.getRestClient(), language);
            }
        }, completion);
    }

    /**
     * Gets localized nutrition keys.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param language
     *         [Optional] The preferred language (e.g.: 'en' or 'de'), default: 'en'
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: All
     *         nutrition keys and as their value the translation to the
     * @return a Future object to optionally wait for the {@code Map<String, String>} result or to cancel the
     * query preferred language
     */
    public static Future<Map<String, String>> getLocalizedNutrition(final PLYAndroid client, final String
            language, PLYCompletion<Map<String, String>> completion) {
        return client.submit(new PLYAndroid.Query<Map<String, String>>() {
            @Override
            public Map<String, String> execute() {
                return com.productlayer.rest.client.services.ProductService.getLocalizedNutrition(client
                        .getRestClient(), language);
            }
        }, completion);
    }

    /**
     * Gets the overall product count or the count for a specific timeframe.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param from_date
     *         [Optional] Start date, format: yyyy-MM-dd HH:mm:ss
     * @param to_date
     *         [Optional] End date, format: yyyy-MM-dd HH:mm:ss
     * @param categoryKey
     *         [Optional] The category key starting with 'pl-prod-cat-', e.g.: pl-prod-cat-books
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         product count
     * @return a Future object to optionally wait for the {@code Count} result or to cancel the query
     */
    public static Future<Count> getProductCount(final PLYAndroid client, final Date from_date, final Date
            to_date, final String categoryKey, PLYCompletion<Count> completion) {
        return client.submit(new PLYAndroid.Query<Count>() {
            @Override
            public Count execute() {
                return com.productlayer.rest.client.services.ProductService.getProductCount(client
                        .getRestClient(), from_date, to_date, categoryKey);
            }
        }, completion);
    }

    /**
     * Gets a product by GTIN and language. If language=auto the best language match will be returned. Best
     * language match using preferred language header and predefined secondary languages.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param gtin
     *         The GTIN (barcode) of the product
     * @param language
     *         [Optional] The preferred language (e.g.: 'en' or 'de')
     * @param suggestions
     *         [Optional] Make product suggestions if search returns no results. Product suggestions are all
     *         returned products without a pl-id parameter. Default: false
     * @param fetchOnly
     *         [Optional] Fetch only specific keys
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         identified product
     * @return a Future object to optionally wait for the {@code Product} result or to cancel the query
     */
    public static Future<Product> getProductForGtin(final PLYAndroid client, final String gtin, final
    String language, final Boolean suggestions, final String fetchOnly, PLYCompletion<Product> completion) {
        return client.submit(new PLYAndroid.Query<Product>() {
            @Override
            public Product execute() {
                return com.productlayer.rest.client.services.ProductService.getProductForGtin(client
                        .getRestClient(), gtin, language, suggestions, fetchOnly);
            }
        }, completion);
    }

    /**
     * Gets a product by GTIN and language.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param gtin
     *         The GTIN (barcode) of the product
     * @param language
     *         The preferred language (e.g.: 'en' or 'de')
     * @param searchAlsoOtherLocales
     *         Whether to also try different locales if the one specified is not found
     * @param fetchOnly
     *         [Optional] Fetch only specific keys
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         identified product or null if not found
     * @return a Future object to optionally wait for the {@code Product} result or to cancel the query
     */
    public static Future<Product> getProductForGtinAndLocale(final PLYAndroid client, final String gtin,
            final String language, final boolean searchAlsoOtherLocales, final boolean suggestions, final
    String fetchOnly, PLYCompletion<Product> completion) {
        return client.submit(new PLYAndroid.Query<Product>() {
            @Override
            public Product execute() {
                return com.productlayer.rest.client.services.ProductService.getProductForGtinAndLocale
                        (client.getRestClient(), gtin, language, searchAlsoOtherLocales, suggestions,
                                fetchOnly);
            }
        }, completion);
    }

    /**
     * Gets the values of a specific key.<br> <br> e.g.: ?key=pl-brand-name&amp;language=en returns all brands
     * which have been entered for products with the locale en.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param key
     *         The key to query the values of.
     * @param language
     *         [Optional] The preferred language (e.g.: 'en' or 'de'), default: 'en'
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: All values
     *         of the specified key
     * @return a Future object to optionally wait for the {@code ValuesForKey} result or to cancel the query
     */
    public static Future<ValuesForKey> getValuesForKey(final PLYAndroid client, final String key, final
    String language, PLYCompletion<ValuesForKey> completion) {
        return client.submit(new PLYAndroid.Query<ValuesForKey>() {
            @Override
            public ValuesForKey execute() {
                return com.productlayer.rest.client.services.ProductService.getValuesForKey(client
                        .getRestClient(), key, language);
            }
        }, completion);
    }

    /**
     * Sends a report about copyright infringements or any other problems with the product.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param productID
     *         The identifier of the product (identifies a specific localized product, e.g.: Apple iPhone 5S
     *         (en))
     * @param report
     *         The report
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         problem report object
     * @return a Future object to optionally wait for the {@code ProblemReport} result or to cancel the query
     */
    public static Future<ProblemReport> reportProduct(final PLYAndroid client, final String productID,
            final ProblemReport report, PLYCompletion<ProblemReport> completion) {
        return client.submit(new PLYAndroid.Query<ProblemReport>() {
            @Override
            public ProblemReport execute() {
                return com.productlayer.rest.client.services.ProductService.reportProduct(client
                        .getRestClient(), productID, report);
            }
        }, completion);
    }

    /**
     * Searches for products. If no search parameters are present the first 50 products will be presented.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param query
     *         [Optional] The query may contain the name, GTIN or brand of the product. <b>ATTENTION: If the
     *         query is set all other url parameters will be ignored!</b>
     * @param page
     *         [Optional] The page to be displayed starting with 0 - if no page has been provided, the first
     *         page will be shown
     * @param recordsPerPage
     *         [Optional] The amount of items to be displayed per page, default: '200'
     * @param gtin
     *         [Optional] The GTIN (barcode) of the product
     * @param brand
     *         [Optional] The brand of the product
     * @param brandOwner
     *         [Optional] The brand owner of the product
     * @param language
     *         [Optional] The preferred language (e.g.: 'en' or 'de')
     * @param suggestions
     *         [Optional] Make product suggestions if search returns no results. Product suggestions are all
     *         returned products without a pl-id parameter. Default: false
     * @param fetchOnly
     *         [Optional] Fetch only specific keys
     * @param name
     *         [Optional] The name of the product or a substring of it.
     * @param categoryKey
     *         [Optional] The category key starting with 'pl-prod-cat-', e.g.: pl-prod-cat-books
     * @param order_by
     *         [Optional] Used to sort the result-set by one or more columns. The order by parameters are
     *         <strong>seperated by a semicolon</strong>. Also you need to provide a prefix <strong>asc for
     *         ascending</strong> or <strong>desc for descending order</strong><br> <br>
     *         <strong>Default:</strong> pl-prod-name_asc (Product names ascending)
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: Any
     *         products matching the specified criteria
     * @return a Future object to optionally wait for the {@code Product[]} result or to cancel the query
     */
    public static Future<Product[]> searchProducts(final PLYAndroid client, final String query, final
    Integer page, final Integer recordsPerPage, final String gtin, final String brand, final String
            brandOwner, final String language, final Boolean suggestions, final String fetchOnly, final
    String name, final String categoryKey, final String order_by, PLYCompletion<Product[]> completion) {
        return client.submit(new PLYAndroid.Query<Product[]>() {
            @Override
            public Product[] execute() {
                return com.productlayer.rest.client.services.ProductService.searchProducts(client
                        .getRestClient(), query, page, recordsPerPage, gtin, brand, brandOwner, language,
                        suggestions, fetchOnly, name, categoryKey, order_by);
            }
        }, completion);
    }

    /**
     * Searches for products by brand.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param brand
     *         The brand of the product
     * @param page
     *         [Optional] The page to be displayed starting with 0 - if no page has been provided, the first
     *         page will be shown
     * @param recordsPerPage
     *         [Optional] The amount of items to be displayed per page, default: '200'
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: Any
     *         products matching the brand
     * @return a Future object to optionally wait for the {@code Product[]} result or to cancel the query
     */
    public static Future<Product[]> searchProductsByBrand(final PLYAndroid client, final String brand,
            final Integer page, final Integer recordsPerPage, PLYCompletion<Product[]> completion) {
        return client.submit(new PLYAndroid.Query<Product[]>() {
            @Override
            public Product[] execute() {
                return com.productlayer.rest.client.services.ProductService.searchProductsByBrand(client
                        .getRestClient(), brand, page, recordsPerPage);
            }
        }, completion);
    }

    /**
     * Searches for products by brand owner.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param brandOwner
     *         The brand owner of the product
     * @param page
     *         [Optional] The page to be displayed starting with 0 - if no page has been provided, the first
     *         page will be shown
     * @param recordsPerPage
     *         [Optional] The amount of items to be displayed per page, default: '200'
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: Any
     *         products matching the brand owner
     * @return a Future object to optionally wait for the {@code Product[]} result or to cancel the query
     */
    public static Future<Product[]> searchProductsByBrandOwner(final PLYAndroid client, final String
            brandOwner, final Integer page, final Integer recordsPerPage, PLYCompletion<Product[]>
            completion) {
        return client.submit(new PLYAndroid.Query<Product[]>() {
            @Override
            public Product[] execute() {
                return com.productlayer.rest.client.services.ProductService.searchProductsByBrandOwner
                        (client.getRestClient(), brandOwner, page, recordsPerPage);
            }
        }, completion);
    }

    /**
     * Searches for a product by GTIN (more than one result may be returned). The GTIN is unique for a product
     * but a Product object will be returned per locale.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param gtin
     *         [Optional] The GTIN (barcode) of the product
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: Any
     *         products matching the GTIN
     * @return a Future object to optionally wait for the {@code Product[]} result or to cancel the query
     */
    public static Future<Product[]> searchProductsByGtin(final PLYAndroid client, final String gtin, final
    boolean suggestions, PLYCompletion<Product[]> completion) {
        return client.submit(new PLYAndroid.Query<Product[]>() {
            @Override
            public Product[] execute() {
                return com.productlayer.rest.client.services.ProductService.searchProductsByGtin(client
                        .getRestClient(), gtin, suggestions);
            }
        }, completion);
    }

    /**
     * Searches for products by using the query string.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param query
     *         The query may contain the name, GTIN or brand of the product
     * @param categoryKey
     *         [Optional] The category key starting with 'pl-prod-cat-', e.g.: pl-prod-cat-books
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: Any
     *         products matching the specified criteria
     * @return a Future object to optionally wait for the {@code Product[]} result or to cancel the query
     */
    public static Future<Product[]> searchProductsByQuery(final PLYAndroid client, final String query,
            final String categoryKey, final boolean suggestions, PLYCompletion<Product[]> completion) {
        return client.submit(new PLYAndroid.Query<Product[]>() {
            @Override
            public Product[] execute() {
                return com.productlayer.rest.client.services.ProductService.searchProductsByQuery(client
                        .getRestClient(), query, categoryKey, suggestions);
            }
        }, completion);
    }

    /**
     * Upvotes a specific product. By using the product ID instead of the GTIN it's possible to vote for a
     * specific localized product.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param productID
     *         The identifier of the product (identifies a specific localized product, e.g.: Apple iPhone 5S
     *         (en))
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         product with the new vote score
     * @return a Future object to optionally wait for the {@code Product} result or to cancel the query
     */
    public static Future<Product> upVoteProduct(final PLYAndroid client, final String productID,
            PLYCompletion<Product> completion) {
        return client.submit(new PLYAndroid.Query<Product>() {
            @Override
            public Product execute() {
                return com.productlayer.rest.client.services.ProductService.upVoteProduct(client
                        .getRestClient(), productID);
            }
        }, completion);
    }

    /**
     * Updates a specific product. If the user earns points for this operation 'X-ProductLayer-User-Points'
     * and 'X-ProductLayer-User-Points-Changed' will be present in the response header.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param product
     *         The product
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         updated product
     * @return a Future object to optionally wait for the {@code Product} result or to cancel the query
     */
    public static Future<Product> updateProduct(final PLYAndroid client, final Product product,
            PLYCompletion<Product> completion) {
        return client.submit(new PLYAndroid.Query<Product>() {
            @Override
            public Product execute() {
                return com.productlayer.rest.client.services.ProductService.updateProduct(client
                        .getRestClient(), product);
            }
        }, completion);
    }

    /**
     * Updates a specific product. If the user earns points for this operation 'X-ProductLayer-User-Points'
     * and 'X-ProductLayer-User-Points-Changed' will be present in the response header.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param gtin
     *         The GTIN (barcode) of the product
     * @param product
     *         The product
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         updated product
     * @return a Future object to optionally wait for the {@code Product} result or to cancel the query
     */
    public static Future<Product> updateProduct(final PLYAndroid client, final String gtin, final Product
            product, PLYCompletion<Product> completion) {
        return client.submit(new PLYAndroid.Query<Product>() {
            @Override
            public Product execute() {
                return com.productlayer.rest.client.services.ProductService.updateProduct(client
                        .getRestClient(), gtin, product);
            }
        }, completion);
    }
} public class TimelineService {

    /**
     * Gets the most recent social content posted by the signed in user. The timeline is always sorted by
     * date.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param count
     *         [Optional] The amount of results to be returned, default and maximum: '200'
     * @param sinceID
     *         [Optional] Results with an ID greater than (that is, more recent than) the specified ID
     * @param untilID
     *         [Optional] Results with an ID less than (that is, older than) the specified ID
     * @param showOpines
     *         [Optional] Display opines, default: 'true'
     * @param showReviews
     *         [Optional] Display reviews, default: 'true'
     * @param showPictures
     *         [Optional] Display uploaded images, default: 'true'
     * @param showProducts
     *         [Optional] Display created/updated products, default: 'true'
     * @param includeFriends
     *         [Optional] Show also content created by friends, default: 'false'
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         timeline of the currently signed in user and URLs to load the
     * @return a Future object to optionally wait for the {@code ResultSetWithCursor} result or to cancel the
     * query timeline since or until the provided timeline
     */
    public static Future<ResultSetWithCursor> getMyTimeline(final PLYAndroid client, final Integer count,
            final String sinceID, final String untilID, final Boolean showOpines, final Boolean
            showReviews, final Boolean showPictures, final Boolean showProducts, final Boolean
            includeFriends, PLYCompletion<ResultSetWithCursor> completion) {
        return client.submit(new PLYAndroid.Query<ResultSetWithCursor>() {
            @Override
            public ResultSetWithCursor execute() {
                return com.productlayer.rest.client.services.TimelineService.getMyTimeline(client
                        .getRestClient(), count, sinceID, untilID, showOpines, showReviews, showPictures,
                        showProducts, includeFriends);
            }
        }, completion);
    }

    /**
     * Gets the most recent social content for a product. The timeline is always sorted by date.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param gtin
     *         The GTIN (barcode) of the product
     * @param count
     *         [Optional] The amount of results to be returned, default and maximum: '200'
     * @param sinceID
     *         [Optional] Results with an ID greater than (that is, more recent than) the specified ID
     * @param untilID
     *         [Optional] Results with an ID less than (that is, older than) the specified ID
     * @param showOpines
     *         [Optional] Display opines, default: 'true'
     * @param showReviews
     *         [Optional] Display reviews, default: 'true'
     * @param showPictures
     *         [Optional] Display uploaded images, default: 'true'
     * @param showProducts
     *         [Optional] Display created/updated products, default: 'true'
     * @param showFriendsOnly
     *         [Optional] Show only content created by friends (followed users), default: 'false'
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         timeline and URLs to load the timeline since or until the
     * @return a Future object to optionally wait for the {@code ResultSetWithCursor} result or to cancel the
     * query provided timeline.
     */
    public static Future<ResultSetWithCursor> getProductTimeline(final PLYAndroid client, final String
            gtin, final Integer count, final String sinceID, final String untilID, final Boolean
            showOpines, final Boolean showReviews, final Boolean showPictures, final Boolean showProducts,
            final Boolean showFriendsOnly, PLYCompletion<ResultSetWithCursor> completion) {
        return client.submit(new PLYAndroid.Query<ResultSetWithCursor>() {
            @Override
            public ResultSetWithCursor execute() {
                return com.productlayer.rest.client.services.TimelineService.getProductTimeline(client
                        .getRestClient(), gtin, count, sinceID, untilID, showOpines, showReviews,
                        showPictures, showProducts, showFriendsOnly);
            }
        }, completion);
    }

    /**
     * Gets the most recent social content posted. The timeline is always sorted by date.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param categoryKey
     *         [Optional] The category key starting with 'pl-prod-cat-', e.g.: pl-prod-cat-books
     * @param brand
     *         [Optional] The brand of the product
     * @param brandOwner
     *         [Optional] The brand owner of the product
     * @param count
     *         [Optional] The amount of results to be returned, default and maximum: '200'
     * @param sinceID
     *         [Optional] Results with an ID greater than (that is, more recent than) the specified ID
     * @param untilID
     *         [Optional] Results with an ID less than (that is, older than) the specified ID
     * @param showOpines
     *         [Optional] Display opines, default: 'true'
     * @param showReviews
     *         [Optional] Display reviews, default: 'true'
     * @param showPictures
     *         [Optional] Display uploaded images, default: 'true'
     * @param showProducts
     *         [Optional] Display created/updated products, default: 'true'
     * @param showFriendsOnly
     *         [Optional] Show only content created by friends (followed users), default: 'false'
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         timeline and URLs to load the timeline since or until the
     * @return a Future object to optionally wait for the {@code ResultSetWithCursor} result or to cancel the
     * query provided timeline.
     */
    public static Future<ResultSetWithCursor> getTimeline(final PLYAndroid client, final String
            categoryKey, final String brand, final String brandOwner, final Integer count, final String
            sinceID, final String untilID, final Boolean showOpines, final Boolean showReviews, final
    Boolean showPictures, final Boolean showProducts, final Boolean showFriendsOnly,
            PLYCompletion<ResultSetWithCursor> completion) {
        return client.submit(new PLYAndroid.Query<ResultSetWithCursor>() {
            @Override
            public ResultSetWithCursor execute() {
                return com.productlayer.rest.client.services.TimelineService.getTimeline(client
                        .getRestClient(), categoryKey, brand, brandOwner, count, sinceID, untilID,
                        showOpines, showReviews, showPictures, showProducts, showFriendsOnly);
            }
        }, completion);
    }

    /**
     * Gets the most recent social content posted using parameters stored in the URL. The timeline is always
     * sorted by date.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param url
     *         The URL containing the path to the endpoint and any filter parameters
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         timeline and URLs to load the timeline since or until the
     * @return a Future object to optionally wait for the {@code ResultSetWithCursor} result or to cancel the
     * query provided timeline
     */
    public static Future<ResultSetWithCursor> getTimelineFromURL(final PLYAndroid client, final String url,
            PLYCompletion<ResultSetWithCursor> completion) {
        return client.submit(new PLYAndroid.Query<ResultSetWithCursor>() {
            @Override
            public ResultSetWithCursor execute() {
                return com.productlayer.rest.client.services.TimelineService.getTimelineFromURL(client
                        .getRestClient(), url);
            }
        }, completion);
    }

    /**
     * Gets the most recent social content posted by a user ID. The timeline is always sorted by date.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param userID
     *         The identifier of the user
     * @param count
     *         [Optional] The amount of results to be returned, default and maximum: '200'
     * @param sinceID
     *         [Optional] Results with an ID greater than (that is, more recent than) the specified ID
     * @param untilID
     *         [Optional] Results with an ID less than (that is, older than) the specified ID
     * @param showOpines
     *         [Optional] Display opines, default: 'true'
     * @param showReviews
     *         [Optional] Display reviews, default: 'true'
     * @param showPictures
     *         [Optional] Display uploaded images, default: 'true'
     * @param showProducts
     *         [Optional] Display created/updated products, default: 'true'
     * @param includeFriends
     *         [Optional] Show also content created by friends, default: 'false'
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         timeline of a user and URLs to load the timeline since or
     * @return a Future object to optionally wait for the {@code ResultSetWithCursor} result or to cancel the
     * query until the provided timeline
     */
    public static Future<ResultSetWithCursor> getUserTimeline(final PLYAndroid client, final String userID,
            final Integer count, final String sinceID, final String untilID, final Boolean showOpines,
            final Boolean showReviews, final Boolean showPictures, final Boolean showProducts, final
    Boolean includeFriends, PLYCompletion<ResultSetWithCursor> completion) {
        return client.submit(new PLYAndroid.Query<ResultSetWithCursor>() {
            @Override
            public ResultSetWithCursor execute() {
                return com.productlayer.rest.client.services.TimelineService.getUserTimeline(client
                        .getRestClient(), userID, count, sinceID, untilID, showOpines, showReviews,
                        showPictures, showProducts, includeFriends);
            }
        }, completion);
    }
}
import android.app.Activity;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.ViewGroup;
import com.productlayer.android.common.R;
import com.productlayer.android.common.global.LoadingIndicator;
import com.productlayer.android.common.handler.DataChangeListener;
import com.productlayer.android.common.handler.NavigationHandler;
import com.productlayer.android.common.handler.UserHandler;
import com.productlayer.android.common.view.ImagePreview;
import com.productlayer.android.common.view.OpinionView;
import com.productlayer.android.common.view.ProductPreview;
import com.productlayer.android.sdk.PLYAndroid;
import com.productlayer.android.sdk.PLYCompletion;
import com.productlayer.android.sdk.services.ImageService;
import com.productlayer.android.sdk.services.ProductService;
import com.productlayer.android.sdk.services.TimelineService;
import com.productlayer.core.beans.BaseObject;
import com.productlayer.core.beans.Opine;
import com.productlayer.core.beans.Product;
import com.productlayer.core.beans.ProductImage;
import com.productlayer.core.beans.ResultSetWithCursor;
import com.productlayer.core.beans.SimpleUserInfo;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/*
 * Copyright (c) 2015, ProductLayer GmbH All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.productlayer.android.common.adapter;




/**
 * Provides the data feed of product information created by the ProductLayer community as an adapter to be
 * attached to a RecyclerView.
 */
public class TimelineAdapter extends RecyclerView.Adapter<TimelineAdapter.FeedItemHolder> {

    private final Activity activity;
    private final NavigationHandler navigationHandler;
    private final UserHandler userHandler;
    private final PLYAndroid client;

    private Retrieval retrieval;

    private int loadItems;
    private int settings;

    private int itemWidthPx;
    private int imageMaxHeightPx;
    private int avatarSizePx;

    private List<BaseObject> feedItems = new ArrayList<BaseObject>();
    private String[] feedUrls = new String[TimeRel.values().length];

    // listeners would be garbage-collected without reference
    private DataChangeListener.OnProductCreateListener onProductCreateListener;
    private DataChangeListener.OnProductUpdateListener onProductUpdateListener;
    private DataChangeListener.OnOpinionCreateListener onOpinionCreateListener;
    private DataChangeListener.OnOpinionUpdateListener onOpinionUpdateListener;
    private DataChangeListener.OnImageCreateListener onImageCreateListener;
    private DataChangeListener.OnImageUpdateListener onImageUpdateListener;

    private AtomicBoolean loading = new AtomicBoolean(false);

    /**
     * Creates the social feed adapter and starts to retrieve the first batch of data.
     *
     * Use {@link #setItemDimensions} before attaching the adapter to a view. Use {@link #setupListeners} and
     * {@link #destroyListeners} to listen for data changes by the user.
     *
     * @param activity
     *         the activity the adapter's view is attached to
     * @param navigationHandler
     *         the activity's navigation handler to open new screens
     * @param userHandler
     *         the activity's user handler to get current user information
     * @param client
     *         the PLYAndroid REST client to use for querying for data
     * @param retrieval
     *         configured call to retrieve the initial set of items
     * @param loadItems
     *         the amount of items to load in one batch
     * @param settings
     *         the display settings for this timeline
     */
    public TimelineAdapter(Activity activity, NavigationHandler navigationHandler, UserHandler userHandler,
            PLYAndroid client, Retrieval retrieval, int loadItems, int settings) {
        this.activity = activity;
        this.navigationHandler = navigationHandler;
        this.userHandler = userHandler;
        this.client = client;
        this.retrieval = retrieval;
        this.loadItems = loadItems;
        this.settings = settings;
        setHasStableIds(true);
        retrieveFeedItems(TimeRel.INITIAL);
    }

    /**
     * Compares an updated or new image to the current default image of a product, checking if it would make
     * the new default image.
     *
     * @param currentDefault
     *         the product's current default image
     * @param image
     *         the new or updated image
     * @return 1 if the specified image is the new default image of the product; -1 if the specified image is
     * the current default and was down-voted (to trigger updates even though it might still remain the
     * default image); 0 else
     */
    public static int triggersDefaultImageChange(ProductImage currentDefault, ProductImage image) {
        if (currentDefault == null) {
            // no default image yet - new image is the new default
            return 1;
        }
        List<SimpleUserInfo> currentUpVoters = currentDefault.getUpVoters();
        List<SimpleUserInfo> currentDownVoters = currentDefault.getDownVoters();
        int cntCurrentUpVoters = currentUpVoters == null ? 0 : currentUpVoters.size();
        int cntCurrentDownVoters = currentDownVoters == null ? 0 : currentDownVoters.size();
        List<SimpleUserInfo> newUpVoters = image.getUpVoters();
        List<SimpleUserInfo> newDownVoters = image.getDownVoters();
        int cntNewUpVoters = newUpVoters == null ? 0 : newUpVoters.size();
        int cntNewDownVoters = newDownVoters == null ? 0 : newDownVoters.size();
        int scoreDifference = cntNewUpVoters - cntNewDownVoters - (cntCurrentUpVoters - cntCurrentDownVoters);
        if (currentDefault.getId().equals(image.getId())) {
            // current default image was downvoted - another image may become default (else no change)
            return scoreDifference < 0 ? -1 : 0;
        }
        if (scoreDifference > 0) {
            // new image has more upvoters and is the new default
            return 1;
        }
        if (scoreDifference == 0) {
            if (image.getCreated() > currentDefault.getCreated()) {
                // new image has same amount of upvoters and is newer making it the new default
                return 1;
            }
        }
        return 0;
    }

    /**
     * Removes an item with the specified ID of a list of feed items if found.
     *
     * @param items
     *         the list of feed items
     * @param id
     *         the ID of the object to remove
     */
    private static void filterItem(Iterable<BaseObject> items, String id) {
        Iterator<BaseObject> it = items.iterator();
        while (it.hasNext()) {
            BaseObject obj = it.next();
            if (id.equals(obj.getId())) {
                it.remove();
                break;
            }
        }
    }

    /**
     * Sets the fixed width of items and the maximum height of images. Images are retrieved in {@link
     * #onBindViewHolder(FeedItemHolder, int)} in the dimensions set here.
     *
     * @param itemWidthPx
     *         the width of an item
     * @param imageMaxHeightPx
     *         the maximum height of an image
     * @param avatarSizePx
     *         the width/height of the avatar of an item's author
     */
    public void setItemDimensions(int itemWidthPx, int imageMaxHeightPx, int avatarSizePx) {
        this.itemWidthPx = itemWidthPx;
        this.imageMaxHeightPx = imageMaxHeightPx;
        this.avatarSizePx = avatarSizePx;
    }

    /**
     * Retrieves items from the timeline.
     *
     * @param timeRel
     *         whether to retrieve the initial batch of items, or earlier or later items
     * @return false if a previous retrieval is still ongoing, true else
     */
    public boolean retrieveFeedItems(TimeRel timeRel) {
        if (!loading.compareAndSet(false, true)) {
            // TODO reset loading if true for a long time (might happen due to orientation changes)
            // already retrieving items
            return false;
        }
        final TimeRel finalTimeRel = feedItems.isEmpty() ? TimeRel.INITIAL : timeRel;
        Log.d(getClass().getSimpleName(), "Retrieving " + finalTimeRel.name() + " timeline ...");
        LoadingIndicator.show();
        PLYCompletion<ResultSetWithCursor> completion = new PLYCompletion<ResultSetWithCursor>() {