Java tutorial
/* * Copyright 2014 Phil Bayfield * * 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. */ package me.philio.ghost.ui; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AccountManagerCallback; import android.accounts.AccountManagerFuture; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.Rect; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarActivity; import android.support.v7.app.ActionBarDrawerToggle; import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import com.activeandroid.content.ContentProvider; import com.squareup.picasso.Picasso; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import butterknife.ButterKnife; import butterknife.InjectView; import butterknife.OnClick; import me.philio.ghost.R; import me.philio.ghost.account.AccountConstants; import me.philio.ghost.model.Blog; import me.philio.ghost.model.Setting; import me.philio.ghost.model.User; import me.philio.ghost.sync.SyncHelper; import me.philio.ghost.ui.widget.BezelImageView; import me.philio.ghost.ui.widget.ScrimInsetsScrollView; import me.philio.ghost.util.DatabaseUtils; import me.philio.ghost.util.ImageUtils; import static me.philio.ghost.account.AccountConstants.KEY_BLOG_URL; import static me.philio.ghost.account.AccountConstants.KEY_EMAIL; /** * Fragment used for managing interactions for and presentation of a navigation drawer. * See the <a href="https://developer.android.com/design/patterns/navigation-drawer.html#Interaction"> * design guidelines</a> for a complete explanation of the behaviors implemented here. */ public class NavigationDrawerFragment extends Fragment implements View.OnClickListener, LoaderManager.LoaderCallbacks<Cursor>, AccountManagerCallback<Bundle> { /** * Logging tag */ private static final String TAG = NavigationDrawerFragment.class.getName(); /** * Remember the selected account */ private static final String STATE_SELECTED_ACCOUNT = "selected_navigation_drawer_account"; /** * Remember the position of the selected item. */ private static final String STATE_SELECTED_POSITION = "selected_navigation_drawer_position"; /** * Per the design guidelines, you should show the drawer on launch until the user manually * expands it. This shared preference tracks this. */ private static final String PREF_USER_LEARNED_DRAWER = "navigation_drawer_learned"; /** * Last account that was used is remembered in a preference */ private static final String PREF_LAST_ACCOUNT = "navigation_drawer_account_name"; /** * Loader ids */ private static final int LOADER_BLOG_NAME = 0; private static final int LOADER_USER = 1; /** * Loader bundle keys */ private static final String LOADER_KEY_BLOG_ID = "blog_id"; private static final String LOADER_KEY_EMAIL = "email"; /** * A pointer to the current callbacks instance (the Activity). */ private NavigationDrawerCallbacks mCallbacks; /** * Helper component that ties the action bar to the navigation drawer. */ private ActionBarDrawerToggle mDrawerToggle; /** * The main drawer layout */ private DrawerLayout mDrawerLayout; /** * The nav drawer container view */ private View mContainerView; /** * State/prefs of the drawer */ private int mCurrentSelectedItem = 0; private boolean mFromSavedInstanceState; private boolean mUserLearnedDrawer; /** * Account manager */ private AccountManager mAccountManager; /** * List of accounts */ private List<Account> mAccounts; /** * Active account */ private Account mSelectedAccount; /** * Item ids */ private static final int ITEM_DIVIDER = -1; public static final int ITEM_POSTS = 0; public static final int ITEM_PAGES = 1; public static final int ITEM_SETTINGS = 2; public static final int ITEM_ABOUT = 3; /** * Item icons */ private static final int[] ICONS = new int[] { R.drawable.ic_action_action_description, R.drawable.ic_action_action_description, R.drawable.ic_action_action_settings, R.drawable.ic_action_action_help }; /** * Item titles */ private static final int[] TITLES = new int[] { R.string.navigation_drawer_posts, R.string.navigation_drawer_pages, R.string.navigation_drawer_settings, R.string.navigation_drawer_about }; /** * Items in the drawer */ private List<Integer> mItems = new ArrayList<>(); /** * Views in the drawer list */ private View[] mItemViews; /** * Set to true if the account view is visible */ private boolean mAccountListVisible; /** * Injected views */ @InjectView(R.id.header) View mHeader; @InjectView(R.id.header_content) View mHeaderContent; @InjectView(R.id.img_cover) ImageView mCover; @InjectView(R.id.img_avatar) BezelImageView mAvatar; @InjectView(R.id.txt_name) TextView mName; @InjectView(R.id.txt_email) TextView mEmail; @InjectView(R.id.txt_blog_title) TextView mBlogName; @InjectView(R.id.txt_blog_url) TextView mBlogUrl; @InjectView(R.id.img_account_indicator) ImageView mAccountIndictor; @InjectView(R.id.item_list) ViewGroup mItemList; @InjectView(R.id.account_list) ViewGroup mAccountList; @Override public void onAttach(Activity activity) { super.onAttach(activity); try { mCallbacks = (NavigationDrawerCallbacks) activity; } catch (ClassCastException e) { throw new ClassCastException("Activity must implement NavigationDrawerCallbacks."); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Get the account manager and get accounts mAccountManager = AccountManager.get(getActivity()); // Read in the flag indicating whether or not the user has demonstrated awareness of the // drawer. See PREF_USER_LEARNED_DRAWER for details. SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity()); mUserLearnedDrawer = sp.getBoolean(PREF_USER_LEARNED_DRAWER, false); if (savedInstanceState != null) { mSelectedAccount = savedInstanceState.getParcelable(STATE_SELECTED_ACCOUNT); mCurrentSelectedItem = savedInstanceState.getInt(STATE_SELECTED_POSITION); mFromSavedInstanceState = true; } // Get accounts and set active account getAccounts(sp.getString(PREF_LAST_ACCOUNT, null)); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = getActivity().getLayoutInflater().inflate(R.layout.fragment_navigation_drawer, container, false); ButterKnife.inject(this, view); return view; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // Indicate that this fragment would like to influence the set of actions in the action bar. setHasOptionsMenu(true); // Populate the account details in the header section populateAccountDetails(); // Select either the default item (0) or the last selected item. selectItem(mCurrentSelectedItem); } @Override public void onResume() { super.onResume(); // Check if any account changes took place while paused or stopped checkAccounts(); } @Override public void onDetach() { super.onDetach(); mCallbacks = null; } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(STATE_SELECTED_ACCOUNT, mSelectedAccount); outState.putInt(STATE_SELECTED_POSITION, mCurrentSelectedItem); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); // Forward the new configuration the drawer toggle component. mDrawerToggle.onConfigurationChanged(newConfig); } @Override public boolean onOptionsItemSelected(MenuItem item) { return mDrawerToggle.onOptionsItemSelected(item) || super.onOptionsItemSelected(item); } @OnClick(R.id.header_content) @Override public void onClick(View v) { switch (v.getId()) { case R.id.header_content: toggleAccountView(); break; } } @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { // Get args String blogId = args.getString(LOADER_KEY_BLOG_ID); String email = args.getString(LOADER_KEY_EMAIL); // Return loader for the specified id switch (id) { case LOADER_BLOG_NAME: // Get title setting return new CursorLoader(getActivity(), ContentProvider.createUri(Setting.class, null), null, "blog_id = ? AND key = ?", new String[] { blogId, Setting.Key.TITLE.toString() }, null); case LOADER_USER: // Get user details return new CursorLoader(getActivity(), ContentProvider.createUri(User.class, null), null, "blog_id = ? AND email = ?", new String[] { blogId, email }, null); } return null; } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { switch (loader.getId()) { case LOADER_BLOG_NAME: // Validity check if (cursor == null || cursor.getCount() == 0) { Log.e(TAG, "Failed to load setting"); // Make sure a sync is scheduled to fix the problem SyncHelper.requestSync(mSelectedAccount, getString(R.string.content_authority)); return; } // Create a model from the cursor as it's easier than lots of string manipulation cursor.moveToFirst(); Setting setting = new Setting(); setting.loadFromCursor(cursor); // Set the title/url mBlogName.setText(setting.value); mBlogUrl.setText(setting.blog.url); break; case LOADER_USER: // Validity check if (cursor == null || cursor.getCount() == 0) { Log.e(TAG, "Failed to load user"); // Make sure a sync is scheduled to fix the problem SyncHelper.requestSync(mSelectedAccount, getString(R.string.content_authority)); return; } // Create a model from the cursor as it's easier than lots of string manipulation cursor.moveToFirst(); User user = new User(); user.loadFromCursor(cursor); // Show cover image if (user.cover != null) { try { String path = ImageUtils.getUrl(user.blog, user.cover); String filename = ImageUtils.getFilename(getActivity(), user.blog, path); File cover = new File(filename); if (cover.exists()) { Picasso.with(getActivity()).load(cover).fit().centerCrop().into(mCover); } } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { Log.e(TAG, "Error loading image"); } } else { mCover.setImageDrawable(null); } // Show avatar if (user.image != null) { try { String path = ImageUtils.getUrl(user.blog, user.image); String filename = ImageUtils.getFilename(getActivity(), user.blog, path); File avatar = new File(filename); if (avatar.exists()) { Picasso.with(getActivity()).load(avatar).fit().into(mAvatar); } } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { Log.e(TAG, "Error loading image"); } } else { mAvatar.setImageResource(R.drawable.ic_action_social_person); } // Set text values mName.setText(user.name); mEmail.setText(user.email); break; } } @Override public void onLoaderReset(Loader<Cursor> loader) { } @Override public void run(AccountManagerFuture<Bundle> future) { try { Bundle result = future.getResult(); } catch (OperationCanceledException | IOException | AuthenticatorException e) { Log.e(TAG, "Add account operation failed: " + e.getMessage()); } } /** * Users of this fragment must call this method to set up the navigation drawer interactions. * * @param viewId The android:id of the top level drawer layout * @param drawerLayout The DrawerLayout containing this fragment's UI. */ public void setUp(int viewId, DrawerLayout drawerLayout) { mContainerView = getActivity().findViewById(viewId); mDrawerLayout = drawerLayout; // Resize the header if necessary if (mContainerView instanceof ScrimInsetsScrollView) { ScrimInsetsScrollView layout = (ScrimInsetsScrollView) mContainerView; final int headerOriginalHeight = getResources() .getDimensionPixelSize(R.dimen.navigation_drawer_header_height); layout.setOnInsetsCallback(new ScrimInsetsScrollView.OnInsetsCallback() { @Override public void onInsetsChanged(Rect insets) { // Update the height of the header ViewGroup.LayoutParams headerParams = mHeader.getLayoutParams(); headerParams.height = headerOriginalHeight + insets.top; mHeader.setLayoutParams(headerParams); // Update the top margin of the content ViewGroup.MarginLayoutParams contentParams = (ViewGroup.MarginLayoutParams) mHeaderContent .getLayoutParams(); contentParams.topMargin = insets.top; mHeaderContent.setLayoutParams(contentParams); } }); } // Set status bar colour for Lollipop Resources.Theme theme = getActivity().getTheme(); TypedArray typedArray = theme.obtainStyledAttributes(new int[] { R.attr.colorPrimaryDark }); mDrawerLayout.setStatusBarBackgroundColor(getResources().getColor(typedArray.getResourceId(0, 0))); // Set a custom shadow that overlays the main content when the drawer opens mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); // Setup actionbar button and toggle ActionBar actionBar = getActionBar(); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setHomeButtonEnabled(true); mDrawerToggle = new ActionBarDrawerToggle(getActivity(), /* host Activity */ mDrawerLayout, /* DrawerLayout object */ R.string.navigation_drawer_open, /* "open drawer" description for accessibility */ R.string.navigation_drawer_close /* "close drawer" description for accessibility */ ) { @Override public void onDrawerClosed(View drawerView) { super.onDrawerClosed(drawerView); if (!isAdded()) { return; } if (mAccountListVisible) { toggleAccountView(); } getActivity().supportInvalidateOptionsMenu(); // calls onPrepareOptionsMenu() } @Override public void onDrawerOpened(View drawerView) { super.onDrawerOpened(drawerView); if (!isAdded()) { return; } if (!mUserLearnedDrawer) { // The user manually opened the drawer; store this flag to prevent auto-showing // the navigation drawer automatically in the future. mUserLearnedDrawer = true; SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity()); sp.edit().putBoolean(PREF_USER_LEARNED_DRAWER, true).apply(); } getActivity().supportInvalidateOptionsMenu(); // calls onPrepareOptionsMenu() } }; // If the user hasn't 'learned' about the drawer, open it to introduce them to the drawer, // per the navigation drawer design guidelines. if (!mUserLearnedDrawer && !mFromSavedInstanceState) { mDrawerLayout.openDrawer(mContainerView); } // Defer code dependent on restoration of previous instance state. mDrawerLayout.post(new Runnable() { @Override public void run() { mDrawerToggle.syncState(); } }); mDrawerLayout.setDrawerListener(mDrawerToggle); // Populate the drawer populateDrawerItems(); populateAccountItems(); } public boolean isDrawerOpen() { return mDrawerLayout != null && mDrawerLayout.isDrawerOpen(mContainerView); } public Account getSelectedAccount() { return mSelectedAccount; } private void selectItem(int item) { if (mCallbacks != null && mCallbacks.onNavigationDrawerItemSelected(item)) { mCurrentSelectedItem = item; if (mItemViews != null) { for (int i = 0; i < mItemViews.length; i++) { if (mItems.get(i) != ITEM_DIVIDER) { mItemViews[i].setSelected(mItems.get(i) == item); colorView(mItemViews[i], mItems.get(i) == item); } } } } if (mDrawerLayout != null) { mDrawerLayout.closeDrawer(mContainerView); } } private ActionBar getActionBar() { return ((ActionBarActivity) getActivity()).getSupportActionBar(); } /** * Load the accounts list and make sure that an account is selected/active */ private void getAccounts(String selectedName) { Account[] accounts = mAccountManager.getAccountsByType(getString(R.string.account_type)); mAccounts = new ArrayList<>(Arrays.asList(accounts)); if (mSelectedAccount == null && selectedName != null) { for (Account account : mAccounts) { if (account.name.equals(selectedName)) { mSelectedAccount = account; break; } } } if (mSelectedAccount == null) { mSelectedAccount = mAccounts.get(0); } } /** * Check to see if accounts have changed and fix any stale data/UI */ private void checkAccounts() { Account[] accounts = mAccountManager.getAccountsByType(getString(R.string.account_type)); List<Account> currentAcccounts = new ArrayList<>(Arrays.asList(accounts)); // If no accounts found bump to the splash screen if (currentAcccounts.size() == 0) { Intent intent = new Intent(getActivity(), SplashActivity.class); startActivity(intent); getActivity().finish(); return; } // Look for account changes Set<Account> existingSet = new HashSet<>(); existingSet.addAll(mAccounts); Set<Account> currentSet = new HashSet<>(); currentSet.addAll(currentAcccounts); if (!currentSet.equals(existingSet)) { mAccounts = currentAcccounts; if (!mAccounts.contains(mSelectedAccount)) { changeAccount(mAccounts.get(0)); } else { populateAccountItems(); } } } /** * Change the active account * * @param account */ private void changeAccount(Account account) { // Change selected account mSelectedAccount = account; // Update preference SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity()); sp.edit().putString(PREF_LAST_ACCOUNT, account.name).apply(); // Update UI populateAccountItems(); populateAccountDetails(); selectItem(ITEM_POSTS); } /** * Populate account details for the current account */ private void populateAccountDetails() { // Get user details String blogUrl = mAccountManager.getUserData(mSelectedAccount, KEY_BLOG_URL); String email = mAccountManager.getUserData(mSelectedAccount, KEY_EMAIL); Blog blog = DatabaseUtils.getBlog(blogUrl, email); // If blog record is missing, create it and request a sync so that we can start the loaders // and they will load the missing data as it becomes available if (blog == null) { blog = new Blog(); blog.url = blogUrl; blog.email = email; blog.save(); // Show dialog AccountWarningDialog dialog = new AccountWarningDialog(); dialog.show(getFragmentManager(), "warning"); // Make sure a sync is scheduled to fix the problem SyncHelper.requestSync(mSelectedAccount, getString(R.string.content_authority)); } // Create bundle of loader args Bundle args = new Bundle(); args.putString(LOADER_KEY_BLOG_ID, blog.getId().toString()); args.putString(LOADER_KEY_EMAIL, email); // Load the blog and account data using the content provider so that any changes to the // data will be notified getLoaderManager().restartLoader(LOADER_BLOG_NAME, args, this); getLoaderManager().restartLoader(LOADER_USER, args, this); } /** * Populate the drawer items */ private void populateDrawerItems() { // Generate a list of items to show mItems.clear(); mItems = new ArrayList<>(); mItems.add(ITEM_POSTS); mItems.add(ITEM_PAGES); mItems.add(ITEM_DIVIDER); mItems.add(ITEM_SETTINGS); mItems.add(ITEM_ABOUT); // Generate views mItemList.removeAllViews(); mItemViews = new View[mItems.size()]; LayoutInflater layoutInflater = getActivity().getLayoutInflater(); int position = 0; for (final Integer item : mItems) { View view; if (item == ITEM_DIVIDER) { view = layoutInflater.inflate(R.layout.item_navigation_drawer_divider, mItemList, false); } else { view = layoutInflater.inflate(R.layout.item_navigation_drawer, mItemList, false); ImageView iconView = (ImageView) view.findViewById(R.id.img_icon); TextView titleView = (TextView) view.findViewById(R.id.txt_title); iconView.setImageResource(ICONS[item]); titleView.setText(TITLES[item]); view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { selectItem(item); } }); view.setSelected(mCurrentSelectedItem == item); colorView(view, mCurrentSelectedItem == item); } mItemViews[position] = view; mItemList.addView(view); position++; } } /** * Populate the account items */ private void populateAccountItems() { // Add accounts mAccountList.removeAllViews(); LayoutInflater layoutInflater = getActivity().getLayoutInflater(); for (final Account account : mAccounts) { if (account.equals(mSelectedAccount)) { continue; } View view = layoutInflater.inflate(R.layout.item_navigation_drawer_account, mAccountList, false); TextView titleView = (TextView) view.findViewById(R.id.txt_title); titleView.setText(mAccountManager.getUserData(account, AccountConstants.KEY_EMAIL)); TextView subtitleView = (TextView) view.findViewById(R.id.txt_subtitle); subtitleView.setText(mAccountManager.getUserData(account, AccountConstants.KEY_BLOG_URL)); view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Change selected account changeAccount(account); } }); colorView(view, false); mAccountList.addView(view); } // Add add account view View view = layoutInflater.inflate(R.layout.item_navigation_drawer_account_action, mAccountList, false); ImageView iconView = (ImageView) view.findViewById(R.id.img_icon); TextView titleView = (TextView) view.findViewById(R.id.txt_title); iconView.setImageResource(R.drawable.ic_action_content_add); titleView.setText(R.string.navigation_drawer_add_account); view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mAccountManager.addAccount(getString(R.string.account_type), null, null, null, getActivity(), NavigationDrawerFragment.this, null); } }); colorView(view, false); mAccountList.addView(view); } /** * Set the view colours based on selection state * * @param view View to apply colours to * @param selected If the view is the selected item */ private void colorView(View view, boolean selected) { // Find views ImageView iconView = (ImageView) view.findViewById(R.id.img_icon); TextView titleView = (TextView) view.findViewById(R.id.txt_title); TextView subtitleView = (TextView) view.findViewById(R.id.txt_subtitle); // Get colours from theme Resources.Theme theme = getActivity().getTheme(); TypedArray typedArray = theme .obtainStyledAttributes(new int[] { R.attr.colorPrimary, R.attr.colorPrimaryDark }); int colorPrimary = getResources().getColor(typedArray.getResourceId(0, 0)); int colorPrimaryDark = getResources().getColor(typedArray.getResourceId(1, 0)); // Colour the icon/title if (selected) { if (iconView != null) { iconView.setColorFilter(colorPrimaryDark); } if (titleView != null) { titleView.setTextColor(colorPrimaryDark); } if (subtitleView != null) { subtitleView.setTextColor(colorPrimaryDark); } } else { if (iconView != null) { iconView.setColorFilter(colorPrimary); } if (titleView != null) { titleView.setTextColor(colorPrimary); } if (subtitleView != null) { subtitleView.setTextColor(colorPrimary); } } } /** * Toggle the account view */ private void toggleAccountView() { if (mAccountListVisible) { mAccountIndictor.setImageResource(R.drawable.ic_drawer_accounts_expand); mAccountList.setVisibility(View.GONE); mItemList.setVisibility(View.VISIBLE); mAccountListVisible = false; } else { mAccountIndictor.setImageResource(R.drawable.ic_drawer_accounts_collapse); mAccountList.setVisibility(View.VISIBLE); mItemList.setVisibility(View.GONE); mAccountListVisible = true; } } /** * Callbacks interface that all activities using this fragment must implement. */ public static interface NavigationDrawerCallbacks { /** * Called when an item in the navigation drawer is selected. */ public boolean onNavigationDrawerItemSelected(int id); } }