Java tutorial
package com.jbirdvegas.mgerrit; /* * Copyright (C) 2013 Android Open Kang Project (AOKP) * Author: Jon Stanford (JBirdVegas), 2013 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import android.app.Activity; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.support.v4.widget.SwipeRefreshLayout; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import com.android.volley.RequestQueue; import com.android.volley.toolbox.Volley; import com.google.analytics.tracking.android.EasyTracker; import com.jbirdvegas.mgerrit.adapters.HeaderAdapterDecorator; import com.jbirdvegas.mgerrit.adapters.HeaderAdapterWrapper; import com.nhaarman.listviewanimations.appearance.SingleAnimationAdapter; import com.jbirdvegas.mgerrit.adapters.ChangeListAdapter; import com.jbirdvegas.mgerrit.adapters.EndlessAdapterWrapper; import com.jbirdvegas.mgerrit.cards.CommitCardBinder; import com.jbirdvegas.mgerrit.database.Changes; import com.jbirdvegas.mgerrit.database.MoreChanges; import com.jbirdvegas.mgerrit.database.SyncTime; import com.jbirdvegas.mgerrit.database.UserChanges; import com.jbirdvegas.mgerrit.helpers.Tools; import com.jbirdvegas.mgerrit.message.ChangeLoadingFinished; import com.jbirdvegas.mgerrit.message.ErrorDuringConnection; import com.jbirdvegas.mgerrit.message.Finished; import com.jbirdvegas.mgerrit.message.NewChangeSelected; import com.jbirdvegas.mgerrit.message.SearchQueryChanged; import com.jbirdvegas.mgerrit.message.StartingRequest; import com.jbirdvegas.mgerrit.objects.GerritURL; import com.jbirdvegas.mgerrit.search.AfterSearch; import com.jbirdvegas.mgerrit.search.BeforeSearch; import com.jbirdvegas.mgerrit.search.SearchKeyword; import com.jbirdvegas.mgerrit.tasks.GerritService; import com.jbirdvegas.mgerrit.tasks.GerritService.Direction; import com.jbirdvegas.mgerrit.views.GerritSearchView; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Set; import de.greenrobot.event.EventBus; import se.emilsjolander.stickylistheaders.ExpandableStickyListHeadersListView; import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; public abstract class CardsFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> { private static int sChangesLimit = 0; protected String TAG = "CardsFragment"; private GerritURL mUrl; private FragmentActivity mParent; // Indicates that this fragment will need to be refreshed private boolean mIsDirty = false; // Indicates whether a request to force an update is pending private boolean mNeedsForceUpdate = false; // Indicates whether the current fragment is refreshing private boolean mIsRefreshing = false; private static boolean sIsTabletMode = false; private ExpandableStickyListHeadersListView mListView; // Adapter that binds data to the listview private ChangeListAdapter mAdapter; // Wrapper for mAdapter, enabling animations private SingleAnimationAdapter mAnimAdapter; // Wrapper for above listview adapters to help provide infinite list functionality private EndlessAdapterWrapper mEndlessAdapter; // Whether animations have been enabled private Boolean mAnimationsEnabled = null; private GerritSearchView mSearchView; private SwipeRefreshLayout mSwipeLayout; private SwipeRefreshLayout.OnRefreshListener mRefreshListener = new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { refresh(true); } }; private EventBus mEventBus; private StickyListHeadersAdapter mHeaderAdapterWrapper; private HeaderAdapterWrapper mHeaderAdapter; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); init(savedInstanceState); setup(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.commit_list, container, false); // Its important to attach this reference to every view created // if not then when a CardsFragment gets created for each subsequent // subclass the referenced layout is actually pointing to the original // fragment. Hence all other fragments will throw null when we attempt // to perform actions (like swiping) on the current fragment. setSwipeRefreshLayout(view); return view; } private void setSwipeRefreshLayout(View view) { mSwipeLayout = (SwipeRefreshLayout) view.findViewById(R.id.swipe_container); mSwipeLayout.setColorScheme(R.color.text_orange, R.color.text_green, R.color.text_red, android.R.color.transparent); mSwipeLayout.setOnRefreshListener(mRefreshListener); } private void init(Bundle savedInstanceState) { mParent = this.getActivity(); View mCurrentFragment = this.getView(); RequestQueue mRequestQueue = Volley.newRequestQueue(mParent); // Setup the list int[] to = new int[] { R.id.commit_card_title, R.id.commit_card_commit_owner, R.id.commit_card_project_name, R.id.commit_card_last_updated, R.id.commit_card_commit_status }; String[] from = new String[] { UserChanges.C_SUBJECT, UserChanges.C_NAME, UserChanges.C_PROJECT, UserChanges.C_UPDATED, UserChanges.C_STATUS }; mListView = (ExpandableStickyListHeadersListView) mCurrentFragment.findViewById(R.id.commit_cards); registerForContextMenu(mListView); mAdapter = new ChangeListAdapter(mParent, R.layout.commit_card, null, from, to, 0, getQuery()); mAdapter.setViewBinder(new CommitCardBinder(mParent, mRequestQueue)); mHeaderAdapter = new HeaderAdapterWrapper(mParent, mAdapter); mEndlessAdapter = new EndlessAdapterWrapper(mParent, mHeaderAdapter) { @Override public void loadData() { Set<SearchKeyword> keywords = mSearchView.getLastQuery(); String updated = Changes.getOldestUpdatedTime(mParent, getQuery()); if (updated != null) { keywords = SearchKeyword.retainOldest(keywords, new BeforeSearch(updated)); } sendRequest(Direction.Older, keywords); } }; mEndlessAdapter.setEnabled(MoreChanges.areOlderChanges(mParent, getQuery())); mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { mAdapter.itemClickListener(view); } }); mHeaderAdapterWrapper = new HeaderAdapterDecorator(mEndlessAdapter, mHeaderAdapter); mListView.setAdapter(mHeaderAdapterWrapper); mListView.setDrawingListUnderStickyHeader(false); sChangesLimit = mParent.getResources().getInteger(R.integer.changes_limit); mUrl = new GerritURL(); // Need the account id of the owner here to maintain FK db constraint mUrl.setRequestDetailedAccounts(true); mUrl.setStatus(getQuery()); mSearchView = (GerritSearchView) mParent.findViewById(R.id.search); mEventBus = EventBus.getDefault(); sIsTabletMode = Prefs.isTabletMode(mParent); } private void setup() { loadNewerChanges(); // We cannot use the search query here as the SearchView may not have been initialised yet. getLoaderManager().initLoader(0, null, this); } @Override public void onStart() { super.onStart(); if (mEndlessAdapter != null) { toggleAnimations(mEndlessAdapter); mListView.setOnScrollListener(mEndlessAdapter); } else toggleAnimations(mHeaderAdapter); EasyTracker.getInstance(getActivity()).activityStart(getActivity()); mEventBus.registerSticky(this); NewChangeSelected ev = mEventBus.getStickyEvent(NewChangeSelected.class); if (ev != null && ev.compareStatuses(getQuery())) { mAdapter.setSelectedChangeId(ev.getChangeId()); } } @Override public void onStop() { super.onStop(); EasyTracker.getInstance(getActivity()).activityStop(getActivity()); mEventBus.unregister(this); } /** * Each tab provides its own query for ?p=status:[open:merged:abandoned] * * @return current tab name used for query { open, merged, abandoned } */ abstract String getQuery(); private void loadNewerChanges() { Set<SearchKeyword> keywords = null; String updated = Changes.getNewestUpdatedTime(mParent, getQuery()); if (updated != null && !updated.isEmpty()) { keywords = mSearchView.getLastQuery(); SearchKeyword.replaceKeyword(keywords, new AfterSearch(updated)); } sendRequest(Direction.Newer, keywords); } /** * Start the updater to check for an update if necessary */ private void sendRequest(Direction direction, Set<SearchKeyword> keywords) { if (mNeedsForceUpdate) { mNeedsForceUpdate = false; SyncTime.clear(mParent); } GerritURL url = new GerritURL(mUrl); if (sChangesLimit > 0) url.setLimit(sChangesLimit); if (keywords == null || keywords.isEmpty()) { keywords = mSearchView.getLastQuery(); } url.addSearchKeywords(keywords); Intent it = new Intent(mParent, GerritService.class); it.putExtra(GerritService.DATA_TYPE_KEY, GerritService.DataType.Commit); it.putExtra(GerritService.URL_KEY, url); it.putExtra(GerritService.CHANGES_LIST_DIRECTION, direction); mParent.startService(it); } /** * Refresh this fragment if it was marked as dirty by restarting the loader * @param forceUpdate If set, forces an update of the data from the server. * This can be independent of refreshing, so this method * can be used to force an update. */ protected void refresh(boolean forceUpdate) { /* If the fragment has been attached to an activity, refresh it. * Otherwise it will be refreshed when it is attached by checking * whether it is marked as dirty. */ if (this.isAdded() && mIsDirty) { mIsDirty = false; getLoaderManager().restartLoader(0, null, this); } mNeedsForceUpdate = forceUpdate; if (this.isAdded() && forceUpdate) loadNewerChanges(); } @Override public void onAttach(Activity activity) { super.onAttach(activity); if (mSwipeLayout != null) { mSwipeLayout.setRefreshing(mIsRefreshing); } /* Refresh if necessary. As the fragment has just been attached, * we can assume it is added here */ if (!mIsDirty) return; mIsDirty = false; getLoaderManager().restartLoader(0, null, this); if (mNeedsForceUpdate) loadNewerChanges(); } private void setMenuItemTitle(MenuItem menuItem, String formatString, String parameters) { String title = String.format(formatString, parameters); menuItem.setTitle(title); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); MenuInflater inflater = mParent.getMenuInflater(); inflater.inflate(R.menu.change_list_menu, menu); AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; View targetView = info.targetView; // Set the title of the user tracking menu item MenuItem userMenuItem = menu.findItem(R.id.menu_change_track_user); setMenuItemTitle(userMenuItem, getResources().getString(R.string.context_menu_track_user), (String) targetView.getTag(R.id.userName)); } @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); View targetView = info.targetView; String webAddress = (String) targetView.getTag(R.id.webAddress); switch (item.getItemId()) { case R.id.menu_change_details: mListView.getWrappedList().performItemClick(targetView, info.position, info.id); return true; case R.id.menu_change_browser: Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(webAddress)); mParent.startActivity(browserIntent); return true; case R.id.menu_change_track_user: int user = (int) targetView.getTag(R.id.user); Prefs.setTrackingUser(mParent, user); return true; case R.id.menu_change_track_project: String project = (String) targetView.getTag(R.id.project); Prefs.setCurrentProject(mParent, project); return true; case R.id.menu_change_share: String changeid = (String) targetView.getTag(R.id.changeID); Intent intent = Tools.createShareIntent(mParent, changeid, webAddress); mParent.startActivity(intent); return true; default: return super.onContextItemSelected(item); } } /** * Enables or disables listview animations. This simply toggles the * adapter, initialising a new adapter if necessary. * @param baseAdapter The current adapter for the listview */ public void toggleAnimations(BaseAdapter baseAdapter) { boolean anim = Prefs.getAnimationPreference(mParent); if (mAnimationsEnabled != null && anim == mAnimationsEnabled) return; else mAnimationsEnabled = anim; /* If animations have been enabled, setup and use an animation adapter, otherwise use * the regular adapter. The data should always be bound to mAdapter */ BaseAdapter adapter = Tools.toggleAnimations(mAnimationsEnabled, mListView.getWrappedList(), mAnimAdapter, baseAdapter); if (mAnimationsEnabled) { mAnimAdapter = (SingleAnimationAdapter) adapter; if (baseAdapter == mEndlessAdapter) { mEndlessAdapter.setParentAdatper(mAnimAdapter); } } } public void markDirty() { mIsDirty = true; } public void markChangeAsSelected(String changeid) { if (mAdapter != null) mAdapter.setSelectedChangeId(changeid); } @Override public Loader<Cursor> onCreateLoader(int id, @Nullable Bundle args) { if (args == null) { SearchQueryChanged ev = mEventBus.getStickyEvent(SearchQueryChanged.class); if (ev != null) { String to = ev.getClazzName(); if (mParent.getClass().getSimpleName().equals(to)) args = ev.getBundle(); } } if (args != null) { String databaseQuery = args.getString(SearchQueryChanged.KEY_WHERE); if (databaseQuery != null && !databaseQuery.isEmpty()) { if (args.getStringArrayList(SearchQueryChanged.KEY_BINDARGS) != null) { /* Create a copy as the findCommits function can modify the contents of bindArgs * and we want each receiver to use the bindArgs from the original broadcast */ ArrayList<String> bindArgs = new ArrayList<>(); bindArgs.addAll(args.getStringArrayList(SearchQueryChanged.KEY_BINDARGS)); return UserChanges.findCommits(mParent, getQuery(), databaseQuery, bindArgs); } } } return UserChanges.findCommits(mParent, getQuery(), null, null); } @Override public void onLoaderReset(Loader<Cursor> loader) { // Naive implementation mAdapter.swapCursor(null); } @Override public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) { mAdapter.swapCursor(cursor); if (sIsTabletMode) { // Broadcast that we have finished loading changes mEventBus.post(new ChangeLoadingFinished(getQuery())); } } public void onStartRefresh() { if (isAdded() && mSwipeLayout != null) { mSwipeLayout.setRefreshing(true); } else { mIsRefreshing = true; } } public void onStopRefresh() { if (isAdded() && mSwipeLayout != null) { mSwipeLayout.setRefreshing(false); } else { mIsRefreshing = false; } } // Listen for processed search query changes public void onEventMainThread(SearchQueryChanged ev) { String to = ev.getClazzName(); if (isAdded() && mParent.getClass().getSimpleName().equals(to)) { getLoaderManager().restartLoader(0, null, this); } } /* Tell the endless adapter we have finished loading when there was no data */ public void onEventMainThread(Finished ev) { if (!getQuery().equals(ev.getStatus())) { return; } onStopRefresh(); Intent processed = ev.getIntent(); Direction direction = (Direction) processed.getSerializableExtra(GerritService.CHANGES_LIST_DIRECTION); if (mEndlessAdapter == null || direction == Direction.Newer) return; if (ev.getItems() < sChangesLimit) { // Remove the endless adapter as we have no more changes to load mEndlessAdapter.finishedDataLoading(); } } public void onEventMainThread(StartingRequest ev) { if (getQuery().equals(ev.getStatus())) { onStartRefresh(); } } public void onEventMainThread(ErrorDuringConnection ev) { if (getQuery().equals(ev.getStatus())) { onStopRefresh(); } } }