public class BrandAdapter extends BaseAdapter implements Filterable {

    private final LayoutInflater layoutInflater;

    private final Set<SimpleBrand> likelyBrands;
    private final TreeSet<String> allBrands;

    private int resource;

    private List<Object> filteredBrands;
    private int positionStartAllBrands;

    /**
     * Creates a new adapter using the brand and brand owners contained in the specified {@code
     * likelyBrandOwners} array as well as all brands in {@code allBrands}.
     *
     * @param context
     *         the application context
     * @param resource
     *         the layout to use per brand information (must include brand and brand_owner fields)
     * @param likelyBrandOwners
     *         likely brand owners and brands
     * @param allBrands
     *         all available brands
     */
    public BrandAdapter(Context context, int resource, BrandOwner[] likelyBrandOwners, String[] allBrands) {
        layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        this.resource = resource;
        this.allBrands = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
        if (allBrands != null) {
            this.allBrands.addAll(Arrays.asList(allBrands));
        }
        likelyBrands = new HashSet<SimpleBrand>();
        if (likelyBrandOwners != null) {
            for (BrandOwner brandOwner : likelyBrandOwners) {
                String brandOwnerName = brandOwner.getName();
                if ("unknown".equals(brandOwnerName)) {
                    brandOwnerName = "";
                }
                for (Brand brand : brandOwner.getBrands()) {
                    String brandName = brand.getName();
                    likelyBrands.add(new SimpleBrand(brandName, brandOwnerName));
                    this.allBrands.remove(brandName);
                }
            }
        }
        filteredBrands = new ArrayList<Object>();
        filteredBrands.addAll(likelyBrands);
        positionStartAllBrands = likelyBrands.size();
    }

    /**
     * @param position
     *         the item's position in the data set
     * @return whether the item at this position is a likely brand and of type {@code SimpleBrand}
     */
    private boolean isLikelyBrand(int position) {
        return position < positionStartAllBrands;
    }

    /**
     * @param position
     *         an item's position
     * @return whether the specified position is valid
     */
    private boolean isInBounds(int position) {
        return position < getCount();
    }

    @Override
    public int getCount() {
        return filteredBrands.size();
    }

    @Override
    public Object getItem(int position) {
        return filteredBrands.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = convertView;
        if (view == null) {
            // create new view
            view = layoutInflater.inflate(resource, parent, false);
        }
        // fill in values
        String brandName;
        String brandOwnerName;
        if (isLikelyBrand(position)) {
            SimpleBrand simpleBrand = (SimpleBrand) getItem(position);
            brandName = simpleBrand.brand;
            brandOwnerName = simpleBrand.brandOwner;
        } else if (isInBounds(position)) {
            brandName = (String) getItem(position);
            brandOwnerName = null;
        } else {
            return view;
        }
        TextView brand = (TextView) view.findViewById(R.id.brand);
        brand.setText(brandName);
        TextView brandOwner = (TextView) view.findViewById(R.id.brand_owner);
        if (brandOwnerName != null && !brandOwnerName.isEmpty()) {
            brandOwner.setText(brandOwnerName);
            brandOwner.setVisibility(View.VISIBLE);
        } else {
            brandOwner.setVisibility(View.GONE);
        }
        return view;
    }

    @Override
    public Filter getFilter() {
        return new Filter() {
            @Override
            protected FilterResults performFiltering(CharSequence constraint) {
                FilterResults filterResults = new FilterResults();
                Collection<Object> filteredBrands = new ArrayList<Object>();
                if (constraint == null || constraint.length() == 0) {
                    // no constraining input
                    if (!likelyBrands.isEmpty()) {
                        // only show likely brands
                        filteredBrands.addAll(likelyBrands);
                    }
                    filterResults.values = filteredBrands;
                    filterResults.count = filteredBrands.size();
                    return filterResults;
                }
                // filter by constraint
                String constraintStr = constraint.toString();
                String constraintStrLower = constraintStr.toLowerCase();
                // likely brands
                for (SimpleBrand simpleBrand : likelyBrands) {
                    if (simpleBrand.brandLower.contains(constraintStrLower) || simpleBrand
                            .brandAlphaNumeric.contains(constraintStrLower)) {
                        filteredBrands.add(simpleBrand);
                    }
                }
                // all brands
                Set<String> tailSet = allBrands.tailSet(constraintStr);
                for (String brandName : tailSet) {
                    if (brandName.toLowerCase().startsWith(constraintStrLower)) {
                        filteredBrands.add(brandName);
                    } else {
                        break;
                    }
                }
                filterResults.values = filteredBrands;
                filterResults.count = filteredBrands.size();
                return filterResults;
            }

            @Override
            protected void publishResults(CharSequence constraint, FilterResults results) {
                @SuppressWarnings("unchecked") List<Object> values = (List<Object>) results.values;
                int positionFirstStringObject = results.count;
                for (int i = 0; i < results.count; i++) {
                    if (values.get(i) instanceof String) {
                        positionFirstStringObject = i;
                        break;
                    }
                }
                filteredBrands = values;
                positionStartAllBrands = positionFirstStringObject;
                notifyDataSetChanged();
            }
        };
    }
} public class BrandOwnerAdapter extends BaseAdapter implements Filterable {

    private final LayoutInflater layoutInflater;

    private final Set<String> likelyBrandOwners;
    private final TreeSet<String> allBrandOwners;

    private int resource;

    private List<String> filteredBrandOwners;

    /**
     * Creates a new adapter using the brand owners contained in the specified {@code likelyBrandOwners} array
     * as well as all brands in {@code allBrandOwners}.
     *
     * @param context
     *         the application context
     * @param resource
     *         the text view resource to fill per brand owner
     * @param likelyBrandOwners
     *         likely brand owners
     * @param allBrandOwners
     *         all available brand owners
     */
    public BrandOwnerAdapter(Context context, int resource, BrandOwner[] likelyBrandOwners, String[]
            allBrandOwners) {
        layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        this.resource = resource;
        this.allBrandOwners = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
        if (allBrandOwners != null) {
            this.allBrandOwners.addAll(Arrays.asList(allBrandOwners));
        }
        this.likelyBrandOwners = new HashSet<String>();
        if (likelyBrandOwners != null) {
            for (BrandOwner brandOwner : likelyBrandOwners) {
                String brandOwnerName = brandOwner.getName();
                if (brandOwnerName != null && !brandOwnerName.isEmpty() && !"unknown".equals
                        (brandOwnerName)) {
                    this.likelyBrandOwners.add(brandOwnerName);
                    this.allBrandOwners.remove(brandOwnerName);
                }
            }
        }
        filteredBrandOwners = new ArrayList<String>();
        filteredBrandOwners.addAll(this.likelyBrandOwners);
    }

    /**
     * @param position
     *         an item's position
     * @return whether the specified position is valid
     */
    private boolean isInBounds(int position) {
        return position < getCount();
    }

    @Override
    public int getCount() {
        return filteredBrandOwners.size();
    }

    @Override
    public Object getItem(int position) {
        return filteredBrandOwners.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = convertView;
        if (view == null) {
            // create new view
            view = layoutInflater.inflate(resource, parent, false);
        }
        if (!isInBounds(position)) {
            return view;
        }
        // fill in values
        CharSequence brandOwnerName = (CharSequence) getItem(position);
        TextView brandOwner = (TextView) view.findViewById(R.id.brand_owner);
        brandOwner.setText(brandOwnerName);
        return view;
    }

    @Override
    public Filter getFilter() {
        return new Filter() {
            @Override
            protected FilterResults performFiltering(CharSequence constraint) {
                FilterResults filterResults = new FilterResults();
                Collection<String> filteredBrandOwners = new ArrayList<String>();
                if (constraint == null || constraint.length() == 0) {
                    // no constraining input
                    if (!likelyBrandOwners.isEmpty()) {
                        // only show likely brand owners
                        filteredBrandOwners.addAll(likelyBrandOwners);
                    }
                    filterResults.values = filteredBrandOwners;
                    filterResults.count = filteredBrandOwners.size();
                    return filterResults;
                }
                // filter by constraint
                String constraintStr = constraint.toString();
                String constraintStrLower = constraintStr.toLowerCase();
                // likely brands
                for (String brandOwnerName : likelyBrandOwners) {
                    if (brandOwnerName.toLowerCase().contains(constraintStrLower)) {
                        filteredBrandOwners.add(brandOwnerName);
                    }
                }
                // all brands
                Set<String> tailSet = allBrandOwners.tailSet(constraintStr);
                for (String brandOwnerName : tailSet) {
                    if (brandOwnerName.toLowerCase().startsWith(constraintStrLower)) {
                        filteredBrandOwners.add(brandOwnerName);
                    } else {
                        break;
                    }
                }
                filterResults.values = filteredBrandOwners;
                filterResults.count = filteredBrandOwners.size();
                return filterResults;
            }

            @Override
            protected void publishResults(CharSequence constraint, FilterResults results) {
                //noinspection unchecked
                filteredBrandOwners = (List<String>) results.values;
                notifyDataSetChanged();
            }
        };
    }
} public class CategorySelectionDialogFragment extends VerboseDialogFragment {

    public static final int REQUEST_CODE_SELECT_CATEGORY = 200;
    public static final String KEY_SELECTION = "selection";
    private static final String KEY_CATEGORIES = "categories";

    public static CategorySelectionDialogFragment newInstance(CategoryListItem[] categories) {
        CategorySelectionDialogFragment categorySelectionDialogFragment = new
                CategorySelectionDialogFragment();
        Bundle args = new Bundle();
        args.putParcelableArray(KEY_CATEGORIES, categories);
        categorySelectionDialogFragment.setArguments(args);
        return categorySelectionDialogFragment;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        super.onCreateDialog(savedInstanceState);
        // get categories from arguments
        Bundle args = getArguments();
        Parcelable[] parcelables = args.getParcelableArray(KEY_CATEGORIES);
        assert parcelables != null;
        CategoryListItem[] categories = new CategoryListItem[parcelables.length];
        //noinspection SuspiciousSystemArraycopy
        System.arraycopy(parcelables, 0, categories, 0, parcelables.length);
        // build dialog
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        LayoutInflater inflater = getActivity().getLayoutInflater();
        View categorySelectionLayout = inflater.inflate(R.layout.dialog_category_selection, null);
        ExpandableListView categoryList = (ExpandableListView) categorySelectionLayout.findViewById(R.id
                .category_list);
        final CategoryListAdapter categoryAdapter = new CategoryListAdapter(getActivity(), categories, R
                .layout.list_group_item_expanded, R.layout.list_group_item_collapsed, R.layout
                .list_child_item, R.layout.list_child_item, R.id.indicator);
        categoryList.setAdapter(categoryAdapter);
        categoryList.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
            @Override
            public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int
                    childPosition, long id) {
                return categorySelected((Parcelable) parent.getExpandableListAdapter().getChild
                        (groupPosition, childPosition));
            }
        });
        categoryList.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
            @Override
            public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) {
                CategoryListItem category = (CategoryListItem) parent.getExpandableListAdapter().getGroup
                        (groupPosition);
                // only categories without sub-categories are selected directly
                return category.getSubItems().length == 0 && categorySelected(category);
            }
        });
        SearchView categorySearch = (SearchView) categorySelectionLayout.findViewById(R.id.category_search);
        categorySearch.setSubmitButtonEnabled(false);
        categorySearch.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextChange(String newText) {
                categoryAdapter.getFilter().filter(newText);
                return true;
            }

            @Override
            public boolean onQueryTextSubmit(String query) {
                return true;
            }
        });
        builder.setView(categorySelectionLayout);
        return builder.create();
    }

    /**
     * Sends the category back to the target fragment.
     *
     * @param category
     *         the selected category
     * @return false if no target fragment was set, true else
     */
    private boolean categorySelected(Parcelable category) {
        InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context
                .INPUT_METHOD_SERVICE);
        imm.hideSoftInputFromWindow(getDialog().getWindow().getDecorView().getWindowToken(), 0);
        // package the clicked category
        Intent data = new Intent();
        data.putExtra(KEY_SELECTION, category);
        // call back to the parent
        if (getParentFragment() != null) {
            getParentFragment().onActivityResult(REQUEST_CODE_SELECT_CATEGORY, 0, data);
            dismiss();
            return true;
        } else {
            Log.w(getClass().getSimpleName(), "getParentFragment() returned null");
            dismiss();
            return false;
        }
    }
} public class NewProductDialogFragment extends VerboseDialogFragment {

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        super.onCreateDialog(savedInstanceState);
        Context context = getContext();
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View newProductDialogLayout = inflater.inflate(R.layout.dialog_new_product, null);
        // set up the dialog supplying the layout and button actions
        builder.setView(newProductDialogLayout).setPositiveButton(R.string.take_photo_button, new
                DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int id) {
                // start camera to take a photo
                PhotoUtil.takePhoto(getActivity(), getParentFragment());
            }
        }).setNegativeButton(R.string.cancel_button, new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int id) {
            }
        });
        return builder.create();
    }
} public class LoadingIndicator {

    private static WeakReference<View> loadingIndicatorRef;

    private static Handler mainHandler;

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

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

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

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

    private static final String TAG_CATEGORIES = "categories";
    private static final int MAX_AGE_CATEGORIES = 7 * 86400; // 7 days

    private static final String TAG_BRANDS = "brands";
    private static final int MAX_AGE_BRANDS = 86400; // 1 day

    private static final String TAG_BRAND_OWNERS = "brand_owners";
    private static final int MAX_AGE_BRAND_OWNERS = 86400; // 1 day

    private static Map<String, Object> memCache = new ConcurrentHashMap<String, Object>();

    /**
     * Gets all categories either from the (memory or disk) cache or remotely using the ProductLayer API. The
     * latter saves the retrieved categories in the local cache.
     *
     * Both disk and network access are blocking - do not run on the UI thread unless you know what you are
     * doing (i.e. variable needs to be available and is sure to have been cached in memory)!
     *
     * @param context
     *         the application context
     * @param client
     *         the ProductLayer Android SDK client
     * @param fromCacheOnly
     *         true to check the cache only and to not run any network query
     * @param forceRefresh
     *         true to skip checking the local cache
     * @return an array of all categories or null on any error
     */
    public static Category[] getCategories(final Context context, final PLYAndroid client, boolean
            fromCacheOnly, boolean forceRefresh) {
        Callable<Future<Category[]>> serviceCall = null;
        if (!fromCacheOnly) {
            serviceCall = new Callable<Future<Category[]>>() {
                @Override
                public Future<Category[]> call() throws Exception {
                    return CategoryService.getMainCategories(client, null, new PLYCompletion<Category[]>() {
                        @Override
                        public void onSuccess(Category[] result) {
                            Log.d("GetCategories", "Retrieved " + result.length + " categories");
                        }

                        @Override
                        public void onError(PLYAndroid.QueryError error) {
                            Log.d("GetCategories", error.getMessage());
                        }
                    });
                }
            };
        }
        return get(context, fromCacheOnly, forceRefresh, TAG_CATEGORIES, MAX_AGE_CATEGORIES, serviceCall);
    }

    /**
     * Gets all brand names either from the (memory or disk) cache or remotely using the ProductLayer API. The
     * latter saves the retrieved brand names in the local cache.
     *
     * Both disk and network access are blocking - do not run on the UI thread unless you know what you are
     * doing (i.e. variable needs to be available and is sure to have been cached in memory)!
     *
     * @param context
     *         the application context
     * @param client
     *         the ProductLayer Android SDK client
     * @param fromCacheOnly
     *         true to check the cache only and to not run any network query
     * @param forceRefresh
     *         true to skip checking the local cache
     * @return an array of all brand names or null on any error
     */
    public static String[] getBrands(final Context context, final PLYAndroid client, boolean fromCacheOnly,
            boolean forceRefresh) {
        Callable<Future<String[]>> serviceCall = null;
        if (!fromCacheOnly) {
            serviceCall = new Callable<Future<String[]>>() {
                @Override
                public Future<String[]> call() throws Exception {
                    return ProductService.getBrands(client, new PLYCompletion<String[]>() {
                        @Override
                        public void onSuccess(String[] result) {
                            Log.d("GetBrands", "Retrieved " + result.length + " brand names");
                        }

                        @Override
                        public void onError(PLYAndroid.QueryError error) {
                            Log.d("GetBrands", error.getMessage());
                        }
                    });
                }
            };
        }
        return get(context, fromCacheOnly, forceRefresh, TAG_BRANDS, MAX_AGE_BRANDS, serviceCall);
    }

    /**
     * Gets all brand owner names either from the (memory or disk) cache or remotely using the ProductLayer
     * API. The latter saves the retrieved brand owner names in the local cache.
     *
     * Both disk and network access are blocking - do not run on the UI thread unless you know what you are
     * doing (i.e. variable needs to be available and is sure to have been cached in memory)!
     *
     * @param context
     *         the application context
     * @param client
     *         the ProductLayer Android SDK client
     * @param fromCacheOnly
     *         true to check the cache only and to not run any network query
     * @param forceRefresh
     *         true to skip checking the local cache
     * @return an array of all brand owner names or null on any error
     */
    public static String[] getBrandOwners(final Context context, final PLYAndroid client, boolean
            fromCacheOnly, boolean forceRefresh) {
        Callable<Future<String[]>> serviceCall = null;
        if (!fromCacheOnly) {
            serviceCall = new Callable<Future<String[]>>() {
                @Override
                public Future<String[]> call() throws Exception {
                    return ProductService.getBrandOwners(client, new PLYCompletion<String[]>() {
                        @Override
                        public void onSuccess(String[] result) {
                            Log.d("GetBrandOwners", "Retrieved " + result.length + " brand owner names");
                        }

                        @Override
                        public void onError(PLYAndroid.QueryError error) {
                            Log.d("GetBrandOwners", error.getMessage());
                        }
                    });
                }
            };
        }
        return get(context, fromCacheOnly, forceRefresh, TAG_BRAND_OWNERS, MAX_AGE_BRAND_OWNERS, serviceCall);
    }

    /**
     * Gets an object either from the (memory or disk) cache or remotely using the ProductLayer API. The
     * latter saves the retrieved object in the local cache.
     *
     * Both disk and network access are blocking - do not run on the UI thread unless you know what you are
     * doing (i.e. variable needs to be available and is sure to have been cached in memory)!
     *
     * @param context
     *         the application context
     * @param fromCacheOnly
     *         true to check the cache only and to not run any network query
     * @param forceRefresh
     *         true to skip checking the local cache
     * @param tag
     *         the unique tag associated with the result
     * @param maxAge
     *         the amount of seconds after which the object expires
     * @param serviceCall
     *         the call to the SDK service returning a Future
     * @param <T>
     *         the type of the result
     * @return the cached or remotely retrieved object or null on any error
     */
    private static <T> T get(final Context context, boolean fromCacheOnly, boolean forceRefresh, final
    String tag, long maxAge, Callable<Future<T>> serviceCall) {
        T object = null;
        if (!forceRefresh) {
            // check if already looked up
            if (memCache.containsKey(tag)) {
                Log.d(ObjectCache.class.getSimpleName(), "Requested " + tag + " from memory");
                //noinspection unchecked
                return (T) memCache.get(tag);
            }
            // look up in disk cache
            try {
                //noinspection unchecked
                object = (T) CacheUtil.getFromDiskCache(context, tag, maxAge);
            } catch (Exception e) {
                Log.w(ObjectCache.class.getSimpleName(), "Error getting " + tag + " from cache", e);
            }
            if (object != null) {
                Log.d(ObjectCache.class.getSimpleName(), "Requested " + tag + " from disk cache");
                memCache.put(tag, object);
                return object;
            }
            if (!fromCacheOnly) {
                Log.d(ObjectCache.class.getSimpleName(), tag + " not found in cache, doing remote retrieval");
            }
        }
        if (fromCacheOnly) {
            Log.d(ObjectCache.class.getSimpleName(), tag + " not found in cache, returning null");
            return null;
        }
        // remote retrieval
        try {
            Future<T> objectFuture = serviceCall.call();
            object = objectFuture.get();
        } catch (Exception e) {
            return null;
        }
        if (object == null) {
            return null;
        }
        // save in local cache in the background
        final T finalObject = object;
        new Thread(new Runnable() {
            @Override
            public void run() {
                CacheUtil.saveToDiskCache(context, tag, finalObject);
            }
        }).start();
        memCache.put(tag, object);
        return object;
    }
} 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 HasAppBarHandler {

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

} public interface HasPLYAndroidHolder {

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

} public interface PLYAndroidHolder {

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

} public class CategoryListItem implements ExpandableListItem<CategoryListItem>, Parcelable {

    public static final String KEY_NAME = "name";
    public static final String KEY_FULL_NAME = "fullName";
    public static final String KEY_KEY = "key";

    public static final Parcelable.Creator<CategoryListItem> CREATOR = new Parcelable
            .Creator<CategoryListItem>() {

        @Override
        public CategoryListItem createFromParcel(Parcel source) {
            return new CategoryListItem(source);
        }

        @Override
        public CategoryListItem[] newArray(int size) {
            return new CategoryListItem[size];
        }
    };

    private final String name;
    private final String fullName;
    private final String key;
    private final CategoryListItem[] subItems;

    /**
     * Converts a Category object into a CategoryListItem.
     *
     * @param category
     *         the category to extract the name and sub-categories from
     * @param parentItem
     *         the parent category or null if this is a root category
     */
    public CategoryListItem(Category category, CategoryListItem parentItem) {
        name = category.getName();
        fullName = parentItem == null ? name : parentItem.fullName + " / " + name;
        key = category.getKey();
        List<Category> subCategories = category.getSubCategories();
        int cntSubCategories = subCategories == null ? 0 : subCategories.size();
        if (cntSubCategories == 0) {
            subItems = new CategoryListItem[0];
        } else {
            subItems = new CategoryListItem[cntSubCategories];
            for (int i = 0; i < cntSubCategories; i++) {
                subItems[i] = new CategoryListItem(subCategories.get(i), this);
            }
            // add a clickable item for the expandable root category
            subItems[cntSubCategories - 1] = new CategoryListItem(name, fullName, key, null);
        }
    }

    /**
     * Creates a new instance initializing all of its fields.
     *
     * @param name
     *         the name of the category
     * @param fullName
     *         the path to this category if it is not at the root
     * @param key
     *         the unique key identifying the category
     * @param subItems
     *         any sub-categories
     */
    public CategoryListItem(String name, String fullName, String key, CategoryListItem[] subItems) {
        this.name = name;
        this.fullName = fullName == null ? name : fullName;
        this.key = key;
        this.subItems = subItems == null ? new CategoryListItem[0] : subItems;
    }

    /**
     * Creates a new instance with data read from a Parcel.
     *
     * @param source
     *         the data to read
     */
    private CategoryListItem(Parcel source) {
        name = source.readString();
        fullName = source.readString();
        key = source.readString();
        subItems = source.createTypedArray(CREATOR);
    }

    /**
     * Transforms {@link Category} beans into {@code CategoryListItem} objects flattened to a sub-category
     * hierarchy of at most two levels.
     *
     * @param categories
     *         the input objects
     * @return the transformed objects
     */
    public static CategoryListItem[] fromCategories(Category[] categories) {
        List<CategoryListItem> categoryListItems = new ArrayList<CategoryListItem>();
        for (Category category : categories) {
            List<CategoryListItem> flatCategories = flattenRootCategory(new CategoryListItem(category, null));
            categoryListItems.addAll(flatCategories);
        }
        return categoryListItems.toArray(new CategoryListItem[categoryListItems.size()]);
    }

    /**
     * Flattens a category so that it is at most two levels deep.
     *
     * @param category
     *         the category to flatten
     * @return the sub-categories that were moved up a level due to having sub-sub-categories and the original
     * category having those sub-categories removed, with all sub-(sub-...-)categories being treated the same
     * way
     */
    private static List<CategoryListItem> flattenRootCategory(CategoryListItem category) {
        CategoryListItem[] subCategories = category.subItems;
        List<CategoryListItem> siblingCategories = new ArrayList<CategoryListItem>();
        for (int i = 0; i < subCategories.length; i++) {
            CategoryListItem subCategory = subCategories[i];
            CategoryListItem[] subSubCategories = subCategory.subItems;
            if (subSubCategories.length != 0) {
                // remove sub-category from category
                CategoryListItem[] newSubCategories = new CategoryListItem[subCategories.length - 1];
                System.arraycopy(subCategories, 0, newSubCategories, 0, i);
                System.arraycopy(subCategories, i + 1, newSubCategories, i, subCategories.length -
                        i - 1);
                category = new CategoryListItem(category.name, category.fullName, category.key,
                        newSubCategories);
                // add new sibling category from removed sub-category and flatten it as well
                CategoryListItem siblingCategory = new CategoryListItem(category.name + " / " +
                        subCategory.name, null, subCategory.key, subSubCategories);
                List<CategoryListItem> flattenedSiblingCategory = flattenRootCategory(siblingCategory);
                siblingCategories.addAll(flattenedSiblingCategory);
            }
        }
        if (!siblingCategories.isEmpty()) {
            Log.v("FlattenCategories", category.fullName + " had " + siblingCategories.size() + " " +
                    "sub-categories moved up by one level");
        }
        siblingCategories.add(0, category);
        return siblingCategories;
    }

    /**
     * Retrieves a category name for a specified key from a list of categories.
     *
     * @param key
     *         the key to search for
     * @param categories
     *         the categories to search in
     * @return the name of the category matching the specified key or null if none found
     */
    public static String getName(String key, CategoryListItem[] categories) {
        if (key == null || categories == null) {
            return null;
        }
        for (CategoryListItem category : categories) {
            if (key.equals(category.key)) {
                return category.name;
            }
            String name = getName(key, category.getSubItems());
            if (name != null) {
                return name;
            }
        }
        return null;
    }

    @Override
    public Object get(String key) {
        if (KEY_FULL_NAME.equals(key)) {
            return fullName;
        } else if (KEY_KEY.equals(key)) {
            return this.key;
        } else {
            return name;
        }
    }

    @Override
    public CategoryListItem[] getSubItems() {
        return subItems;
    }

    @Override
    public String toString() {
        return name;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeString(fullName);
        dest.writeString(key);
        dest.writeTypedArray(subItems, flags);
    }
} public class SimpleBrand {

    public String brand;
    public String brandOwner;

    public String brandLower;
    public String brandAlphaNumeric;

    /**
     * Creates a new SimpleBrand instance.
     *
     * @param brand
     *         the name of the brand
     * @param brandOwner
     *         the name of the owner of the brand
     */
    public SimpleBrand(String brand, String brandOwner) {
        this.brand = brand;
        this.brandOwner = brandOwner;
        brandLower = brand.toLowerCase();
        brandAlphaNumeric = brandLower.replaceAll("[^A-Za-z0-9 ]", "");
    }

    @Override
    public String toString() {
        return brand;
    }
} public class BitmapUtil {

    /**
     * Transforms a bitmap into a rounded bitmap drawable using default corner radius from {@code
     * R.integer.corner_radius_default_min} and {@code R.integer.corner_radius_divisor_default}.
     *
     * @param context
     *         the application context
     * @param bitmap
     *         the bitmap to transform
     * @return the rounded bitmap drawable
     */
    public static RoundedBitmapDrawable getRoundedBitmapDrawable(Context context, Bitmap bitmap) {
        Resources res = context.getResources();
        float cornerRadius = Math.max(res.getInteger(R.integer.corner_radius_default_min), Math.min(bitmap
                .getHeight(), bitmap.getWidth()) / (float) res.getInteger(R.integer
                .corner_radius_divisor_default));
        return getRoundedBitmapDrawable(context, bitmap, cornerRadius);
    }

    /**
     * Transforms a bitmap into a rounded bitmap drawable.
     *
     * @param context
     *         the application context
     * @param bitmap
     *         the bitmap to transform
     * @param cornerRadius
     *         the corner radius in px
     * @return the rounded bitmap drawable
     */
    public static RoundedBitmapDrawable getRoundedBitmapDrawable(Context context, Bitmap bitmap, float
            cornerRadius) {
        Resources res = context.getResources();
        RoundedBitmapDrawable dr = RoundedBitmapDrawableFactory.create(res, bitmap);
        dr.setCornerRadius(cornerRadius);
        return dr;
    }

    /**
     * Copies a bitmap's pixels to a byte array.
     *
     * @param bitmap
     *         the bitmap to put into the byte array
     * @return the byte array reflecting the bitmap
     */
    public static byte[] toByteArray(Bitmap bitmap) {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.PNG, 0, outputStream);
        return outputStream.toByteArray();
    }

    /**
     * Creates a bitmap from a byte array.
     *
     * @param bytes
     *         the byte array to create the bitmap with
     * @return the newly created bitmap
     */
    public static Bitmap fromByteArray(byte[] bytes) {
        return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
    }

    /**
     * Loads a scaled bitmap from a file.
     *
     * @param path
     *         the path of the file to load
     * @param width
     *         the width of the scaled bitmap
     * @param height
     *         the height of the scaled bitmap
     * @param exactDimensions
     *         if false does less processing and only scales it so both dimensions remain equal or somewhat
     *         bigger than the specified dimensions
     * @return the scaled bitmap or null if the specified dimensions are zero
     */
    public static Bitmap fromFileScaled(String path, int width, int height, boolean exactDimensions) {
        if (width == 0 || height == 0) {
            return null;
        }
        BitmapFactory.Options orig = new BitmapFactory.Options();
        orig.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(path, orig);
        int scale = 1;
        while (orig.outWidth / (scale * 2) >= width && orig.outHeight / (scale * 2) >= height) {
            scale *= 2;
        }
        BitmapFactory.Options scaled = new BitmapFactory.Options();
        scaled.inSampleSize = scale;
        Bitmap scaledBitmap = BitmapFactory.decodeFile(path, scaled);
        if (exactDimensions) {
            return ThumbnailUtils.extractThumbnail(scaledBitmap, width, height, ThumbnailUtils
                    .OPTIONS_RECYCLE_INPUT);
        } else {
            return scaledBitmap;
        }
    }

    /**
     * Applies a Gaussian blur effect to a bitmap.
     *
     * @param context
     *         the application context
     * @param bitmap
     *         the input bitmap
     * @return a new blurred version of the input bitmap or null on any error
     */
    public static Bitmap blur(Context context, Bitmap bitmap) {
        int width = Math.round(bitmap.getWidth() / 2);
        int height = Math.round(bitmap.getHeight() / 2);
        Bitmap bitmapIn = null;
        Bitmap bitmapOut = null;
        try {
            bitmapIn = Bitmap.createScaledBitmap(bitmap, width, height, false);
            bitmapOut = Bitmap.createBitmap(bitmapIn);
            RenderScript rs = RenderScript.create(context);
            ScriptIntrinsicBlur intrinsicBlur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
            Allocation allocationIn = Allocation.createFromBitmap(rs, bitmapIn);
            Allocation allocationOut = Allocation.createFromBitmap(rs, bitmapOut);
            intrinsicBlur.setRadius(6);
            intrinsicBlur.setInput(allocationIn);
            intrinsicBlur.forEach(allocationOut);
            allocationOut.copyTo(bitmapOut);
        } catch (Exception e) {
            Log.e(BitmapUtil.class.getSimpleName(), "Error creating blurred image", e);
        } finally {
            if (bitmapIn != null && bitmapIn != bitmap && bitmapIn != bitmapOut) {
                bitmapIn.recycle();
            }
        }
        return bitmapOut;
    }

    /**
     * Applies a blur effect to a bitmap without using any native or support RenderScript library.
     *
     * @param bitmapIn
     *         the input bitmap
     * @param radius
     *         the blur radius (min 1)
     * @return a new blurred version of the input bitmap
     */
    public static Bitmap stackBlur(Bitmap bitmapIn, int radius) {
        // Stack Blur v1.0 from
        // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
        //
        // Java Author: Mario Klingemann <mario at quasimondo.com>
        // http://incubator.quasimondo.com
        // created Feburary 29, 2004
        // Android port : Yahel Bouaziz <yahel at kayenko.com>
        // http://www.kayenko.com
        // ported april 5th, 2012

        // This is a compromise between Gaussian Blur and Box blur
        // It creates much better looking blurs than Box Blur, but is
        // 7x faster than my Gaussian Blur implementation.
        //
        // I called it Stack Blur because this describes best how this
        // filter works internally: it creates a kind of moving stack
        // of colors whilst scanning through the image. Thereby it
        // just has to add one new block of color to the right side
        // of the stack and remove the leftmost color. The remaining
        // colors on the topmost layer of the stack are either added on
        // or reduced by one, depending on if they are on the right or
        // on the left side of the stack.
        //
        // If you are using this algorithm in your code please add
        // the following line:
        //
        // Stack Blur Algorithm by Mario Klingemann <mario@quasimondo.com>

        Bitmap bitmap = bitmapIn.copy(bitmapIn.getConfig(), true);
        int w = bitmap.getWidth();
        int h = bitmap.getHeight();

        int[] pix = new int[w * h];
        bitmap.getPixels(pix, 0, w, 0, 0, w, h);

        int wm = w - 1;
        int hm = h - 1;
        int wh = w * h;
        int div = radius + radius + 1;
        int r[] = new int[wh];
        int g[] = new int[wh];
        int b[] = new int[wh];
        int rsum;
        int gsum;
        int bsum;
        int x;
        int y;
        int i;
        int p;
        int yp;
        int yi = 0;
        int yw = 0;
        int vmin[] = new int[Math.max(w, h)];

        int divsum = (div + 1) >> 1;
        divsum *= divsum;
        int dv[] = new int[256 * divsum];
        for (i = 0; i < 256 * divsum; i++) {
            dv[i] = i / divsum;
        }

        int[][] stack = new int[div][3];
        int stackpointer;
        int stackstart;
        int[] sir;
        int rbs;
        int r1 = radius + 1;
        int routsum;
        int goutsum;
        int boutsum;
        int rinsum;
        int ginsum;
        int binsum;

        for (y = 0; y < h; y++) {
            rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
            for (i = -radius; i <= radius; i++) {
                p = pix[yi + Math.min(wm, Math.max(i, 0))];
                sir = stack[i + radius];
                sir[0] = (p & 0xff0000) >> 16;
                sir[1] = (p & 0x00ff00) >> 8;
                sir[2] = p & 0x0000ff;
                rbs = r1 - Math.abs(i);
                rsum += sir[0] * rbs;
                gsum += sir[1] * rbs;
                bsum += sir[2] * rbs;
                if (i > 0) {
                    rinsum += sir[0];
                    ginsum += sir[1];
                    binsum += sir[2];
                } else {
                    routsum += sir[0];
                    goutsum += sir[1];
                    boutsum += sir[2];
                }
            }
            stackpointer = radius;
            for (x = 0; x < w; x++) {
                r[yi] = dv[rsum];
                g[yi] = dv[gsum];
                b[yi] = dv[bsum];

                rsum -= routsum;
                gsum -= goutsum;
                bsum -= boutsum;

                stackstart = stackpointer - radius + div;
                sir = stack[stackstart % div];

                routsum -= sir[0];
                goutsum -= sir[1];
                boutsum -= sir[2];

                if (y == 0) {
                    vmin[x] = Math.min(x + radius + 1, wm);
                }
                p = pix[yw + vmin[x]];

                sir[0] = (p & 0xff0000) >> 16;
                sir[1] = (p & 0x00ff00) >> 8;
                sir[2] = p & 0x0000ff;

                rinsum += sir[0];
                ginsum += sir[1];
                binsum += sir[2];

                rsum += rinsum;
                gsum += ginsum;
                bsum += binsum;

                stackpointer = (stackpointer + 1) % div;
                sir = stack[stackpointer % div];

                routsum += sir[0];
                goutsum += sir[1];
                boutsum += sir[2];

                rinsum -= sir[0];
                ginsum -= sir[1];
                binsum -= sir[2];

                yi++;
            }
            yw += w;
        }

        for (x = 0; x < w; x++) {
            rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
            yp = -radius * w;
            for (i = -radius; i <= radius; i++) {
                yi = Math.max(0, yp) + x;

                sir = stack[i + radius];

                sir[0] = r[yi];
                sir[1] = g[yi];
                sir[2] = b[yi];

                rbs = r1 - Math.abs(i);

                rsum += r[yi] * rbs;
                gsum += g[yi] * rbs;
                bsum += b[yi] * rbs;

                if (i > 0) {
                    rinsum += sir[0];
                    ginsum += sir[1];
                    binsum += sir[2];
                } else {
                    routsum += sir[0];
                    goutsum += sir[1];
                    boutsum += sir[2];
                }

                if (i < hm) {
                    yp += w;
                }
            }
            yi = x;
            stackpointer = radius;
            for (y = 0; y < h; y++) {
                // Preserve alpha channel: ( 0xff000000 & pix[yi] )
                pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum];

                rsum -= routsum;
                gsum -= goutsum;
                bsum -= boutsum;

                stackstart = stackpointer - radius + div;
                sir = stack[stackstart % div];

                routsum -= sir[0];
                goutsum -= sir[1];
                boutsum -= sir[2];

                if (x == 0) {
                    vmin[y] = Math.min(y + r1, hm) * w;
                }
                p = x + vmin[y];

                sir[0] = r[p];
                sir[1] = g[p];
                sir[2] = b[p];

                rinsum += sir[0];
                ginsum += sir[1];
                binsum += sir[2];

                rsum += rinsum;
                gsum += ginsum;
                bsum += binsum;

                stackpointer = (stackpointer + 1) % div;
                sir = stack[stackpointer];

                routsum += sir[0];
                goutsum += sir[1];
                boutsum += sir[2];

                rinsum -= sir[0];
                ginsum -= sir[1];
                binsum -= sir[2];

                yi += w;
            }
        }

        bitmap.setPixels(pix, 0, w, 0, 0, w, h);
        return bitmap;
    }

} public class CameraUtil {

    /**
     * @param context
     *         the application context
     * @return true if the device has a rear camera, false else
     */
    public static boolean hasCameraBack(Context context) {
        PackageManager pm = context.getPackageManager();
        return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA);
    }

    /**
     * @param context
     *         the application context
     * @return true if the device has a front camera, false else
     */
    public static boolean hasCameraFront(Context context) {
        PackageManager pm = context.getPackageManager();
        return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
    }

    /**
     * @param context
     *         the application context
     * @return true if the device has any camera, false else
     */
    public static boolean hasCameraAny(Context context) {
        if (hasCameraBack(context) || hasCameraFront(context)) {
            return true;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            PackageManager pm = context.getPackageManager();
            return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
        }
        return false;
    }
} 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 MetricsUtil {

    private static int orientation;

    // parsed directly from DisplayMetrics
    private static float density;
    private static int densityDpi;
    private static int widthPx;
    private static int heightPx;

    // calculated from above
    private static int widthDp;
    private static int heightDp;

    /**
     * Stores all data from DisplayMetrics and translates dimensions into density-independent pixels. Must be
     * called once before getting data.
     *
     * @param activity
     *         the current activity
     */
    public static void update(Activity activity) {
        orientation = activity.getResources().getConfiguration().orientation;
        DisplayMetrics displayMetrics = new DisplayMetrics();
        activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        density = displayMetrics.density;
        densityDpi = displayMetrics.densityDpi;
        widthPx = displayMetrics.widthPixels;
        heightPx = displayMetrics.heightPixels;
        updateDp();
        Log.d(MetricsUtil.class.getSimpleName(), "Orientation: " + nameForOrientation(orientation) +
                "\nWidth (px): " + widthPx + ", Height (px): " + heightPx + "\nDPI: " + densityDpi + ", " +
                "Density Factor: " + density + "\nWidth (dp): " + widthDp + ", Height (dp): " + heightDp);
    }

    /**
     * Translates pixels (px) into density-independent pixels (dp) by dividing px by {@link
     * DisplayMetrics#density}.
     *
     * @param px
     *         a value in pixels (px)
     * @return the corresponding value in density-independent pixels (dp)
     */
    public static int inDp(int px) {
        return Math.round(px / density);
    }

    /**
     * Translates density-independent pixels (dp) into pixels (px) by multiplying dp with {@link
     * DisplayMetrics#density}.
     *
     * @param dp
     *         a value in density-independent pixels (dp)
     * @return the corresponsing value in pixels (px)
     */
    public static int inPx(int dp) {
        return Math.round(dp * density);
    }

    private static void updateDp() {
        widthDp = inDp(widthPx);
        heightDp = inDp(heightPx);
    }

    /**
     * @param orientation
     *         the screen orientation retrieved by {@link #getOrientation()}
     * @return the textual representation of the orientation value, either Portrait, Landscape, or Undefined
     */
    public static String nameForOrientation(int orientation) {
        String orientationName = "Undefined";
        if (orientation == Configuration.ORIENTATION_PORTRAIT) {
            orientationName = "Portrait";
        } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
            orientationName = "Landscape";
        }
        return orientationName;
    }

    /**
     * @return the screen orientation, one of {@link Configuration#ORIENTATION_PORTRAIT}, {@link
     * Configuration#ORIENTATION_LANDSCAPE}
     */
    public static int getOrientation() {
        return orientation;
    }

    /**
     * @return the screen density
     * @see DisplayMetrics#density
     */
    public static float getDensity() {
        return density;
    }

    /**
     * @return the screen density as dots per inch
     * @see DisplayMetrics#densityDpi
     */
    public static int getDensityDpi() {
        return densityDpi;
    }

    /**
     * @return the width of the screen in pixels
     * @see DisplayMetrics#widthPixels
     */
    public static int getWidthPx() {
        return widthPx;
    }

    /**
     * @return the height of the screen in pixels
     * @see DisplayMetrics#heightPixels
     */
    public static int getHeightPx() {
        return heightPx;
    }

    /**
     * @return the width of the screen in density-independent pixels
     */
    public static int getWidthDp() {
        return widthDp;
    }

    /**
     * @return the height of the screen in density-independent pixels
     */
    public static int getHeightDp() {
        return heightDp;
    }

} public class PhotoUtil {

    private static final int REQUEST_CODE_TAKE_PHOTO = 13014;
    private static final int REQUEST_CODE_PICK_IMAGE = 13015;

    private static final String tempPhotoPath = getPhotoPathShared();

    /**
     * Adds an OnClickListener to the provided view to display a popup menu allowing the user to choose
     * between taking a photo and picking one from the gallery.
     *
     * @param targetActivity
     *         the target activity receiving the camera's/gallery's result (may be null if fragment is set)
     * @param targetFragment
     *         the target fragment receiving the camera's/gallery's result (may be null if activity is set)
     * @param anchor
     *         the view to anchor the popup menu to and to register the OnClickListener with
     */
    public static void registerSelectorPopup(final Activity targetActivity, final Fragment targetFragment,
            View anchor) {
        Context context = targetFragment != null ? targetFragment.getActivity() : targetActivity;
        if (context == null) {
            return;
        }
        final PopupMenu popup = new PopupMenu(context, anchor);
        popup.getMenuInflater().inflate(R.menu.popup_photo_source, popup.getMenu());
        popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(MenuItem item) {
                int selectedId = item.getItemId();
                if (selectedId == R.id.popup_take_photo) {
                    takePhoto(targetActivity, targetFragment);
                    return true;
                } else if (selectedId == R.id.popup_pick_from_gallery) {
                    pickGalleryImage(targetActivity, targetFragment);
                    return true;
                }
                return false;
            }
        });
        try {
            // try to display icons
            Field mPopupField = popup.getClass().getDeclaredField("mPopup");
            mPopupField.setAccessible(true);
            MenuPopupHelper mPopup = (MenuPopupHelper) mPopupField.get(popup);
            mPopup.setForceShowIcon(true);
            Integer tintColor = ThemeUtil.getIntegerValue(context, android.R.attr.textColorPrimary);
            if (tintColor != null) {
                ColorUtil.tintMenuItems(popup.getMenu(), tintColor, -1);
            }
        } catch (Exception ignored) {
        }
        anchor.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                popup.show();
            }
        });
    }

    /**
     * Starts the camera app to take a photo.
     *
     * @param targetActivity
     *         the target activity receiving the camera's result (may be null if fragment is set)
     * @param targetFragment
     *         the target fragment receiving the camera's result (may be null if activity is set)
     */
    public static void takePhoto(Activity targetActivity, Fragment targetFragment) {
        Context context = targetFragment != null ? targetFragment.getActivity() : targetActivity;
        if (context == null) {
            return;
        }
        if (CameraUtil.hasCameraAny(context)) {
            Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(tempPhotoPath)));
            if (targetFragment != null) {
                targetFragment.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO);
            } else {
                targetActivity.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO);
            }
        } else {
            if (targetFragment != null) {
                SnackbarUtil.make(targetFragment.getActivity(), targetFragment.getView(), R.string
                        .no_camera_found, Snackbar.LENGTH_SHORT).show();
            }
        }
    }

    /**
     * Opens the gallery for the user to pick an image.
     *
     * @param targetActivity
     *         the target activity receiving the gallery's result (may be null if fragment is set)
     * @param targetFragment
     *         the target fragment receiving the gallery's result (may be null if activity is set)
     */
    public static void pickGalleryImage(Activity targetActivity, Fragment targetFragment) {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
        intent.setType("image/*");
        if (targetFragment != null) {
            targetFragment.startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE);
        } else if (targetActivity != null) {
            targetActivity.startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE);
        }
    }

    /**
     * Convenience method to call from an activity's or a fragment's {@code onActivityResult} method to check
     * if a photo was taken by the camera app or if the user picked an image from the gallery. If so, moves
     * the file to the specified path.
     *
     * @param requestCode
     *         the request code passed to the method
     * @param resultCode
     *         the result code passed to the method
     * @param data
     *         any attached result data
     * @param context
     *         the application context
     * @param moveToPath
     *         the file path to move any taken/selected photo to
     * @return true if a photo was taken, false else
     */
    public static boolean onActivityResult(int requestCode, int resultCode, Intent data, Context context,
            String moveToPath) {
        if (requestCode == REQUEST_CODE_TAKE_PHOTO && resultCode != AppCompatActivity.RESULT_CANCELED) {
            // photo taken
            Log.d(PhotoUtil.class.getSimpleName(), "New photo at " + tempPhotoPath);
            StorageUtil.copyFile(new File(tempPhotoPath), new File(moveToPath), true);
            // TODO bug: some devices save to gallery too -> find and delete
            return true;
        } else if (requestCode == REQUEST_CODE_PICK_IMAGE && resultCode != AppCompatActivity
                .RESULT_CANCELED) {
            // gallery image selected
            if (context == null) {
                return false;
            }
            Uri imageUri = data.getData();
            Log.d(PhotoUtil.class.getSimpleName(), "Selected gallery image at " + imageUri);
            InputStream is;
            try {
                is = context.getContentResolver().openInputStream(imageUri);
            } catch (FileNotFoundException e) {
                Log.w(PhotoUtil.class.getSimpleName(), e);
                return false;
            }
            StorageUtil.copyFile(is, new File(moveToPath));
            return true;
        }
        // TODO copying should be done in a background thread
        // TODO clear cache dir periodically
        return false;
    }

    /**
     * @param id
     *         the object ID
     * @param activity
     *         the current activity
     * @return the file path in internal cache any taken photos of products are moved to
     */
    public static String getPhotoPathCache(String id, Activity activity) {
        File photoFileCache = new File(activity.getCacheDir(), id + ".jpg");
        return photoFileCache.getAbsolutePath();
    }

    /**
     * @return the file path to have the external camera app save photos at
     */
    private static String getPhotoPathShared() {
        File photoFileShared = new File(StorageUtil.getExternalDir(), "prodly.jpg");
        return photoFileShared.getAbsolutePath();
    }

} public class PicassoTarget {

    /**
     * Creates a Picasso target to transform a bitmap to having rounded corners and load it into the provided
     * ImageView. The bitmap's rounded corner radius is calculated using default values from {@code
     * R.integer.corner_radius_default_min} and {@code R.integer.corner_radius_divisor_default}.
     *
     * Make sure to keep a reference to the returned target, else it may be garbage-collected before
     * completing the call.
     *
     * @param context
     *         the application context
     * @param imageViewTarget
     *         the target view for the transformed bitmap
     * @return the Target object to be used with Picasso
     */
    public static Target roundedCornersImage(final Context context, final ImageView imageViewTarget) {
        return roundedCornersImage(context, imageViewTarget, null);
    }

    /**
     * Creates a Picasso target to transform a bitmap to having rounded corners and load it into the provided
     * ImageView.
     *
     * Make sure to keep a reference to the returned target, else it may be garbage-collected before
     * completing the call.
     *
     * @param context
     *         the application context
     * @param imageViewTarget
     *         the target view for the transformed bitmap
     * @param cornerRadius
     *         the corner radius in px
     * @return the Target object to be used with Picasso
     */
    public static Target roundedCornersImage(final Context context, final ImageView imageViewTarget, final
    Float cornerRadius) {
        return new Target() {
            @Override
            public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
                Drawable drawable = cornerRadius == null ? BitmapUtil.getRoundedBitmapDrawable(context,
                        bitmap) : BitmapUtil.getRoundedBitmapDrawable(context, bitmap, cornerRadius);
                imageViewTarget.setImageDrawable(drawable);
            }

            @Override
            public void onBitmapFailed(Drawable errorDrawable) {
                Log.e(getClass().getSimpleName(), "Error loading image with rounded corners");
            }

            @Override
            public void onPrepareLoad(Drawable placeHolderDrawable) {

            }
        };
    }

} 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 FocusAutoCompleteTextView extends AppCompatAutoCompleteTextView {

    public FocusAutoCompleteTextView(Context context) {
        super(context);
    }

    public FocusAutoCompleteTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FocusAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean enoughToFilter() {
        return true;
    }

    @Override
    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
        super.onFocusChanged(focused, direction, previouslyFocusedRect);
        if (getWindowVisibility() == View.GONE) {
            return;
        }
        if (focused && getAdapter() != null) {
            performFiltering(getText(), 0);
            showDropDown();
        }
    }
} public class PLYAndroid {

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

    private final ExecutorService threadPool;

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

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

    private volatile Thread stateLoader;
    private volatile boolean stateLoaded;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    }

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

        private final Exception exception;

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

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

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

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

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

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

    }

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

        private final Query<T> query;

        private final PLYCompletion<T> completion;

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

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

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

    }

} public abstract class PLYCompletion<T> {

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

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

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

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

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

} public class ImageService {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    /**
     * Updates a specific product. If the user earns points for this operation 'X-ProductLayer-User-Points'
     * and 'X-ProductLayer-User-Points-Changed' will be present in the response header.
     *
     * @param client
     *         the PLYAndroid SDK client configured to handle communications with the ProductLayer API server
     * @param gtin
     *         The GTIN (barcode) of the product
     * @param product
     *         The product
     * @param completion
     *         dealing with any errors or successful completion of the query, handling the results: The
     *         updated product
     * @return a Future object to optionally wait for the {@code Product} result or to cancel the query
     */
    public static Future<Product> updateProduct(final PLYAndroid client, final String gtin, final Product
            product, PLYCompletion<Product> completion) {
        return client.submit(new PLYAndroid.Query<Product>() {
            @Override
            public Product execute() {
                return com.productlayer.rest.client.services.ProductService.updateProduct(client
                        .getRestClient(), gtin, product);
            }
        }, completion);
    }
}
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Handler;
import android.support.design.widget.Snackbar;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.KeyEvent;
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.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import com.productlayer.android.common.R;
import com.productlayer.android.common.adapter.BrandAdapter;
import com.productlayer.android.common.adapter.BrandOwnerAdapter;
import com.productlayer.android.common.dialog.CategorySelectionDialogFragment;
import com.productlayer.android.common.dialog.NewProductDialogFragment;
import com.productlayer.android.common.global.LoadingIndicator;
import com.productlayer.android.common.global.ObjectCache;
import com.productlayer.android.common.handler.AppBarHandler;
import com.productlayer.android.common.handler.DataChangeListener;
import com.productlayer.android.common.handler.HasAppBarHandler;
import com.productlayer.android.common.handler.HasPLYAndroidHolder;
import com.productlayer.android.common.handler.PLYAndroidHolder;
import com.productlayer.android.common.model.CategoryListItem;
import com.productlayer.android.common.model.SimpleBrand;
import com.productlayer.android.common.util.BitmapUtil;
import com.productlayer.android.common.util.CameraUtil;
import com.productlayer.android.common.util.LocaleUtil;
import com.productlayer.android.common.util.MetricsUtil;
import com.productlayer.android.common.util.PhotoUtil;
import com.productlayer.android.common.util.PicassoTarget;
import com.productlayer.android.common.util.SnackbarUtil;
import com.productlayer.android.common.view.FocusAutoCompleteTextView;
import com.productlayer.android.sdk.PLYAndroid;
import com.productlayer.android.sdk.PLYCompletion;
import com.productlayer.android.sdk.services.ImageService;
import com.productlayer.android.sdk.services.ProductService;
import com.productlayer.core.beans.BrandOwner;
import com.productlayer.core.beans.Category;
import com.productlayer.core.beans.Product;
import com.productlayer.core.beans.ProductImage;
import com.productlayer.core.error.PLYStatusCodes;
import com.productlayer.core.utils.GTINValidator;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
     * @param imagePath
     *         the path to the image
     * @param imageView
     *         the view to display the image in
     */
    private void loadProductImage(String imagePath, ImageView imageView) {
        Bitmap bitmap = BitmapUtil.fromFileScaled(imagePath, imageView.getWidth(), imageView.getHeight(),
                true);
        if (bitmap == null) {
            Log.d(getClass().getSimpleName(), "Failed to load product image: image view dimensions not yet " +
                    "" + "determined");
        } else {
            Activity activity = getActivity();
            if (activity != null) {
                imageView.setImageDrawable(BitmapUtil.getRoundedBitmapDrawable(activity, bitmap));
            }
        }
    }

    /**
     * Adds brand suggestions to the respective auto-complete text fields.
     *
     * Must be run on the UI thread.
     */
    private void addSuggestedBrands() {
        Activity activity = getActivity();
        if (brand == null || activity == null) {
            return;
        }
        String[] allBrands = allBrandsLoaded ? ObjectCache.getBrands(activity, client, true, false) : null;
        BrandAdapter brandAdapter = new BrandAdapter(activity, R.layout.dropdown_brand_item,
                suggestedBrandOwners, allBrands);
        brand.setAdapter(brandAdapter);
        brand.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Object object = parent.getItemAtPosition(position);
                String brandName;
                String brandOwnerName = null;
                if (object instanceof SimpleBrand) {
                    brandName = object.toString();
                    brandOwnerName = ((SimpleBrand) object).brandOwner;
                } else {
                    brandName = (String) object;
                }
                brand.setText(brandName);
                if (brandOwnerName != null && !brandOwnerName.isEmpty()) {
                    // also set the brand's owner
                    brandOwner.setText(brandOwnerName);
                }
            }
        });
    }

    /**
     * Adds brand owner suggestions to the respective auto-complete text fields.
     *
     * Must be run on the UI thread.
     */
    private void addSuggestedBrandOwners() {
        Activity activity = getActivity();
        if (brandOwner == null || activity == null) {
            return;
        }
        String[] allBrandOwners = allBrandOwnersLoaded ? ObjectCache.getBrandOwners(activity, client, true,
                false) : null;
        BrandOwnerAdapter brandOwnerAdapter = new BrandOwnerAdapter(activity, R.layout
                .dropdown_brand_owner_item, suggestedBrandOwners, allBrandOwners);
        brandOwner.setAdapter(brandOwnerAdapter);
    }

    /**
     * Creates and shows the category selection dialog fragment.
     */
    private void openCategorySelection() {
        CategorySelectionDialogFragment categorySelectionDialogFragment = CategorySelectionDialogFragment
                .newInstance(categories);
        categorySelectionDialogFragment.show(getChildFragmentManager(), "CategorySelectionDialogFragment");
    }

    /**
     * Sets the title of the "Select Category" button to the name of the category identified by the specified
     * key - provided a category key selection has been made and the categories list has been populated and
     * the layout has been inflated.
     *
     * If no category key selection has been made yet, attempts to get a key from the suggested categories for
     * the current GTIN.
     *
     * @see #setSuggestedCategory()
     */
    private void setCategoryButtonTextFromKey() {
        setSuggestedCategory();
        if (selectedCategoryKey != null && selectCategory != null) {
            String selectedCategoryName = CategoryListItem.getName(selectedCategoryKey, categories);
            if (selectedCategoryName != null) {
                selectCategory.setText(selectedCategoryName);
            }
        }
    }

    /**
     * Sets the title of the "Select Category" button (non-localized) and the selected category key to the
     * first of any suggested categories - provided categories were suggested for the GTIN and no category has
     * yet been selected.
     */
    private void setSuggestedCategory() {
        if (selectedCategoryKey != null) {
            return;
        }
        if (suggestedCategories != null && suggestedCategories.length != 0) {
            selectedCategoryKey = suggestedCategories[0].getKey();
            selectCategory.setText(suggestedCategories[0].getName());
        }
    }

    /**
     * Opens a dialog asking the user to take a photo of the product.
     */
    private void askTakePhoto() {
        Activity activity = getActivity();
        if (activity != null && CameraUtil.hasCameraAny(activity)) {