Yarrn/src/main/java/de/vanmar/android/yarrn/queues/QueuedProjectFragment.java
@EFragment(R.layout.fragment_queue_detail)
@OptionsMenu(R.menu.fragment_menu)
public class QueuedProjectFragment extends Fragment {

    public static final String ARG_QUEUED_PROJECT_ID = "queuedProjectId";
    public static final String ARG_USERNAME = "username";

    protected SpiceManager spiceManager;
    private ImageDialog dialog;

    public interface QueuedProjectFragmentListener extends IRavelryActivity {
    }

    @ViewById(R.id.gallery)
    HorizontalListView gallery;

    @ViewById(R.id.name)
    TextView name;

    @ViewById(R.id.author)
    TextView author;

    @ViewById(R.id.gauge_description)
    TextView gauge_description;

    @ViewById(R.id.yarn_weight_description)
    TextView yarn_weight_description;

    @ViewById(R.id.yardage_description)
    TextView yardage_description;

    @ViewById(R.id.needles)
    TextView needles;

    @ViewById(R.id.notes)
    WebView notes;

    @FragmentArg(ARG_QUEUED_PROJECT_ID)
    int queuedProjectId;

    @FragmentArg(ARG_USERNAME)
    String username;

    private QueuedProjectFragmentListener listener;

    private PhotoAdapter adapter;

    @Pref
    YarrnPrefs_ prefs;

    @AfterViews
    public void afterViews() {
        if (spiceManager == null) {
            spiceManager = new SpiceManager(YarrnSpiceService.class);
        }

        adapter = new PhotoAdapter(getActivity());
        gallery.setAdapter(adapter);
        gallery.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                dialog = new ImageDialog(getActivity(), adapter.getItem(position).getMediumUrl());
                dialog.show();
            }
        });
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (dialog != null && dialog.isShowing()) {
            dialog.dismiss();
            dialog = null;
        }
    }

    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);
        if (activity instanceof QueuedProjectFragmentListener) {
            listener = (QueuedProjectFragmentListener) activity;
        } else {
            throw new ClassCastException(activity.toString()
                    + " must implement QueuedProjectFragmentListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        listener = null;
    }

    public void onQueuedProjectSelected(final int queuedProjectId) {
        clearPattern();
        if (queuedProjectId != 0) {
            GetQueuedProjectRequest request = new GetQueuedProjectRequest(this.getActivity().getApplication(), prefs, queuedProjectId, username);
            spiceManager.execute(request, request.getCacheKey(), AbstractRavelryGetRequest.CACHE_DURATION, new QueuedProjectListener(listener));
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        spiceManager.start(this.getActivity());

        onQueuedProjectSelected(queuedProjectId);
    }

    @Override
    public void onResume() {
        super.onResume();
    }

    @Override
    public void onStop() {
        if (spiceManager.isStarted()) {
            spiceManager.shouldStop();
        }
        super.onStop();
    }

    @UiThread
    protected void clearPattern() {
        View view = getView();
        if (view != null) {
            view.setVisibility(View.GONE);
        }
    }

    protected void displayQueuedProject(final QueuedProjectResult queuedProjectResult) {
        QueuedProject queuedProject = queuedProjectResult.queuedProject;
        if (queuedProject != null && queuedProject.pattern != null) {
            Pattern pattern = queuedProject.pattern;
            getActivity().setTitle(queuedProject.name);
            name.setText(queuedProject.name);
            author.setText(pattern.patternAuthor == null ? null : pattern.patternAuthor.name);
            gauge_description.setText(getString(R.string.gauge_title) + pattern.gauge_description);
            yarn_weight_description.setText(getString(R.string.yarn_title) + pattern.yarn_weight_description);
            yardage_description.setText(getString(R.string.yardage_title) + pattern.yardage_description);
            needles.setText(getString(R.string.needles_title) + getNeedlesDescription(pattern.pattern_needle_sizes));
            notes.loadDataWithBaseURL("", pattern.notes_html, "text/html", "UTF-8", "");
            hideIfEmpty(gauge_description, pattern.gauge_description);
            hideIfEmpty(yarn_weight_description, pattern.yarn_weight_description);
            hideIfEmpty(yardage_description, pattern.yardage_description);
            hideIfEmpty(needles, pattern.pattern_needle_sizes);
            adapter.setItems(pattern.photos);
            getView().setVisibility(View.VISIBLE);
        }
    }

    private void hideIfEmpty(View view, String value) {
        view.setVisibility(value != null && !"".equals(value) ? View.VISIBLE : View.GONE);
    }

    private void hideIfEmpty(View view, Collection values) {
        view.setVisibility(values != null && !values.isEmpty() ? View.VISIBLE : View.GONE);
    }

    private String getNeedlesDescription(List<Needle> needles) {
        StringBuilder needleString = new StringBuilder();
        if (needles != null)
            for (Needle needle : needles) {
                needleString.append('\n').append(needle.name);
            }
        return needleString.toString();
    }

    @OptionsItem(R.id.menu_refresh)
    public void menuRefresh() {
        onQueuedProjectSelected(queuedProjectId);
    }

    @OptionsItem(R.id.menu_add_as_favorite)
    public void menuAddAsFavorite() {
        new AddEditFavoriteDialog(getActivity(), new AddEditFavoriteDialog.AddEditFavoriteDialogListener() {
            @Override
            public void onSave(String comment, String tags) {
                JsonObject updateData = new JsonObject();
                updateData.addProperty("type", "pattern");
                updateData.addProperty("favorited_id", queuedProjectId);
                updateData.addProperty("comment", comment);
                updateData.addProperty("tag_names", tags);
                spiceManager.execute(new AddFavoriteRequest(prefs, getActivity().getApplication(), updateData), new RequestListener<BookmarkShort>() {
                    @Override
                    public void onRequestFailure(SpiceException spiceException) {
                        AQUtility.report(spiceException);
                    }

                    @Override
                    public void onRequestSuccess(BookmarkShort bookmarkShort) {
                        Toast.makeText(getActivity(), R.string.add_favorite_success, Toast.LENGTH_LONG).show();
                    }
                });
            }
        }, prefs).show();

    }

    class QueuedProjectListener extends RavelryResultListener<QueuedProjectResult> {

        protected QueuedProjectListener(IRavelryActivity activity) {
            super(activity);
        }

        @Override
        public void onRequestSuccess(QueuedProjectResult result) {
            if (listener != null) {
                displayQueuedProject(result);
            }
        }
    }
}
Yarrn/src/main/java/de/vanmar/android/yarrn/favorites/FavoritesFragment.java
@EFragment(R.layout.fragment_favorites)
@OptionsMenu(R.menu.favorites_menu)
public class FavoritesFragment extends PagingListFragment<FavoritesResult, BookmarkShort> {

    public static final int MENU_EDIT = 1;
    public static final int MENU_DELETE = 2;
    @SystemService
    InputMethodManager inputMethodManager;
    @ViewById(R.id.favoritelist)
    ListView favoritelist;
    @ViewById(R.id.query)
    EditText query;
    @ViewById(R.id.search_options)
    Spinner searchOptions;
    @Pref
    YarrnPrefs_ prefs;

    public interface FavoritesFragmentListener extends IRavelryActivity {
        /**
         * Project with projectId was selected, 0 if no project selected
         */
        void onProjectSelected(int projectId, String username);

        /**
         * Pattern with patternId was selected, 0 if no pattern selected
         */
        void onPatternSelected(int patternId);
    }

    private FavoritesAdapter adapter;
    private FavoritesFragmentListener listener;
    private String searchQuery = "";
    private SearchOption searchOption;

    @AfterViews
    @Override
    public void afterViews() {
        super.afterViews();
        adapter = new FavoritesAdapter(getActivity()) {

            @Override
            protected void onProjectClicked(final int id, final String username) {
                listener.onProjectSelected(id, username);
            }

            @Override
            protected void onPatternClicked(int patternId) {
                listener.onPatternSelected(patternId);
            }

        };
        favoritelist.setAdapter(adapter);
        registerForContextMenu(favoritelist);

        query.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (event != null && event.getAction() != KeyEvent.ACTION_DOWN) {
                    return false;
                }
                if (actionId == EditorInfo.IME_ACTION_SEARCH || event == null
                        || event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
                    setSearchQuery(query.getText().toString());
                    inputMethodManager.hideSoftInputFromWindow(query.getWindowToken(), 0);
                    query.clearFocus();
                }
                return true;
            }
        });
        searchOptions.setOnItemSelectedListener(null);
        searchOptions.setSelection(prefs.favoriteSearchOption().get());
        searchOptions.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                setSearchOption(position);
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {
            }
        });
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
        if (view.getId() == R.id.favoritelist) {
            menu.add(Menu.NONE, MENU_EDIT, MENU_EDIT, R.string.edit_favorite);
            menu.add(Menu.NONE, MENU_DELETE, MENU_DELETE, R.string.delete_favorite);
        }
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
        final BookmarkShort favorite = adapter.getItem(info.position);
        if (item.getItemId() == MENU_EDIT) {
            new AddEditFavoriteDialog(getActivity(), new AddEditFavoriteDialog.AddEditFavoriteDialogListener() {
                @Override
                public void onSave(String comment, String tags) {
                    JsonObject updateData = new JsonObject();
                    updateData.addProperty("comment", comment);
                    updateData.addProperty("tag_names", tags);
                    favorite.comment = comment;
                    favorite.tags = Arrays.asList(StringUtils.split(tags, ' '));
                    adapter.notifyDataSetChanged();

                    spiceManager.execute(new EditFavoriteRequest(prefs, getActivity().getApplication(), favorite.id, updateData), new RequestListener<BookmarkShort>() {
                        @Override
                        public void onRequestFailure(SpiceException spiceException) {
                            AQUtility.report(spiceException);
                        }

                        @Override
                        public void onRequestSuccess(BookmarkShort bookmarkShort) {
                            Toast.makeText(getActivity(), R.string.edit_favorite_success, Toast.LENGTH_LONG).show();
                        }
                    });

                }
            }, prefs, favorite).show();
        } else if (item.getItemId() == MENU_DELETE) {
            String favoriteId = favorite.id;
            spiceManager.execute(new DeleteFavoriteRequest(prefs, getActivity().getApplication(), favoriteId), new RequestListener<BookmarkShort>() {
                @Override
                public void onRequestFailure(SpiceException spiceException) {
                    AQUtility.report(spiceException);
                }

                @Override
                public void onRequestSuccess(BookmarkShort bookmarkShort) {
                    Toast.makeText(getActivity(), R.string.favorite_deleted, Toast.LENGTH_LONG).show();
                    adapter.remove(favorite);
                }
            });
            return true;
        }
        return super.onContextItemSelected(item);
    }

    private void setSearchQuery(String newQuery) {
        if (!searchQuery.equals(newQuery)) {
            searchQuery = newQuery;
            loadData(1);
        }
    }

    private void setSearchOption(int position) {
        SearchOption newOption = SearchOption.values()[position];
        if (searchOption != newOption) {
            searchOption = newOption;
            prefs.favoriteSearchOption().put(position);
            loadData(1);
        }
    }

    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);
        if (activity instanceof FavoritesFragmentListener) {
            listener = (FavoritesFragmentListener) activity;
        } else {
            throw new ClassCastException(activity.toString()
                    + " must implement FavoritesFragmentListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        listener = null;
    }

    @OptionsItem(R.id.menu_refresh)
    public void menuRefresh() {
        super.menuRefresh();
    }

    @Click(R.id.delete)
    public void onDeleteClicked() {
        query.setText(null);
    }

    protected void displayResult(final FavoritesResult result) {
        FragmentActivity activity = getActivity();
        if (activity != null) {
            super.displayResult(result);
            activity.setTitle(R.string.my_favorites_title);
        }
    }

    @Override
    protected ListView getListView() {
        return favoritelist;
    }

    @Override
    protected YarrnAdapter<BookmarkShort> getAdapter() {
        return adapter;
    }

    @Override
    protected AbstractRavelryGetRequest<FavoritesResult> getRequest(int page) {
        return new ListFavoritesRequest(this.getActivity().getApplication(), prefs, page, PAGE_SIZE, searchQuery, searchOption);
    }

    @Override
    protected IRavelryActivity getRavelryActivity() {
        return listener;
    }
}
Yarrn/src/main/java/de/vanmar/android/yarrn/stashes/StashesFragment.java
@EFragment(R.layout.fragment_stashes)
@OptionsMenu(R.menu.stashes_menu)
public class StashesFragment extends PagingListFragment<StashesResult, StashShort> {

    @ViewById(R.id.stashlist)
    ListView stashlist;
    @ViewById(R.id.sort)
    Spinner sort;
    @ViewById(R.id.sort_reverse)
    CheckBox sortReverse;
    @Pref
    YarrnPrefs_ prefs;

    public interface StashesFragmentListener extends IRavelryActivity {
        /**
         * Stash with stashId was selected, 0 if no stash selected
         */
        void onStashSelected(int stashId, String username);
    }

    private StashesFragment.StashesFragmentListener listener;
    private StashesAdapter adapter;

    @AfterViews
    @Override
    public void afterViews() {
        super.afterViews();
        adapter = new StashesAdapter(getActivity()) {

            @Override
            protected void onStashClicked(final StashShort stash) {
                listener.onStashSelected(stash.id, prefs.username().get());
            }

        };
        stashlist.setAdapter(adapter);

        sort.setOnItemSelectedListener(null);
        sort.setSelection(prefs.stashSort().get(), false);
        sort.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                applySort();
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {
            }
        });

        sortReverse.setOnCheckedChangeListener(null);
        sortReverse.setChecked(prefs.stashSortReverse().get());
        sortReverse.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                applySort();
            }
        });
    }

    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);
        if (activity instanceof StashesFragment.StashesFragmentListener) {
            listener = (StashesFragment.StashesFragmentListener) activity;
        } else {
            throw new ClassCastException(activity.toString()
                    + " must implement StashesFragmentListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        listener = null;
    }

    private void applySort() {
        prefs.stashSort().put(sort.getSelectedItemPosition());
        prefs.stashSortReverse().put(sortReverse.isChecked());
        loadData(1);
    }

    @OptionsItem(R.id.menu_refresh)
    public void menuRefresh() {
        super.menuRefresh();
    }

    protected void displayResult(final StashesResult result) {
        super.displayResult(result);
        getActivity().setTitle(R.string.my_stashes_title);
    }

    @Override
    protected ListView getListView() {
        return stashlist;
    }

    @Override
    protected YarrnAdapter<StashShort> getAdapter() {
        return adapter;
    }

    @Override
    protected AbstractRavelryGetRequest<StashesResult> getRequest(int page) {
        return new ListStashesRequest(this.getActivity().getApplication(), prefs, page, PAGE_SIZE);
    }

    @Override
    protected IRavelryActivity getRavelryActivity() {
        return listener;
    }
}
Yarrn/src/main/java/de/vanmar/android/yarrn/projects/ProjectFragment.java
@EFragment(R.layout.fragment_project_detail)
@OptionsMenu(R.menu.project_fragment_menu)
public class ProjectFragment extends Fragment {

    public static final String ARG_PROJECT_ID = "projectId";
    public static final String ARG_USERNAME = "username";
    protected SpiceManager spiceManager;
    private AdapterView.OnItemSelectedListener progressListener;
    private ViewEditText.OnSaveListener notesListener;
    private AdapterView.OnItemSelectedListener statusListener;
    private AdapterView.OnItemSelectedListener ratingListener;
    private View.OnClickListener progressBarListener;
    private List<Photo> photos;
    private List<Photo> photosOriginal;
    private ImageDialog dialog;

    public interface ProjectFragmentListener extends IRavelryActivity {

        void takePhoto();

        void pickImage();

        void onPatternSelected(int patternId);
    }

    @ViewById(R.id.name)
    TextView name;

    @ViewById(R.id.pattern_name)
    TextView patternName;

    @ViewById(R.id.status)
    Spinner status;

    @ViewById(R.id.gallery_edit_done)
    ImageButton galleryEditDone;

    @ViewById(R.id.gallery_edit_cancel)
    ImageButton galleryEditCancel;

    @ViewById(R.id.gallery)
    HorizontalListView gallery;

    @ViewById(R.id.progressBar)
    ProgressBar progressBar;

    @ViewById(R.id.progressSpinner)
    Spinner progressSpinner;

    @ViewById(R.id.started)
    public TextView started;

    @ViewById(R.id.completed)
    public TextView completed;

    @ViewById(R.id.notes)
    public ViewEditText notes;

    @ViewById(R.id.rating)
    public Spinner rating;

    @FragmentArg(ARG_PROJECT_ID)
    int projectId;

    @FragmentArg(ARG_USERNAME)
    String username;

    private ProjectFragmentListener listener;

    private PhotoAdapter adapter;

    private boolean isEditable = false;
    private boolean isOwn = false;
    private boolean isLoaded = false;

    @Pref
    YarrnPrefs_ prefs;

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (dialog != null && dialog.isShowing()) {
            dialog.dismiss();
            dialog = null;
        }
    }

    @AfterViews
    public void afterViews() {
        if (spiceManager == null) {
            spiceManager = new SpiceManager(YarrnSpiceService.class);
        }
        notesListener = new ViewEditText.OnSaveListener() {
            @Override
            public void onSave(ViewEditText view, Editable text) {
                JsonObject updateData = new JsonObject();
                updateData.addProperty("notes", text.toString());
                executeUpdate(updateData);
            }
        };

        setupStatusSpinner();
        setupRatingSpinner();

        progressBarListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                progressSpinner.performClick();
            }
        };

        adapter = new PhotoAdapter(getActivity());
        gallery.setAdapter(adapter);
        gallery.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                dialog = new ImageDialog(getActivity(), adapter.getItem(position).getMediumUrl());
                dialog.show();
            }
        });
        gallery.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
            @Override
            public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
                setGalleryEditable(true);
                return true;
            }
        });
        adapter.setPhotoAdapterListener(new PhotoAdapter.PhotoAdapterListener() {
            private void movePhotoFromTo(int fromPosition, int toPosition) {
                Photo photo = photos.remove(fromPosition);
                photos.add(toPosition, photo);
                adapter.setItems(photos);
            }

            @Override
            public void onMoveLeft(int position) {
                movePhotoFromTo(position, position - 1);
            }

            @Override
            public void onMoveAllLeft(int position) {
                movePhotoFromTo(position, 0);
            }

            @Override
            public void onMoveRight(int position) {
                movePhotoFromTo(position, position + 1);
            }

            @Override
            public void onMoveAllRight(int position) {
                movePhotoFromTo(position, photos.size() - 1);
            }
        });

        progressListener = new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                int newProgress = position * 5;
                displayProgress(newProgress);
                JsonObject updateData = new JsonObject();
                updateData.addProperty("progress", newProgress);
                executeUpdate(updateData);
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {
                // do nothing
            }
        };
    }

    private void setupStatusSpinner() {
        statusListener = new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                int newStatus = position + 1;
                JsonObject updateData = new JsonObject();
                updateData.addProperty("project_status_id", newStatus);
                executeUpdate(updateData);
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {
                // do nothing
            }
        };
    }

    private void setupRatingSpinner() {
        ratingListener = new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                int newRating = position;
                JsonObject updateData = new JsonObject();
                updateData.addProperty("rating", newRating);
                executeUpdate(updateData);
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {
                // do nothing
            }
        };
        SimpleImageArrayAdapter ratingAdapter = new SimpleImageArrayAdapter(getActivity(), new Integer[]{R.drawable.smiley1, R.drawable.smiley2, R.drawable.smiley3, R.drawable.smiley4, R.drawable.smiley5});
        rating.setAdapter(ratingAdapter);
    }

    private void setGalleryEditable(boolean shouldBeEditable) {
        boolean editable = isEditable && shouldBeEditable;
        adapter.setEditable(editable);
        galleryEditDone.setVisibility(editable ? View.VISIBLE : View.GONE);
        galleryEditCancel.setVisibility(editable ? View.VISIBLE : View.GONE);
    }

    private void executeUpdate(JsonObject updateData) {
        spiceManager.execute(new UpdateProjectRequest(prefs, getActivity().getApplication(), projectId, updateData), new ProjectListener(listener));
        spiceManager.removeDataFromCache(ProjectResult.class, new GetProjectRequest(getActivity().getApplication(), prefs, projectId, prefs.username().get()).getCacheKey());
    }

    private void savePhotoOrder() {
        spiceManager.execute(new ReorderProjectPhotosRequest(prefs, getActivity().getApplication(), projectId, photos), new RavelryResultListener<PhotoResult>(listener) {
            @Override
            public void onRequestSuccess(PhotoResult photoResult) {
                photos = photoResult.photos;
                adapter.setItems(photos);
            }
        });
        spiceManager.removeDataFromCache(ProjectResult.class, new GetProjectRequest(getActivity().getApplication(), prefs, projectId, prefs.username().get()).getCacheKey());
    }


    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);
        if (activity instanceof ProjectFragmentListener) {
            listener = (ProjectFragmentListener) activity;
        } else {
            throw new ClassCastException(activity.toString()
                    + " must implement ProjectFragmentListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        listener = null;
    }

    public void onProjectSelected(final int projectId, final String username) {
        clearProject();
        if (projectId != 0) {
            GetProjectRequest request = new GetProjectRequest(this.getActivity().getApplication(), prefs, projectId, username);
            spiceManager.execute(request, request.getCacheKey(), AbstractRavelryGetRequest.CACHE_DURATION, new ProjectListener(listener));
            isLoaded = false;
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        spiceManager.start(this.getActivity());

        onProjectSelected(projectId, username);
    }

    @Override
    public void onResume() {
        super.onResume();
    }

    @Override
    public void onStop() {
        if (spiceManager.isStarted()) {
            spiceManager.shouldStop();
        }
        super.onStop();
    }

    @UiThread
    protected void clearProject() {
        View view = getView();
        if (view != null) {
            view.setVisibility(View.GONE);
        }
    }

    protected void displayProject(final ProjectResult projectResult) {
        final Project project = projectResult.project;
        getActivity().setTitle(project.name);
        name.setText(project.name);
        rating.setOnItemSelectedListener(null);
        rating.setSelection(project.rating, false);
        setPatternName(project);
        status.setOnItemSelectedListener(null);
        status.setSelection(project.statusId - 1, false);
        started.setText(getCompletedDateText(project.started, project.startedDaySet));
        completed.setText(getCompletedDateText(project.completed, project.completedDaySet));
        notes.setBodyText(project.notes);
        adapter.clear();
        photos = project.photos;
        photosOriginal = new ArrayList<Photo>(project.photos);
        adapter.setItems(photos);
        setGalleryEditable(false);
        displayProgress(project.progress);
        progressSpinner.setOnItemSelectedListener(null);
        getView().setVisibility(View.VISIBLE);
        if (project.user != null && prefs.username().get().equals(project.user.username)) {
            setEditable();
            isOwn = true;
        } else {
            setNonEditable();
            isOwn = false;
        }
        isLoaded = true;
    }

    private void setPatternName(final Project project) {
        String name = project.patternName == null ? "" : project.patternName;
        SpannableString patternNameText = new SpannableString(name);
        if (project.patternId != null) {
            patternNameText.setSpan(new UnderlineSpan(), 0, name.length(), 0);
            patternName.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    listener.onPatternSelected(project.patternId);
                }
            });
        } else {
            patternName.setOnClickListener(null);
        }
        patternName.setText(patternNameText);
    }

    private void setEditable() {
        progressSpinner.setOnItemSelectedListener(progressListener);
        progressBar.setOnClickListener(progressBarListener);
        notes.setOnSaveListener(notesListener);
        status.setOnItemSelectedListener(statusListener);
        rating.setOnItemSelectedListener(ratingListener);
        isEditable = true;
        setFieldsEditable();
    }

    private void setNonEditable() {
        progressSpinner.setOnItemSelectedListener(null);
        progressSpinner.setOnItemSelectedListener(null);
        progressBar.setOnClickListener(null);
        notes.setOnSaveListener(null);
        status.setOnItemSelectedListener(null);
        rating.setOnItemSelectedListener(null);
        isEditable = false;
        setFieldsEditable();
    }

    private void setFieldsEditable() {
        getActivity().supportInvalidateOptionsMenu();
        setSpinnerEditable(status, isEditable);
        setSpinnerEditable(rating, isEditable);
        setSpinnerEditable(progressSpinner, isEditable);
        notes.setEditable(isEditable);
    }

    private void setSpinnerEditable(Spinner spinner, boolean editable) {
        spinner.setEnabled(editable);
        spinner.setClickable(editable);
        spinner.setFocusable(editable);
    }

    private String getCompletedDateText(Date date, boolean withDay) {
        if (date == null) {
            return getActivity().getString(R.string.date_unknown);
        }
        DateFormat dateFormat;
        if (withDay) {
            dateFormat = SimpleDateFormat.getDateInstance();
        } else {
            dateFormat = new SimpleDateFormat("MMMM yyyy");
        }
        return dateFormat.format(date);
    }

    private void displayProgress(int progress) {
        progressBar.setProgress(progress);
        progressSpinner.setOnItemSelectedListener(null);
        progressSpinner.setSelection(progress / 5, false);
        progressSpinner.setOnItemSelectedListener(progressListener);
    }

    public void uploadPhotoToProject(final Uri photoUri) {
        Toast.makeText(getActivity(), getActivity().getString(R.string.upload_started), Toast.LENGTH_LONG).show();
        spiceManager.execute(new PhotoUploadRequest(getActivity().getApplication(), prefs, photoUri, projectId), new RequestListener<String>() {

            @Override
            public void onRequestFailure(SpiceException spiceException) {
                AQUtility.report(spiceException);
            }

            @Override
            public void onRequestSuccess(String s) {
                onPhotoUploadSuccess();
            }
        });
    }

    @UiThread
    public void onPhotoUploadSuccess() {
        Toast.makeText(getActivity(), getActivity().getString(R.string.upload_successful), Toast.LENGTH_LONG).show();
        spiceManager.removeDataFromCache(ProjectResult.class, new GetProjectRequest(getActivity().getApplication(), prefs, projectId, prefs.username().get()).getCacheKey());
    }

    @Click(R.id.gallery_edit_done)
    public void onGalleryEditDoneClicked() {
        savePhotoOrder();
        setGalleryEditable(false);
    }

    @Click(R.id.gallery_edit_cancel)
    public void onGalleryEditCancelClicked() {
        photos = new ArrayList<Photo>(photosOriginal);
        adapter.setItems(photos);
        setGalleryEditable(false);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        menu.findItem(R.id.menu_add_photo).setVisible(isEditable);
        menu.findItem(R.id.menu_take_photo).setVisible(isEditable);
        menu.findItem(R.id.menu_reorder_photos).setVisible(isEditable);
        menu.findItem(R.id.menu_add_as_favorite).setVisible(isLoaded && !isOwn);
    }

    @OptionsItem(R.id.menu_add_photo)
    public void onAddPhotoClicked() {
        listener.pickImage();
    }

    @OptionsItem(R.id.menu_take_photo)
    public void onTakePhotoClicked() {
        listener.takePhoto();
    }

    @OptionsItem(R.id.menu_refresh)
    public void menuRefresh() {
        onProjectSelected(projectId, username);
    }

    @OptionsItem(R.id.menu_add_as_favorite)
    public void menuAddAsFavorite() {
        new AddEditFavoriteDialog(getActivity(), new AddEditFavoriteDialog.AddEditFavoriteDialogListener() {
            @Override
            public void onSave(String comment, String tags) {
                JsonObject updateData = new JsonObject();
                updateData.addProperty("type", "project");
                updateData.addProperty("favorited_id", projectId);
                updateData.addProperty("comment", comment);
                updateData.addProperty("tag_names", tags);
                spiceManager.execute(new AddFavoriteRequest(prefs, getActivity().getApplication(), updateData), new RequestListener<BookmarkShort>() {
                    @Override
                    public void onRequestFailure(SpiceException spiceException) {
                        AQUtility.report(spiceException);
                    }

                    @Override
                    public void onRequestSuccess(BookmarkShort bookmarkShort) {
                        Toast.makeText(getActivity(), R.string.add_favorite_success, Toast.LENGTH_LONG).show();
                    }
                });
            }
        }, prefs).show();

    }

    @OptionsItem(R.id.menu_reorder_photos)
    public void menuReorderPhotos() {
        setGalleryEditable(true);
    }

    class ProjectListener extends RavelryResultListener<ProjectResult> {

        protected ProjectListener(IRavelryActivity activity) {
            super(activity);
        }

        @Override
        public void onRequestSuccess(ProjectResult result) {
            if (listener != null) {
                displayProject(result);
            }
        }
    }
}
Yarrn/src/main/java/de/vanmar/android/yarrn/util/RequestCode.java
public final class RequestCode {

    public static final int REQUEST_CODE_GET_TOKEN = 1;
    public static final int REQUEST_CODE_GALLERY = 2;
    public static final int REQUEST_CODE_CAMERA = 3;
}
Yarrn/src/main/java/de/vanmar/android/yarrn/stashes/StashSearchFragment.java
@EFragment(R.layout.fragment_search_stashes)
@OptionsMenu(R.menu.fragment_menu)
public class StashSearchFragment extends PagingListFragment<StashSearchResult, StashShort> {

    private Map<SearchCriteria.SearchType, SearchCriteria> searchCriteria = new HashMap<SearchCriteria.SearchType, SearchCriteria>();
    private FlowLayout searchCriteriaView;

    public interface StashSearchFragmentListener extends IRavelryActivity {
        void onStashSelected(int stashId, String username);
    }

    @SystemService
    InputMethodManager inputMethodManager;

    @ViewById(R.id.stashlist)
    ListView stashlist;

    @ViewById(R.id.query)
    EditText query;

    @Pref
    YarrnPrefs_ prefs;

    private StashesAdapter adapter;

    private StashSearchFragmentListener listener;

    @AfterViews
    @Override
    public void afterViews() {
        super.afterViews();
        adapter = new StashesAdapter(getActivity()) {

            @Override
            protected void onStashClicked(final StashShort stash) {
                listener.onStashSelected(stash.id, stash.user.username);
            }
        };
        if (searchCriteriaView == null) {
            searchCriteriaView = new FlowLayout(getActivity());
            stashlist.addHeaderView(searchCriteriaView);
        }
        stashlist.setAdapter(adapter);

        query.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (event != null && event.getAction() != KeyEvent.ACTION_DOWN) {
                    return false;
                }
                if (actionId == EditorInfo.IME_ACTION_SEARCH || event == null
                        || event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
                    startSearch();
                }
                return true;
            }
        });
    }

    private void startSearch() {
        adapter.clear();
        inputMethodManager.hideSoftInputFromWindow(
                query.getWindowToken(), 0);
        loadData(1);
    }

    @Override
    public void onResume() {
        super.onResume();
        getActivity().setTitle(R.string.search_stashes_title);
        updateSearchCriteriaDescription();
    }

    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);
        if (activity instanceof StashSearchFragmentListener) {
            listener = (StashSearchFragmentListener) activity;
        } else {
            throw new ClassCastException(activity.toString()
                    + " must implement StashSearchFragmentListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        listener = null;
    }

    @OptionsItem(R.id.menu_refresh)
    public void menuRefresh() {
        super.menuRefresh();
    }

    @Click(R.id.search_button)
    public void onSearchButtonClicked() {
        startSearch();
    }

    @Click(R.id.add_search_criteria)
    public void onAddSearchCriteriaClicked() {
        final SearchCriteriaDialog searchCriteriaDialog = new SearchCriteriaDialog(getActivity(), SearchCriteria.SearchContext.STASH, prefs);
        searchCriteriaDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                SearchCriteria.SearchType type = searchCriteriaDialog.getSearchCriteriaType();
                SearchCriteria criteria = searchCriteriaDialog.getSearchCriteria();
                if (criteria == null) {
                    searchCriteria.remove(type);
                } else {
                    searchCriteria.put(type, criteria);
                }
                updateSearchCriteriaDescription();
                startSearch();
            }
        });
        searchCriteriaDialog.show();
    }

    private void updateSearchCriteriaDescription() {
        searchCriteriaView.removeAllViews();
        if (searchCriteria.isEmpty()) {
            searchCriteriaView.setVisibility(View.GONE);
        } else {
            for (final SearchCriteria criteria : searchCriteria.values()) {
                final DeletableTag tag = new DeletableTag(getActivity(), criteria.getDescription());
                tag.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        searchCriteria.remove(criteria.getType());
                        searchCriteriaView.removeView(tag);
                        startSearch();
                    }
                });
                searchCriteriaView.addView(tag);
            }
            searchCriteriaView.setVisibility(View.VISIBLE);
        }
    }

    protected void displayResult(final StashSearchResult result) {
        super.displayResult(result);
    }

    @Override
    protected ListView getListView() {
        return stashlist;
    }

    @Override
    protected YarrnAdapter<StashShort> getAdapter() {
        return adapter;
    }

    @Override
    protected AbstractRavelryGetRequest<StashSearchResult> getRequest(int page) {
        Collection<SearchCriteria> searchCriteriaList = new LinkedList<SearchCriteria>(searchCriteria.values());
        if (query.getText().length() > 0) {
            String queryText = query.getText().toString();
            searchCriteriaList.add(SearchCriteria.byQuery(queryText));
        }
        return new SearchStashesRequest(this.getActivity().getApplication(), prefs, searchCriteriaList, page, PAGE_SIZE);
    }

    @Override
    protected IRavelryActivity getRavelryActivity() {
        return listener;
    }
}
Yarrn/src/main/java/de/vanmar/android/yarrn/patterns/PatternSearchFragment.java
@EFragment(R.layout.fragment_search_patterns)
@OptionsMenu(R.menu.fragment_menu)
public class PatternSearchFragment extends PagingListFragment<PatternsResult, PatternShort> {

    private Map<SearchCriteria.SearchType, SearchCriteria> searchCriteria = new HashMap<SearchCriteria.SearchType, SearchCriteria>();
    private FlowLayout searchCriteriaView;

    public interface PatternSearchFragmentListener extends IRavelryActivity {
        /**
         * Pattern with patternId was selected, 0 if no pattern selected
         */
        void onPatternSelected(int patternId);
    }

    @SystemService
    InputMethodManager inputMethodManager;

    @ViewById(R.id.patternlist)
    ListView patternlist;

    @ViewById(R.id.query)
    EditText query;

    @Pref
    YarrnPrefs_ prefs;

    private PatternsAdapter adapter;

    private PatternSearchFragmentListener listener;

    @AfterViews
    @Override
    public void afterViews() {
        super.afterViews();
        adapter = new PatternsAdapter(getActivity()) {

            @Override
            protected void onPatternClicked(int patternId) {
                listener.onPatternSelected(patternId);
            }

        };
        if (searchCriteriaView == null) {
            searchCriteriaView = new FlowLayout(getActivity());
            patternlist.addHeaderView(searchCriteriaView);
        }
        patternlist.setAdapter(adapter);

        query.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (event != null && event.getAction() != KeyEvent.ACTION_DOWN) {
                    return false;
                }
                if (actionId == EditorInfo.IME_ACTION_SEARCH || event == null
                        || event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
                    startSearch();
                }
                return true;
            }
        });
    }

    private void startSearch() {
        adapter.clear();
        inputMethodManager.hideSoftInputFromWindow(
                query.getWindowToken(), 0);
        loadData(1);
    }

    @Override
    public void onResume() {
        super.onResume();
        getActivity().setTitle(R.string.search_patterns_title);
        updateSearchCriteriaDescription();
    }

    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);
        if (activity instanceof PatternSearchFragmentListener) {
            listener = (PatternSearchFragmentListener) activity;
        } else {
            throw new ClassCastException(activity.toString()
                    + " must implement PatternSearchFragmentListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        listener = null;
    }

    @OptionsItem(R.id.menu_refresh)
    public void menuRefresh() {
        super.menuRefresh();
    }

    @Click(R.id.search_button)
    public void onSearchButtonClicked() {
        startSearch();
    }

    @Click(R.id.add_search_criteria)
    public void onAddSearchCriteriaClicked() {
        final SearchCriteriaDialog searchCriteriaDialog = new SearchCriteriaDialog(getActivity(), SearchCriteria.SearchContext.PATTERN, prefs);
        searchCriteriaDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                SearchCriteria.SearchType type = searchCriteriaDialog.getSearchCriteriaType();
                SearchCriteria criteria = searchCriteriaDialog.getSearchCriteria();
                if (criteria == null) {
                    searchCriteria.remove(type);
                } else {
                    searchCriteria.put(type, criteria);
                }
                updateSearchCriteriaDescription();
                startSearch();
            }
        });
        searchCriteriaDialog.show();
    }

    private void updateSearchCriteriaDescription() {
        searchCriteriaView.removeAllViews();
        if (searchCriteria.isEmpty()) {
            searchCriteriaView.setVisibility(View.GONE);
        } else {
            for (final SearchCriteria criteria : searchCriteria.values()) {
                final DeletableTag tag = new DeletableTag(getActivity(), criteria.getDescription());
                tag.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        searchCriteria.remove(criteria.getType());
                        searchCriteriaView.removeView(tag);
                        startSearch();
                    }
                });
                searchCriteriaView.addView(tag);
            }
            searchCriteriaView.setVisibility(View.VISIBLE);
        }
    }

    protected void displayResult(final PatternsResult result) {
        super.displayResult(result);
    }

    @Override
    protected ListView getListView() {
        return patternlist;
    }

    @Override
    protected YarrnAdapter<PatternShort> getAdapter() {
        return adapter;
    }

    @Override
    protected AbstractRavelryGetRequest<PatternsResult> getRequest(int page) {
        Collection<SearchCriteria> searchCriteriaList = new LinkedList<SearchCriteria>(searchCriteria.values());
        if (query.getText().length() > 0) {
            String queryText = query.getText().toString();
            searchCriteriaList.add(SearchCriteria.byQuery(queryText));
        }
        return new SearchPatternsRequest(this.getActivity().getApplication(), prefs, searchCriteriaList, page, PAGE_SIZE);
    }

    @Override
    protected IRavelryActivity getRavelryActivity() {
        return listener;
    }
}
Yarrn/src/main/java/de/vanmar/android/yarrn/projects/ProjectSearchFragment.java
@EFragment(R.layout.fragment_search_projects)
@OptionsMenu(R.menu.fragment_menu)
public class ProjectSearchFragment extends PagingListFragment<ProjectsResult, ProjectShort> {

    private Map<SearchCriteria.SearchType, SearchCriteria> searchCriteria = new HashMap<SearchCriteria.SearchType, SearchCriteria>();
    private FlowLayout searchCriteriaView;

    public interface ProjectSearchFragmentListener extends IRavelryActivity {
        /**
         * Project with projectId was selected, 0 if no project selected
         */
        void onProjectSelected(int projectId, String username);
    }

    @SystemService
    InputMethodManager inputMethodManager;

    @ViewById(R.id.projectlist)
    ListView projectlist;

    @ViewById(R.id.query)
    EditText query;

    @Pref
    YarrnPrefs_ prefs;

    private ProjectsAdapter adapter;

    private ProjectSearchFragmentListener listener;

    @AfterViews
    @Override
    public void afterViews() {
        super.afterViews();
        adapter = new ProjectsAdapter(getActivity()) {

            @Override
            protected void onProjectClicked(final ProjectShort project) {
                listener.onProjectSelected(project.id, prefs.username().get());
            }

        };
        if (searchCriteriaView == null) {
            searchCriteriaView = new FlowLayout(getActivity());
            projectlist.addHeaderView(searchCriteriaView);
        }
        projectlist.setAdapter(adapter);

        query.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (event != null && event.getAction() != KeyEvent.ACTION_DOWN) {
                    return false;
                }
                if (actionId == EditorInfo.IME_ACTION_SEARCH || event == null
                        || event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
                    startSearch();
                }
                return true;
            }
        });
    }

    private void startSearch() {
        adapter.clear();
        inputMethodManager.hideSoftInputFromWindow(
                query.getWindowToken(), 0);
        loadData(1);
    }

    @Override
    public void onResume() {
        super.onResume();
        getActivity().setTitle(R.string.search_projects_title);
        updateSearchCriteriaDescription();
    }

    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);
        if (activity instanceof ProjectSearchFragmentListener) {
            listener = (ProjectSearchFragmentListener) activity;
        } else {
            throw new ClassCastException(activity.toString()
                    + " must implement ProjectSearchFragmentListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        listener = null;
    }

    @OptionsItem(R.id.menu_refresh)
    public void menuRefresh() {
        super.menuRefresh();
    }

    @Click(R.id.search_button)
    public void onSearchButtonClicked() {
        startSearch();
    }

    @Click(R.id.add_search_criteria)
    public void onAddSearchCriteriaClicked() {
        final SearchCriteriaDialog searchCriteriaDialog = new SearchCriteriaDialog(getActivity(), SearchCriteria.SearchContext.PROJECT, prefs);
        searchCriteriaDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                SearchCriteria.SearchType type = searchCriteriaDialog.getSearchCriteriaType();
                SearchCriteria criteria = searchCriteriaDialog.getSearchCriteria();
                if (criteria == null) {
                    searchCriteria.remove(type);
                } else {
                    searchCriteria.put(type, criteria);
                }
                updateSearchCriteriaDescription();
                startSearch();
            }
        });
        searchCriteriaDialog.show();
    }

    private void updateSearchCriteriaDescription() {
        searchCriteriaView.removeAllViews();
        if (searchCriteria.isEmpty()) {
            searchCriteriaView.setVisibility(View.GONE);
        } else {
            for (final SearchCriteria criteria : searchCriteria.values()) {
                final DeletableTag tag = new DeletableTag(getActivity(), criteria.getDescription());
                tag.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        searchCriteria.remove(criteria.getType());
                        searchCriteriaView.removeView(tag);
                        startSearch();
                    }
                });
                searchCriteriaView.addView(tag);
            }
            searchCriteriaView.setVisibility(View.VISIBLE);
        }
    }

    protected void displayResult(final ProjectsResult result) {
        super.displayResult(result);
    }

    @Override
    protected ListView getListView() {
        return projectlist;
    }

    @Override
    protected YarrnAdapter<ProjectShort> getAdapter() {
        return adapter;
    }

    @Override
    protected AbstractRavelryGetRequest<ProjectsResult> getRequest(int page) {
        Collection<SearchCriteria> searchCriteriaList = new LinkedList<SearchCriteria>(searchCriteria.values());
        if (query.getText().length() > 0) {
            String queryText = query.getText().toString();
            searchCriteriaList.add(SearchCriteria.byQuery(queryText));
        }
        return new SearchProjectsRequest(this.getActivity().getApplication(), prefs, searchCriteriaList, page, PAGE_SIZE);
    }

    @Override
    protected IRavelryActivity getRavelryActivity() {
        return listener;
    }
}
Yarrn/src/main/java/de/vanmar/android/yarrn/queues/QueuesFragment.java
@EFragment(R.layout.fragment_queues)
@OptionsMenu(R.menu.fragment_menu)
public class QueuesFragment extends PagingListFragment<QueuesResult, QueuedProjectSmall> {

    public interface QueuesFragmentListener extends IRavelryActivity {
        /**
         * Project with projectId was selected, 0 if no project selected
         */
        void onQueuedProjectSelected(int projectId, String username);
    }

    @ViewById(R.id.queuelist)
    ListView queuelist;

    @Pref
    YarrnPrefs_ prefs;

    private QueuesAdapter adapter;

    private QueuesFragmentListener listener;

    @AfterViews
    @Override
    public void afterViews() {
        super.afterViews();
        adapter = new QueuesAdapter(getActivity()) {

            @Override
            protected void onProjectClicked(final QueuedProjectSmall project) {
                listener.onQueuedProjectSelected(project.id, prefs.username().get());
            }

        };
        queuelist.setAdapter(adapter);
    }

    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);
        if (activity instanceof QueuesFragmentListener) {
            listener = (QueuesFragmentListener) activity;
        } else {
            throw new ClassCastException(activity.toString()
                    + " must implement QueuesFragmentListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        listener = null;
    }

    @OptionsItem(R.id.menu_refresh)
    public void menuRefresh() {
        super.menuRefresh();
    }

    protected void displayResult(final QueuesResult result) {
        super.displayResult(result);
        if (getActivity() != null) {
            getActivity().setTitle(R.string.my_queues_title);
        }
    }

    @Override
    protected ListView getListView() {
        return queuelist;
    }

    @Override
    protected YarrnAdapter<QueuedProjectSmall> getAdapter() {
        return adapter;
    }

    @Override
    protected AbstractRavelryGetRequest<QueuesResult> getRequest(int page) {
        return new ListQueuesRequest(this.getActivity().getApplication(), prefs, page, PAGE_SIZE);
    }

    @Override
    protected IRavelryActivity getRavelryActivity() {
        return listener;
    }
}
Yarrn/src/main/java/de/vanmar/android/yarrn/stashes/StashFragment.java
@EFragment(R.layout.fragment_stash_detail)
@OptionsMenu(R.menu.fragment_menu)
public class StashFragment extends Fragment {

    public static final String ARG_STASH_ID = "stashId";
    public static final String ARG_USERNAME = "username";

    protected SpiceManager spiceManager;
    private ImageDialog dialog;

    public interface StashFragmentListener extends IRavelryActivity {
    }

    @ViewById(R.id.gallery)
    HorizontalListView gallery;

    @ViewById(R.id.name)
    TextView name;

    @ViewById(R.id.location)
    TextView location;

    @ViewById(R.id.details)
    TextView details;

    @ViewById(R.id.color_title)
    TextView colorTitle;

    @ViewById(R.id.color)
    TextView color;

    @ViewById(R.id.notes)
    WebView notes;

    @FragmentArg(ARG_STASH_ID)
    int stashId;
    @FragmentArg(ARG_USERNAME)
    String username;

    private StashFragmentListener listener;

    private PhotoAdapter adapter;

    @Pref
    YarrnPrefs_ prefs;

    @AfterViews
    public void afterViews() {
        if (spiceManager == null) {
            spiceManager = new SpiceManager(YarrnSpiceService.class);
        }

        adapter = new PhotoAdapter(getActivity());
        gallery.setAdapter(adapter);
        gallery.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                dialog = new ImageDialog(getActivity(), adapter.getItem(position).getMediumUrl());
                dialog.show();
            }
        });
    }

    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);
        if (activity instanceof StashFragmentListener) {
            listener = (StashFragmentListener) activity;
        } else {
            throw new ClassCastException(activity.toString()
                    + " must implement StashFragmentListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        listener = null;
    }

    public void onStashSelected(final int stashId) {
        clearStash();
        if (stashId != 0) {
            GetStashRequest request = new GetStashRequest(this.getActivity().getApplication(), prefs, stashId, username);
            spiceManager.execute(request, request.getCacheKey(), AbstractRavelryGetRequest.CACHE_DURATION, new StashListener(listener));
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        spiceManager.start(this.getActivity());

        onStashSelected(stashId);
    }

    @Override
    public void onResume() {
        super.onResume();
    }

    @Override
    public void onStop() {
        if (spiceManager.isStarted()) {
            spiceManager.shouldStop();
        }
        super.onStop();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (dialog != null && dialog.isShowing()) {
            dialog.dismiss();
            dialog = null;
        }
    }

    @UiThread
    protected void clearStash() {
        View view = getView();
        if (view != null) {
            view.setVisibility(View.GONE);
        }
    }

    protected void displayStash(final StashResult stashResult) {
        Stash stash = stashResult.stash;
        getActivity().setTitle(stash.name);
        name.setText(stash.name);
        location.setText(stash.location);
        hideIfEmpty(location, stash.location);
        String detailString = getDetailString(stash);
        details.setText(detailString);
        hideIfEmpty(details, detailString);
        color.setText(stash.color);
        hideIfEmpty(color, stash.color);
        hideIfEmpty(colorTitle, stash.color);
        notes.loadDataWithBaseURL("", stash.notes_html, "text/html", "UTF-8", "");
        adapter.setItems(stash.photos);
        getView().setVisibility(View.VISIBLE);
    }

    private String getDetailString(Stash stash) {
        StringBuilder details = new StringBuilder();
        if (stash.yarn != null && stash.yarn.yarnWeight != null) {
            if (stash.yarn.yarnWeight.name != null)
                details.append(stash.yarn.yarnWeight.name);
            if (stash.yarn.yarnWeight.ply != null)
                details.append(' ').append(stash.yarn.yarnWeight.ply).append(getString(R.string.ply));
        }
        if (stash.yarn != null && stash.yarn.yarnFibers != null) {
            for (YarnFiber fiber : stash.yarn.yarnFibers) {
                if (fiber.fiberType != null) {
                    details.append(' ');
                    if (fiber.percentage > 0)
                        details.append(fiber.percentage).append("% ");
                    details.append(fiber.fiberType.name);
                }
            }
        }
        return details.toString();
    }

    private void hideIfEmpty(View view, String value) {
        view.setVisibility(value != null && !"".equals(value) ? View.VISIBLE : View.GONE);
    }

    /*
    private void hideIfEmpty(View view, Collection values) {
        view.setVisibility(values != null && !values.isEmpty() ? View.VISIBLE : View.GONE);
    }*/

    @OptionsItem(R.id.menu_refresh)
    public void menuRefresh() {
        onStashSelected(stashId);
    }

    class StashListener extends RavelryResultListener<StashResult> {

        protected StashListener(IRavelryActivity activity) {
            super(activity);
        }

        @Override
        public void onRequestSuccess(StashResult result) {
            displayStash(result);
        }
    }
}
Yarrn/src/standard/java/de/vanmar/android/yarrn/util/AdHelper.java
public class AdHelper {

    public static void loadAds(Activity context) {
        AdView adView = (AdView) context.findViewById(R.id.adView);
        AdRequest adRequest = new AdRequest.Builder().build();
        adView.loadAd(adRequest);
    }
}
Yarrn/src/main/java/de/vanmar/android/yarrn/projects/ProjectsFragment.java
public interface ProjectsFragmentListener extends IRavelryActivity {
    /**
     * Project with projectId was selected, 0 if no project selected
     */
    void onProjectSelected(int projectId, String username);
}
Yarrn/src/main/java/de/vanmar/android/yarrn/projects/ProjectsFragment.java
@EFragment(R.layout.fragment_projects)
@OptionsMenu(R.menu.fragment_menu)
public class ProjectsFragment extends PagingListFragment<ProjectsResult, ProjectShort> {

    public interface ProjectsFragmentListener extends IRavelryActivity {
        /**
         * Project with projectId was selected, 0 if no project selected
         */
        void onProjectSelected(int projectId, String username);
    }

    @ViewById(R.id.projectlist)
    ListView projectlist;

    @ViewById(R.id.sort)
    Spinner sort;

    @ViewById(R.id.sort_reverse)
    CheckBox sortReverse;

    @Pref
    YarrnPrefs_ prefs;

    private ProjectsAdapter adapter;

    private ProjectsFragmentListener listener;

    @AfterViews
    @Override
    public void afterViews() {
        super.afterViews();
        adapter = new ProjectsAdapter(getActivity()) {

            @Override
            protected void onProjectClicked(final ProjectShort project) {
                listener.onProjectSelected(project.id, prefs.username().get());
            }

        };
        projectlist.setAdapter(adapter);

        sort.setOnItemSelectedListener(null);
        sort.setSelection(prefs.projectSort().get(), false);
        sort.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                applySort();
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {
            }
        });

        sortReverse.setOnCheckedChangeListener(null);
        sortReverse.setChecked(prefs.projectSortReverse().get());
        sortReverse.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                applySort();
            }
        });
    }

    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);
        if (activity instanceof ProjectsFragmentListener) {
            listener = (ProjectsFragmentListener) activity;
        } else {
            throw new ClassCastException(activity.toString()
                    + " must implement ProjectsFragmentListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        listener = null;
    }

    private void applySort() {
        prefs.projectSort().put(sort.getSelectedItemPosition());
        prefs.projectSortReverse().put(sortReverse.isChecked());
        loadData(1);
    }


    @OptionsItem(R.id.menu_refresh)
    public void menuRefresh() {
        super.menuRefresh();
    }

    protected void displayResult(final ProjectsResult result) {
        super.displayResult(result);
        if (getActivity() != null) {
            getActivity().setTitle(R.string.my_projects_title);
        }
    }

    @Override
    protected ListView getListView() {
        return projectlist;
    }

    @Override
    protected YarrnAdapter<ProjectShort> getAdapter() {
        return adapter;
    }

    @Override
    protected AbstractRavelryGetRequest<ProjectsResult> getRequest(int page) {
        return new ListProjectsRequest(this.getActivity().getApplication(), prefs, page, PAGE_SIZE);
    }

    @Override
    protected IRavelryActivity getRavelryActivity() {
        return listener;
    }
}
Yarrn/src/main/java/de/vanmar/android/yarrn/patterns/PatternFragment.java
@EFragment(R.layout.fragment_pattern_detail)
@OptionsMenu(R.menu.pattern_fragment_menu)
public class PatternFragment extends Fragment {

    public static final String ARG_PATTERN_ID = "patternId";

    protected SpiceManager spiceManager;
    private ImageDialog dialog;
    private String patternUrl;

    public interface PatternFragmentListener extends IRavelryActivity {
    }

    @ViewById(R.id.gallery)
    HorizontalListView gallery;

    @ViewById(R.id.name)
    TextView name;

    @ViewById(R.id.open)
    ImageButton openPattern;

    @ViewById(R.id.author)
    TextView author;

    @ViewById(R.id.gauge_description)
    TextView gauge_description;

    @ViewById(R.id.yarn_weight_description)
    TextView yarn_weight_description;

    @ViewById(R.id.yardage_description)
    TextView yardage_description;

    @ViewById(R.id.needles)
    TextView needles;

    @ViewById(R.id.notes)
    WebView notes;

    @FragmentArg(ARG_PATTERN_ID)
    int patternId;

    private PatternFragmentListener listener;

    private PhotoAdapter adapter;

    @Pref
    YarrnPrefs_ prefs;

    @AfterViews
    public void afterViews() {
        if (spiceManager == null) {
            spiceManager = new SpiceManager(YarrnSpiceService.class);
        }

        adapter = new PhotoAdapter(getActivity());
        gallery.setAdapter(adapter);
        gallery.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                dialog = new ImageDialog(getActivity(), adapter.getItem(position).getMediumUrl());
                dialog.show();
            }
        });
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (dialog != null && dialog.isShowing()) {
            dialog.dismiss();
            dialog = null;
        }
    }

    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);
        if (activity instanceof PatternFragmentListener) {
            listener = (PatternFragmentListener) activity;
        } else {
            throw new ClassCastException(activity.toString()
                    + " must implement PatternFragmentListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        listener = null;
    }

    public void onPatternSelected(final int patternId) {
        clearPattern();
        if (patternId != 0) {
            GetPatternRequest request = new GetPatternRequest(this.getActivity().getApplication(), prefs, patternId);
            spiceManager.execute(request, request.getCacheKey(), AbstractRavelryGetRequest.CACHE_DURATION, new PatternListener(listener));
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        spiceManager.start(this.getActivity());

        onPatternSelected(patternId);
    }

    @Override
    public void onResume() {
        super.onResume();
    }

    @Override
    public void onStop() {
        if (spiceManager.isStarted()) {
            spiceManager.shouldStop();
        }
        super.onStop();
    }

    @UiThread
    protected void clearPattern() {
        View view = getView();
        if (view != null) {
            view.setVisibility(View.GONE);
        }
    }

    protected void displayPattern(final PatternResult patternResult) {
        Pattern pattern = patternResult.pattern;
        getActivity().setTitle(pattern.name);
        name.setText(pattern.name);
        author.setText(pattern.patternAuthor == null ? null : pattern.patternAuthor.name);
        gauge_description.setText(getString(R.string.gauge_title) + pattern.gauge_description);
        yarn_weight_description.setText(getString(R.string.yarn_title) + pattern.yarn_weight_description);
        yardage_description.setText(getString(R.string.yardage_title) + pattern.yardage_description);
        needles.setText(getString(R.string.needles_title) + getNeedlesDescription(pattern.pattern_needle_sizes));
        notes.loadDataWithBaseURL("", pattern.notes_html, "text/html", "UTF-8", "");
        hideIfEmpty(gauge_description, pattern.gauge_description);
        hideIfEmpty(yarn_weight_description, pattern.yarn_weight_description);
        hideIfEmpty(yardage_description, pattern.yardage_description);
        hideIfEmpty(needles, pattern.pattern_needle_sizes);
        showLinkIfAvailable(pattern);
        adapter.setItems(pattern.photos);
        getView().setVisibility(View.VISIBLE);
    }

    private void showLinkIfAvailable(Pattern pattern) {
        this.patternUrl = pattern.url;
        hideIfEmpty(openPattern, pattern.url);
        SpannableString patternNameText = new SpannableString(pattern.name);
        if (StringUtils.isNotEmpty(patternUrl)) {
            patternNameText.setSpan(new UnderlineSpan(), 0, pattern.name.length(), 0);
        }
        name.setText(patternNameText);
    }

    private void hideIfEmpty(View view, String value) {
        view.setVisibility(StringUtils.isNotEmpty(value) ? View.VISIBLE : View.GONE);
    }

    private void hideIfEmpty(View view, Collection values) {
        view.setVisibility(values != null && !values.isEmpty() ? View.VISIBLE : View.GONE);
    }

    private String getNeedlesDescription(List<Needle> needles) {
        StringBuilder needleString = new StringBuilder();
        if (needles != null)
            for (Needle needle : needles) {
                needleString.append('\n').append(needle.name);
            }
        return needleString.toString();
    }

    @OptionsItem(R.id.menu_refresh)
    public void menuRefresh() {
        onPatternSelected(patternId);
    }

    @OptionsItem(R.id.menu_add_as_favorite)
    public void menuAddAsFavorite() {
        new AddEditFavoriteDialog(getActivity(), new AddEditFavoriteDialog.AddEditFavoriteDialogListener() {
            @Override
            public void onSave(String comment, String tags) {
                JsonObject updateData = new JsonObject();
                updateData.addProperty("type", "pattern");
                updateData.addProperty("favorited_id", patternId);
                updateData.addProperty("comment", comment);
                updateData.addProperty("tag_names", tags);
                spiceManager.execute(new AddFavoriteRequest(prefs, getActivity().getApplication(), updateData), new RequestListener<BookmarkShort>() {
                    @Override
                    public void onRequestFailure(SpiceException spiceException) {
                        AQUtility.report(spiceException);
                    }

                    @Override
                    public void onRequestSuccess(BookmarkShort bookmarkShort) {
                        Toast.makeText(getActivity(), R.string.add_favorite_success, Toast.LENGTH_LONG).show();
                    }
                });
            }
        }, prefs).show();

    }

    @Click(R.id.name)
    public void onOpenPatternClicked() {
        openPatternUrl();
    }

    @Click(R.id.open)
    public void onOpenPatternIconClicked() {
        openPatternUrl();
    }

    private void openPatternUrl() {
        if (patternUrl != null && !"".equals(patternUrl)) {
            Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(patternUrl));
            startActivity(i);
        }
    }

    class PatternListener extends RavelryResultListener<PatternResult> {

        protected PatternListener(IRavelryActivity activity) {
            super(activity);
        }

        @Override
        public void onRequestSuccess(PatternResult result) {
            if (listener != null) {
                displayPattern(result);
            }
        }
    }
}


Question: package de.vanmar.android.yarrn;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar;
import android.view.View;
import com.androidquery.util.AQUtility;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.NonConfigurationInstance;
import org.androidannotations.annotations.OnActivityResult;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.ViewById;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import de.vanmar.android.yarrn.favorites.FavoritesFragment;
import de.vanmar.android.yarrn.patterns.PatternFragment;
import de.vanmar.android.yarrn.patterns.PatternSearchFragment;
import de.vanmar.android.yarrn.projects.ProjectFragment;
import de.vanmar.android.yarrn.projects.ProjectSearchFragment;
import de.vanmar.android.yarrn.projects.ProjectsFragment;
import de.vanmar.android.yarrn.projects.ProjectsFragment.ProjectsFragmentListener;
import de.vanmar.android.yarrn.queues.QueuedProjectFragment;
import de.vanmar.android.yarrn.queues.QueuesFragment;
import de.vanmar.android.yarrn.stashes.StashFragment;
import de.vanmar.android.yarrn.stashes.StashSearchFragment;
import de.vanmar.android.yarrn.stashes.StashesFragment;
import de.vanmar.android.yarrn.util.AdHelper;
import de.vanmar.android.yarrn.util.RequestCode;






@EActivity(resName = "activity_main")
public class MainActivity extends AbstractRavelryActivity implements
Answer:         ProjectsFragmentListener,