ohmage/src/main/java/org/ohmage/app/SurveyActivity.java
public static class SurveyPromptLoader extends AsyncTaskLoader<ArrayList<Prompt>> {
    @Inject Gson gson;

    Uri mUri;
    String[] mProjection;
    String mSelection;
    String[] mSelectionArgs;

    Cursor mCursor;

    private ArrayList<Prompt> mPrompts;

    /* Runs on a worker thread */
    @Override
    public ArrayList<Prompt> loadInBackground() {
        Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
                mSelectionArgs, null);
        if (cursor != null && cursor.moveToFirst()) {
            mPrompts = gson.fromJson(cursor.getString(0), new TypeToken<ArrayList<Prompt>>() {
            }.getType());
            cursor.close();
        }
        return mPrompts;
    }

    /* Runs on the UI thread */
    @Override
    public void deliverResult(ArrayList<Prompt> result) {
        if (isReset()) {
            // An async query came in while the loader is stopped
            mPrompts = null;
            return;
        }
        mPrompts = result;
        if (isStarted()) {
            super.deliverResult(result);
        }
    }

    /**
     * Creates a Loader for prompt items
     */
    public SurveyPromptLoader(Context context, String surveyId, long surveyVersion) {
        super(context);
        ((SurveyActivity) context).inject(this);

        mUri = OhmageContract.Surveys.CONTENT_URI;
        mProjection = new String[]{
                OhmageContract.Surveys.SURVEY_ITEMS
        };
        mSelection = OhmageContract.Surveys.SURVEY_ID + "=? AND " +
                     OhmageContract.Surveys.SURVEY_VERSION + "=?";
        mSelectionArgs = new String[]{
                surveyId, String.valueOf(surveyVersion)
        };
    }

    public SurveyPromptLoader(Context context, Uri surveyUri) {
        super(context);
        ((SurveyActivity) context).inject(this);

        mUri = surveyUri;
        mProjection = new String[]{
                OhmageContract.Surveys.SURVEY_ITEMS
        };
    }

    /**
     * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
     * will be called on the UI thread. If a previous load has been completed and is still valid
     * the result may be passed to the callbacks immediately.
     * <p/>
     * Must be called from the UI thread
     */
    @Override
    protected void onStartLoading() {
        if (mPrompts != null) {
            deliverResult(mPrompts);
        }
        if (takeContentChanged() || mCursor == null) {
            forceLoad();
        }
    }

    /**
     * Must be called from the UI thread
     */
    @Override
    protected void onStopLoading() {
        // Attempt to cancel the current load task if possible.
        cancelLoad();
    }

    @Override
    protected void onReset() {
        super.onReset();

        // Ensure the loader is stopped
        onStopLoading();

        mPrompts = null;
    }

    @Override
    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
        super.dump(prefix, fd, writer, args);
        writer.print(prefix);
        writer.print("mPrompts=");
        writer.println(mPrompts);
    }
}
ohmage/src/main/java/org/ohmage/provider/StreamContentProvider.java
public class StreamContentProvider extends ContentProvider implements OnAccountsUpdateListener {

    @Inject AccountManager am;

    private AsyncBulkInsertHandler mAsyncQueryHandler;

    private String mAccount;

    // enum of the URIs we can match using sUriMatcher
    private interface MatcherTypes {
        int STREAMS = 0;

        int STREAMS_ID = 1;

        int COUNTS = 2;
    }

    private OhmageDbHelper dbHelper;

    private static UriMatcher sUriMatcher;

    {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(StreamContract.CONTENT_AUTHORITY, "streams", MatcherTypes.STREAMS);
        sUriMatcher
                .addURI(StreamContract.CONTENT_AUTHORITY, "streams/*/*", MatcherTypes.STREAMS_ID);
        sUriMatcher.addURI(StreamContract.CONTENT_AUTHORITY, "counts", MatcherTypes.COUNTS);
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        int count = 0;

        switch (sUriMatcher.match(uri)) {

            case MatcherTypes.STREAMS:
                count = dbHelper.getWritableDatabase().delete(Tables.StreamData, selection,
                        selectionArgs);
                break;
            case MatcherTypes.COUNTS:

            default:
                throw new UnsupportedOperationException("insert(): Unknown URI: " + uri);
        }

        notifyInsert(uri, count);
        return count;
    }

    @Override
    public String getType(Uri uri) {
        switch (sUriMatcher.match(uri)) {

            case MatcherTypes.STREAMS:
                return StreamContract.Streams.CONTENT_TYPE;
            case MatcherTypes.COUNTS:
                return StreamContract.StreamCounts.CONTENT_TYPE;
            default:
                throw new UnsupportedOperationException("getType(): Unknown URI: " + uri);
        }
    }

    public static int counter = 0;

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        long id = -1;

        ContentResolver cr = getContext().getContentResolver();

        switch (sUriMatcher.match(uri)) {
            case MatcherTypes.STREAMS:

                if (mAccount == null) {
                    return null;
                }

                // Set the correct username
                values.put(StreamContract.Streams.USERNAME, mAccount);

                SQLiteDatabase db = dbHelper.getWritableDatabase();
                db.insert(Tables.StreamData, BaseColumns._ID, values);

                break;
            default:
                throw new UnsupportedOperationException("insert(): Unknown URI: " + uri);
        }
        if (id != -1)
            return ContentUris.withAppendedId(StreamContract.Streams.CONTENT_URI, id);
        return null;
    }

    @Override
    public boolean onCreate() {
        dbHelper = new OhmageDbHelper(getContext());

        ((Ohmage) getContext().getApplicationContext()).getApplicationGraph().inject(this);

        am.addOnAccountsUpdatedListener(this, null, true);

        mAsyncQueryHandler = new AsyncBulkInsertHandler(getContext().getContentResolver(),
                StreamContract.Streams.CONTENT_URI);
        return true;
    }

    @Override
    public void onAccountsUpdated(Account[] accounts) {
        for (Account account : accounts) {
            if (AuthUtil.ACCOUNT_TYPE.equals(account.type)) {
                mAccount = account.name;
                return;
            }
        }
        mAccount = null;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
            String sortOrder) {
        Cursor cursor;
        switch (sUriMatcher.match(uri)) {
            case MatcherTypes.COUNTS:
                SQLiteQueryBuilder streams = new SQLiteQueryBuilder();
                streams.setTables(Tables.StreamData);
                cursor = streams.query(dbHelper.getReadableDatabase(), projection, selection,
                        selectionArgs, StreamContract.Streams.STREAM_ID + ", "
                                       + StreamContract.Streams.STREAM_VERSION, null, sortOrder);
                break;
            case MatcherTypes.STREAMS:
                cursor = dbHelper.getReadableDatabase()
                        .query(Tables.StreamData, projection, selection,
                                selectionArgs, null, null, sortOrder);
                break;
            default:
                throw new UnsupportedOperationException("query(): Unknown URI: " + uri);
        }

        cursor.setNotificationUri(getContext().getContentResolver(), uri);
        return cursor;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException("Update not allowed");
    }

    @Override
    public synchronized int bulkInsert(Uri uri, ContentValues[] values) {
        int count = 0;

        // We can't insert any points if no account exists.
        String account = mAccount;
        if (TextUtils.isEmpty(account)) {
            return 0;
        }

        SQLiteDatabase db = dbHelper.getWritableDatabase();

        try {
            db.beginTransaction();

            String table;
            switch (sUriMatcher.match(uri)) {
                case MatcherTypes.STREAMS:
                    table = Tables.StreamData;
                    break;
                default:
                    throw new UnsupportedOperationException("bulkInsert(): Unknown URI: " + uri);
            }

            for (ContentValues v : values) {
                v.put(StreamContract.Streams.USERNAME, mAccount);
                if (db.insert(table, BaseColumns._ID, v) != -1)
                    count++;
            }

            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }

        notifyInsert(uri, count);

        return count;
    }

    private void notifyInsert(Uri uri, Integer count) {
        if (count > 0) {
            ContentResolver cr = getContext().getContentResolver();

            switch (sUriMatcher.match(uri)) {
                case MatcherTypes.STREAMS:
                    cr.notifyChange(StreamContract.Streams.CONTENT_URI, null, false);
                    break;
            }
        }
    }
}
ohmage/src/main/java/org/ohmage/auth/AuthenticateFragment.java
public class AuthenticateFragment extends TransitionFragment implements View.OnClickListener {

    /**
     * Keeps track of the state of if the progress spinner is shown
     */
    private boolean mShowProgress;

    /**
     * The view which shows the progress spinner
     */
    private View mAuthLoadingView;

    /**
     * The view which shows the authentication buttons
     */
    private View mAuthButtonsView;

    private Callbacks mCallbacks;

    public AuthenticateFragment() {
        setDefaultAnimation(R.anim.abc_fade_in, R.anim.abc_fade_out);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_authenticate, container, false);

        SignInButton signInButton = (SignInButton) view.findViewById(R.id.sign_in_google_button);
        signInButton.setOnClickListener(this);
        signInButton.setSize(SignInButton.SIZE_WIDE);

        Button createAccount = (Button) view.findViewById(R.id.create_account_button);
        createAccount.setOnClickListener(this);
        Button signInEmail = (Button) view.findViewById(R.id.sign_in_email_button);
        signInEmail.setOnClickListener(this);

        mAuthLoadingView = view.findViewById(R.id.authenticate_loading);
        mAuthButtonsView = view.findViewById(R.id.authenticate_buttons);

        // Set the visibility of the views
        mAuthLoadingView.setVisibility(mShowProgress ? View.VISIBLE : View.INVISIBLE);
        mAuthButtonsView.setVisibility(mShowProgress ? View.INVISIBLE : View.VISIBLE);

        return view;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean("showProgress", mShowProgress);
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        mCallbacks = (Callbacks) activity;
    }

    @Override public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }

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

    /**
     * Shows the progress UI and hides the authentication buttons.
     */
    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
    public void showProgress(boolean show) {
        mShowProgress = show;

        // On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow
        // for very easy animations. If available, use these APIs to fade-in
        // the progress spinner.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
            int shortAnimTime = getResources().getInteger(android.R.integer.config_shortAnimTime);

            mAuthLoadingView.setVisibility(View.VISIBLE);
            mAuthLoadingView.animate()
                    .setDuration(shortAnimTime)
                    .alpha(show ? 1 : 0)
                    .setListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            mAuthLoadingView.setVisibility(
                                    mShowProgress ? View.VISIBLE : View.INVISIBLE);
                        }
                    });

            mAuthButtonsView.setVisibility(View.VISIBLE);
            mAuthButtonsView.animate()
                    .setDuration(shortAnimTime)
                    .alpha(show ? 0 : 1)
                    .setListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            mAuthButtonsView.setVisibility(
                                    mShowProgress ? View.INVISIBLE : View.VISIBLE);
                        }
                    });
        } else {
            // The ViewPropertyAnimator APIs are not available, so simply showProgress
            // and hide the relevant UI components.
            mAuthLoadingView.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
            mAuthButtonsView.setVisibility(show ? View.INVISIBLE : View.VISIBLE);
        }
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.sign_in_google_button:
                mCallbacks.onGoogleSignInClick();
                break;
            case R.id.create_account_button:
                mCallbacks.onCreateAccountClick();
                break;
            case R.id.sign_in_email_button:
                mCallbacks.onEmailSignInClick();
                break;
        }
    }

    public static interface Callbacks {
        void onGoogleSignInClick();

        void onCreateAccountClick();

        void onEmailSignInClick();
    }
}
ohmage/src/main/java/org/ohmage/app/SurveyActivity.java
public class SurveyActivity extends InjectedActionBarActivity
        implements LoaderCallbacks<ArrayList<Prompt>>, ConnectionCallbacks,
        OnConnectionFailedListener {
    private static final String TAG = SurveyActivity.class.getSimpleName();

    @Inject Gson gson;

    /**
     * The pager widget, which handles animation and allows swiping horizontally to access previous
     * and next wizard steps.
     */
    private VerticalViewPager mPager;

    /**
     * The pager adapter, which provides the pages to the view pager widget.
     */
    private PromptFragmentAdapter mPagerAdapter;

    private CirclePageIndicator indicator;

    /**
     * Location client to get accurate location
     */
    private LocationClient mLocationClient;

    private static final LocationRequest REQUEST = LocationRequest.create()
            .setInterval(5000)         // 5 seconds
            .setFastestInterval(16)    // 16ms = 60fps
            .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

    private SurveyStateFragment mState;

    public static final int MSG_SHOW_INSTALL_DEPENDENCIES = 0;

    private Handler handler = new Handler() {

        @Override
        public void handleMessage(Message msg) {
            if(msg.what == MSG_SHOW_INSTALL_DEPENDENCIES) {
                FragmentManager fm = getSupportFragmentManager();
                InstallDependenciesDialog fragment =
                        (InstallDependenciesDialog) fm.findFragmentByTag("install");
                if (fragment == null) {
                    fragment = InstallDependenciesDialog.getInstance((ApkSet) msg.obj, true);
                }
                fragment.show(fm, "install");
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_survey);

        // Instantiate a ViewPager and a PagerAdapter.
        mPager = (VerticalViewPager) findViewById(R.id.pager);
        mPager.setPageMargin(getResources().getDimensionPixelSize(R.dimen.gutter));

        //Bind the title indicator to the adapter
        indicator = (CirclePageIndicator) findViewById(R.id.titles);

        if (savedInstanceState == null) {
            mState = new SurveyStateFragment();
            getSupportFragmentManager().beginTransaction().add(mState, "state").commit();
        } else {
            mState = (SurveyStateFragment) getSupportFragmentManager().findFragmentByTag("state");
        }

        if (mState.prompts != null) {
            setPromptFragmentAdapter();
        } else {
            getSupportLoaderManager().initLoader(0, null, this);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        setUpLocationClientIfNeeded();
        mLocationClient.connect();
    }

    @Override
    public void onPause() {
        super.onPause();
        if (mLocationClient != null) {
            mLocationClient.disconnect();
        }
    }

    private void setUpLocationClientIfNeeded() {
        if (mLocationClient == null) {
            mLocationClient = new LocationClient(
                    getApplicationContext(),
                    this,  // ConnectionCallbacks
                    this); // OnConnectionFailedListener
        }
    }

    @Override
    public void onBackPressed() {
        // If nothing has been answered just go back
        if (mPagerAdapter == null || mPagerAdapter.getAnsweredCount() == 0) {
            discardSurvey();
            return;
        }

        // Otherwise show a dialog so they don't lose their responses
        FragmentManager fm = getSupportFragmentManager();
        CancelResponseDialogFragment fragment =
                (CancelResponseDialogFragment) fm.findFragmentByTag("cancel");
        if (fragment == null) {
            fragment = new CancelResponseDialogFragment();
        }
        fragment.show(fm, "cancel");
    }

    private void discardSurvey() {
        if(mPagerAdapter != null)
            mPagerAdapter.clearExtras();
        super.onBackPressed();
    }

    @Override public SurveyPromptLoader onCreateLoader(int id, Bundle args) {
        return new SurveyPromptLoader(this, getIntent().getData());
    }

    @Override public void onLoadFinished(Loader<ArrayList<Prompt>> loader, ArrayList<Prompt> data) {
        if(data == null) {
            Toast.makeText(this, R.string.survey_not_found, Toast.LENGTH_SHORT).show();
            finish();
            return;
        }

        if (mPagerAdapter == null) {
            setPrompts(data);

            // Check for remote activity prompts and make sure they all exist
            if(data != null) {
                ApkSet appItems = ApkSet.fromPromptsIgnoreSkippable(data);
                appItems.clearInstalled(this);

                if (!appItems.isEmpty()) {
                    Message msg = handler.obtainMessage(MSG_SHOW_INSTALL_DEPENDENCIES);
                    msg.obj = appItems;
                    msg.sendToTarget();
                }
            }
        }
    }

    @Override public void onLoaderReset(Loader<ArrayList<Prompt>> loader) {

    }

    public void setPromptFragmentAdapter() {
        mPagerAdapter = new PromptFragmentAdapter(getSupportFragmentManager(), mState.prompts);
        mPager.setAdapter(mPagerAdapter);
        indicator.setViewPager(mPager);
    }

    public void setPrompts(ArrayList<Prompt> data) {
        mState.prompts = new Prompts(data);
        setPromptFragmentAdapter();
    }

    public void submit() {
        // Tell reminders that the survey was taken
        TriggerFramework.notifySurveyTaken(this, Surveys.getId(getIntent().getData()));

        ContentValues values = new ContentValues();
        values.put(Responses.SURVEY_ID, Surveys.getId(getIntent().getData()));
        values.put(Responses.SURVEY_VERSION, Surveys.getVersion(getIntent().getData()));
        values.put(Responses.RESPONSE_METADATA,
                new StreamPointBuilder().now().withId()
                        .withLocation(mLocationClient.getLastLocation())
                        .getMetadata()
        );
        mPagerAdapter.buildResponse(values);
        getContentResolver().insert(Responses.CONTENT_URI, values);
        finish();
    }

    @Override public void onConnected(Bundle bundle) {
        mLocationClient.requestLocationUpdates(REQUEST, new LocationListener() {
            @Override public void onLocationChanged(Location location) {
            }
        });
    }

    @Override public void onDisconnected() {

    }

    @Override public void onConnectionFailed(ConnectionResult connectionResult) {

    }

    public PromptFragmentAdapter getPagerAdapter() {
        return mPagerAdapter;
    }

    public class PromptFragmentAdapter extends FragmentStatePagerAdapter {
        private final Prompts prompts;

        private final FragmentManager mFragmentManager;

        /**
         * Keeps track of what fragments have been created
         */
        private BitSet mFragments = new BitSet();

        /**
         * Keeps track of any prompts which have just changed due to an answer being updated.
         */
        int mLastUpdate = 0;

        public PromptFragmentAdapter(FragmentManager fm, Prompts prompts) {
            super(fm);
            mFragmentManager = fm;
            this.prompts = prompts;
        }

        @Override
        public SurveyItemFragment getItem(final int position) {
            Prompt prompt = prompts.getPromptAt(position);
            SurveyItemFragment fragment = null;
            if (prompt == null) {
                fragment = new SubmitResponseFragment();
            } else {
                fragment = prompt.getFragment();
                fragment.showButtons(position == getCount() - 1 ? View.VISIBLE : View.GONE);
            }

            if (fragment != null) {
                mFragments.set(position);
            }

            return fragment;
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            mFragments.clear(position);
            super.destroyItem(container, position, object);
        }

        @Override
        public int getCount() {
            return prompts.getAnsweredCount() + 1;
        }

        @Override
        public float getPageWidth(int position) {
            if (position == prompts.size()) {
                return 1.0f;
            }
            return 0.1f;
        }

        @Override public Parcelable saveState() {
            Parcelable parent = super.saveState();
            Bundle state = new Bundle();
            state.putParcelable("parent", parent);
            return state;
        }

        @Override
        public void restoreState(Parcelable state, ClassLoader loader) {
            Bundle bundle = (Bundle) state;
            state = bundle.getBundle("parent");

            // Hijack the fragment state from our parent
            if (state != null) {
                bundle = (Bundle) state;
                mFragments.clear();
                Iterable<String> keys = bundle.keySet();
                for (String key : keys) {
                    if (key.startsWith("f")) {
                        int index = Integer.parseInt(key.substring(1));
                        Fragment f = mFragmentManager.getFragment(bundle, key);
                        if (f != null) {
                            mFragments.set(index, true);
                        } else {
                            Log.w(TAG, "Bad fragment at key " + key);
                        }
                    }
                }
            }
            super.restoreState(state, loader);
        }

        public void buildResponse(ContentValues values) {
            Log.d(TAG, gson.toJson(prompts.answers));
            values.put(Responses.RESPONSE_DATA, gson.toJson(prompts.answers));
            values.put(Responses.RESPONSE_EXTRAS, gson.toJson(prompts.extras));
        }

        public void updateAnswer(PromptFragment promptFragment) {
            promptFragment.showButtons(View.GONE);
            Prompt prompt = promptFragment.getPrompt();
            boolean alreadyAnswered = prompts.isAnswered(prompt);

            mLastUpdate = prompts.updateAnswer(prompt);
            notifyDataSetChanged();

            if (!alreadyAnswered) {
                moveToLastPrompt();
            }
        }

        /**
         * Moves to the last prompt, waits until it has been added to the view
         */
        private void moveToLastPrompt() {
            final int position = getCount() - 1;
            mPager.post(new Runnable() {
                @Override public void run() {
                    mPager.bringPositionUpOnScreen(position);
                }
            });
        }

        @Override
        public int getItemPosition(Object object) {
            if (object instanceof PromptFragment) {
                if (prompts.positionOf(((PromptFragment) object).getPrompt()) >= mLastUpdate) {
                    return PagerAdapter.POSITION_NONE;
                }
                return POSITION_UNCHANGED;
            }
            return PagerAdapter.POSITION_NONE;
        }

        public int getPosition(SurveyItemFragment fragment) {
            if (fragment instanceof PromptFragment) {
                return prompts.positionOf(((PromptFragment) fragment).getPrompt());
            } else if (fragment instanceof SubmitResponseFragment) {
                return prompts.size();
            }
            return -1;
        }

        public SurveyItemFragment getPromptFragmentAt(int position) {
            if (mFragments.get(position)) {
                return getItem(position);
            }
            return null;
        }

        public int getAnsweredCount() {
            return prompts.getAnsweredCount();
        }

        public void clearExtras() {
            prompts.clearExtras();
        }
    }

    public static class SubmitResponseFragment extends SurveyItemFragment {

        boolean submitted = false;

        @Override
        public View onCreateView(LayoutInflater inflater, final ViewGroup container,
                Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.prompt_submit, container, false);
            final Button submit = (Button) view.findViewById(R.id.submit);
            submit.setOnClickListener(new OnClickListener() {
                @Override public void onClick(View v) {
                    submit.setEnabled(false);
                    if (!submitted) {
                        submitted = true;
                        ((SurveyActivity) getActivity()).submit();
                    }
                }
            });
            return view;
        }
    }

    /**
     * Static library support version of the framework's {@link android.content.CursorLoader}.
     * Used to write apps that run on platforms prior to Android 3.0.  When running
     * on Android 3.0 or above, this implementation is still used; it does not try
     * to switch to the framework's implementation.  See the framework SDK
     * documentation for a class overview.
     */
    public static class SurveyPromptLoader extends AsyncTaskLoader<ArrayList<Prompt>> {
        @Inject Gson gson;

        Uri mUri;
        String[] mProjection;
        String mSelection;
        String[] mSelectionArgs;

        Cursor mCursor;

        private ArrayList<Prompt> mPrompts;

        /* Runs on a worker thread */
        @Override
        public ArrayList<Prompt> loadInBackground() {
            Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
                    mSelectionArgs, null);
            if (cursor != null && cursor.moveToFirst()) {
                mPrompts = gson.fromJson(cursor.getString(0), new TypeToken<ArrayList<Prompt>>() {
                }.getType());
                cursor.close();
            }
            return mPrompts;
        }

        /* Runs on the UI thread */
        @Override
        public void deliverResult(ArrayList<Prompt> result) {
            if (isReset()) {
                // An async query came in while the loader is stopped
                mPrompts = null;
                return;
            }
            mPrompts = result;
            if (isStarted()) {
                super.deliverResult(result);
            }
        }

        /**
         * Creates a Loader for prompt items
         */
        public SurveyPromptLoader(Context context, String surveyId, long surveyVersion) {
            super(context);
            ((SurveyActivity) context).inject(this);

            mUri = OhmageContract.Surveys.CONTENT_URI;
            mProjection = new String[]{
                    OhmageContract.Surveys.SURVEY_ITEMS
            };
            mSelection = OhmageContract.Surveys.SURVEY_ID + "=? AND " +
                         OhmageContract.Surveys.SURVEY_VERSION + "=?";
            mSelectionArgs = new String[]{
                    surveyId, String.valueOf(surveyVersion)
            };
        }

        public SurveyPromptLoader(Context context, Uri surveyUri) {
            super(context);
            ((SurveyActivity) context).inject(this);

            mUri = surveyUri;
            mProjection = new String[]{
                    OhmageContract.Surveys.SURVEY_ITEMS
            };
        }

        /**
         * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
         * will be called on the UI thread. If a previous load has been completed and is still valid
         * the result may be passed to the callbacks immediately.
         * <p/>
         * Must be called from the UI thread
         */
        @Override
        protected void onStartLoading() {
            if (mPrompts != null) {
                deliverResult(mPrompts);
            }
            if (takeContentChanged() || mCursor == null) {
                forceLoad();
            }
        }

        /**
         * Must be called from the UI thread
         */
        @Override
        protected void onStopLoading() {
            // Attempt to cancel the current load task if possible.
            cancelLoad();
        }

        @Override
        protected void onReset() {
            super.onReset();

            // Ensure the loader is stopped
            onStopLoading();

            mPrompts = null;
        }

        @Override
        public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
            super.dump(prefix, fd, writer, args);
            writer.print(prefix);
            writer.print("mPrompts=");
            writer.println(mPrompts);
        }
    }

    public static class SurveyStateFragment extends Fragment {
        public Prompts prompts;

        @Override public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setRetainInstance(true);
        }
    }

    /**
     * This class keeps track of all the prompts and how the user answered them. It provides the
     * functions {@link #answer(AnswerablePrompt)}, {@link #ignore(Prompt)}, and
     * {@link #skip(Prompt)} based on the users actions. {@link #getPromptAt(int)} will return the
     * next prompt ignoring all prompts which weren't displayed.
     */
    public static class Prompts {
        final ArrayList<Prompt> prompts;

        // Answers
        final public HashMap<String, Object> answers;
        final public HashMap<String, String> extras;

        // List of the skipped prompts
        final ArrayList<String> mSkipped;

        // List of not displayed prompts
        final ArrayList<String> mNotDisplayed;

        public Prompts(ArrayList<Prompt> data) {
            this.prompts = data;

            answers = new HashMap<String, Object>();
            extras = new HashMap<String, String>();
            mSkipped = new ArrayList<String>();
            mNotDisplayed = new ArrayList<String>();
        }

        /**
         * Returns the prompt at the given position ignoring prompts which are not displayed
         *
         * @param position
         * @return
         */
        public Prompt getPromptAt(int position) throws IllegalStateException {
            Prompt prompt = null;
            Map<String, Object> prev = null;
            int index = promptIndex(position);

            while (prompt == null && index < prompts.size()) {
                prompt = prompts.get(index);
                if (prompt.getCondition() != null) {
                    if (prev == null) {
                        prev = getPreviousResponses(index);
                    }
                    Condition condition = new Condition(prompt.getCondition());

                    if (!condition.evaluate(prev)) {
                        ignore(prompt);
                        updatePrevious(prompt, prev);
                        prompt = null;
                        index++;
                    }
                }
            }
            return prompt;
        }

        /**
         * Returns the index into {@link #prompts} given the position of shown prompts
         *
         * @param position
         * @return
         */
        private int promptIndex(int position) {
            return position + positionOffset(position);
        }

        private int positionOffset(int index) {
            int count = 0;
            for (int i = 0; i <= index && i < prompts.size(); i++) {
                if (isIgnored(prompts.get(i))) {
                    count++;
                    index++;
                }
            }
            return count;
        }

        /**
         * Count the number of prompts that have been ignored before the given position
         *
         * @param index
         * @return
         */
        private int ignoredPromptsBefore(int index) {
            int count = 0;
            for (int i = 0; i < index && i < prompts.size(); i++) {
                if (isIgnored(prompts.get(i))) {
                    count++;
                }
            }
            return count;
        }

        /**
         * Gets the map of all answers before the current answer to be used to evaluate the
         * the condition for the prompt at the given position
         *
         * @return a map of all previous responses
         */
        private Map<String, Object> getPreviousResponses(int index) {
            AbstractMap prev = new HashMap<String, Object>();
            for (int i = 0; i < index; i++) {
                updatePrevious(prompts.get(i), prev);
            }
            return prev;
        }

        /**
         * Convenience method to update the previous responses map for the given prompt
         *
         * @param prompt
         * @param prev
         */
        private void updatePrevious(Prompt prompt, Map<String, Object> prev) {
            if (answers.keySet().contains(prompt.getId())) {
                prev.put(prompt.getId(), answers.get(prompt.getId()));
            } else if (mSkipped.contains(prompt.getId())) {
                prev.put(prompt.getId(), NoResponse.SKIPPED);
            } else if (mNotDisplayed.contains(prompt.getId())) {
                prev.put(prompt.getId(), NoResponse.NOT_DISPLAYED);
            }
        }

        /**
         * Calculates the number of prompts that were actively answered or skipped.
         *
         * @return
         */
        public int getAnsweredCount() {
            return answers.size() + mSkipped.size();
        }

        /**
         * Will add an answer given the state of the prompt item
         *
         * @param prompt
         * @return the index of the first position which changed due to conditions
         */
        public int updateAnswer(Prompt prompt) {
            if (prompt instanceof AnswerablePrompt) {
                if (((AnswerablePrompt) prompt).hasValidResponse()) {
                    answer((AnswerablePrompt) prompt);
                } else if (prompt.isSkippable()) {
                    skip(prompt);
                } else {
                    // If this prompt is no longer valid remove all answers after it
                    int invalidPrompt = prompts.indexOf(prompt);
                    removeAnswersAfter(invalidPrompt);
                    return invalidPrompt - ignoredPromptsBefore(invalidPrompt) + 1;
                }
            } else {
                answer(prompt);
            }

            return resetFutureAnswers(prompt);
        }

        private void answer(Prompt prompt) {
            remove(prompt);
            answers.put(prompt.getId(), null);
        }

        private void answer(AnswerablePrompt prompt) {
            remove(prompt);
            answers.put(prompt.getId(), prompt.getAnswer());
            extras.put(prompt.getId(), prompt.getAnswerExtras());
        }

        private void ignore(Prompt prompt) {
            remove(prompt);
            mNotDisplayed.add(prompt.getId());
        }

        private void skip(Prompt prompt) {
            remove(prompt);
            mSkipped.add(prompt.getId());
        }

        private void remove(Prompt prompt) {
            mNotDisplayed.remove(prompt.getId());
            mSkipped.remove(prompt.getId());
            answers.remove(prompt.getId());
            extras.remove(prompt.getId());
        }

        /**
         * Calculates if a prompt was answered
         *
         * @param prompt
         * @return true if it was answered or skipped
         */
        public boolean isAnswered(Prompt prompt) {
            return answers.containsKey(prompt.getId()) || mSkipped.contains(prompt.getId());
        }

        /**
         * Calculates if a prompt was ignored due to conditions
         *
         * @param prompt
         * @return true if it was ignored
         */
        public boolean isIgnored(Prompt prompt) {
            return mNotDisplayed.contains(prompt.getId());
        }

        public int ignoredPromptsSize() {
            return mNotDisplayed.size();
        }

        public int size() {
            return prompts.size() - mNotDisplayed.size();
        }

        public int positionOf(Prompt prompt) {
            int idx = prompts.indexOf(prompt);
            return idx - ignoredPromptsBefore(idx);
        }

        private void removeAnswersAfter(int index) {
            for (int i = index; i < prompts.size(); i++) {
                Prompt prompt = prompts.get(i);
                String promptId = prompt.getId();
                if (mSkipped.contains(promptId)) {
                    mSkipped.remove(promptId);
                } else if (answers.keySet().contains(promptId)) {
                    answers.remove(promptId);
                    if(extras.keySet().contains(promptId)) {
                        extras.remove(promptId);
                    }
                } else if (mNotDisplayed.contains(promptId)) {
                    mNotDisplayed.remove(promptId);
                }
            }
        }

        public void clearExtras() {
            for (int i = 0; i < prompts.size(); i++) {
                String fileName = extras.get(prompts.get(i).getId());
                if (fileName != null) {
                    new File(fileName).delete();
                }
            }
        }

        /**
         * Since the answer changed for this response we should re-calculate all of the next responses
         *
         * @param prompt
         */
        private int resetFutureAnswers(Prompt prompt) throws IllegalStateException {
            int pivot = prompts.indexOf(prompt);
            Map<String, Object> prev = null;

            int lastUpdate = size();

            // Calculate the first prompt which no longer passes the condition test
            int after = prompts.size();
            for (int i = pivot + 1;
                 i < getAnsweredCount() + ignoredPromptsSize() + 1 && i < prompts.size(); i++) {
                prompt = prompts.get(i);
                if (prev == null) {
                    prev = getPreviousResponses(i);
                }
                if (prompt.getCondition() != null) {
                    Condition condition = new Condition(prompt.getCondition());
                    // The first prompt which is not the same as it used to be and is shown
                    boolean show = condition.evaluate(prev);
                    if (show == isIgnored(prompt)) {
                        lastUpdate = Math.min(lastUpdate, i);
                        if (!show) {
                            ignore(prompt);
                        } else {
                            after = i;
                            break;
                        }
                    }
                }

                updatePrevious(prompt, prev);
            }

            // Remove all responses after the last valid one
            removeAnswersAfter(after);

            return lastUpdate - ignoredPromptsBefore(lastUpdate + 1);
        }
    }

    public static class CancelResponseDialogFragment extends DialogFragment {

        @Override public Dialog onCreateDialog(Bundle savedInstanceState) {
            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
            builder.setTitle(getString(R.string.discard_survey_title))
                    .setMessage(getString(R.string.discard_survey_message))
                    .setPositiveButton(R.string.discard, new DialogInterface.OnClickListener() {
                        @Override public void onClick(DialogInterface dialog, int which) {
                            ((SurveyActivity) getActivity()).discardSurvey();
                        }
                    })
                    .setNegativeButton(R.string.cancel, null);
            return builder.create();
        }
    }
}
ohmage/src/main/java/org/ohmage/app/MainActivity.java
public class MainActivity extends InjectedActionBarActivity
        implements AdapterView.OnItemClickListener,
        OnAccountsUpdateListener {

    private static final String TAG = MainActivity.class.getSimpleName();

    /**
     * If this action is set, the streams fragment will be shown
     */
    public static final String EXTRA_VIEW_STREAMS = "extra_view_streams";

    /**
     * If this action is set, the surveys fragment will be shown
     */
    public static final String EXTRA_VIEW_SURVEYS = "extra_view_surveys";

    @Inject AccountManager accountManager;

    /**
     * The sliding drawer
     */
    private DrawerLayout mDrawerLayout;

    /**
     * A list of navigation items which are shown in the sliding drawer
     */
    private String[] mNavigationItems;

    /**
     * The list which holds the navigation items
     */
    private ListView mDrawerList;

    /**
     * The drawer toggle button on the actionbar
     */
    private ActionBarDrawerToggle mDrawerToggle;

    /**
     * An array which holds the icons for the navigation items
     */
    private TypedArray mNavigationIcons;

    /**
     * Set when the user wants to logout
     */
    private boolean mLoggingOut;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        mNavigationItems = getResources().getStringArray(R.array.navigation_items);
        mNavigationIcons = getResources().obtainTypedArray(R.array.navigation_icons);

        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        mDrawerList = (ListView) findViewById(R.id.left_drawer);

        // Set the adapter for the list view
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
                R.layout.simple_list_item_activated_1, mNavigationItems) {

            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                // Set the left drawable of the text view to be the icon for that item
                TextView view = (TextView) super.getView(position, convertView, parent);
                Drawable d = getResources()
                        .getDrawable(mNavigationIcons.getResourceId(position, -1));

                if (d != null && view != null) {
                    int bounds = getResources()
                            .getDimensionPixelSize(R.dimen.navigation_icon_bounds);
                    d.setBounds(0, 0, bounds, bounds);
                    view.setCompoundDrawables(d, null, null, null);
                }
                return view;
            }
        };

        mDrawerList.setAdapter(adapter);

        // Set the list's click listener
        mDrawerList.setOnItemClickListener(this);
        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);

        mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
                R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close);

        // Set the drawer toggle as the DrawerListener
        mDrawerLayout.setDrawerListener(mDrawerToggle);

        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeButtonEnabled(true);

        if (savedInstanceState == null) {
            if (getIntent().getBooleanExtra(EXTRA_VIEW_STREAMS, false)) {
                setFragment(getString(R.string.streams));
            } else {
                if (!handleNewIntent(getIntent())) {
                    setFragment(0);
                }
            }
        }
    }

    @Override protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        handleNewIntent(intent);
    }

    private boolean handleNewIntent(Intent intent) {
        boolean handled = false;
        boolean showStreams = intent.getBooleanExtra(EXTRA_VIEW_STREAMS, false);
        boolean showSurveys = intent.getBooleanExtra(EXTRA_VIEW_SURVEYS, false);

        Uri data = intent.getData();
        if (data != null && "/stream/authorized".equals(data.getPath())) {

            String packageName = data.getQueryParameter("packageName");
            String error = data.getQueryParameter("error");
            if (error != null) {
                StreamNotAuthorizedError e =
                        StreamNotAuthorizedError.newInstance(packageName, error);
                e.show(getSupportFragmentManager(), "stream_error");
                Log.d(TAG, "There was an error authorizing " + packageName + ": " + error);
            } else {
                Log.d(TAG, "Successfully authorized: " + packageName);
            }

            showStreams = true;
        }

        if (showStreams || showSurveys) {
            String page = showSurveys ? getString(R.string.home) : getString(R.string.streams);
            for (int i = 0; i < mNavigationItems.length; i++) {
                if(page.equals(mNavigationItems[i])) {
                    setFragment(i);
                    handled = true;
                    break;
                }
            }
        }
        return handled;
    }

    public void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        mDrawerToggle.syncState();
    }

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

        // Watch to make sure the account still exists.
        accountManager.addOnAccountsUpdatedListener(this, null, true);
    }

    @Override protected void onPause() {
        super.onPause();

        accountManager.removeOnAccountsUpdatedListener(this);
    }

    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        mDrawerToggle.onConfigurationChanged(newConfig);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        if (getString(R.string.reminders).equals(mNavigationItems[position])) {
            Intent intent = new Intent(this, TriggerListActivity.class);
            startActivity(intent);
        } else {
            setFragment(position);
        }
    }

    private void setFragment(int position) {
        // Set the fragment by the name
        setFragment(mNavigationItems[position]);

        // Highlight the selected item, update the title, and close the drawer
        mDrawerList.setItemChecked(position, true);
        if (position == 0) {
            setTitle(R.string.app_name);
        } else {
            setTitle(mNavigationItems[position]);
        }
        mDrawerLayout.closeDrawer(mDrawerList);
    }

    public void setFragment(String id) {
        // Find the correct fragment to show
        Fragment fragment = new HomeFragment();
        if (getString(R.string.ohmlets).equals(id)) {
            fragment = new OhmletsFragment();
        } else if (getString(R.string.surveys).equals(id)) {
            fragment = new SurveysFragment();
        } else if (getString(R.string.streams).equals(id)) {
            fragment = new StreamsFragment();
        }

        // Insert the fragment by replacing any existing fragment
        FragmentManager fragmentManager = getSupportFragmentManager();
        fragmentManager.beginTransaction().replace(R.id.content_frame, fragment).commit();
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (mDrawerToggle.onOptionsItemSelected(item)) {
            return true;
        }

        switch (item.getItemId()) {
            case R.id.action_settings:
                //TODO: show settings
                return true;

            case R.id.action_sign_out:
                mLoggingOut = true;
                FragmentManager fm = getSupportFragmentManager();
                LogoutTaskFragment logoutTaskFragment =
                        (LogoutTaskFragment) fm.findFragmentByTag("logout");

                // If the Fragment is non-null, then it is currently being
                // retained across a configuration change.
                if (logoutTaskFragment == null) {
                    logoutTaskFragment = new LogoutTaskFragment();
                    fm.beginTransaction().add(logoutTaskFragment, "logout").commit();
                }

                return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onBackPressed() {

        // This is a workaround for this bug
        // https://code.google.com/p/android/issues/detail?id=40323

        // If the fragment exists and has some back-stack entry
        Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.content_frame);
        if (fragment != null && fragment.getChildFragmentManager().getBackStackEntryCount() > 0) {
            // Get the fragment fragment manager - and pop the back stack
            fragment.getChildFragmentManager().popBackStack();
        } else {
            // otherwise let super handle the back press
            super.onBackPressed();
        }
    }

    @Override public void onAccountsUpdated(Account[] accounts) {
        for (Account account : accounts) {
            if (AuthUtil.ACCOUNT_TYPE.equals(account.type)) {
                return;
            }
        }

        // No ohmage accounts so start the authenticator activity
        Intent intent = new Intent(this, AuthenticatorActivity.class);
        if (mLoggingOut)
            intent.putExtra(AuthenticatorActivity.EXTRA_CLEAR_DEFAULT_ACCOUNT, true);
        startActivity(intent);
        finish();
    }

    public static class StreamNotAuthorizedError extends DialogFragment {

        public static StreamNotAuthorizedError newInstance(String packageName, String error) {
            StreamNotAuthorizedError fragment = new StreamNotAuthorizedError();
            Bundle args = new Bundle();
            args.putString("error", error);
            args.putString("packageName", packageName);
            fragment.setArguments(args);
            return fragment;
        }

        @Override public Dialog onCreateDialog(Bundle savedInstanceState) {
            String packageName = getArguments().getString("packageName");
            String error = getArguments().getString("error");
            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
            builder.setTitle(R.string.title_stream_not_authorized)
                    .setMessage(
                            getString(R.string.message_stream_not_authorized, packageName, error));
            builder.setPositiveButton(android.R.string.ok, null);
            return builder.create();
        }
    }
}
ohmage/src/main/java/org/ohmage/app/OhmageService.java
public interface OhmageService {

    @GET("/auth_token") AccessToken getAccessToken(@Query("refresh_token") String refreshToken)
            throws AuthenticationException;

    @GET("/auth_token")
    AccessToken getAccessToken(@Query("email") String email, @Query("password") String password)
            throws AuthenticationException;

    @GET("/auth_token")
    void getAccessToken(@Query("email") String email, @Query("password") String password,
            CancelableCallback<AccessToken> callback);

    //This path is checked in the OkClient to prevent redirects
    @GET("/oauth/authorize?response_type=code")
    Observable<String> OAuthAuthorize(@Query("client_id") String clientId, @Query("scope") String scope,
            @Query("state") String state);

    //This path is checked in the OkClient to prevent redirects
    @POST("/oauth/authorization_with_token")
    Observable<String> OAuthAuthorized(@Query("granted") boolean granted, @Query("code") String code);

    @GET("/oauth/clients/{client_id}")
    Observable<OAuthClient> OAuthClientInfo(@Path("client_id") String clientId);

    @POST("/people") void createUser(@Query("provider") AuthUtil.GrantType grantType,
            @Query("access_token") String accessToken, @Body User user,
            CancelableCallback<User> callback);

    @POST("/people") void createUser(@Query("password") String password, @Body User user,
            CancelableCallback<User> callback);

    @POST("/people") void createUser(@Query("password") String password, @Body User user,
            @Query("user_invitation_id") String inviteCode, CancelableCallback<User> callback);

    @GET("/people/{userId}/current/")
    Observable<User> getCurrentStateForUser(@Path("userId") String userId)
            throws AuthenticationException;

    @GET("/ohmlets")
    void searchOhmlets(@Query("query") String query, Callback<Ohmlets> ohmletsCallback);

    @GET("/ohmlets") void getOhmlets(Callback<Ohmlets> ohmletsCallback);

    @GET("/ohmlets/{ohmletId}") Observable<Ohmlet> getOhmlet(@Path("ohmletId") String id);

    @POST("/ohmlets/{ohmletId}/people")
    Response updateMemberForOhmlet(@Path("ohmletId") String ohmletId, @Body Member member)
            throws AuthenticationException;

    @POST("/ohmlets/{ohmletId}/people")
    Response updateMemberForOhmlet(@Path("ohmletId") String ohmletId, @Body Member member,
            @Query("ohmlet_invitation_id") String inviteCode) throws AuthenticationException;

    @DELETE("/ohmlets/{ohmletId}/people/{userId}")
    Response removeUserFromOhmlet(@Path("ohmletId") String ohmletId, @Path("userId") String userId)
            throws AuthenticationException;

    @GET("/surveys") void getSurveys(Callback<Surveys> surveysCallback);

    @GET("/surveys")
    Observable<List<Survey>> getDefaultSurveys();

    @GET("/surveys/{surveyId}/{surveyVersion}")
    Observable<Survey> getSurvey(@Path("surveyId") String surveyId,
            @Path("surveyVersion") String surveyVersion);

    @POST("/surveys/{surveyId}/{surveyVersion}/data")
    Observable<Response> uploadResponse(@Path("surveyId") String surveyId,
            @Path("surveyVersion") long surveyVersion, @Body ResponseTypedOutput data)
            throws AuthenticationException;

    @GET("/streams/{streamId}")
    Observable<Collection<Integer>> getStreamVersions(@Path("streamId") String streamId);

    @GET("/streams/{streamId}/{streamVersion}")
    Observable<Stream> getStream(@Path("streamId") String streamId,
            @Path("streamVersion") long streamVersion);

    @POST("/streams/{streamId}/{streamVersion}/data")
    Response uploadStreamData(@Path("streamId") String streamId,
            @Path("streamVersion") long streamVersion, @Body StreamWriterOutput data)
            throws AuthenticationException;

    // *** START OMH-DSU *** //
    @Headers(AuthUtil.OMH_AUTH_HEADER)
    @GET("/google-signin") AccessToken getAccessTokenWithCode(@Query("code") String code,
            @Query("client_id") String clientId) throws AuthenticationException;

    @Headers(AuthUtil.OMH_AUTH_HEADER)
    @GET("/google-signin") void getAccessTokenWithCode(@Query("code") String code,
            @Query("client_id") String clientId, CancelableCallback<AccessToken> callback);

    @Headers(AuthUtil.OMH_AUTH_HEADER)
    @FormUrlEncoded
    @POST("/oauth/token") AccessToken refreshAccessToken(@Field("refresh_token") String refreshToken, @Field("grant_type") String type)
            throws AuthenticationException;

    @POST("/dataPoints")
    Observable<Response> uploadDataPoint(@Body DataPointTypedOutput data)
            throws AuthenticationException;

    // *** END OMH-DSU *** //
    public abstract static class CancelableCallback<T> implements Callback<T> {

        private boolean mCancelled;

        public void cancel() {
            mCancelled = true;
        }

        public boolean isCancelled() {
            return mCancelled;
        }
    }
}
ohmage/src/main/java/org/ohmage/fragments/HomeFragment.java
public class HomeFragment extends GridFragment implements LoaderCallbacks<Cursor>,
        OnRefreshListener, SyncStatusObserver {

    @Inject AccountManager am;

    private static final String TAG = HomeFragment.class.getSimpleName();
    private PullToRefreshLayout mPullToRefreshLayout;

    private Object syncObserverHandle;
    private Handler mHandler = new Handler();
    private Runnable mRefreshCompleteRunnable = new Runnable() {
        @Override public void run() {
            if(getView() != null) {
                setEmptyText("No Surveys");
                mPullToRefreshLayout.setRefreshComplete();
            }
        }
    };

    @Override public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

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

        syncObserverHandle = ContentResolver.addStatusChangeListener(
                ContentResolver.SYNC_OBSERVER_TYPE_PENDING |
                ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, this
        );

        checkSyncStatus();
    }

    @Override public void onPause() {
        super.onPause();
        if(syncObserverHandle != null) {
            ContentResolver.removeStatusChangeListener(syncObserverHandle);
            syncObserverHandle = null;
        }
    }

    @Override public void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacks(mRefreshCompleteRunnable);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        setEmptyText("No surveys");

        setGridShown(false);

        getLoaderManager().initLoader(0, null, this);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.refreshable_grid_layout, container, false);

        // Now find the PullToRefreshLayout to setup
        mPullToRefreshLayout = (PullToRefreshLayout) view.findViewById(R.id.ptr_layout);

        // Now setup the PullToRefreshLayout
        ActionBarPullToRefresh.from(getActivity())
                // Mark All Children as pullable
                .allChildrenArePullable()
                        // Set the OnRefreshListener
                .listener(this)
                .theseChildrenArePullable(R.id.grid, R.id.internalEmpty)
                .useViewDelegate(TextView.class, new ViewDelegate() {
                    @Override public boolean isReadyForPull(View view, float v, float v2) {
                        return true;
                    }
                })
                .useViewDelegate(GridView.class, new AbsListViewDelegate())
                        // Finally commit the setup to our PullToRefreshLayout
                .setup(mPullToRefreshLayout);

        return view;
    }

    @Override
    public SurveyAdapter getListAdapter() {
        return (SurveyAdapter) super.getListAdapter();
    }

    @Override
    public void onGridItemClick(GridView g, View v, int position, long id) {
        Cursor cursor = getListAdapter().getItem(position).getData();
        if (cursor != null) {
            startActivity(new Intent(Intent.ACTION_VIEW, OhmageContract.Surveys
                    .getUriForSurveyIdVersion(cursor.getString(0), cursor.getString(1))));
        }
    }

    @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        return new CursorLoader(getActivity(), OhmageContract.Surveys.CONTENT_URI, new String[]{
                Surveys.SURVEY_ID, Surveys.SURVEY_VERSION, Surveys.SURVEY_NAME,
                Surveys.SURVEY_DESCRIPTION, Surveys.SURVEY_PENDING_TIME,
                Surveys.SURVEY_PENDING_TIMEZONE}, null, null,
                Surveys.SURVEY_PENDING_TIME + " desc"
        );
    }

    @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) {

        ArrayList<Item> items = new ArrayList<Item>();
        boolean pivot = true;
        data.moveToPosition(-1);
        while (data.moveToNext()) {
            if (items.isEmpty() && data.getLong(4) != Reminders.NOT_PENDING) {
                pivot = false;
                items.add(new Item("Pending"));
            } else if (!items.isEmpty() && data.getLong(4) == Reminders.NOT_PENDING && !pivot) {
                pivot = true;
                items.add(new Item("Surveys"));
            }
            items.add(new Item(data, 0));
        }

        SurveyAdapter adapter = getListAdapter();
        if (adapter == null) {
            adapter = new SurveyAdapter(getActivity(), items);
            setListAdapter(adapter);
        } else {
            adapter.setData(items);
        }

        setGridShown(true);
    }

    @Override public void onLoaderReset(Loader<Cursor> loader) {
        getListAdapter().setData(null);
    }

    @Override public void onRefreshStarted(View view) {
        Account[] accounts = am.getAccountsByType(AuthUtil.ACCOUNT_TYPE);
        for (Account account : accounts) {
            Bundle bundle = new Bundle();
            bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
            ContentResolver.requestSync(account, OhmageContract.CONTENT_AUTHORITY, bundle);
        }
    }

    @Override public void onStatusChanged(int which) {
        checkSyncStatus();
    }

    public void checkSyncStatus() {
        Account[] accounts = am.getAccountsByType(AuthUtil.ACCOUNT_TYPE);
        if(accounts.length == 0) {
            return;
        }

        mHandler.removeCallbacks(mRefreshCompleteRunnable);
        if(ContentResolver.isSyncActive(accounts[0], OhmageContract.CONTENT_AUTHORITY) ||
           ContentResolver.isSyncPending(accounts[0], OhmageContract.CONTENT_AUTHORITY)) {
            getActivity().runOnUiThread(new Runnable() {
                @Override public void run() {
                    if(getListAdapter() == null || getListAdapter().isEmpty()) {
                        mPullToRefreshLayout.setRefreshing(true);
                        setEmptyText("Please wait for Surveys to Synchronize...");
                    }
                }
            });
        } else {
            mHandler.postDelayed(mRefreshCompleteRunnable, 500);
        }
    }

    private static class Item {
        private int dataPosition = -1;

        /**
         * The index of the id field for the cursor
         */
        private final int idIdex;

        private final Cursor data;
        private final String header;

        public Item(String header) {
            this(header, null, -1);
            if (header == null) {
                throw new IllegalArgumentException("the header string can't be null");
            }
        }

        public Item(Cursor data, int idIdx) {
            this(null, data, idIdx);
            if (data == null) {
                throw new IllegalArgumentException("the cursor data shouldn't be null");
            }
        }

        private Item(String header, Cursor data, int idIdx) {
            this.header = header;
            this.data = data;
            if (data != null) {
                this.dataPosition = data.getPosition();
            } else {
                this.dataPosition = -1;
            }
            this.idIdex = idIdx;
        }

        public boolean isHeader() {
            return data == null;
        }

        public Cursor getData() {
            data.moveToPosition(dataPosition);
            return data;
        }

        public long getItemId() {
            return data == null ? header.hashCode() : getData().getString(0).hashCode();
        }
    }

    public static class SurveyAdapter extends BaseAdapter {
        private static final int HEADER = 0;
        private static final int DATA = 1;

        private final LayoutInflater mInflater;
        private final Context mContext;
        private ArrayList<Item> mData;

        private final DateTimeFormatter mTimeFormatter;

        public SurveyAdapter(Context context, ArrayList<Item> data) {
            super();
            mContext = context;
            mData = data;
            mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            mTimeFormatter = DateTimeFormat.shortTime().withZone(DateTimeZone.getDefault());
        }

        public boolean areAllItemsEnabled() {
            return false;
        }

        public boolean isEnabled(int position) {
            return !getItem(position).isHeader();
        }

        public int getItemViewType(int position) {
            return getItem(position).isHeader() ? HEADER : DATA;
        }

        public int getViewTypeCount() {
            return 2;
        }

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

        @Override public Item getItem(int position) {
            return mData.get(position);
        }

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

        @Override public View getView(int position, View convertView, ViewGroup parent) {
            Item item = getItem(position);
            if (convertView == null) {
                if (item.isHeader()) {
                    convertView = new TextView(mContext);
                } else {
                    convertView = mInflater.inflate(R.layout.list_item_local_survey, parent, false);
                }
            }

            if (item.isHeader()) {
                ((TextView) convertView).setText(item.header);
            } else {
                TextView name = ViewHolder.get(convertView, R.id.name);
                TextView description = ViewHolder.get(convertView, R.id.description);
                TextView pending = ViewHolder.get(convertView, R.id.pendingTime);

                Cursor data = item.getData();

                name.setText(data.getString(2));
                description.setText(data.getString(3));
                long pendingTime = data.getLong(4);
                String zone = data.getString(5);
                pending.setText(
                        mTimeFormatter.withZone(DateTimeZone.forID(zone)).print(pendingTime));
                pending.setVisibility(
                        pendingTime == Reminders.NOT_PENDING ? View.GONE : View.VISIBLE);
            }

            return convertView;
        }

        public void setData(ArrayList<Item> data) {
            if (data == null) {
                mData.clear();
            } else {
                mData = data;
            }
            notifyDataSetChanged();
        }
    }
}
ohmage/src/main/java/org/ohmage/auth/AuthHelper.java
public class AuthHelper {

    private final Context mContext;

    public AuthHelper(Context context) {
        mContext = context;
    }

    public String googleAuthGetToken(String googleAccount) throws UserRecoverableAuthException,
            IOException, GoogleAuthException {
        String code = GoogleAuthUtil.getToken(mContext, googleAccount, AuthUtil.Google.CODE_SCOPE, null);
        // clear it immediately to avoid stale code from being cached
        GoogleAuthUtil.clearToken(mContext, code);
        return "fromApp_" + code;
    }
}
ohmage/src/androidTest/java/org/ohmage/app/MainActivityOhmletsTest.java
@LargeTest
public class MainActivityOhmletsTest extends InjectedActivityInstrumentationTestCase<MainActivity> {

    @Inject AccountManager fakeAccountManager;
    @Inject OhmageService fakeOhmageService;

    Account fakeAccount = new Account("username", AuthUtil.ACCOUNT_TYPE);

    public MainActivityOhmletsTest() {
        super(MainActivity.class);
    }

    @Override
    public void setUp() throws Exception {
        super.setUp();

        when(fakeAccountManager.getAccountsByType(AuthUtil.ACCOUNT_TYPE))
                .thenReturn(new Account[]{fakeAccount});
        MainActivity activity = getActivity();

        // Open the ohmlets page
        activity.setFragment(activity.getString(R.string.ohmlets));
    }

    public void testClickingSearchIcon_initialState_showsSearchTextBox() {
        onView(withId(R.id.search_button)).perform(click());
        onView(withId(R.id.search_src_text))
                .check(matches(isDisplayed()))
                .check(matches(hasFocus()));
    }

    public void testTypingText_inSearchBox_sendsTheCorrectQueryToTheNetwork()
            throws InterruptedException {
        onView(withId(R.id.search_button)).perform(click());
        String query = "a";

        onView(withId(R.id.search_src_text)).perform(typeText(query)).perform();
        // Wait for a few milliseconds so the request will actually happen
        Thread.sleep(250);

        verify(fakeOhmageService).searchOhmlets(eq(query), any(Callback.class));
    }

    public void testReceivingList_withItems_showsItemsInList() {
        onView(withId(R.id.search_button)).perform(click());
        String query = "a";
        final Ohmlets ohmlets = new Ohmlets();
        addFakeOhmlet(ohmlets);
        doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
                Callback<Ohmlets> cb = (Callback<Ohmlets>) invocationOnMock.getArguments()[1];
                cb.success(ohmlets, null);
                return null;
            }
        }).when(fakeOhmageService).searchOhmlets(eq(query), any(Callback.class));

        onView(withId(R.id.search_src_text)).perform(typeText(query));
    }
//
//    public void testTypingText_noPreviousQuery_showsSpinnerWhileRequestIsBeingMade() {
//
//    }
//
//    public void testTypingText_noDataForPreviousQuery_showsSpinnerWhileRequestIsBeingMade() {
//
//    }
//
//    public void testQuicklyTypingText_searchBoxIsSelected_onlySendsOneNetworkRequestAtTheEnd() {
//
//    }
//
//    public void testTypingText_afterPreviousQueryMadeWithReturnedData_continuesToShowOldData() {
//
//    }
//
//    public void testTypingText_afterPreviousQueryMadeWithReturnedData_showsNewItemsAfterTheRequestIsComplete() {
//
//    }

    private void addFakeOhmlet(Ohmlets ohmlets) {
        Ohmlet p = new Ohmlet();
        p.ohmletId = ohmlets.size() + 1 + "";
        p.name = "Blah Ohmlet";
        p.description = "This is a fake ohmlet to collect blah data";
        ohmlets.add(p);
    }
}
ohmage/src/main/java/org/ohmage/operators/ContentProviderSaver.java
public class ContentProviderSaver implements Action1<Savable> {
    private static final String TAG = ContentProviderSaver.class.getSimpleName();

    private final boolean mIsSyncAdapter;

    @Inject Gson gson;

    public ContentProviderSaver() {
        this(false);
    }

    public ContentProviderSaver(boolean isSyncAdapter) {
        Ohmage.app().getApplicationGraph().inject(this);
        mIsSyncAdapter = isSyncAdapter;
    }

    @Override public void call(Savable savable) {
        Uri uri = savable.getUrl();
        if (mIsSyncAdapter)
            uri = OhmageSyncAdapter.appendSyncAdapterParam(uri);
        Ohmage.app().getContentResolver().insert(uri, savable.toContentValues(this));
        savable.onSaved();
    }

    public Gson gson() {
        return gson;
    }

    public static interface Savable {
        ContentValues toContentValues(ContentProviderSaver saver);

        Uri getUrl();

        void onSaved();
    }

    public static class ContentProviderSaverSubscriber extends Subscriber<Savable> {

        private final boolean mIsSyncAdapter;

        public ContentProviderSaverSubscriber(boolean isSyncAdapter) {
            mIsSyncAdapter = isSyncAdapter;
        }

        @Override public void onCompleted() {

        }

        @Override public void onError(Throwable e) {
            e.printStackTrace();
        }

        @Override public void onNext(Savable args) {
            new ContentProviderSaver(mIsSyncAdapter).call(args);
        }
    }
}
ohmage/src/main/java/org/ohmage/auth/Authenticator.java
public class Authenticator extends AbstractAccountAuthenticator {

    @Inject AccountManager am;
    @Inject OhmageService ohmageService;
    @Inject AuthHelper authHelper;

    /**
     * This is the google email of an account that can be used for authentication
     */
    public static final String USER_DATA_GOOGLE_ACCOUNT = "user_data_google_account";

    /**
     * This is set when the password for the account is the users actual password rather than
     * a refreshToken. This happens if the user has not verified their account yet.
     */
    public static final String USE_PASSWORD = "use_password";

    /**
     * The user id for the account as provided by the server.
     */
    public static final String USER_ID = "user_id";

    // Authentication Service context
    private final Context mContext;

    public Authenticator(Context context) {
        super(context);
        mContext = context;
        Ohmage.app().getApplicationGraph().inject(this);
    }

    @Override
    public Bundle addAccount(AccountAuthenticatorResponse response,
            String accountType, String authTokenType, String[] requiredFeatures,
            Bundle options) {
        // TODO: decide if we should allow more than one account, and if not, make it clear to the
        // user that they need to logout and login with a new account

        final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
        intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, accountType);
        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
        final Bundle bundle = new Bundle();
        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
        return bundle;
    }

    @Override
    public Bundle confirmCredentials(AccountAuthenticatorResponse response,
            Account account, Bundle options) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Bundle editProperties(AccountAuthenticatorResponse response,
            String accountType) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
            String authTokenType, Bundle loginOptions)
            throws NetworkErrorException {


        // Extract the email and refresh_token from the Account Manager, and ask
        // the server for a new refresh_token.
        String authToken = am.peekAuthToken(account, authTokenType);

        // Lets give another try to authenticate the user
        AccessToken token = null;
        UserRecoverableAuthException userRecoverableAuthException = null;
        Log.e(Authenticator.class.getSimpleName(), "Get Auth token " + authToken + " Refresh " + am.getPassword(account));
        if (TextUtils.isEmpty(authToken)) {
            final String refreshToken = am.getPassword(account);

            if (refreshToken != null) {

                try {
                    // If the account credentials have not gone to the server yet we saved the
                    // password for the user
                    if (Boolean.parseBoolean(am.getUserData(account, USE_PASSWORD))) {
                        token = ohmageService.getAccessToken(account.name, refreshToken);
                        if (token != null) {
                            am.setUserData(account, Authenticator.USE_PASSWORD,
                                    String.valueOf(false));
                            am.setUserData(account, Authenticator.USER_ID, token.getUserId());
                        }
                    } else {
                        // refresh token
                        if(Ohmage.USE_DSU_DATAPOINTS_API) {
                            token = ohmageService.refreshAccessToken(refreshToken, "refresh_token");
                        }else {
                            token = ohmageService.getAccessToken(refreshToken);
                        }
                    }
                } catch (AuthenticationException e) {
                    // This will happen if the refresh token was already used, or it was
                    // invalidated or something

                    // We can try getting the token from google
                    String googleAccount = am.getUserData(account, USER_DATA_GOOGLE_ACCOUNT);
                    if (googleAccount != null) {
                        try {
                            token = getTokenFromGoogle(googleAccount);
                        } catch (UserRecoverableAuthException e1) {
                            userRecoverableAuthException = e1;
                        }
                    }
                } catch (RetrofitError e) {
                    if (e.getResponse() != null && e.getResponse().getStatus() == 409) {
                        // The user hasn't activated their account by clicking the link
                        final Bundle bundle = new Bundle();
                        bundle.putString(AccountManager.KEY_AUTH_FAILED_MESSAGE,
                                Ohmage.app().getString(R.string.account_not_activated));
                        bundle.putParcelable(AccountManager.KEY_INTENT, new Intent(mContext,
                                AccountNotActivatedDialog.class));
                        return bundle;
                    } else {
                        throw new NetworkErrorException();
                    }
                } catch(Exception e){
                    Log.e(Authenticator.class.getSimpleName(), "", e);
                }
            }
        }

        // If we get an authToken - we return it
        if (token != null) {
            am.setPassword(account, token.getRefreshToken());
            authToken = token.getAccessToken();
        }

        if (!TextUtils.isEmpty(authToken)) {
            final Bundle result = new Bundle();
            result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
            result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
            result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
            return result;
        }

        // If we get here, then we couldn't access the user's password - so we
        // need to re-prompt them for their credentials. We do that by creating
        // an intent to display our AuthenticatorActivity.
        final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
        intent.putExtra(AuthenticatorActivity.EXTRA_FROM_AUTHENTICATOR, true);
        intent.putExtra(AuthenticatorActivity.EXTRA_HANDLE_USER_RECOVERABLE_ERROR,
                userRecoverableAuthException);
        final Bundle bundle = new Bundle();
        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
        return bundle;
    }

    private AccessToken getTokenFromGoogle(String googleAccount)
            throws NetworkErrorException, UserRecoverableAuthException {
        // We can check for a google account to automatically update the refresh token

        try {
            String googleToken = authHelper.googleAuthGetToken(googleAccount);
            return ohmageService.getAccessTokenWithCode(googleToken, AuthUtil.OMH_CLIENT_ID);

        } catch (UserRecoverableAuthException userAuthEx) {
            throw userAuthEx;
        } catch (GoogleAuthException authEx) {
            // We can't really deal with this.. hopefully it doesn't happen
        } catch (RetrofitError e) {
            if (e.isNetworkError()) {
                throw new NetworkErrorException();
            }
            //TODO: what should we catch for retrofit exceptions?
        } catch (IOException transientEx) {
            throw new NetworkErrorException();
        } catch (AuthenticationException e) {
            //TODO: what should we do if the token is invalid?
        }
        return null;
    }

    @Override
    public String getAuthTokenLabel(String authTokenType) {
        if (authTokenType.equals(AuthUtil.AUTHTOKEN_TYPE)) {
            return mContext.getString(R.string.app_name);
        }
        return null;
    }

    @Override
    public Bundle hasFeatures(AccountAuthenticatorResponse response,
            Account account, String[] features) {
        final Bundle result = new Bundle();
        result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
        return result;
    }

    @Override
    public Bundle updateCredentials(AccountAuthenticatorResponse response,
            Account account, String authTokenType, Bundle loginOptions) {
        throw new UnsupportedOperationException();
    }
}
ohmage/src/main/java/org/ohmage/auth/CreateAccountFragment.java
public class CreateAccountFragment extends TransitionFragment {

    private static final String TAG = CreateAccountFragment.class.getSimpleName();

    @Inject OhmageService ohmageService;

    // UI references.
    private EditText mFullnameView;
    private EditText mEmailView;
    private EditText mPasswordView;

    private User mUser = new User();

    /**
     * Stores the password for the user if it is an ohmage account
     */
    private String mPassword;

    /**
     * The grant type should be set to determine which views will be shown to the user.
     */
    private AuthUtil.GrantType mGrantType;

    /**
     * Callbacks for the activity to handle create button pressed
     */
    private Callbacks mCallbacks;

    public CreateAccountFragment() {
        setDefaultAnimation(R.anim.slide_in_top, R.anim.slide_out_top);
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        mCallbacks = (Callbacks) activity;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }

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

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_create_account, container, false);

        // Set up the login form.
        mFullnameView = (EditText) view.findViewById(R.id.fullname);
        mEmailView = (EditText) view.findViewById(R.id.email);
        mPasswordView = (EditText) view.findViewById(R.id.password);
        mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) {
                if (id == R.id.create_account || id == EditorInfo.IME_NULL) {
                    attemptAccountCreate();
                    return true;
                }
                return false;
            }
        });

        mEmailView.setText(mUser.email);
        mFullnameView.setText(mUser.fullName);

        view.findViewById(R.id.create_account_button)
                .setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        attemptAccountCreate();
                    }
                });

        setViewState(view);

        return view;
    }

    /**
     * Sets the visibility state of the prompts based on the grant type
     *
     * @param view
     */
    private void setViewState(View view) {
        if (view != null) {
            view.findViewById(R.id.ohmage_account_prompts).setVisibility(
                    (showOhmageAccountPrompts()) ? View.VISIBLE : View.GONE);
            ((TextView) view.findViewById(R.id.header_text)).setText((showOhmageAccountPrompts())
                    ? R.string.create_ohmage_account_title
                    : R.string.common_signin_button_text_long);
        }
    }

    /**
     * Called when the user clicks the create button. This will callback to the activity if
     * the values supplied by the user are valid.
     */
    public void attemptAccountCreate() {

        mEmailView.setError(null);

        String fullName = mFullnameView.getText().toString();
        mPassword = mPasswordView.getText().toString();
        String email = mEmailView.getText().toString();

        boolean cancel = false;
        View focusView = null;

        // Check for a valid email.
        if (showOhmageAccountPrompts() && TextUtils.isEmpty(email)) {
            mEmailView.setError(getString(R.string.error_field_required));
            focusView = mEmailView;
            cancel = true;
        } else if (showOhmageAccountPrompts() && !isValidEmail(email)) {
            mEmailView.setError(getString(R.string.error_invalid_email_address));
            focusView = mEmailView;
            cancel = true;
        } else if (showOhmageAccountPrompts() && TextUtils.isEmpty(mPassword)) {
            mPasswordView.setError(getString(R.string.error_field_required));
            focusView = mPasswordView;
            cancel = true;
        }

        if (cancel) {
            // There was an error; don't attempt login and focus the first
            // form field with an error.
            focusView.requestFocus();
        } else {
            mUser.fullName = TextUtils.isEmpty(fullName) ? null : fullName;
            mUser.email = TextUtils.isEmpty(email) ? null : email;

            // First notify the activity that the account is being created
            if (mCallbacks != null) {
                mCallbacks.onCreateAccount();
            }

            // Either ask for the token or start to create the account
            if (!showOhmageAccountPrompts()) {
                mCallbacks.fetchToken(new UseToken() {
                    @Override
                    public void useToken(String token) {
                        createAccount(token);
                    }
                });
            } else {
                createAccount(mPassword);
            }
        }
    }

    /**
     * Just a simple check for now to see if there is an at sign and a period
     *
     * @param email
     * @return true if the email is valid enough..
     */
    private boolean isValidEmail(String email) {
        return Patterns.EMAIL_ADDRESS.matcher(email).matches();
    }

    /**
     * Make the network call to create a user account
     *
     * @param token
     */
    private void createAccount(final String token) {
        if (mGrantType == AuthUtil.GrantType.CLIENT_CREDENTIALS) {
            CancelableCallback<User> callback = new CancelableCallback<User>() {
                @Override public void success(User user, Response response) {
                    ((AuthenticatorActivity) getActivity()).createAccount(user, token);
                }

                @Override public void failure(RetrofitError error) {
                    ((AuthenticatorActivity) getActivity()).onRetrofitError(error);
                }
            };

            String emailVerificationCode = getActivity().getIntent().getStringExtra(
                    AuthenticatorActivity.EXTRA_USER_INVITATION_CODE);
            if (emailVerificationCode != null)
                ohmageService.createUser(token, mUser, emailVerificationCode, callback);
            else
                ohmageService.createUser(token, mUser, callback);
        } else {
            ohmageService.createUser(mGrantType, token, mUser,
                    new OhmageService.CancelableCallback<User>() {
                        @Override public void success(final User user, Response response) {
                            mCallbacks.fetchToken(new UseToken() {
                                @Override
                                public void useToken(String token) {
                                    ohmageService.getAccessTokenWithCode(token, AuthUtil.OMH_CLIENT_ID,
                                            new OhmageService.CancelableCallback<AccessToken>() {
                                                @Override
                                                public void success(AccessToken accessToken,
                                                        Response response) {
                                                    ((AuthenticatorActivity) getActivity())
                                                            .createAccount(user.email,
                                                                    accessToken);
                                                }

                                                @Override public void failure(RetrofitError error) {
                                                    ((AuthenticatorActivity) getActivity())
                                                            .onRetrofitError(error);
                                                }
                                            });
                                }
                            });
                        }

                        @Override public void failure(RetrofitError error) {
                            ((AuthenticatorActivity) getActivity()).onRetrofitError(error);
                        }
                    });
        }
    }

    /**
     * Set the default full name value
     *
     * @param fullName
     */
    public void setFullName(String fullName) {
        if (!TextUtils.isEmpty(fullName))
            mUser.fullName = fullName;
    }

    /**
     * Check if we should show the ohmage account specific prompts
     *
     * @return true if we are creating an ohmage account
     */
    private boolean showOhmageAccountPrompts() {
        return mGrantType == AuthUtil.GrantType.CLIENT_CREDENTIALS;
    }

    /**
     * Set the grant type for this fragment
     *
     * @param grantType
     */
    public void setGrantType(AuthUtil.GrantType grantType) {
        mGrantType = grantType;
        setViewState(getView());
    }

    public void setEmail(String email) {
        if (!TextUtils.isEmpty(email))
            mUser.email = email;
    }

    public static interface Callbacks {
        /**
         * Called when the user clicks the create account button if we are performing an action
         * which requires the calling activity to fetch the token. The parent activity must
         * call {@link #createAccount(String)} with the token.
         */
        void fetchToken(UseToken useToken);

        /**
         * Called when the account is actually being created
         */
        void onCreateAccount();
    }

    public static interface UseToken {
        /**
         * A callback that the activity can use to send a token to this fragment
         *
         * @param token
         */
        void useToken(String token);
    }
}
ohmage/src/main/java/org/ohmage/sync/OhmageSyncAdapter.java
public class OhmageSyncAdapter extends AbstractThreadedSyncAdapter {

    public static final String IS_SYNCADAPTER = "is_syncadapter";

    private static final int NOTIFICATION_STREAM_APPS_ID = 0;
    private static final int NOTIFICATION_REMOTE_APPS_ID = 1;

    @Inject AccountManager am;

    @Inject OhmageService ohmageService;

    @Inject Gson gson;

    private static final String TAG = OhmageSyncAdapter.class.getSimpleName();
    private SyncResult mSyncResult;

    /**
     * Set up the sync adapter
     */
    public OhmageSyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
        Ohmage.app().getApplicationGraph().inject(this);
    }

    /**
     * Set up the sync adapter. This form of the
     * constructor maintains compatibility with Android 3.0
     * and later platform versions
     */
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public OhmageSyncAdapter(
            Context context,
            boolean autoInitialize,
            boolean allowParallelSyncs) {
        super(context, autoInitialize, allowParallelSyncs);
        Ohmage.app().getApplicationGraph().inject(this);
    }

    @Override
    public void onPerformSync(Account account, Bundle extras, String authority,
            final ContentProviderClient provider, final SyncResult syncResult) {

        mSyncResult = syncResult;

        // Check for authtoken
        String token = null;
        try {
            token = am.blockingGetAuthToken(account, AuthUtil.AUTHTOKEN_TYPE, true);
        } catch (OperationCanceledException e) {
            syncResult.stats.numSkippedEntries++;
        } catch (IOException e) {
            syncResult.stats.numIoExceptions++;
        } catch (AuthenticatorException e) {
            syncResult.stats.numAuthExceptions++;
        }

        // If the token wasn't found or there was a problem, we can stop now
        if (token == null || syncResult.stats.numSkippedEntries > 0 ||
            syncResult.stats.numIoExceptions > 0 || syncResult.stats.numAuthExceptions > 0) {
            return;
        }

        String userId = am.getUserData(account, Authenticator.USER_ID);

        try {
            if (!Ohmage.USE_DSU_DATAPOINTS_API || userId != null) {
                synchronizeOhmlets(userId, provider);
                // TODO: download streams and surveys that are not part of ohmlets

                // Don't continue if there were already errors
                if (mSyncResult.stats.numIoExceptions > 0 ||
                    mSyncResult.stats.numAuthExceptions > 0) {
                    return;
                }
                synchronizeData(userId, provider);
            } else {
                downloadSurveys(provider);
            }
        } catch (AuthenticationException e) {
            Log.e(TAG, "Error authenticating user", e);
            syncResult.stats.numAuthExceptions++;
        } catch (RemoteException e) {
            Log.e(TAG, "Error synchronizing account", e);
            syncResult.stats.numIoExceptions++;
        } catch (InterruptedException e) {
            Log.e(TAG, "Error synchronizing account", e);
            syncResult.stats.numIoExceptions++;
        }
    }

    private void synchronizeOhmlets(final String userId, ContentProviderClient provider)
            throws AuthenticationException, RemoteException, InterruptedException {
        Log.d(TAG, "state of ohmlets sync");

        final CountDownLatch upload;

        // First, sync ohmlet join state. As described by the people field.
        Cursor cursor = null;
        try {
            cursor = provider.query(OhmageContract.Ohmlets.CONTENT_URI,
                    new String[]{
                            OhmageContract.Ohmlets.OHMLET_ID,
                            OhmageContract.Ohmlets.OHMLET_MEMBERS},
                    Ohmlets.OHMLET_DIRTY + "=1", null, null
            );

            upload = new CountDownLatch(cursor.getCount());

            while (cursor.moveToNext()) {
                Member.List members = gson.fromJson(cursor.getString(1), Member.List.class);
                Member m = members.getMember(userId);
                if (m == null) {
                    m = members.getMember("me");
                    if (m != null) {
                        m.memberId = userId;
                    }
                }
                final Member localMember = m;
                ohmageService.getOhmlet(cursor.getString(0)).first(
                        new Func1<Ohmlet, Boolean>() {
                            @Override public Boolean call(Ohmlet ohmlet) {
                                Member remoteMember = ohmlet.people.getMember(userId);
                                try {
                                    if (localMember != null) {
                                        if (remoteMember == null ||
                                            localMember.role != remoteMember.role) {
                                            // Check for join verification code to send
                                            if (localMember.code != null) {
                                                String code = localMember.code;
                                                localMember.code = null;
                                                ohmageService.updateMemberForOhmlet(ohmlet.ohmletId,
                                                        localMember, code);
                                            } else {
                                                ohmageService.updateMemberForOhmlet(ohmlet.ohmletId,
                                                        localMember);
                                            }
                                        }
                                    }
                                    if (localMember == null && remoteMember != null) {
                                        ohmageService.removeUserFromOhmlet(ohmlet.ohmletId, userId);
                                    }
                                } catch (AuthenticationException e) {
                                    Log.e(TAG, "Error authenticating user", e);
                                    mSyncResult.stats.numAuthExceptions++;
                                } catch (RetrofitError e) {
                                    Log.e(TAG, "Error synchronizing ohmlet member state", e);
                                    mSyncResult.stats.numIoExceptions++;
                                }
                                return true;
                            }
                        }
                ).finallyDo(new Action0() {
                    @Override public void call() {
                        upload.countDown();
                    }
                }).subscribe();
            }
            cursor.close();

            // Wait for the upload sync operation to finish before downloading the user state
            upload.await();

        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    private void downloadSurveys(ContentProviderClient provider)
            throws AuthenticationException, RemoteException, InterruptedException {

        final CountDownLatch download = new CountDownLatch(1);

        Observable<List<Survey>> surveyList = ohmageService.getDefaultSurveys();

        Observable<Survey> surveys = surveyList.flatMap(new Func1<List<Survey>, Observable<Survey>>() {
            @Override public Observable<Survey> call(List<Survey> surveys) {
                return Observable.from(surveys);
            }
        });

        surveyList.subscribe(new SyncSubscriber<List<Survey>>(mSyncResult,
                new ContentProviderStateSyncSubscriber(Surveys.CONTENT_URI, true)));

        surveys.subscribe(
                new SyncSubscriber<Survey>(mSyncResult, new ContentProviderSaverSubscriber(true)));

        Observable.merge(surveyList, surveys).last()
                .finallyDo(new Action0() {
                    @Override public void call() {
                        download.countDown();
                    }
                }).subscribe();

        // Wait for any async download operations to finish
        download.await();
    }

    private void synchronizeData(String userId, ContentProviderClient provider)
            throws AuthenticationException, RemoteException, InterruptedException {

        final CountDownLatch download = new CountDownLatch(1);

        Observable<Ohmlet> current = ohmageService.getCurrentStateForUser(userId).flatMap(
                new Func1<User, Observable<Ohmlet>>() {
                    @Override
                    public Observable<Ohmlet> call(User user) {
                        return Observable.from(user.ohmlets);
                    }
                }
        ).cache();

        // Synchronize the current ohmlets to the db (this removes other ohmlets)
        current.toList().subscribe(new SyncSubscriber<List<Ohmlet>>(mSyncResult,
                new ContentProviderStateSyncSubscriber(Ohmlets.CONTENT_URI, true)));

        Observable<Ohmlet> refreshedOhmlets = current.flatMap(new RefreshOhmlet());
        refreshedOhmlets.subscribe(
                new SyncSubscriber<Ohmlet>(mSyncResult, new ContentProviderSaverSubscriber(true)));


        Observable<Survey> surveys = current.flatMap(new SurveysFromOhmlet());
        surveys.toList().subscribe(new SyncSubscriber<List<Survey>>(mSyncResult,
                new ContentProviderStateSyncSubscriber(Surveys.CONTENT_URI, true)));


        Observable<Survey> refreshedSurveys =
                surveys.filter(new FilterUpToDateSurveys(provider))
                        .flatMap(new RefreshSurvey());
        refreshedSurveys.subscribe(
                new SyncSubscriber<Survey>(mSyncResult, new ContentProviderSaverSubscriber(true)));
        refreshedSurveys.subscribe(new SyncSubscriber<Survey>(mSyncResult,
                new Subscriber<Survey>() {
                    public ApkSet surveys = new ApkSet();

                    @Override public void onCompleted() {
                        showInstallSurveyApkNotification(surveys);
                    }

                    @Override public void onError(Throwable e) {
                        showInstallSurveyApkNotification(surveys);
                    }

                    @Override public void onNext(Survey survey) {
                        for (Prompt prompt : survey.surveyItems) {
                            if (prompt instanceof RemotePrompt &&
                                ((RemotePrompt) prompt).getApp() != null &&
                                !((RemotePrompt) prompt).getApp()
                                        .isInstalled(getContext())) {
                                surveys.add(((RemotePrompt) prompt).getApp());
                                return;
                            }
                        }
                    }
                }
        ));

        Observable<Stream> streams = current.flatMap(new StreamsFromOhmlet());
        streams.toList().subscribe(new SyncSubscriber<List<Stream>>(mSyncResult,
                new ContentProviderStateSyncSubscriber(Streams.CONTENT_URI, true)));


        Observable<Stream> refreshedStreams =
                streams.filter(new FilterUpToDateStreams(provider)).flatMap(
                        new RefreshStream());
        refreshedStreams.subscribe(
                new SyncSubscriber<Stream>(mSyncResult, new ContentProviderSaverSubscriber(true)));
        refreshedStreams.subscribe(
                new SyncSubscriber<Stream>(mSyncResult, new Subscriber<Stream>() {
                    public ApkSet streams = new ApkSet();

                    @Override public void onCompleted() {
                        showInstallStreamApkNotification(streams);
                    }

                    @Override public void onError(Throwable e) {
                        showInstallStreamApkNotification(streams);
                    }

                    @Override public void onNext(Stream stream) {
                        if (!stream.app.isInstalled(getContext())) {
                            stream.app.android.appName = stream.name;
                            streams.add(stream.app);
                        }
                    }
                })
        );

        Observable.merge(current, refreshedStreams, refreshedSurveys, refreshedOhmlets).last()
                .finallyDo(new Action0() {
                    @Override public void call() {
                        download.countDown();
                    }
                }).subscribe();

        // Wait for any async download operations to finish
        download.await();
    }

    public void showInstallStreamApkNotification(ApkSet streams) {
        if (streams == null || streams.isEmpty()) {
            return;
        }

        Intent resultIntent = new Intent(getContext(), MainActivity.class);
        resultIntent.putExtra(MainActivity.EXTRA_VIEW_STREAMS, true);
        Builder builder = new Builder(getContext());

        if (streams.size() > 1 && VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
            builder.setSmallIcon(R.drawable.stat_notify_update_collapse).setContentText(getContext()
                    .getString(R.string.install_multiple_apps_for_stream_message, streams.size()));
        } else {
            builder.setSmallIcon(R.drawable.stat_notify_update).setContentText(getContext()
                    .getString(R.string.install_app_for_stream_message,
                            streams.iterator().next().getAppName()));
        }

        builder.setContentTitle(getContext().getString(R.string.install_apps_for_stream_title));
        showInstallApkNotification(NOTIFICATION_STREAM_APPS_ID, builder, resultIntent);
    }

    public void showInstallSurveyApkNotification(ApkSet surveys) {
        if (surveys == null || surveys.isEmpty()) {
            return;
        }

        Intent resultIntent = new Intent(getContext(), InstallSurveyDependencies.class);
        Builder builder = new Builder(getContext());

        if (surveys.size() > 1 && VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
            builder.setSmallIcon(R.drawable.stat_notify_update_collapse).setContentText(getContext()
                    .getString(R.string.install_multiple_apps_for_surveys_message, surveys.size()));
        } else {
            builder.setSmallIcon(R.drawable.stat_notify_update).setContentText(getContext()
                    .getString(R.string.install_apps_for_survey_message,
                            surveys.iterator().next().getAppName()));
        }

        builder.setContentTitle(getContext().getString(R.string.install_apps_for_survey_title));
        showInstallApkNotification(NOTIFICATION_REMOTE_APPS_ID, builder, resultIntent);
    }

    private void showInstallApkNotification(int id, Builder builder, Intent intent) {
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent resultPendingIntent = PendingIntent.getActivity(getContext(), 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        builder.setContentIntent(resultPendingIntent)
                .setAutoCancel(true)
                .setDefaults(Notification.DEFAULT_ALL);

        NotificationManager mNotificationManager =
                (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationManager.notify(id, builder.build());
    }

    public static class SurveysFromOhmlet implements Func1<Ohmlet, Observable<Survey>> {
        @Override public Observable<Survey> call(Ohmlet ohmlet) {
            return Observable.from(ohmlet.surveys);
        }
    }

    public static class StreamsFromOhmlet implements Func1<Ohmlet, Observable<Stream>> {
        @Override public Observable<Stream> call(Ohmlet ohmlet) {
            return Observable.from(ohmlet.streams);
        }
    }

    public class RefreshStream implements Func1<Stream, Observable<Stream>> {
        @Override public Observable<Stream> call(Stream stream) {
            return ohmageService.getStream(stream.schemaId, stream.schemaVersion);
        }
    }

    public class RefreshSurvey implements Func1<Survey, Observable<Survey>> {
        @Override public Observable<Survey> call(Survey survey) {
            return ohmageService.getSurvey(survey.schemaId.toString(), survey.schemaId.getVersion());
        }
    }

    public class RefreshOhmlet implements Func1<Ohmlet, Observable<Ohmlet>> {
        @Override public Observable<Ohmlet> call(Ohmlet ohmlet) {
            return ohmageService.getOhmlet(ohmlet.ohmletId);
        }
    }

    public static class FilterUpToDateSurveys implements Func1<Survey, Boolean> {

        private final ContentProviderClient provider;

        public FilterUpToDateSurveys(ContentProviderClient provider) {
            this.provider = provider;
        }

        @Override public Boolean call(Survey survey) {
            Cursor c = null;
            try {
                c = provider.query(
                        Surveys.getUriForSurveyIdVersion(survey.schemaId),
                        new String[]{Surveys.SURVEY_ID, Surveys.SURVEY_VERSION}, null, null, null);
                if (c.moveToFirst()) {
                    return !c.getString(0).equals(survey.schemaId) ||
                           !c.getString(1).equals(survey.schemaId.getVersion());
                }
            } catch (RemoteException e) {
                e.printStackTrace();
            } finally {
                if(c != null) {
                    c.close();
                }
            }
            return true;
        }
    }

    public static class FilterUpToDateStreams implements Func1<Stream, Boolean> {

        private final ContentProviderClient provider;

        public FilterUpToDateStreams(ContentProviderClient provider) {
            this.provider = provider;
        }

        @Override public Boolean call(Stream stream) {
            Cursor c = null;
            try {
                c = provider.query(
                        Streams.getUriForStreamIdVersion(stream.schemaId, stream.schemaVersion),
                        new String[]{Streams.STREAM_ID, Streams.STREAM_VERSION}, null, null, null);
                if (c.moveToFirst()) {
                    return !c.getString(0).equals(stream.schemaId) ||
                           c.getInt(1) != stream.schemaVersion;
                }
            } catch (RemoteException e) {
                e.printStackTrace();
            } finally {
                if(c != null) {
                    c.close();
                }
            }
            return true;
        }
    }

    /**
     * Same as the safe subscriber except it will log errors and continue running
     *
     * @param <T>
     */
    public static class SyncSubscriber<T> extends SafeSubscriber<T> {

        private final SyncResult mSyncResult;

        public SyncSubscriber(SyncResult syncResult, Subscriber<? super T> actual) {
            super(actual);
            mSyncResult = syncResult;
        }

        @Override
        public void onError(Throwable e) {
            synchronized (mSyncResult) {
                mSyncResult.stats.numIoExceptions++;
            }
        }
    }


    public static Uri appendSyncAdapterParam(Uri uri) {
        return uri.buildUpon().appendQueryParameter(IS_SYNCADAPTER, "true").build();
    }
}
ohmage/src/main/java/org/ohmage/app/OhmletActivity.java
public class OhmletActivity extends InjectedActionBarActivity {

    private static final String TAG = OhmletActivity.class.getSimpleName();
    private View mLoadingView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_single_fragment);

        mLoadingView = findViewById(R.id.loading);

        String action = getIntent().getAction();
        Bundle extras = new Bundle();
        Uri data = getIntent().getData();
        if (data.getScheme().equals("https") || data.getScheme().equals("http")) {
            if (data.getPathSegments().size() > 3) {
                if (data.getPathSegments().get(3).equals("invitation")) {
                    action = OhmletFragment.ACTION_JOIN;
                }
            }
            extras.putString(OhmletFragment.EXTRA_OHMLET_INVITATION_ID,
                    data.getQueryParameter("ohmlet_invitation_id"));
            extras.putString(AuthenticatorActivity.EXTRA_USER_INVITATION_CODE,
                    data.getQueryParameter("user_invitation_id"));
            extras.putString(AuthenticatorActivity.EXTRA_EMAIL, data.getQueryParameter("email"));
            data = Ohmlets.getUriForOhmletId(data.getPathSegments().get(2));
        }

        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction().add(R.id.container,
                    OhmletFragment.getInstance(action, data, extras)).commit();
        }
    }

    public void showLoadingView(boolean show) {
        mLoadingView.setVisibility(show ? View.VISIBLE : View.GONE);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {

        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.ohmlet, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    /**
     * A placeholder fragment containing a simple view.
     */
    public static class OhmletFragment extends InjectedFragment
            implements Observer<Ohmlet>, OnClickListener, LoaderCallbacks<Cursor> {

        public static final String EXTRA_OHMLET_INVITATION_ID = "extra_ohmlet_invitation_id";

        /**
         * Join an ohmlet
         */
        public static final String ACTION_JOIN = "org.ohmage.action.JOIN";

        public static Fragment getInstance(String action, Uri uri, Bundle extras) {
            OhmletFragment fragment = new OhmletFragment();
            extras.putString("action", action);
            extras.putParcelable("uri", uri);
            fragment.setArguments(extras);
            return fragment;
        }

        @Inject OhmageService ohmageService;

        @Inject AccountManager am;

        private TextView mDescription;

        private Ohmlet mOhmlet;

        private Subscription ohmletSupscription;

        private Button mJoinButton;

        private String userId;

        private Uri uri;

        private String ohmletId;

        private String action;

        private String invitationCode;

        private JoinOhmletDialog mJoinDialog;

        @Override public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            action = getArguments().getString("action");
            uri = getArguments().getParcelable("uri");
            invitationCode = getArguments().getString(EXTRA_OHMLET_INVITATION_ID);
            ohmletId = uri.getLastPathSegment();
        }

        @Override public void onActivityCreated(Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);

            Account[] accounts = am.getAccountsByType(AuthUtil.ACCOUNT_TYPE);
            if (accounts.length == 0) {
                if (ACTION_JOIN.equals(action)) {
                    Intent intent = new Intent(getActivity(), AuthenticatorActivity.class);
                    Bundle extras = new Bundle(getArguments());
                    extras.remove("uri");
                    extras.remove("action");
                    intent.putExtras(extras);
                    intent.putExtra(AuthenticatorActivity.EXTRA_JOIN_OHMLET_ID, ohmletId);
                    startActivity(intent);
                }
                getActivity().finish();
                return;
            }

            userId = am.getUserData(accounts[0], Authenticator.USER_ID);

            if (savedInstanceState == null) {
                if (ACTION_JOIN.equals(action)) {
                    mJoinDialog = (JoinOhmletDialog) getChildFragmentManager().findFragmentByTag("join_dialog");
                }
            }

            getLoaderManager().initLoader(0, null, this);
        }

        @Override public void onDestroy() {
            super.onDestroy();
            if (ohmletSupscription != null)
                ohmletSupscription.unsubscribe();
        }

        @Override public void onCompleted() {
            updateView();
        }

        @Override public void onError(Throwable e) {
            //TODO: handle error correctly
            Toast.makeText(getActivity(), "error", Toast.LENGTH_SHORT).show();
            e.printStackTrace();
        }

        @Override public void onNext(Ohmlet ohmlet) {
            setOhmlet(ohmlet);
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_ohmlet, container, false);
            assert rootView != null;

            mDescription = (TextView) rootView.findViewById(R.id.description);
            mDescription.setVisibility(View.GONE);
            mJoinButton = (Button) rootView.findViewById(R.id.join);
            mJoinButton.setVisibility(View.GONE);
            mJoinButton.setOnClickListener(this);

            if (mOhmlet != null)
                updateView();

            return rootView;
        }

        public void setOhmlet(Ohmlet ohmlet) {
            mOhmlet = ohmlet;
        }

        public Ohmlet getOhmlet() {
            return mOhmlet;
        }

        public String getUserId() {
            return userId;
        }

        public String getInvitationCode() {
            return invitationCode;
        }

        private void updateView() {
            ((OhmletActivity) getActivity()).showLoadingView(false);
            getActivity().setTitle(mOhmlet.name);

            if (getView() != null) {
                mDescription.setVisibility(View.VISIBLE);
                mDescription.setText(mOhmlet.description);
                mJoinButton.setVisibility(mOhmlet.isMember(userId) ? View.GONE : View.VISIBLE);
            }
        }

        @Override public void onClick(View v) {
            JoinOhmletDialog.getInstance().show(getChildFragmentManager(), "join_dialog");
        }

        @Override public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
            return new CursorLoader(getActivity(), Ohmlets.getUriForOhmletId(ohmletId),
                    Ohmlets.DEFAULT_PROJECTION, null, null, null);
        }

        @Override public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
            if (cursor.moveToFirst()) {
                Ohmlet ohmlet = new Ohmlet();
                new ContentProviderReader().read(ohmlet, cursor);
                setOhmlet(ohmlet);
                updateView();
            } else {
                // It is not in the db so go to the network
                Observable<Ohmlet> ohmletObservable =
                        ohmageService.getOhmlet(ohmletId).single().cache();

                ohmletSupscription = AndroidObservable.fromFragment(this, ohmletObservable)
                        .subscribe(this);
            }
        }

        @Override public void onLoaderReset(Loader<Cursor> cursorLoader) {

        }
    }

    public static class JoinOhmletDialog extends InjectedDialogFragment
            implements DialogInterface.OnClickListener {

        public static JoinOhmletDialog getInstance() {
            return new JoinOhmletDialog();
        }

        @Override public Dialog onCreateDialog(Bundle savedInstanceState) {
            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
            builder.setTitle("Join ohmlet?")
                    .setMessage("Are you sure you want to join this ohmlet?")
                    .setPositiveButton(R.string.join, this)
                    .setNegativeButton(R.string.cancel, null);
            return builder.create();
        }

        @Override public void onClick(DialogInterface dialog, int which) {

            OhmletFragment parent = ((OhmletFragment) getParentFragment());

            String userId = parent.getUserId();
            Ohmlet ohmlet = parent.getOhmlet();
            String id = TextUtils.isEmpty(userId) ? "me" : userId;

            if (!ohmlet.isMember(id)) {
                Member m = new Member();
                m.memberId = id;
                m.role = Role.MEMBER;
                m.code = parent.getInvitationCode();
                ohmlet.people.add(m);
                ohmlet.dirty = true;

                // Save the state of the ohmlet
                Observable.from(ohmlet).subscribeOn(Schedulers.io()).doOnNext(
                        new ContentProviderSaver()).doOnError(new Action1<Throwable>() {
                    @Override public void call(Throwable throwable) {
                        throwable.printStackTrace();
                    }
                }).subscribe();
            }
        }
    }
}
ohmage/src/androidTest/java/org/ohmage/auth/AuthenticatorTest.java
public class AuthenticatorTest extends InjectedAndroidTestCase {

    @Inject AuthHelper fakeAuthHelper;
    @Inject AccountManager fakeAccountManager;
    @Inject OhmageService fakeOhmageService;

    private Authenticator mAuthenticator;
    private Context fakeContext;
    private Account fakeAccount;
    private static final String fakeGoogleEmail = "fake@gmail.com";
    private static final String accessToken = "token";
    private static final String refreshToken = "refresh";
    private AccessToken token;
    private static final String stubGoogleToken = "google_token";
    private static final IOException ioe = new IOException();
    private static final RetrofitError networkError = RetrofitError.networkError("", ioe);
    private static final RetrofitError serverError = RetrofitError.networkError("", ioe);

    @Override
    public void setUp() throws Exception {
        super.setUp();

        fakeAccount = new Account("name", AuthUtil.AUTHTOKEN_TYPE);
        token = new AccessToken(accessToken, refreshToken, fakeAccount.name);

        fakeContext = mock(Context.class);

        mAuthenticator = new Authenticator(fakeContext);
    }

    public void testGetAuthToken_whenCached_returnsToken() throws Exception {
        setAuthTokenCached(true);

        Bundle bundle = mAuthenticator.getAuthToken(null, fakeAccount,
                AuthUtil.AUTHTOKEN_TYPE, null);

        verify(fakeAccountManager).peekAuthToken(fakeAccount, AuthUtil.AUTHTOKEN_TYPE);
        verifyNoMoreInteractions(fakeAccountManager);
        verifyTokenInBundle(bundle);
    }

    public void testGetAuthToken_whenNotCached_usesRefreshToken() throws Exception {
        setAuthTokenCached(false);
        setAccountRefreshToken();
        setAccessTokenSuccess(refreshToken);

        mAuthenticator.getAuthToken(null, fakeAccount, AuthUtil.AUTHTOKEN_TYPE, null);

        // Get the refresh password
        verify(fakeAccountManager).getPassword(fakeAccount);
        // Use the refresh password to get new tokens
        verify(fakeOhmageService).getAccessToken(refreshToken);
    }

    public void testGetAuthToken_whenNotCached_savesNewRefreshToken() throws Exception {
        setAuthTokenCached(false);
        setAccountRefreshToken();
        setAccessTokenSuccess(refreshToken);

        mAuthenticator.getAuthToken(null, fakeAccount, AuthUtil.AUTHTOKEN_TYPE, null);

        // Saves the refresh token
        verify(fakeAccountManager).setPassword(fakeAccount, refreshToken);
    }

    public void testGetAuthToken_whenUsingRefreshToken_returnsToken() throws Exception {
        setAuthTokenCached(false);
        setAccountRefreshToken();
        setAccessTokenSuccess(refreshToken);

        Bundle bundle = mAuthenticator.getAuthToken(null, fakeAccount,
                AuthUtil.AUTHTOKEN_TYPE, null);
        verifyTokenInBundle(bundle);
    }

    public void testGetAuthToken_noConnectionErrorUsingRefreshToken_throwsNetworkErrorException()
            throws Exception {
        setAuthTokenCached(false);
        setAccountRefreshToken();
        setAccessTokenFailure(refreshToken, networkError);

        try {
            mAuthenticator.getAuthToken(null, fakeAccount, AuthUtil.AUTHTOKEN_TYPE, null);
            fail("No Exception Thrown");
        } catch (Exception e) {
            assertTrue(e instanceof NetworkErrorException);
        }
    }

    public void testGetAuthToken_serverErrorUsingRefreshToken_throwsNetworkErrorException()
            throws Exception {
        setAuthTokenCached(false);
        setAccountRefreshToken();
        setAccessTokenFailure(refreshToken, serverError);

        try {
            mAuthenticator.getAuthToken(null, fakeAccount, AuthUtil.AUTHTOKEN_TYPE, null);
            fail("No Exception Thrown");
        } catch (Exception e) {
            assertTrue(e instanceof NetworkErrorException);
        }
    }

    public void testGetAuthToken_authErrorUsingRefreshToken_notifiesUser() throws Exception {
        setAuthTokenCached(false);
        setAccountRefreshToken();
        setAccessTokenFailure(refreshToken, new AuthenticationException(""));

        Bundle data = mAuthenticator.getAuthToken(null, fakeAccount, AuthUtil.AUTHTOKEN_TYPE, null);

        verifyNotifyUserBundle(data);
    }

    public void testGetAuthToken_authErrorUsingRefreshTokenAndGoogleAccount_triesToGetGoogleAuth()
            throws Exception {
        setAuthTokenCached(false);
        setAccountRefreshToken();
        when(fakeOhmageService.getAccessToken(refreshToken))
                .thenThrow(new AuthenticationException("")).thenReturn(token);
        setHasGoogleAccount(true);
        setGetGoogleAuthTokenResult(null);

        mAuthenticator.getAuthToken(null, fakeAccount, AuthUtil.AUTHTOKEN_TYPE, null);

        verify(fakeAccountManager).getUserData(fakeAccount, Authenticator.USER_DATA_GOOGLE_ACCOUNT);
        verify(fakeAuthHelper).googleAuthGetToken(fakeGoogleEmail);
    }

    public void testGetAuthToken_userRecoverableErrorWhenAuthtokenFromGoogle_sendsErrorViaIntent()
            throws Exception {
        setAuthTokenCached(false);
        setAccountRefreshToken();
        setAccessTokenFailure(refreshToken, new AuthenticationException(""));
        setHasGoogleAccount(true);
        UserRecoverableAuthException fakeException = new UserRecoverableAuthException("msg",
                new Intent());
        setGetGoogleAuthTokenResult(fakeException);


        Bundle data = mAuthenticator.getAuthToken(null, fakeAccount, AuthUtil.AUTHTOKEN_TYPE, null);

        Intent intent = data.getParcelable(AccountManager.KEY_INTENT);
        assertNotNull(intent);
        assertEquals(intent.getComponent().getClassName(), AuthenticatorActivity.class.getName());
        assertEquals(fakeException, intent
                .getSerializableExtra(AuthenticatorActivity.EXTRA_HANDLE_USER_RECOVERABLE_ERROR));
    }

    public void testGetAuthToken_whenAuthWithGoogle_returnsToken() throws Exception {
        setAuthTokenCached(false);
        setAccountRefreshToken();
        setAccessTokenFailure(refreshToken, new AuthenticationException(""));
        setHasGoogleAccount(true);
        setGetGoogleAuthTokenResult(null);
        setAccessTokenSuccess(AuthUtil.GrantType.GOOGLE_OAUTH2, stubGoogleToken);

        Bundle data = mAuthenticator.getAuthToken(null, fakeAccount, AuthUtil.AUTHTOKEN_TYPE, null);

        verifyTokenInBundle(data);
    }

    public void testGetAuthToken_noConnectionErrorWhenAuthWithGoogle_throwsNetworkErrorException()
            throws Exception {
        setAuthTokenCached(false);
        setAccountRefreshToken();
        setAccessTokenFailure(refreshToken, new AuthenticationException(""));
        setHasGoogleAccount(true);
        setGetGoogleAuthTokenResult(null);
        setAccessTokenFailure(AuthUtil.GrantType.GOOGLE_OAUTH2, stubGoogleToken, networkError);

        try {
            mAuthenticator.getAuthToken(null, fakeAccount, AuthUtil.AUTHTOKEN_TYPE, null);
            fail("No Exception Thrown");
        } catch (Exception e) {
            assertTrue(e instanceof NetworkErrorException);
        }
    }

    public void testGetAuthToken_serverErrorWhenAuthWithGoogleToken_throwsNetworkException()
            throws Exception {
        setAuthTokenCached(false);
        setAccountRefreshToken();
        setAccessTokenFailure(refreshToken, new AuthenticationException(""));
        setHasGoogleAccount(true);
        setGetGoogleAuthTokenResult(null);
        setAccessTokenFailure(AuthUtil.GrantType.GOOGLE_OAUTH2, stubGoogleToken, serverError);

        try {
            mAuthenticator.getAuthToken(null, fakeAccount, AuthUtil.AUTHTOKEN_TYPE, null);
            fail("No Exception Thrown");
        } catch (Exception e) {
            assertTrue(e instanceof NetworkErrorException);
        }
    }

    private void setAuthTokenCached(boolean cached) {
        when(fakeAccountManager.peekAuthToken(fakeAccount, AuthUtil.AUTHTOKEN_TYPE))
                .thenReturn(cached ? accessToken : null);
    }

    private void setAccountRefreshToken() {
        when(fakeAccountManager.getPassword(fakeAccount)).thenReturn(refreshToken);
    }

    private void setAccessTokenSuccess(String refreshToken) throws Exception {
        setAccessTokenResult(refreshToken, null);
    }

    private void setAccessTokenFailure(String refreshToken, Throwable error) throws Exception {
        setAccessTokenResult(refreshToken, error);
    }

    private void setAccessTokenResult(String refreshToken, Throwable error) throws Exception {
        OngoingStubbing<AccessToken> when = when(fakeOhmageService.getAccessToken(refreshToken));
        if (error == null)
            when.thenReturn(token);
        else
            when.thenThrow(error);
    }

    private void setAccessTokenSuccess(GrantType type, String token) throws Exception {
        setAccessTokenResult(type, token, null);
    }

    private void setAccessTokenFailure(GrantType type, String token, Throwable error)
            throws Exception {
        setAccessTokenResult(type, token, error);
    }

    private void setAccessTokenResult(GrantType type, String t, Throwable error) throws Exception {
        OngoingStubbing<AccessToken> when = when(fakeOhmageService.getAccessToken(type, t));
        if (error == null)
            when.thenReturn(token);
        else
            when.thenThrow(error);
    }

    private void setHasGoogleAccount(boolean hasAccount) {
        when(fakeAccountManager.getUserData(fakeAccount, Authenticator.USER_DATA_GOOGLE_ACCOUNT))
                .thenReturn(fakeGoogleEmail);
    }

    private void setGetGoogleAuthTokenResult(Exception e) throws Exception {
        OngoingStubbing<String> stub = when(fakeAuthHelper.googleAuthGetToken(fakeGoogleEmail));
        if (e == null) {
            stub.thenReturn(stubGoogleToken);
        } else {
            stub.thenThrow(e);
        }
    }

    private void verifyTokenInBundle(Bundle bundle) {
        assertEquals(accessToken, bundle.getString(AccountManager.KEY_AUTHTOKEN));
    }

    private void verifyNotifyUserBundle(Bundle bundle) {
        Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
        assertNotNull(intent);
        assertEquals(intent.getComponent().getClassName(), AuthenticatorActivity.class.getName());
        assertTrue(intent.getBooleanExtra(AuthenticatorActivity.EXTRA_FROM_AUTHENTICATOR, false));
    }
}
ohmage/src/main/java/org/ohmage/sync/ResponseSyncAdapter.java
public class ResponseSyncAdapter extends AbstractThreadedSyncAdapter {

    public static final String IS_SYNCADAPTER = "is_syncadapter";

    @Inject AccountManager am;

    @Inject OhmageService ohmageService;

    @Inject Gson gson;

    private static final String TAG = ResponseSyncAdapter.class.getSimpleName();

    /**
     * Set up the sync adapter
     */
    public ResponseSyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
        Ohmage.app().getApplicationGraph().inject(this);
    }

    /**
     * Set up the sync adapter. This form of the
     * constructor maintains compatibility with Android 3.0
     * and later platform versions
     */
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public ResponseSyncAdapter(
            Context context,
            boolean autoInitialize,
            boolean allowParallelSyncs) {
        super(context, autoInitialize, allowParallelSyncs);
        Ohmage.app().getApplicationGraph().inject(this);
    }

    @Override
    public void onPerformSync(Account account, Bundle extras, String authority,
            final ContentProviderClient provider, final SyncResult syncResult) {
        // Check for authtoken
        String token = null;
        try {
            token = am.blockingGetAuthToken(account, AuthUtil.AUTHTOKEN_TYPE, true);
        } catch (OperationCanceledException e) {
            syncResult.stats.numSkippedEntries++;
        } catch (IOException e) {
            syncResult.stats.numIoExceptions++;
        } catch (AuthenticatorException e) {
            syncResult.stats.numAuthExceptions++;
        }

        // If the token wasn't found or there was a problem, we can stop now
        if (token == null || syncResult.stats.numSkippedEntries > 0 ||
            syncResult.stats.numIoExceptions > 0 || syncResult.stats.numAuthExceptions > 0)
            return;

        // Upload responses
        Observable<Long> toDelete = null;
        Observable<ResponseFiles> filesToDelete = null;

        Cursor cursor = null;

        try {
            cursor = provider.query(Responses.CONTENT_URI,
                    new String[]{BaseColumns._ID, Responses.SURVEY_ID, Responses.SURVEY_VERSION,
                            Responses.RESPONSE_DATA, Responses.RESPONSE_METADATA,
                            Responses.RESPONSE_EXTRAS}, null, null, null);

            while (cursor.moveToNext()) {
                final ResponseFiles files = gson.fromJson(cursor.getString(5), ResponseFiles.class);
                try {
                    // Make the call to upload responses
                    Observable<Response> uploadResponse = null;

                    if(Ohmage.USE_DSU_DATAPOINTS_API) {
                        uploadResponse = uploadDatapoint(cursor, files);
                    } else {
                        uploadOhmagePoint(cursor, files);
                    }

                    // Map the data for the upload response to the local id in the db
                    final long localResponseId = cursor.getLong(0);
                    Observable<Long> responseId = uploadResponse.map(new Func1<Response, Long>() {
                        @Override public Long call(Response response) {
                            return localResponseId;
                        }
                    });

                    if(toDelete == null) {
                        toDelete = responseId;
                    } else {
                        toDelete = Observable.mergeDelayError(responseId, toDelete);
                    }

                    // Map the data for the upload response to the files in the db
                    Observable<ResponseFiles> responseFiles = uploadResponse.map(new Func1<Response, ResponseFiles>() {
                        @Override public ResponseFiles call(Response response) {
                            return files;
                        }
                    });

                    if(filesToDelete == null) {
                        filesToDelete = responseFiles;
                    } else {
                        filesToDelete = Observable.mergeDelayError(responseFiles, filesToDelete);
                    }
                } catch (AuthenticationException e) {
                    Log.e(TAG, "Auth", e);
                    syncResult.stats.numAuthExceptions++;
                }
            }
            cursor.close();



        } catch (RemoteException e) {
            Log.e(TAG, "Remote", e);
            syncResult.stats.numIoExceptions++;
        } finally {
            if (cursor != null)
                cursor.close();
        }

        if(toDelete != null) {
            toDelete.flatMap(new Func1<Long, Observable<ContentProviderOperation>>() {
                @Override public Observable<ContentProviderOperation> call(Long aLong) {
                    return Observable.from(ContentProviderOperation.newDelete(appendSyncAdapterParam(
                            ContentUris.withAppendedId(Responses.CONTENT_URI, aLong))).build());
                }
            }).subscribe(new Subscriber<ContentProviderOperation>() {
                ArrayList<ContentProviderOperation> toDelete =
                        new ArrayList<ContentProviderOperation>();

                @Override public void onCompleted() {
                    try {
                        getContext().getContentResolver()
                                .applyBatch(ResponseContract.CONTENT_AUTHORITY, toDelete);
                    } catch (RemoteException e) {
                        syncResult.stats.numIoExceptions++;
                    } catch (OperationApplicationException e) {
                        syncResult.stats.numIoExceptions++;
                    }
                    unsubscribe();
                }

                @Override public void onError(Throwable e) {
                    // Delete the successful ones
                    onCompleted();
                }

                @Override public void onNext(ContentProviderOperation args) {
                    toDelete.add(args);
                }
            });
        }

        if(filesToDelete != null) {
            filesToDelete.doOnNext(new Action1<ResponseFiles>() {
                @Override public void call(ResponseFiles responseFiles) {
                    for (String s : responseFiles.getIds()) {
                        responseFiles.getFile(s).delete();
                    }
                }
            }).subscribe(new Subscriber<ResponseFiles>() {
                @Override public void onCompleted() {
                    unsubscribe();
                }

                @Override public void onError(Throwable e) {

                }

                @Override public void onNext(ResponseFiles responseFiles) {

                }
            });
        }
    }

    public static Uri appendSyncAdapterParam(Uri uri) {
        return uri.buildUpon().appendQueryParameter(IS_SYNCADAPTER, "true").build();
    }

    private Observable<Response> uploadOhmagePoint(Cursor cursor, ResponseFiles files) throws AuthenticationException {
        // Make the call to upload responses
        return ohmageService.uploadResponse(cursor.getString(1), cursor.getLong(2),
                new ResponseTypedOutput(cursor.getString(3),
                        cursor.getString(4), files)).cache();

    }

    private Observable<Response> uploadDatapoint(Cursor cursor, ResponseFiles files) throws AuthenticationException {
        OmhDataPointHeader header = new OmhDataPointHeader();
        header.schemaId = new SchemaId(cursor.getString(1), cursor.getString(2));


        JsonObject metadata =  new JsonParser().parse(cursor.getString(4)).getAsJsonObject();
        header.creationDateTime = metadata.get("timestamp").getAsString();
        header.id = metadata.get("id").getAsString();
        Log.e(TAG, cursor.getString(3) + gson.toJson(header) + files.getIds().toString());
        DataPointTypedOutput point = new DataPointTypedOutput(cursor.getString(3), gson.toJson(header), files);

        // Make the call to upload responses
        return ohmageService.uploadDataPoint(point).cache();
    }
}


Question: import android.accounts.AccountManager;
import android.content.Context;
import com.google.gson.Gson;
import org.ohmage.app.InstallSurveyDependencies;
import org.ohmage.app.InstallSurveyDependencies.InstallSurveyDependenciesFragment;
import org.ohmage.app.MainActivity;
import org.ohmage.app.MainActivityOhmletsTest;
import org.ohmage.app.MainActivityTest;
import org.ohmage.app.OhmageAuthenticator;
import org.ohmage.app.OhmageService;
import org.ohmage.app.OhmletActivity;
import org.ohmage.app.SurveyActivity;
import org.ohmage.app.SurveyActivity.SurveyPromptLoader;
import org.ohmage.auth.AuthHelper;
import org.ohmage.auth.AuthenticateFragment;
import org.ohmage.auth.Authenticator;
import org.ohmage.auth.AuthenticatorTest;
import org.ohmage.auth.CreateAccountFragment;
import org.ohmage.auth.SignInFragment;
import org.ohmage.auth.oauth.OAuthActivity;
import org.ohmage.auth.oauth.OAuthActivity.OAuthFragment;
import org.ohmage.dagger.ForApplication;
import org.ohmage.fragments.HomeFragment;
import org.ohmage.fragments.InstallDependenciesDialog;
import org.ohmage.fragments.OhmletsFragment;
import org.ohmage.fragments.OhmletsGridFragment;
import org.ohmage.fragments.OhmletsSearchFragment;
import org.ohmage.fragments.StreamsFragment;
import org.ohmage.fragments.SurveysFragment;
import org.ohmage.operators.ContentProviderSaver;
import org.ohmage.prompts.PromptFragment;
import org.ohmage.provider.ContentProviderReader;
import org.ohmage.provider.StreamContentProvider;
import org.ohmage.sync.OhmageSyncAdapter;
import org.ohmage.sync.ResponseSyncAdapter;
import org.ohmage.sync.StreamSyncAdapter;
import org.ohmage.sync.StreamSyncAdapterTest;
import org.ohmage.tasks.LogoutTaskFragment;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import static org.mockito.Mockito.mock;
/*
 * Copyright (C) 2013 ohmage
 *
 * 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 org.ohmage.test.dagger;







@Module(
        injects = {
                InjectedAndroidTestCase.class,
                MainActivityTest.class,
                AuthenticatorTest.class,
                StreamSyncAdapterTest.class,
                MainActivityOhmletsTest.class,

                MainActivity.class,
                AuthenticateFragment.class,
                Authenticator.class,
                CreateAccountFragment.class,
                SignInFragment.class,
                LogoutTaskFragment.class,
                StreamContentProvider.class,
                StreamSyncAdapter.class,
                OhmageAuthenticator.class,
                OhmletsSearchFragment.class,
                OhmletsGridFragment.OhmletAdapter.class,
                OhmletsFragment.class,
                OhmageSyncAdapter.class,
Answer:                 OhmletActivity.class,