<repo_name>jessicamindel/fbu-parstagram<file_sep>/FBUParstagram/app/src/main/java/com/jmindel/fbuparstagram/CameraManager.java
package com.jmindel.fbuparstagram;

import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.v4.app.Fragment;
import android.support.v4.content.FileProvider;
import android.util.Log;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class CameraManager {

    public static final int CAPTURE_IMAGE_REQUEST_CODE = 1034;
    public final static int PICK_PHOTO_REQUEST_CODE = 1046;
    public static final int IMG_WIDTH = 600;
    private File photoFile;
    private String photoFileName;

    private Fragment fragment;

    public CameraManager(String photoFileName, Fragment fragment) {
        this.photoFileName = photoFileName;
        this.fragment = fragment;
    }

    public void takePhoto() {
        // create Intent to take a picture and return control to the calling application
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        // Create a File reference to access to future access
        photoFile = getPhotoFileUri(photoFileName);

        // wrap File object into a content provider
        // required for API >= 24
        // See https://guides.codepath.com/android/Sharing-Content-with-Intents#sharing-files-with-api-24-or-higher
        Uri fileProvider = FileProvider.getUriForFile(fragment.getContext(), "com.codepath.fileprovider", photoFile);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, fileProvider);

        // If you call startActivityForResult() using an intent that no app can handle, your app will crash.
        // So as long as the result is not null, it's safe to use the intent.
        if (intent.resolveActivity(fragment.getContext().getPackageManager()) != null) {
            // Start the image capture intent to take photo
            fragment.startActivityForResult(intent, CAPTURE_IMAGE_REQUEST_CODE);
        }
    }

    // Returns the File for a photo stored on disk given the fileName
    public File getPhotoFileUri(String fileName) {
        // Get safe storage directory for photos
        // Use `getExternalFilesDir` on Context to access package-specific directories.
        // This way, we don't need to request external read/write runtime permissions.
        File mediaStorageDir = new File(fragment.getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES), "Parstagram");

        // Create the storage directory if it does not exist
        if (!mediaStorageDir.exists() && !mediaStorageDir.mkdirs()){
            Log.d("CameraLauncher", "Failed to create directory");
        }

        // Return the file target for the photo based on filename
        File file = new File(mediaStorageDir.getPath() + File.separator + fileName);

        return file;
    }

    public Bitmap shrinkImage(Bitmap image, int width) throws IOException {
        Bitmap resizedBitmap = BitmapScaler.scaleToFitWidth(image, width);
        // Configure byte output stream
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        // Compress the image further
        resizedBitmap.compress(Bitmap.CompressFormat.JPEG, 40, bytes);
        // Create a new file for the resized bitmap (`getPhotoFileUri` defined above)
        File resizedFile = getPhotoFileUri(photoFileName + "_resized");
        resizedFile.createNewFile();
        FileOutputStream fos = new FileOutputStream(resizedFile);
        // Write the bytes of the bitmap to file
        fos.write(bytes.toByteArray());
        fos.close();
        return resizedBitmap;
    }

    public Bitmap rotateBitmapOrientation(String photoFilePath) {
        // Create and configure BitmapFactory
        BitmapFactory.Options bounds = new BitmapFactory.Options();
        bounds.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(photoFilePath, bounds);
        BitmapFactory.Options opts = new BitmapFactory.Options();
        Bitmap bm = BitmapFactory.decodeFile(photoFilePath, opts);
        // Read EXIF Data
        ExifInterface exif = null;
        try {
            exif = new ExifInterface(photoFilePath);
        } catch (IOException e) {
            e.printStackTrace();
        }
        String orientString = exif.getAttribute(ExifInterface.TAG_ORIENTATION);
        int orientation = orientString != null ? Integer.parseInt(orientString) : ExifInterface.ORIENTATION_NORMAL;
        int rotationAngle = 0;
        if (orientation == ExifInterface.ORIENTATION_ROTATE_90) rotationAngle = 90;
        if (orientation == ExifInterface.ORIENTATION_ROTATE_180) rotationAngle = 180;
        if (orientation == ExifInterface.ORIENTATION_ROTATE_270) rotationAngle = 270;
        // Rotate Bitmap
        Matrix matrix = new Matrix();
        matrix.setRotate(rotationAngle, (float) bm.getWidth() / 2, (float) bm.getHeight() / 2);
        Bitmap rotatedBitmap = Bitmap.createBitmap(bm, 0, 0, bounds.outWidth, bounds.outHeight, matrix, true);
        // Return result
        return rotatedBitmap;
    }

    public Bitmap fixOrientationAndSize() {
        Bitmap takenImage = rotateBitmapOrientation(photoFile.getAbsolutePath());
        // Resize bitmap
        Bitmap scaledImage;
        try {
            scaledImage = shrinkImage(takenImage, CameraManager.IMG_WIDTH);
        } catch (IOException e) {
            Log.e("MakePostActivity", "Failed to scale image, using fallback");
            e.printStackTrace();
            scaledImage = takenImage;
        }
        return scaledImage;
    }

    public File getPhotoFile() {
        return photoFile;
    }

    // Trigger gallery selection for a photo
    public void pickPhoto() {
        // Create intent for picking a photo from the gallery
        Intent intent = new Intent(Intent.ACTION_PICK,
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI);

        // If you call startActivityForResult() using an intent that no app can handle, your app will crash.
        // So as long as the result is not null, it's safe to use the intent.
        if (intent.resolveActivity(fragment.getContext().getPackageManager()) != null) {
            // Bring up gallery to select a photo
            fragment.startActivityForResult(intent, PICK_PHOTO_REQUEST_CODE);
        }
    }

    /** @source https://stackoverflow.com/questions/31227547/how-to-upload-image-to-parse-in-android */
    public byte[] bitmapToBytes(Bitmap bitmap) {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
        byte[] image = stream.toByteArray();
        return image;
    }
}
<file_sep>/FBUParstagram/app/src/main/java/com/jmindel/fbuparstagram/Utils.java
package com.jmindel.fbuparstagram;

import android.content.Context;
import android.text.format.DateUtils;
import android.widget.ImageView;

import com.bumptech.glide.Glide;
import com.parse.GetFileCallback;
import com.parse.ParseFile;
import com.parse.ParseUser;

import java.io.File;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class Utils {
    // getRelativeTimeAgo("Mon Apr 01 21:16:23 +0000 2014");
    public static String getRelativeTimeAgo(String rawJsonDate) {
        String twitterFormat = "EEE MMM dd HH:mm:ss ZZZZZ yyyy";
        SimpleDateFormat sf = new SimpleDateFormat(twitterFormat, Locale.ENGLISH);
        sf.setLenient(true);

        String relativeDate = "";
        try {
            Date date = sf.parse(rawJsonDate);
            relativeDate = getRelativeTimeAgo(date);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        return relativeDate.toUpperCase();
    }

    public static String getRelativeTimeAgo(Date rawDate) {
        String relativeDate = "";
        long dateMillis = rawDate.getTime();

        long now = (new Date()).getTime();
        long between = now - dateMillis;
        int hoursBetween = (int) ((between / (1000 * 60 * 60)));
        int yearsBetween = (new Date(now)).getYear() - (new Date(dateMillis)).getYear();

        if (yearsBetween >= 1) {
            SimpleDateFormat moreThanAYear = new SimpleDateFormat("MMM d yyyy", Locale.US);
            relativeDate = moreThanAYear.format(dateMillis);
        } else if (hoursBetween >= 24) {
            SimpleDateFormat moreThanADay = new SimpleDateFormat("MMM d", Locale.US);
            relativeDate = moreThanADay.format(dateMillis);
        } else {
            String rawRelativeDate = DateUtils.getRelativeTimeSpanString(dateMillis, System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS).toString();
            String[] parts = rawRelativeDate.split(" ");
            relativeDate = parts[0];
            switch (parts[1]) {
                case "second":
                case "seconds":
                    relativeDate += "s";
                    break;
                case "minute":
                case "minutes":
                    relativeDate += "m";
                    break;
                case "hour":
                case "hours":
                    relativeDate += "h";
                    break;
                case "day":
                case "days":
                    relativeDate += "d";
                    break;
                case "month":
                case "months":
                    relativeDate += "mo";
                    break;
                case "year":
                case "years":
                    relativeDate += "y";
                    break;
                default:
                    relativeDate = rawRelativeDate;
                    break;
            }
        }

        return relativeDate.toUpperCase();
    }

    public static String ellipsize(String str, int maxChars) {
        if (str.length() > maxChars) {
            String ellipsized = str.substring(0, maxChars - 3);
            ellipsized += "...";
            return ellipsized;
        } else {
            return str;
        }
    }

    public static void loadProfileImage(final Context context, final ImageView iv, final ParseUser user) {
        ParseFile img = user.getParseFile("profileImage");
        if (img != null) {
            img.getFileInBackground(new GetFileCallback() {
                @Override
                public void done(File file, com.parse.ParseException e) {
                    Glide.with(context).load(file).into(iv);
                }
            });
        }
    }
}
<file_sep>/FBUParstagram/app/src/main/java/com/jmindel/fbuparstagram/activities/ProfileActivity.java
package com.jmindel.fbuparstagram.activities;

import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.FrameLayout;
import android.widget.Toast;

import com.jmindel.fbuparstagram.R;
import com.jmindel.fbuparstagram.fragments.ProfileFragment;
import com.parse.FindCallback;
import com.parse.ParseException;
import com.parse.ParseQuery;
import com.parse.ParseUser;

import java.util.List;

import butterknife.BindView;

public class ProfileActivity extends AppCompatActivity {

    public static final String KEY_USER_ID = "objectId";

    FragmentManager fragmentManager;
    private ProfileFragment fragment;

    @BindView(R.id.flFragmentContainer) FrameLayout fragmentContainer;

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

        fragmentManager = getSupportFragmentManager();

        // Retrieve user passed in from id
        String uid = getIntent().getStringExtra(KEY_USER_ID);
        ParseQuery<ParseUser> userQuery = ParseUser.getQuery();
        userQuery.whereEqualTo(KEY_USER_ID, uid);
        userQuery.findInBackground(new FindCallback<ParseUser>() {
            @Override
            public void done(List<ParseUser> objects, ParseException e) {
                if (e != null || objects.size() == 0) {
                    Log.e("ProfileActivity", "Failed fetching user");
                    e.printStackTrace();
                    Toast.makeText(ProfileActivity.this, "Failed fetching user", Toast.LENGTH_LONG).show();
                } else {
                    // Launch fragment specific to fetched user
                    ParseUser user = objects.get(0);
                    fragment = new ProfileFragment();
                    fragment.setUser(user);
                    fragmentManager.beginTransaction().replace(R.id.flFragmentContainer, fragment).commit();
                }
            }
        });
    }
}
<file_sep>/FBUParstagram/app/src/main/java/com/jmindel/fbuparstagram/activities/DetailActivity.java
package com.jmindel.fbuparstagram.activities;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.jmindel.fbuparstagram.R;
import com.jmindel.fbuparstagram.adapters.PostAdapter;
import com.jmindel.fbuparstagram.model.Comment;
import com.jmindel.fbuparstagram.model.Post;
import com.jmindel.fbuparstagram.scrolling.CommentLayout;
import com.jmindel.fbuparstagram.scrolling.EdgeDecorator;
import com.parse.FindCallback;
import com.parse.ParseException;
import com.parse.ParseUser;
import com.parse.SaveCallback;

import java.util.ArrayList;
import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;

public class DetailActivity extends AppCompatActivity {

    public static final String KEY_POST_ID = "postId";

    List<Post> posts;

    PostAdapter postAdapter;

    @BindView(R.id.rvPost)      RecyclerView rvPost;
    @BindView(R.id.clComments)  CommentLayout clComments;
    @BindView(R.id.bComment)    Button bComment;
    @BindView(R.id.etComment)   EditText etComment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);
        ButterKnife.bind(this);

        posts = new ArrayList<>();
        postAdapter = new PostAdapter(this, posts);
        postAdapter.setEnablePostDetailClick(false);
        LinearLayoutManager postLayoutManager = new LinearLayoutManager(this) {
            @Override
            public boolean canScrollVertically() {
                return false;
            }
        };
        rvPost.setLayoutManager(postLayoutManager);
        rvPost.setAdapter(postAdapter);
        rvPost.addItemDecoration(new EdgeDecorator(32, true, false));

        bComment.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String body = etComment.getText().toString();
                if (!body.equals("")) {
                    Comment comment = new Comment();
                    comment.setBody(body);
                    comment.setPost(posts.get(0));
                    comment.setUser(ParseUser.getCurrentUser());
                    comment.saveInBackground(new SaveCallback() {
                        @Override
                        public void done(ParseException e) {
                            etComment.setText("");
                            clComments.load();
                            // TODO: Clean this up
                        }
                    });
                } else {
                    Toast.makeText(DetailActivity.this, "Write a message before commenting!", Toast.LENGTH_LONG).show();
                }
            }
        });

        String currPostId = getIntent().getStringExtra(KEY_POST_ID);
        Post.Query currPostQuery = new Post.Query().withUser();
        currPostQuery.whereEqualTo(Post.KEY_ID, currPostId);
        currPostQuery.findInBackground(new FindCallback<Post>() {
            @Override
            public void done(List<Post> objects, ParseException e) {
                posts.add(objects.get(0));
                postAdapter.notifyItemInserted(0);
                clComments.setPost(posts.get(0));
                clComments.load();
            }
        });
    }
}
<file_sep>/README.md
### FBU SEA CodePath
# Parstagram
A simplified Instagram clone built with Parse.

![](https://github.com/jessicamindel/fbu-parstagram/raw/master/demo.mp4)

# Assignment Checkpoints

## Required User Stories
- [x] User sees app icon in home screen. (1 point)
- [x] User can sign up to create a new account using Parse authentication (1 point)
- [x] User can log in and log out of his or her account (1 points)
- [x] The current signed in user is persisted across app restarts (1 point)
- [x] User can take a photo, add a caption, and post it to "Instagram" (2 points)
- [x] User can view the last 20 posts submitted to "Instagram" (2 points)
- [x] User can pull to refresh the last 20 posts submitted to "Instagram" (1 points)
- [x] User can tap a post to view post details, including timestamp and caption. (2 points)

## Stretch Stories
- [ ] Style the login page to look like the real Instagram login page. (2 points)
- [ ] Style the feed to look like the real Instagram feed. (2 points)
  - [x] I styled it to look somewhat like Instagram, but as a CardView instead.
- [x] The user should switch between different tabs - viewing all posts (feed view), capture (camera and photo gallery view) and profile tabs (posts made) using fragments and a Bottom Navigation View. (2 points)
- [x] User can load more posts once he or she reaches the bottom of the feed using endless scrolling. (2 points)
- [x] Show the username and creation time for each post (1 point)
- [ ] After the user submits a new post, show an indeterminate progress bar while the post is being uploaded to Parse (1 point)
- [x] User Profiles:
  - [x] Allow the logged in user to add a profile photo (1 point)
  - [x] Display the profile photo with each post (1 points)
  - [x] Tapping on a post's username or profile photo goes to that user's profile page and shows a grid view of the user's posts (2 points)
- [x] User can comment on a post and see all comments for each post in the post details screen. (2 points)
- [x] User can like a post and see number of likes for each post in the post details screen. (2 points)

## My Own Stories
- [x] Use Butterknife to reduce boilerplate
- [ ] Use ParseLive to automatically update content on a new post
  - [ ] Retain posts in timeline instead of reloading every time (not quite SQL persistence)
- [ ] Add user follow functionality
  - [x] Finished the backend, but not the frontend
- [ ] User can choose image filters before posting
- [ ] User can annotate and draw on images...
  - [ ] With brushes
  - [ ] With stamps
- [ ] User can repost a post
  - [ ] ...with their own filters and/or annotations added
  - [ ] The repost links back to the original post despite having a potentially different image
- [ ] Add fun scroll animations (e.g. background color fade on scroll, segmented scrolling, etc.)
- [ ] Implement search features
<file_sep>/FBUParstagram/app/src/main/java/com/jmindel/fbuparstagram/fragments/ComposeFragment.java
package com.jmindel.fbuparstagram.fragments;

import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;

import com.jmindel.fbuparstagram.CameraManager;
import com.jmindel.fbuparstagram.R;
import com.jmindel.fbuparstagram.model.Post;
import com.parse.ParseException;
import com.parse.ParseFile;
import com.parse.ParseUser;
import com.parse.SaveCallback;

import java.io.IOException;

import butterknife.BindView;
import butterknife.ButterKnife;

public class ComposeFragment extends Fragment {
    private CameraManager cameraManager;
    String photoFileName = "photo.jpg";
    private ParseFile imageFile;

    public static final String KEY_POST = "post";

    @BindView(R.id.ivImage)      ImageView ivImage;
    @BindView(R.id.etCaption)    EditText etCaption;
    @BindView(R.id.bFromLibrary) Button bFromLibrary;
    @BindView(R.id.bFromCamera)  Button bFromCamera;
    @BindView(R.id.bPost)        Button bPost;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_compose, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        ButterKnife.bind(this, view);

        cameraManager = new CameraManager(photoFileName, this);

        bFromLibrary.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                cameraManager.pickPhoto();
            }
        });
        bFromCamera.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                cameraManager.takePhoto();
            }
        });
        bPost.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String caption = etCaption.getText().toString();
                if (imageFile != null && !caption.equals("")) {
                    Post post = new Post();
                    post.setImage(imageFile);
                    post.setCaption(caption);
                    post.setUser(ParseUser.getCurrentUser());
                    post.saveInBackground(new SaveCallback() {
                        @Override
                        public void done(ParseException e) {
                            if (e != null) {
                                Log.e("ComposeFragment", "Failed to post");
                                e.printStackTrace();
                                Toast.makeText(getContext(), "Failed to post", Toast.LENGTH_LONG).show();
                            } else {
                                Log.d("ComposeFragment", "Posted successfully!");
                                Toast.makeText(getContext(), "Posted successfully!", Toast.LENGTH_SHORT).show();
                                etCaption.setText("");
                            }
                        }
                    });
                } else {
                    Toast.makeText(getContext(), "Add an image and caption before posting.", Toast.LENGTH_LONG).show();
                }
            }
        });
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        if (requestCode == CameraManager.CAPTURE_IMAGE_REQUEST_CODE) {
            if (resultCode == Activity.RESULT_OK) {
                Log.d("ComposeFragment", "Successfully took photo");
                // by this point we have the camera photo on disk
                Bitmap scaledImage = cameraManager.fixOrientationAndSize();
                // Load the taken image into a preview
                ivImage.setImageBitmap(scaledImage);
                imageFile = new ParseFile(cameraManager.getPhotoFile());
            } else if (resultCode == Activity.RESULT_CANCELED) {
                Log.d("ComposeFragment", "Canceled open camera");
            }
        } else if (requestCode == CameraManager.PICK_PHOTO_REQUEST_CODE) {
            if (resultCode == Activity.RESULT_OK && data != null) {
                Uri photoUri = data.getData();
                try {
                    Bitmap selectedImage = MediaStore.Images.Media.getBitmap(getContext().getContentResolver(), photoUri);
                    ivImage.setImageBitmap(selectedImage);
                    imageFile = new ParseFile(cameraManager.bitmapToBytes(selectedImage));
                } catch (IOException e) {
                    Log.e("ComposeFragment", "Failed to get image from library");
                    e.printStackTrace();
                    Toast.makeText(getContext(), "Failed to get image from library", Toast.LENGTH_LONG).show();
                }
            } else if (resultCode == Activity.RESULT_CANCELED) {
                Log.d("ComposeFragment", "Canceled open library");
            } else {
                Log.e("ComposeFragment", "No data passed from image library");
            }
        }
    }
}
<file_sep>/FBUParstagram/app/src/main/java/com/jmindel/fbuparstagram/activities/HomeActivity.java
package com.jmindel.fbuparstagram.activities;

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.BottomNavigationView;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity;
import android.view.MenuItem;

import com.jmindel.fbuparstagram.R;
import com.jmindel.fbuparstagram.fragments.ComposeFragment;
import com.jmindel.fbuparstagram.fragments.ProfileFragment;
import com.jmindel.fbuparstagram.fragments.TimelineFragment;

import butterknife.BindView;
import butterknife.ButterKnife;

public class HomeActivity extends AppCompatActivity {

    // ids start with 11
    public static final int MAKE_POST_REQUEST_CODE = 11;

    @BindView(R.id.bottom_navigation)   BottomNavigationView bottomNav;

    FragmentManager fragmentManager;
    TimelineFragment timelineFragment;
    ComposeFragment composeFragment;
    ProfileFragment profileFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        ButterKnife.bind(this);

        fragmentManager = getSupportFragmentManager();
        timelineFragment = new TimelineFragment();
        composeFragment = new ComposeFragment();
        profileFragment = new ProfileFragment();

        bottomNav.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
                Fragment fragment;
                switch (menuItem.getItemId()) {
                    case R.id.miCompose:
                        fragment = composeFragment;
                        break;
                    case R.id.miProfile:
                        fragment = profileFragment;
                        break;
                    case R.id.miHome:
                    default:
                        fragment = timelineFragment;
                        break;
                }
                fragmentManager.beginTransaction().replace(R.id.flFragmentContainer, fragment).commit();
                return true;
            }
        });

        fragmentManager.beginTransaction().replace(R.id.flFragmentContainer, timelineFragment).commit();

        // Set logged out to false
        getIntent().putExtra(LoginActivity.KEY_LOGGED_OUT, false);
    }

//    public void onMakePost(View view) {
//        Intent i = new Intent(this, MakePostActivity.class);
//        startActivityForResult(i, MAKE_POST_REQUEST_CODE);
//    }
//
//    @Override
//    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
//        if (resultCode == RESULT_OK && requestCode == MAKE_POST_REQUEST_CODE) {
////            final Post post = data.getParcelableExtra(MakePostActivity.KEY_POST);
////            post.saveInBackground(new SaveCallback() {
////                @Override
////                public void done(ParseException e) {
////                    if (e != null) {
////                        Log.e("HomeActivity", "Posting failed");
////                        e.printStackTrace();
////                        Toast.makeText(HomeActivity.this, "Posting failed", Toast.LENGTH_LONG).show();
////                    } else {
////                        Log.d("HomeActivity", "Posting succeeded!");
////                        items.add(0, post);
////                        adapter.notifyItemInserted(0);
////                        rvPosts.scrollToPosition(0);
////                        // TODO: Navigate to new post and/or otherwise show it
////                    }
////                }
////            });
//        }
//    }
}
<file_sep>/FBUParstagram/app/src/main/java/com/jmindel/fbuparstagram/scrolling/EndlessScrollRefreshLayout.java
package com.jmindel.fbuparstagram.scrolling;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.util.AttributeSet;
import android.widget.FrameLayout;

import com.jmindel.fbuparstagram.R;

import java.util.ArrayList;
import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;

public abstract class EndlessScrollRefreshLayout<Item, VH extends RecyclerView.ViewHolder> extends FrameLayout {

    protected RecyclerView.Adapter<VH> adapter;
    protected List<Item> items;
    protected EndlessRecyclerViewScrollListener scrollListener;

    @BindView(R.id.rvItems)         protected RecyclerView rvItems;
    @BindView(R.id.swipeContainer)  protected SwipeRefreshLayout swipeContainer;

    public EndlessScrollRefreshLayout(@NonNull Context context) {
        super(context);
        init();
    }

    public EndlessScrollRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public EndlessScrollRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        inflate(getContext(), R.layout.layout_endless_scroll_refresh, this);
        ButterKnife.bind(this, this);

        // Configure RecyclerView
        items = new ArrayList<>();
        adapter = makeAdapter();

        rvItems.setAdapter(adapter);
        rvItems.addItemDecoration(new EdgeDecorator(getEdgePadding(), shouldPadTopEdge(), shouldPadBottomEdge()));

        // LEARN: There's got to be a better and far more properly polymorphic way to do this...
        switch (getLayoutManagerType()) {
            case Grid:
                GridLayoutManager gLayoutManager = makeGridLayoutManager();
                rvItems.setLayoutManager(gLayoutManager);
                // Configure infinite scrolling
                scrollListener = new EndlessRecyclerViewScrollListener(gLayoutManager) {
                    @Override
                    public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
                        // Triggered only when new data needs to be appended to the list
                        loadMore();
                    }
                };
                break;
            case StaggeredGrid:
                StaggeredGridLayoutManager sgLayoutManager = makeStaggeredGridLayoutManager();
                rvItems.setLayoutManager(sgLayoutManager);
                // Configure infinite scrolling
                scrollListener = new EndlessRecyclerViewScrollListener(sgLayoutManager) {
                    @Override
                    public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
                        // Triggered only when new data needs to be appended to the list
                        loadMore();
                    }
                };
                break;
            default:
            case Linear:
                LinearLayoutManager lLayoutManager = makeLinearLayoutManager();
                rvItems.setLayoutManager(lLayoutManager);
                // Configure infinite scrolling
                scrollListener = new EndlessRecyclerViewScrollListener(lLayoutManager) {
                    @Override
                    public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
                        // Triggered only when new data needs to be appended to the list
                        loadMore();
                    }
                };
                break;
        }

        // Add the scroll listener to RecyclerView
        rvItems.addOnScrollListener(scrollListener);

        // Add swipe to refresh actions
        swipeContainer.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                items.clear();
                adapter.notifyDataSetChanged();
                load();
            }
        });
        swipeContainer.setColorSchemeResources(getColorScheme());
    }

    public abstract void load();
    public abstract void loadMore();

    public abstract RecyclerView.Adapter<VH> makeAdapter();

    public abstract LayoutManagerType getLayoutManagerType();
    public abstract LinearLayoutManager makeLinearLayoutManager();
    public abstract GridLayoutManager makeGridLayoutManager();
    public abstract StaggeredGridLayoutManager makeStaggeredGridLayoutManager();

    public abstract int[] getColorScheme();

    public abstract int getEdgePadding();
    public abstract boolean shouldPadTopEdge();
    public abstract boolean shouldPadBottomEdge();

    public enum LayoutManagerType {
        Linear, Grid, StaggeredGrid
    }
}
<file_sep>/FBUParstagram/app/src/main/java/com/jmindel/fbuparstagram/model/Post.java
package com.jmindel.fbuparstagram.model;

import com.parse.FindCallback;
import com.parse.ParseClassName;
import com.parse.ParseFile;
import com.parse.ParseObject;
import com.parse.ParseQuery;
import com.parse.ParseUser;

import java.io.Serializable;

@ParseClassName("Post")
public class Post extends ParseObject implements Serializable {
    public static final String KEY_CAPTION = "caption";
    public static final String KEY_IMAGE = "image";
    public static final String KEY_USER = "user";
    public static final String KEY_CREATED_AT = "createdAt";
    public static final String KEY_ID = "objectId";

    public String getCaption() {
        return getString(KEY_CAPTION);
    }

    public void setCaption(String caption) {
        put(KEY_CAPTION, caption);
    }

    public ParseFile getImage() {
        return getParseFile(KEY_IMAGE);
    }

    public void setImage(ParseFile image) {
        put(KEY_IMAGE, image);
    }

    public ParseUser getUser() {
        return getParseUser(KEY_USER);
    }

    public void setUser(ParseUser user) {
        put(KEY_USER, user);
    }

    public void findLikesInBackground(FindCallback<Like> cb) {
        Like.Query query = new Like.Query().withPost().withUser().forPost(this);
        query.findInBackground(cb);
    }

    public void findCommentsInBackground(FindCallback<Comment> cb) {
        Comment.Query query = new Comment.Query().withPost().withUser().forPost(this);
        query.findInBackground(cb);
    }

    public static class Query extends ParseQuery<Post> {
        public Query() {
            super(Post.class);
        }

        public Query getTop() {
            return getTop(20);
        }

        public Query getTop(int limit) {
            setLimit(limit);
            return this;
        }

        public Query withUser() {
            include("user");
            return this;
        }
    }
}
<file_sep>/FBUParstagram/app/src/main/java/com/jmindel/fbuparstagram/ParseApp.java
package com.jmindel.fbuparstagram;

import android.app.Application;

import com.jmindel.fbuparstagram.model.Comment;
import com.jmindel.fbuparstagram.model.Like;
import com.jmindel.fbuparstagram.model.Post;
import com.parse.Parse;
import com.parse.ParseObject;

public class ParseApp extends Application {

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

        ParseObject.registerSubclass(Post.class);
        ParseObject.registerSubclass(Like.class);
        ParseObject.registerSubclass(Comment.class);

        final Parse.Configuration config = new Parse.Configuration.Builder(this)
                .applicationId("jmindel-fbu-parstagram")
                .clientKey("<KEY>")
                .server("https://jmindel-fbu-parstagram.herokuapp.com/parse")
                .build();

        Parse.initialize(config);
    }
}
<file_sep>/FBUParstagram/app/src/main/java/com/jmindel/fbuparstagram/activities/LoginActivity.java
package com.jmindel.fbuparstagram.activities;

import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

import com.jmindel.fbuparstagram.R;
import com.parse.LogInCallback;
import com.parse.ParseException;
import com.parse.ParseUser;
import com.parse.SignUpCallback;

import butterknife.BindView;
import butterknife.ButterKnife;

public class LoginActivity extends AppCompatActivity {
    @BindView(R.id.etUsername)  EditText etUsername;
    @BindView(R.id.etPassword)  EditText etPassword;
    @BindView(R.id.etEmail)     EditText etEmail;
    @BindView(R.id.bLogin)      Button bLogin;
    @BindView(R.id.bSignUp)     Button bSignUp;

    // ids start with 1
    public static final int HOME_REQUEST_CODE = 1;
    public static final String KEY_LOGGED_OUT = "loggedOut";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        ButterKnife.bind(this);

        // Set up button clicks
        bLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                final String username = etUsername.getText().toString();
                final String password = etPassword.getText().toString();

                if (!username.equals("") && !password.equals("")) {
                    login(username, password);
                }
            }
        });
        bSignUp.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                final String username = etUsername.getText().toString();
                final String password = etPassword.getText().toString();
                final String email = etEmail.getText().toString();

                if (!username.equals("") && !password.equals("") && !email.equals("")) {
                    signUp(username, password, email);
                }
            }
        });

        // Automatically log in cached user
        ParseUser currUser = ParseUser.getCurrentUser();
        if (currUser != null) {
            Log.d("LoginActivity", "Logged-in user persisted successfully.");
            launchHome();
        }
    }

    protected void login(final @NonNull String username, final @NonNull String password) {
        ParseUser.logInInBackground(username, password, new LogInCallback() {
            @Override
            public void done(ParseUser user, ParseException e) {
                if (e == null) {
                    Log.d("LoginActivity", "Login successful!");
                    launchHome();
                } else {
                    Log.e("LoginActivity", "Login failed.");
                    e.printStackTrace();
                }
            }
        });
    }

    protected void signUp(final @NonNull String username, final @NonNull String password, final @NonNull String email) {
        ParseUser user = new ParseUser();
        user.setUsername(username);
        user.setPassword(<PASSWORD>);
        user.setEmail(email);

        user.signUpInBackground(new SignUpCallback() {
            @Override
            public void done(ParseException e) {
                if (e == null) {
                    Log.d("LoginActivity", "Sign up succeeded!");
                    login(username, password);
                } else {
                    Log.d("LoginActivity", "Sign up failed.");
                }
            }
        });
    }

    protected void launchHome() {
        Intent i = new Intent(LoginActivity.this, HomeActivity.class);
        startActivityForResult(i, HOME_REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        if (resultCode == RESULT_OK && requestCode == HOME_REQUEST_CODE) {
            boolean loggedOut = data.getBooleanExtra(KEY_LOGGED_OUT, false);
            if (loggedOut) finish();
        }
    }
}
