Java tutorial
/* * Copyright 2012 Mike Niyonkuru * * 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 net.niyonkuru.koodroid.ui; import android.app.Activity; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.v15.widget.PopupMenu; import android.support.v15.widget.PopupMenu.OnDismissListener; import android.support.v15.widget.PopupMenu.OnMenuItemClickListener; import android.support.v4.app.LoaderManager; import android.support.v4.view.ViewPager; import android.text.format.DateUtils; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnFocusChangeListener; import android.widget.FrameLayout; import android.widget.Toast; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; import java.util.Locale; import net.niyonkuru.koodroid.App; import net.niyonkuru.koodroid.R; import net.niyonkuru.koodroid.provider.SettingsContract.Settings; import net.niyonkuru.koodroid.service.SyncService; import net.niyonkuru.koodroid.util.AnimUtils; import net.niyonkuru.koodroid.util.DetachableResultReceiver; import com.actionbarsherlock.app.SherlockFragment; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; import static net.niyonkuru.koodroid.BuildConfig.DEBUG; /** * Contains functionality shared amongst the 3 main pages: {@link AccountFragment}, {@link OverviewFragment} and * {@link UsageFragment}. */ public abstract class PageFragment extends SherlockFragment implements DetachableResultReceiver.Receiver, View.OnClickListener, PopupMenu.OnMenuItemClickListener, LoaderManager.LoaderCallbacks<Cursor> { private static final String TAG = PageFragment.class.getSimpleName(); private static final int ACCOUNT_PAGE = 0; private static final int OVERVIEW_PAGE = 1; private static final int USAGE_PAGE = 2; private static final String EXTRA_POSITION = "position"; private static final String EXTRA_EMAIL = "email"; private static final String EXTRA_SUBSCRIBER = "subscriber"; protected static final Handler sHandler = new Handler(); protected Context mContext; protected Settings mSettings; private BaseCallBack mCallBack; private DecimalFormat mCurrencyFormat; private NumberFormat mNumberFormat; private DetachableResultReceiver mReceiver; private Menu mOptionsMenu; /** * used to provide page specific synchronization requests */ protected abstract void sync(); protected abstract void updateTimestamps(); protected interface BaseCallBack { public void onLoginRequired(); } /** * Create an appropriate page fragment instance based on position given. * * @param position value that usually comes in from {@link ViewPager} * @param email used to query appropriate data * @param subscriber used to query appropriate data */ public static PageFragment newInstance(int position, String email, String subscriber) { final PageFragment f; switch (position) { case ACCOUNT_PAGE: f = new AccountFragment(); break; case OVERVIEW_PAGE: f = new OverviewFragment(); break; case USAGE_PAGE: f = new UsageFragment(); break; default: return null; } Bundle args = new Bundle(); args.putInt(EXTRA_POSITION, position); args.putString(EXTRA_EMAIL, email); args.putString(EXTRA_SUBSCRIBER, subscriber); f.setArguments(args); return f; } private int getPosition() { return getArguments().getInt(EXTRA_POSITION); } protected String getEmail() { return getArguments().getString(EXTRA_EMAIL); } protected String getSubscriber() { return getArguments().getString(EXTRA_SUBSCRIBER); } protected NumberFormat getNumberFormat() { return (NumberFormat) mNumberFormat.clone(); } protected String formatMoney(Object amount) { if (amount == null) return null; /* * TODO: protect against NPE, better yet, move this to the currency utility class */ if (amount instanceof String) { try { amount = mNumberFormat.parse((String) amount).doubleValue(); } catch (ParseException e) { amount = 0.0; } } return mCurrencyFormat.format(amount); } @Override public void onAttach(Activity activity) { super.onAttach(activity); mContext = activity.getApplicationContext(); mSettings = App.getSettings(); Locale locale = new Locale(getString(R.string.locale), "CA"); mNumberFormat = DecimalFormat.getInstance(locale); mCurrencyFormat = CurrencyUtils.getCurrencyFormat(locale); try { mCallBack = (BaseCallBack) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement BaseCallBack type"); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); setRetainInstance(true); mReceiver = new DetachableResultReceiver(sHandler); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (DEBUG) { Log.d(TAG, " showing position: " + getPosition() + " email: " + getEmail() + " subscriber: " + getSubscriber()); } } @Override public void onResume() { super.onResume(); mReceiver.setReceiver(this); if (canSync()) sync(); showLoading(isSyncing()); final ContentResolver cr = mContext.getContentResolver(); final Uri pageChangeUri = Uri.withAppendedPath(Settings.CONTENT_URI, Settings.PAGE); cr.registerContentObserver(pageChangeUri, false, mPageChangeObserver); updateTimestamps(); } @Override public void onPause() { super.onPause(); mReceiver.clearReceiver(); if (getActivity().isFinishing()) cancelSync(); final ContentResolver cr = mContext.getContentResolver(); cr.unregisterContentObserver(mPageChangeObserver); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { mOptionsMenu = menu; inflater.inflate(R.menu.refresh_menu_item, menu); super.onCreateOptionsMenu(menu, inflater); } @Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.menu_refresh) { if (isSyncing()) { cancelSync(); } else { sync(true); } return true; } return super.onOptionsItemSelected(item); } /** * Toggles the ActionBar's refresh action between progress spinner and refresh icon according to a specified loading * state. */ public void showLoading(boolean loading) { if (mOptionsMenu == null) return; final MenuItem refreshItem = mOptionsMenu.findItem(R.id.menu_refresh); if (refreshItem != null) { if (loading) { refreshItem.setActionView(R.layout.actionbar_indeterminate_progress); refreshItem.getActionView().setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { cancelSync(); } }); } else { refreshItem.setActionView(null); } } } @Override public void onReceiveResult(int resultCode, Bundle resultData) { switch (resultCode) { case SyncService.STATUS_RUNNING: break; case SyncService.STATUS_FINISHED: break; case SyncService.STATUS_ERROR: final String errorText = getString(R.string.toast_sync_error, resultData.getString(Intent.EXTRA_TEXT)); Toast.makeText(mContext, errorText, Toast.LENGTH_LONG).show(); break; case SyncService.STATUS_LOGIN_REQUIRED: mCallBack.onLoginRequired(); break; } showLoading(isSyncing()); } /** * Process sync requests and pass them to the {@link SyncService} */ protected void sync(int request) { final Intent i = new Intent(mContext, SyncService.class); final String subscriber = getSubscriber(); i.putExtra(SyncService.EXTRA_STATUS_RECEIVER, mReceiver); i.putExtra(SyncService.EXTRA_EMAIL, getEmail()); i.putExtra(SyncService.EXTRA_SUBSCRIBER, subscriber); if (!subscriber.equals(mSettings.subscriber())) { /* switch the user first before doing the request */ i.putExtra(SyncService.EXTRA_REQUEST, SyncService.NAVIGATION_SERVICE); mContext.startService(i); } i.putExtra(SyncService.EXTRA_REQUEST, request); mContext.startService(i); } /** * Sync immediately or delay 5 seconds */ protected void sync(boolean immediate) { if (immediate) { new Thread(mSyncTask).run(); Toast.makeText(mContext, R.string.toast_sync_start, Toast.LENGTH_LONG).show(); } else { /* delay the synchronization by 5 seconds */ sHandler.postDelayed(mSyncTask, DateUtils.SECOND_IN_MILLIS * 5); } } private void cancelSync() { if (DEBUG) Log.d(TAG, "cancel sync"); sHandler.removeCallbacks(mSyncTask); mContext.stopService(new Intent(mContext, SyncService.class)); showLoading(isSyncing()); } private static boolean isSyncing() { return SyncService.sSyncing; } private boolean canSync() { /* current page points to this fragment and auto sync is allowed */ return mSettings.page() == getPosition() && mSettings.autoSyncAllowed(); } @Override public void onClick(View v) { int id = v.getId(); switch (id) { case R.id.plan_title: case R.id.bill_title: case R.id.tab_title: case R.id.billing_cycle_title: case R.id.airtime_title: case R.id.data_title: final View options = ((FrameLayout) v).getChildAt(2); AnimUtils.fade(options); v.setFocusableInTouchMode(true); v.requestFocusFromTouch(); v.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (!hasFocus) { AnimUtils.fadeOut(options); v.setFocusableInTouchMode(false); } updateTimestamps(); } }); } /* give the user feedback as to how fresh the data is */ updateTimestamps(); } protected void toggleService(String service, boolean value) { ContentValues cv = new ContentValues(1); cv.put(service, value); mContext.getContentResolver().insert(Settings.buildSubscriberUri(getSubscriber()), cv); } private final ContentObserver mPageChangeObserver = new ContentObserver(sHandler) { @Override public void onChange(boolean selfChange) { if (canSync()) { cancelSync(); sync(false); } updateTimestamps(); } }; private final Runnable mSyncTask = new Runnable() { @Override public void run() { sync(); updateTimestamps(); } }; public static class CurrencyUtils { private CurrencyUtils() { } public static DecimalFormat getCurrencyFormat(Locale locale) { final DecimalFormat currency = (DecimalFormat) NumberFormat.getCurrencyInstance(locale); currency.setNegativePrefix("-" + currency.getNegativePrefix().replace("(", "")); currency.setNegativeSuffix(currency.getNegativeSuffix().replace(")", "")); return currency; } } /** * Display a PopupMenu anchored to a menu option. */ class PopupMenuBuilder { private final PopupMenu mPopup; public PopupMenuBuilder(Context ctx, final View anchor) { mPopup = new PopupMenu(ctx, anchor); mPopup.setOnDismissListener(new OnDismissListener() { @Override public void onDismiss(PopupMenu menu) { /* turn off options mode */ AnimUtils.fadeOut((View) anchor.getParent()); } }); } public PopupMenuBuilder addMenuResource(int resource) { mPopup.getMenuInflater().inflate(resource, mPopup.getMenu()); return this; } public void addMenuItemResource(int resource, int titleRes) { mPopup.getMenu().add(Menu.NONE, resource, 2, titleRes); } public void show(OnMenuItemClickListener listener) { mPopup.setOnMenuItemClickListener(listener); mPopup.show(); } } }