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 interface AppBarHandler {

    /**
     * Transforms the app bar to fit in with the timeline screen.
     *
     * @param view
     *         the fragment's root view
     */
    void setTimelineAppBar(View view);

    /**
     * Transforms the app bar to fit in with the product screen. May include a custom view (i.e. product title
     * and available actions) as well as a backdrop view (i.e. product image) to be scrolled off the screen.
     *
     * @param view
     *         the fragment's root view
     * @param title
     *         the name of the product to display as toolbar title
     * @param customView
     *         the custom view possibly containing the product title and actions
     * @param addCollapsedHeight
     *         the height to collapse to in addition to the standard toolbar size (in px)
     * @param expandedHeight
     *         the height to expand to in total (in px)
     * @param backdrop
     *         the backdrop view to scroll off
     */
    void setProductAppBar(View view, String title, View customView, int addCollapsedHeight, int
            expandedHeight, View backdrop);

    /**
     * Transforms the app bar to fit in with the edit product screen.
     *
     * @param view
     *         the fragment's root view
     * @param title
     *         the name of the product to display as toolbar title
     */
    void setEditProductAppBar(View view, String title);

    /**
     * Transforms the app bar to fit in with the product screen. May include a custom view (i.e. product title
     * and available actions) as well as a backdrop view (i.e. product image) to be scrolled off the screen.
     *
     * @param view
     *         the fragment's root view
     * @param title
     *         the user name to display as toolbar title in collapsed state
     * @param customView
     *         the custom view possibly containing the user's avatar and name
     * @param addCollapsedHeight
     *         the height to collapse to in addition to the standard toolbar size (in px)
     * @param expandedHeight
     *         the height to expand to in total (in px)
     * @param backdrop
     *         the backdrop view to scroll off
     */
    void setProfileAppBar(View view, String title, View customView, int addCollapsedHeight, int
            expandedHeight, View backdrop);

    /**
     * Transforms the app bar to fit in with the user settings screen.
     *
     * @param view
     *         the fragment's root view
     * @param title
     *         the user name to display as toolbar title
     */
    void setProfileEditAppBar(View view, String title);

    /**
     * Transforms the app bar to fit in with the opinion screen.
     *
     * @param view
     *         the fragment's root view
     * @param title
     *         the name of the product to display as toolbar title
     */
    void setOpinionAppBar(View view, String title);

    /**
     * @return true if the status bar is translucent, false else
     */
    boolean hasTranslucentStatusBar();

    /**
     * @return the current height value in px of the toolbar in collapsed state
     */
    int getCurrentCollapsedToolbarHeight();

    /**
     * @return the current height value in px of the toolbar in expanded state
     */
    int getCurrentExpandedToolbarHeight();

    /**
     * @return the current height value in px the content is padded from the top but not clipped by the parent
     * view when scrolling up
     */
    int getCurrentContentPadding();

} 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 FacebookHandler {

    /**
     * Creates a view enabling a user to sign in using Facebook.
     *
     * @param targetActivity
     *         the activity to receiving the result
     * @param loginHandler
     *         the login handler
     * @param client
     *         the PLYAndroid REST client
     * @param dialogFragment
     *         the dialog the sign in view will be attached to
     * @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
     * @return the view enabling a user to sign in using Facebook
     */
    View getFacebookSignInView(Activity targetActivity, LoginHandler loginHandler, PLYAndroid client,
            DialogFragment dialogFragment, PLYAndroid.Query queryOnSuccess, PLYCompletion
            queryOnSuccessCompletion, PLYAndroid.QueryError queryError);

    /**
     * Initiates the Facebook SDK login.
     *
     * @param targetActivity
     *         the activity to receiving the result
     * @param loginHandler
     *         the login handler
     * @param success
     *         code to run on success (on the UI thread)
     * @param failure
     *         code to run on failure (on the UI thread)
     */
    void connectFacebook(Activity targetActivity, LoginHandler loginHandler, Runnable success, Runnable
            failure);

    /**
     * Disconnects the currently logged in user account from Facebook, deauthorizing the app and removing the
     * ProductLayer API link.
     *
     * @param targetActivity
     *         the activity to receiving the result
     * @param userHandler
     *         the user handler
     * @param client
     *         the PLYAndroid REST client
     * @param success
     *         code to run on success (on the UI thread)
     * @param failure
     *         code to run on failure (on the UI thread)
     */
    void disconnectFacebook(Activity targetActivity, UserHandler userHandler, PLYAndroid client, Runnable
            success, Runnable failure);

    /**
     * @return true if the current user has Facebook publish rights, false else
     */
    boolean hasFacebookPublishPermission();

    /**
     * Attempts to get Facebook publish permissions from the user if not yet granted or declined. If the
     * permission has to be granted by the user the Facebook access token is updated as well.
     *
     * @param targetActivity
     *         the activity to receiving the result
     * @param loginHandler
     *         the login handler
     * @param success
     *         code to run on success (on the UI thread)
     * @param failure
     *         code to run on failure (on the UI thread)
     */
    void requestFacebookPublishPermission(Activity targetActivity, LoginHandler loginHandler, Runnable
            success, Runnable failure);

    /**
     * @param context
     *         the app context
     * @return whether to share posts on Facebook by default
     */
    boolean isShareOnFacebook(Context context);

    /**
     * Turns on or off default sharing on Facebook.
     *
     * @param context
     *         the app context
     * @param enabled
     *         whether to share posts on Facebook by default
     */
    void setShareOnFacebook(Context context, boolean enabled);

    /**
     * Extends the Facebook token with all permissions if it has expired or is close to doing so.
     *
     * @param targetActivity
     *         the activity to receiving the result
     * @param loginHandler
     *         the login handler
     * @return true if a renewal of the token was initiated, false else
     */
    boolean renewFacebookToken(Activity targetActivity, LoginHandler loginHandler);

    /**
     * Clears the locally stored Facebook access token.
     */
    void clearLocalFacebookToken();

} public interface HasAppBarHandler {

    /**
     * @return the app bar handler
     */
    AppBarHandler getAppBarHandler();

} public interface HasFacebookHandler {

    /**
     * @return the Facebook handler
     */
    FacebookHandler getFacebookHandler();

} public interface HasLoginHandler {

    /**
     * @return the login handler
     */
    LoginHandler getLoginHandler();

} public interface HasPLYAndroidHolder {

    /**
     * @return the PLYAndroid handler
     */
    PLYAndroidHolder getPLYAndroidHolder();

} public interface HasTwitterHandler {

    /**
     * @return the Twitter handler
     */
    TwitterHandler getTwitterHandler();

} public interface HasUserHandler {

    /**
     * @return the user handler
     */
    UserHandler getUserHandler();

} public interface LoginHandler {

    /**
     * Attempts to either log in the user identified by the specified {@code userName} - {@code password}
     * combination or to resume the session using the token stored in the {@link PLYAndroid} client.
     *
     * @param userName
     *         the user name to log in as
     * @param password
     *         the password set for the user
     * @param resumeSession
     *         whether to attempt resuming the session identified by the client's token instead of logging in
     *         using user name and password
     * @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
     * @param facebookHandler
     *         the handler to use to clear the facebook token in case resuming a session fails
     * @param twitterHandler
     *         the handler to use to clear the twitter session in case resuming a session fails
     * @param navigationHandler
     *         the handler to use to show the retry dialog if login fails
     */
    void login(String userName, String password, boolean resumeSession, PLYAndroid.Query queryOnSuccess,
            PLYCompletion queryOnSuccessCompletion, PLYAndroid.QueryError queryError, FacebookHandler
            facebookHandler, TwitterHandler twitterHandler, NavigationHandler navigationHandler);

    /**
     * Notifies the ProductLayer servers of a new Facebook token for the currently logged in user. If no user
     * is logged in, either results in a login if a corresponding user is found, or in a signup if this
     * Facebook user ID/email/token is new, or in an error if, for example, the email address is found but
     * that user is not connected to Facebook. If a user is logged in, results in a token update if the
     * Facebook user ID/email/token is not already connected to another user.
     *
     * @param providerKey
     *         the key of the social network (i.e. facebook or twitter)
     * @param providerUserId
     *         the user ID as returned by the social network
     * @param providerToken
     *         the token for authentication returned by the social network
     * @param providerSecret
     *         the secret returned by twitter
     * @param success
     *         code to run on success (on the UI thread)
     * @param failure
     *         code to run on failure (on the UI thread)
     * @param onInvalidToken
     *         code to run if the provided social token is invalid
     */
    void loginWithSocialToken(String providerKey, String providerUserId, String providerToken, String
            providerSecret, Runnable success, Runnable failure, Runnable onInvalidToken);

    /**
     * Logs out the current user.
     *
     * @param userInitiated
     *         whether the action has been initiated by the user
     * @param facebookHandler
     *         the handler to use to clear the facebook token
     * @param twitterHandler
     *         the handler to use to clear the twitter session
     */
    void logout(boolean userInitiated, FacebookHandler facebookHandler, TwitterHandler twitterHandler);

    /**
     * Checks if a token is set locally to be possibly logged in. For a guarantee of being logged in use
     * {@link #isLoggedInRemote(Runnable, Runnable)}.
     *
     * @return true if apparently logged in, false else
     */
    boolean isLoggedIn();

    /**
     * Checks in an async fashion whether the current token is valid.
     *
     * @param onTrue
     *         to be executed if logged in (is not run on the main thread)
     * @param onFalse
     *         to be executed if not logged in (is not run on the main thread)
     */
    void isLoggedInRemote(Runnable onTrue, Runnable onFalse);

    /**
     * Adds a listener to be notified of logins.
     *
     * Only a weak reference to the listener is kept - maintain a strong reference to avoid garbage collection
     * of the listener.
     *
     * @param onLoginListener
     *         the listener to add
     */
    void addOnLoginListener(OnLoginListener onLoginListener);

    /**
     * Removes the specified listener.
     *
     * @param onLoginListener
     *         the listener to remove
     */
    void removeOnLoginListener(OnLoginListener onLoginListener);

    /**
     * Adds a listener to be notified of logouts.
     *
     * Only a weak reference to the listener is kept - maintain a strong reference to avoid garbage collection
     * of the listener.
     *
     * @param onLogoutListener
     *         the listener to add
     */
    void addOnLogoutListener(OnLogoutListener onLogoutListener);

    /**
     * Removes the specified listener.
     *
     * @param onLogoutListener
     *         the listener to remove
     */
    void removeOnLogoutListener(OnLogoutListener onLogoutListener);

    /**
     * Listens for login events.
     */
    interface OnLoginListener {

        /**
         * Called whenever the user logs in.
         *
         * @param user
         *         the logged in user
         */
        void onLogin(User user);

    }

    /**
     * Listens for logout events.
     */
    interface OnLogoutListener {

        /**
         * Called whenever the user logs out.
         */
        void onLogout();

    }

} public interface PLYAndroidHolder {

    /**
     * @return the REST client of the ProductLayer SDK to perform API calls with
     */
    PLYAndroid getPLYAndroid();

} public interface TwitterHandler {

    /**
     * Creates a view enabling a user to sign in using Twitter.
     *
     * @param targetActivity
     *         the activity to receiving the result
     * @param loginHandler
     *         the login handler
     * @param client
     *         the PLYAndroid REST client
     * @param dialogFragment
     *         the dialog the sign in view will be attached to
     * @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
     * @return the view enabling a user to sign in using Facebook
     */
    View getTwitterSignInView(Activity targetActivity, LoginHandler loginHandler, PLYAndroid client,
            DialogFragment dialogFragment, PLYAndroid.Query queryOnSuccess, PLYCompletion
            queryOnSuccessCompletion, PLYAndroid.QueryError queryError);

    /**
     * Initiates the Twitter SDK login.
     *
     * @param targetActivity
     *         the activity to receiving the result
     * @param loginHandler
     *         the login handler
     * @param success
     *         code to run on success (on the UI thread)
     * @param failure
     *         code to run on failure (on the UI thread)
     */
    void connectTwitter(Activity targetActivity, LoginHandler loginHandler, Runnable success, Runnable
            failure);

    /**
     * Disconnects the currently logged in user account from Twitter, deauthorizing the app and removing the
     * ProductLayer API link.
     *
     * @param targetActivity
     *         the activity to receiving the result
     * @param userHandler
     *         the user handler
     * @param client
     *         the PLYAndroid REST client
     * @param success
     *         code to run on success (on the UI thread)
     * @param failure
     *         code to run on failure (on the UI thread)
     */
    void disconnectTwitter(Activity targetActivity, UserHandler userHandler, PLYAndroid client, Runnable
            success, Runnable failure);

    /**
     * @param context
     *         the app context
     * @return whether to share posts on Twitter by default
     */
    boolean isShareOnTwitter(Context context);

    /**
     * Turns on or off default sharing on Twitter.
     *
     * @param context
     *         the app context
     * @param enabled
     *         whether to share posts on Twitter by default
     */
    void setShareOnTwitter(Context context, boolean enabled);

    /**
     * Clears the currently persisted Twitter session.
     */
    void clearLocalTwitterSession();

} 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 LocaleUtil {

    /**
     * Gets the language of the keyboard, falling back to the default language on any error.
     *
     * @param context
     *         the application context
     * @return the language of the keyboard or the default language on any error
     */
    public static String getKeyboardLanguage(Context context) {
        if (context == null) {
            return getDefaultLanguage();
        }
        InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
        InputMethodSubtype ims = imm.getCurrentInputMethodSubtype();
        if (ims == null) {
            String defaultLanguage = getDefaultLanguage();
            Log.i(LocaleUtil.class.getSimpleName(), "Failed to get current input method subtype " +
                    "for determining the keyboard locale, falling back to default " + defaultLanguage);
            return defaultLanguage;
        }
        try {
            String localeStr = ims.getLocale().replace("_", "-");
            Locale locale = new Locale(localeStr);
            String validLocaleStr = makeTwoLetterCode(locale.getLanguage());
            if (validLocaleStr == null || validLocaleStr.isEmpty() || !validateLanguage(validLocaleStr)) {
                String defaultLanguage = getDefaultLanguage();
                Log.i(LocaleUtil.class.getSimpleName(), "Failed to determine keyboard locale, falling back " +
                        "to default " + defaultLanguage);
                return defaultLanguage;
            } else {
                return validLocaleStr;
            }
        } catch (Exception e) {
            String defaultLanguage = getDefaultLanguage();
            Log.i(LocaleUtil.class.getSimpleName(), "Failed to determine keyboard locale, falling back to " +
                    "default " + defaultLanguage, e);
            return defaultLanguage;
        }
    }

    /**
     * @return the language of the environment this app was started in
     */
    public static String getDefaultLanguage() {
        String defaultLanguage = makeTwoLetterCode(Locale.getDefault().getLanguage());
        if (!validateLanguage(defaultLanguage)) {
            Log.w(LocaleUtil.class.getSimpleName(), "Failed to get a valid default locale: " + Locale
                    .getDefault().getLanguage() + " -> " + defaultLanguage + ". Falling back to 'en'.");
            return "en";
        }
        return defaultLanguage;
    }

    /**
     * @param language
     *         the language string to validate
     * @return true if the specified language string is a valid ISO 639-1 code, false else
     */
    public static boolean validateLanguage(String language) {
        return Arrays.asList(Locale.getISOLanguages()).contains(language);
    }

    /**
     * @param language
     *         the language string to modify
     * @return a trimmed two-letter lower-case string or the original string on any error
     */
    private static String makeTwoLetterCode(String language) {
        try {
            return language.trim().substring(0, 2).toLowerCase();
        } catch (Exception e) {
            return language;
        }
    }

} public class SnackbarUtil {

    /**
     * Creates a snackbar that can handle a translucent navigation bar instead of popping up under it.
     *
     * Expects a CoordinatorLayout as the provided view or as a parent at some point in the view hierarchy.
     *
     * @param activity
     *         the currently visible activity
     * @param view
     *         the container view
     * @param text
     *         the message to show
     * @param duration
     *         the duration to show it for (see {@link Snackbar#LENGTH_SHORT} and {@link
     *         Snackbar#LENGTH_LONG}
     * @return the snackbar prepared to be shown
     */
    public static Snackbar make(Activity activity, final View view, CharSequence text, int duration) {
        Snackbar snackbar = Snackbar.make(view, text, duration);
        if (activity == null) {
            return snackbar;
        }
        final int navigationBarHeight = SystemBarsUtil.getNavigationBarHeight(activity);
        if (navigationBarHeight == 0 || !SystemBarsUtil.hasTranslucentNavigationBar(activity)) {
            return snackbar;
        }
        snackbar.setCallback(new Snackbar.Callback() {
            @Override
            public void onShown(Snackbar snackbar) {
                View snackbarView = snackbar.getView();
                CoordinatorLayout.LayoutParams layoutParams = new CoordinatorLayout.LayoutParams
                        (snackbarView.getLayoutParams());
                layoutParams.setMargins(0, 0, 0, navigationBarHeight);
                layoutParams.gravity = Gravity.BOTTOM;
                snackbarView.setLayoutParams(layoutParams);
                super.onShown(snackbar);
            }
        });
        return snackbar;
    }

    /**
     * Creates a snackbar that can handle a translucent navigation bar instead of popping up under it.
     *
     * Expects a CoordinatorLayout as the provided view or as a parent at some point in the view hierarchy.
     *
     * @param activity
     *         the currently visible activity
     * @param view
     *         the container view
     * @param resourceId
     *         the message to show
     * @param duration
     *         the duration to show it for (see {@link Snackbar#LENGTH_SHORT} and {@link
     *         Snackbar#LENGTH_LONG}
     * @return the snackbar prepared to be shown
     */
    public static Snackbar make(Activity activity, View view, int resourceId, int duration) {
        return make(activity, view, activity.getResources().getString(resourceId), duration);
    }
} public class SystemBarsUtil {

    /**
     * @param context
     *         the application context
     * @return the height of the status bar in px
     */
    public static int getStatusBarHeight(Context context) {
        // 25 dp is the default as per Google specs
        int defaultDp = 25;
        if (context == null) {
            return MetricsUtil.inPx(defaultDp);
        }
        Resources resources = context.getResources();
        int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            return resources.getDimensionPixelSize(resourceId);
        } else {
            return MetricsUtil.inPx(defaultDp);
        }
    }

    /**
     * @return whether this device has support for a natively enabled translucent status bar (Android 4.4+)
     */
    public static boolean supportsTranslucentStatusBarNative() {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
    }

    /**
     * @param context
     *         the application context
     * @return true if this device has native support for a translucent status bar or enabled through a
     * proprietary launcher flag, false if neither
     */
    public static boolean supportsTranslucentStatusBar(Context context) {
        return supportsTranslucentStatusBarNative() || getProprietaryTranslucentStatusBarFlag(context) != 0;
    }

    /**
     * Attempts to make the status bar translucent on devices with proprietary launchers supporting this
     * feature.
     *
     * @param activity
     *         the activity to set the flag for
     * @return true on success, false else
     */
    public static boolean setProprietaryTranslucentStatusBar(Activity activity) {
        if (activity == null) {
            return false;
        }
        Window window = activity.getWindow();
        if (window == null) {
            return false;
        }
        View decor = window.getDecorView();
        if (decor == null) {
            return false;
        }
        int flag = getProprietaryTranslucentStatusBarFlag(activity);
        if (flag == 0) {
            return false;
        }
        decor.setSystemUiVisibility(flag);
        return true;
    }

    /**
     * Retrieves the status bar transparency flags of proprietary launchers on Samsung and Sony devices. Adds
     * support for a translucent status bar where it is not supported directly by Android - as in Android
     * versions less than 4.4.
     *
     * @param context
     *         the application context
     * @return the flag for a translucent status bar to set via {@link View#setSystemUiVisibility}.
     */
    public static int getProprietaryTranslucentStatusBarFlag(Context context) {
        if (context == null) {
            return 0;
        }
        int flag = 0;
        String[] libs = context.getPackageManager().getSystemSharedLibraryNames();
        if (libs == null) {
            return flag;
        }
        Collection<String> libsSet = new HashSet<String>(Arrays.asList(libs));
        String flagName;
        if (libsSet.contains("touchwiz")) {
            flagName = "SYSTEM_UI_FLAG_TRANSPARENT_BACKGROUND";
        } else if (libsSet.contains("com.sonyericsson.navigationbar")) {
            flagName = "SYSTEM_UI_FLAG_TRANSPARENT";
        } else {
            return flag;
        }
        try {
            Field field = View.class.getField(flagName);
            if (field.getType() == Integer.TYPE) {
                flag = field.getInt(null);
            }
        } catch (Exception ignored) {
        }
        return flag;
    }

    /**
     * Gets the height of the navigation bar in pixels if one is present at the bottom of the screen.
     *
     * @param context
     *         the application context
     * @return the height of the navigation bar in px or 0 if none is present at the bottom of the screen
     */
    public static int getNavigationBarHeight(Context context) {
        if (context == null) {
            return 0;
        }
        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        Display display = windowManager.getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        Point fullSize = new Point();
        if (Build.VERSION.SDK_INT >= 17) {
            display.getRealSize(fullSize);
        } else if (Build.VERSION.SDK_INT >= 14) {
            try {
                fullSize.x = (Integer) Display.class.getMethod("getRawWidth").invoke(display);
                fullSize.y = (Integer) Display.class.getMethod("getRawHeight").invoke(display);
            } catch (Exception ignored) {
            }
        }
        if (size.y < fullSize.y) {
            // navigation bar at the bottom
            return fullSize.y - size.y;
        } else {
            // no navigation bar at the bottom (none at all or one on the right side)
            return 0;
        }
    }

    /**
     * Checks whether the current theme declares the navigation bar to be translucent. This does not check
     * whether a navigation bar is even displayed on the screen.
     *
     * @param context
     *         the application context
     * @return true if the navigation bar is translucent in the current theme, false else
     * @see #getNavigationBarHeight
     */
    public static boolean hasTranslucentNavigationBar(Context context) {
        @SuppressLint("InlinedApi") @SuppressWarnings("UnnecessaryFullyQualifiedName") Boolean
                translucentNav = ThemeUtil.getBooleanValue(context, android.R.attr
                .windowTranslucentNavigation);
        return translucentNav != null && translucentNav;
    }

} 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 OpineService {

    /**
     * Posts 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 opine
     *         The opine
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The newly
     *         created opine
     * @return a Future object to optionally wait for the {@code Opine} result or to cancel the query
     */
    public static Future<Opine> createOpine(final PLYAndroid client, final Opine opine,
            PLYCompletion<Opine> completion) {
        return client.submit(new PLYAndroid.Query<Opine>() {
            @Override
            public Opine execute() {
                return com.productlayer.rest.client.services.OpineService.createOpine(client.getRestClient
                        (), opine);
            }
        }, completion);
    }

    /**
     * Deletes an opine. Only the owner of the opine can delete it. 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 completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         deleted opine
     * @return a Future object to optionally wait for the {@code Opine} result or to cancel the query
     */
    public static Future<Opine> deleteOpine(final PLYAndroid client, final String opineID,
            PLYCompletion<Opine> completion) {
        return client.submit(new PLYAndroid.Query<Opine>() {
            @Override
            public Opine execute() {
                return com.productlayer.rest.client.services.OpineService.deleteOpine(client.getRestClient
                        (), opineID);
            }
        }, completion);
    }

    /**
     * Downvotes a specific opine.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param opineID
     *         The identifier of the opine
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The opine
     *         with the new vote score
     * @return a Future object to optionally wait for the {@code Opine} result or to cancel the query
     */
    public static Future<Opine> downVoteOpine(final PLYAndroid client, final String opineID,
            PLYCompletion<Opine> completion) {
        return client.submit(new PLYAndroid.Query<Opine>() {
            @Override
            public Opine execute() {
                return com.productlayer.rest.client.services.OpineService.downVoteOpine(client
                        .getRestClient(), opineID);
            }
        }, completion);
    }

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

    /**
     * Sends a report about copyright infringements or any other problems with the opine.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param opineID
     *         The identifier of the opine
     * @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> reportOpine(final PLYAndroid client, final String opineID, final
    ProblemReport report, PLYCompletion<ProblemReport> completion) {
        return client.submit(new PLYAndroid.Query<ProblemReport>() {
            @Override
            public ProblemReport execute() {
                return com.productlayer.rest.client.services.OpineService.reportOpine(client.getRestClient
                        (), opineID, report);
            }
        }, completion);
    }

    /**
     * Searches for an opine.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @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 language
     *         [Optional] The preferred language (e.g.: 'en' or 'de')
     * @param showFriendsOnly
     *         [Optional] Show only content created by friends (followed users), default: 'false'
     * @param nickname
     *         [Optional] The nickname of the user
     * @param userID
     *         [Optional] The identifier of the user
     * @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-created-time_asc (The date the opine was created ascending)
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: Any opines
     *         matching the specified criteria
     * @return a Future object to optionally wait for the {@code Opine[]} result or to cancel the query
     */
    public static Future<Opine[]> searchOpines(final PLYAndroid client, final Integer page, final Integer
            recordsPerPage, final String gtin, final String language, final Boolean showFriendsOnly, final
    String nickname, final String userID, final String order_by, PLYCompletion<Opine[]> completion) {
        return client.submit(new PLYAndroid.Query<Opine[]>() {
            @Override
            public Opine[] execute() {
                return com.productlayer.rest.client.services.OpineService.searchOpines(client.getRestClient
                        (), page, recordsPerPage, gtin, language, showFriendsOnly, nickname, userID,
                        order_by);
            }
        }, completion);
    }

    /**
     * Upvotes a specific opine.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param opineID
     *         The identifier of the opine
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The opine
     *         with the new vote score
     * @return a Future object to optionally wait for the {@code Opine} result or to cancel the query
     */
    public static Future<Opine> upVoteOpine(final PLYAndroid client, final String opineID,
            PLYCompletion<Opine> completion) {
        return client.submit(new PLYAndroid.Query<Opine>() {
            @Override
            public Opine execute() {
                return com.productlayer.rest.client.services.OpineService.upVoteOpine(client.getRestClient
                        (), opineID);
            }
        }, completion);
    }
}
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v4.app.FragmentActivity;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import com.productlayer.android.common.R;
import com.productlayer.android.common.global.LoadingIndicator;
import com.productlayer.android.common.handler.AppBarHandler;
import com.productlayer.android.common.handler.DataChangeListener;
import com.productlayer.android.common.handler.FacebookHandler;
import com.productlayer.android.common.handler.HasAppBarHandler;
import com.productlayer.android.common.handler.HasFacebookHandler;
import com.productlayer.android.common.handler.HasLoginHandler;
import com.productlayer.android.common.handler.HasPLYAndroidHolder;
import com.productlayer.android.common.handler.HasTwitterHandler;
import com.productlayer.android.common.handler.HasUserHandler;
import com.productlayer.android.common.handler.LoginHandler;
import com.productlayer.android.common.handler.PLYAndroidHolder;
import com.productlayer.android.common.handler.TwitterHandler;
import com.productlayer.android.common.handler.UserHandler;
import com.productlayer.android.common.util.LocaleUtil;
import com.productlayer.android.common.util.SnackbarUtil;
import com.productlayer.android.common.util.SystemBarsUtil;
import com.productlayer.android.sdk.PLYAndroid;
import com.productlayer.android.sdk.PLYCompletion;
import com.productlayer.android.sdk.services.OpineService;
import com.productlayer.core.beans.ObjectReference;
import com.productlayer.core.beans.Opine;
import com.productlayer.core.beans.Product;
import com.productlayer.core.utils.StringUtils;
/*
 * 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.fragment;



/**
 * The screen to post an opinion about a product.
 *
 * Requires the activity to implement {@link HasAppBarHandler}, {@link HasUserHandler}, {@link
 * HasLoginHandler}, {@link HasFacebookHandler}, {@link HasTwitterHandler}, {@link HasPLYAndroidHolder}.
 */
public class OpinionFragment extends NamedFragment {

    public static final String NAME = "Opinion";

    private static final String KEY_PRODUCT = "product";
    private static final String KEY_PARENT_OPINION = "parentOpinion";

    private AppBarHandler appBarHandler;
    private UserHandler userHandler;
    private LoginHandler loginHandler;
    private FacebookHandler facebookHandler;
    private TwitterHandler twitterHandler;

    private PLYAndroid client;

    private Product product;
    private Opine parentOpinion;

    private int opinionCharMax;

    private EditText opinionText;
    private TextView charsText;
    private ImageView facebookButton;
    private ImageView twitterButton;

    /**
     * Constructs a new instance with the specified parameters. The parameters passed this way survive
     * recreation of the fragment due to orientation changes etc.
     *
     * @param product
     *         the product to post an opinion to
     * @param parentOpinion
     *         the opinion to reply to (if any)
     * @return the fragment with the given parameters
     */
    @SuppressWarnings("TypeMayBeWeakened")
    public static OpinionFragment newInstance(Product product, Opine parentOpinion) {
        OpinionFragment opinionFragment = new OpinionFragment();
        Bundle args = new Bundle();
        args.putSerializable(KEY_PRODUCT, product);
        args.putSerializable(KEY_PARENT_OPINION, parentOpinion);
        opinionFragment.setArguments(args);
        return opinionFragment;
    }

    /**
     * @param product
     *         the product to post an opinion to
     * @param parentOpinion
     *         the opinion to reply to (if any)
     * @return this fragment's name and initialization parameters
     */
    public static String makeInstanceName(Product product, Opine parentOpinion) {
        String productParam = product == null ? "" : product.getId();
        String parentOpinionParam = parentOpinion == null ? "" : parentOpinion.getId();
        return NAME + "(" + productParam + "," + parentOpinionParam + ")";
    }

    @Override
    public String getInstanceName() {
        return makeInstanceName(product, parentOpinion);
    }

    @Override
    public FragmentGrouping getGrouping() {
        return FragmentGrouping.NONE;
    }

    // FRAGMENT LIFECYCLE - START //

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Activity activity = getActivity();
        try {
            appBarHandler = ((HasAppBarHandler) activity).getAppBarHandler();