You are on page 1of 69

Build a

Photo
Sharing
App in a
Day
Step by Step Guide
for Android
www.stackmob.com

SnapStack Android Bootcamp

Welcome!
This series is focused on the creation of SnapStack, a location-based photo sharing app for Android
phones. Well walk through the entire process of building SnapStack, from the initial idea and design to
submitting to the Google Play Store. Along the way, well demonstrate the usefulness of the StackMob
platform and highlight the benefits of incorporating StackMob into your next project. Download
SnapStack from the Google Play Store.

Who should read this tutorial?


This series is aimed at developers of all skill levels who are looking for an example app that showcases
the features of the StackMob platform. The goal is to illustrate how easy it is to build and release to the
Google Play Store using StackMob as the backend of your app. The pace of this tutorial will start out slow
and ramp up quickly. Its recommended that you have a basic understanding of Java and the Eclipse IDE.

Prerequisites
Well be using the Android Developer Tools v21 and our mininum SDK will be Android 4.0.
If youre not already a StackMob customer, sign up for free.

Idea
Our app, SnapStack, will be a simple photo taking app. Users will be able to snap and share photos, view
photos nearby both in a feed and on a map, and post comments. Download SnapStack from the Google
Play Store.

What well cover


Creating an app like SnapStack requires a fair amount of setup. In this tutorial, well be adding all of
the ingredients necessary to construct our app. Well create an Android Eclipse project as well as a
StackMob app. The next part of this tutorial will cover adding the necessary SDKs and configurations to
our project.

www.stackmob.com

SnapStack Android Bootcamp

Part 1: Setup
Using StackMob
Well be using many features provided by the StackMob Android SDK and the StackMob Marketplace. The
modules in the Marketplace are services that can be quickly installed and incorporated into your app.
Our app will utilize the following modules from the StackMob Marketplace:

Access Controls:
The access controls module will give us greater control over schema permissions.
API:
Well use the API to perform CRUD operations on our data.
GeoQueries:
The geoqueries module will enable our app to be powered by GPS location data.

S3:
To integrate photo storage into our app, well use the S3 module.

Download ADT
If you havent done so yet, visit the Android developer center to
download and install the ADT bundle.

Create a new Android project


1. Open up ADT and select a directory for your workspace. The default is ~/Documents/workspace.
2. Choose File > New > Android Application Project.

www.stackmob.com

3. Enter SnapStack for the


name of the application, and
SnapStackAndroid for the
project name. Enter a unique
identifier for your package
name. Set your minimum
Android SDK to 3.0. Set the
project to compile with the
latest Google APIs. Click next.

4. Make sure Create Activity is


selected. Click next twice.

www.stackmob.com

5. Choose Blank Activity and


click next.

6. Enter MainActivity for the


Activity name and activity_
main for the Layout name.
Click finish.

Create a StackMob application


Instead of building our own backend server (which would take weeks of work), we can take advantage
of the StackMob platform immediately. To do this, well create an app on StackMob, which will come
complete with Development and Production Keys, API requests and some Marketplace modules
preinstalled, all for free!

1. Head over to StackMob and


login to your Dashboard (if
you dont have an account,
create one first). Click Create
New App, in the top right
corner.

www.stackmob.com

2. Create an app called


snapstack and choose
Android as your platform.
Click next.

3. Follow the setup instructions


for importing the StackMob
Android SDK.

Setting up S3
Follow the tutorial to create and add S3 credentials to your StackMob app.

Creating the data model


Our app will have a straightforward data model, consisting of three schemas: User, Snap and Comment.
The User schema is automatically created for you when you create an app on StackMob. It is assumed to
be the schema for storing your user objects as well as for password management, etc.
A snap is created by a user, and contains the image taken, a relationship to the User who created it, as
well as the geo location of where it was created.
Users can make comments on snaps. A comment has a relationship to the User who created it, the text of
the comment, and the relationship to its parent Snap object.
1. Navigate to the Schema Configuration tab in
your Dashboard.
2. Click Edit next to the User schema. Add the
following attributes:
A string attribute called email
A binary attribute called photo
Save the User schema.
In the User schema, set the Forgot Password Email Field to email.

www.stackmob.com

3. Click Schema Configuration from the menu. Click Create New Schema. Create a schema called snap
and add the following attributes:
A binary attribute called photo
A geopoint attribute called location

4. StackMob allows you to manage schema permissions using the Access Controls module. Edit the
permissions for this schema:
Set the create permission level to Allow to any logged in user
Set the read permission level to Allow to any logged in user
Set the edit permission level to Allow to sm_owner (object owner)
Set the delete permission level to Allow to sm_owner (object owner)
Save the Snap schema.

5. Create another schema called comment and add the following attribute:
A String attribute called text
Edit the permissions to match those in the snap schema. Add a relationship called snap with the
related object set to snap. Make it a one-to-one relationship. Add another one-to-one relationship
to user, called creator.

6. Go back and edit the snap schema. Add a relationship called comments and set the related object
to comment. Make it a one-to-many relationship. Add a one-to-one relationship to user, called
creator.

Setting up Google Maps


Our app utilizes Google Maps, which requires Google Play Services to be installed.
Visit the Google tutorial for Google Maps Android API v2. Follow the guide up until the last section, Add
a Map.

www.stackmob.com

Creating a User model


Well add a class named User to our project that subclasses StackMobUser. StackMobUser is a
specialized subclass of StackMobModel meant to represent a user of your app. Like StackMobModel, its
meant to be subclassed with whatever data you want to store with your user.
Create a file, User.java, and add the following code:
package com.stackmob.snapstack;
import com.stackmob.sdk.api.StackMobFile;
import com.stackmob.sdk.model.StackMobUser;
public class User extends StackMobUser {


private String email;

private StackMobFile photo;


public User(String username, String password, String email) {

super(User.class, username, password);


this.email = email;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {

this.email = email;
}

public StackMobFile getPhoto() {
return photo;
}

public void setPhoto(StackMobFile photo) {

this.photo = photo;
}
}

Creating a Snap model


To create and save snaps well create a class named Snap, which will subclass StackMobModel. Add a new
class called Snap.java, with the following code:
package com.stackmob.snapstack;
import com.stackmob.sdk.api.StackMobFile;
import com.stackmob.sdk.api.StackMobGeoPoint;
import com.stackmob.sdk.model.StackMobModel;
public class Snap extends StackMobModel {

private User creator;


private StackMobGeoPoint location;

www.stackmob.com

private StackMobFile photo;


public Snap(User creator, StackMobGeoPoint location) {
super(Snap.class);

this.creator = creator;
this.location = location;

}

public User getCreator() {
return creator;
}

public void setCreator(User creator) {

this.creator = creator;
}

public StackMobGeoPoint getLocation() {
return location;
}

public void setLocation(StackMobGeoPoint location) {

this.location = location;
}

public void setPhoto(StackMobFile photo) {

this.photo = photo;
}

public StackMobFile getPhoto() {
return photo;
}
}

Creating a comment model


Our Comment class will also subclass StackMobModel. Add a new class called Comment.java, with the
following code:

package com.stackmob.snapstack;
import com.stackmob.sdk.model.StackMobModel;
public class Comment extends StackMobModel {


private User creator;


private String text;
private Snap snap;


public Comment(User creator, String text, Snap snap) {
super(Comment.class);



}

this.creator = creator;
this.text = text;
this.snap = snap;


public User getCreator() {
return creator;

www.stackmob.com

}

public void setCreator(User creator) {

this.creator = creator;
}

public String getText() {
return text;
}

public void setText(String text) {

this.text = text;
}

public Snap getSnap() {
return snap;
}

public void setSnap(Snap snap) {

this.snap = snap;
}
}

Adding strings
SnapStack makes use of many string constants throughout the app. Edit res/values/strings.xml with
the following strings:
<?xml version=1.0 encoding=utf-8?>
<resources>
<string name=app_name>SnapStack</string>
<string name=action_settings>Settings</string>
<string name=hello_world>Hello world!</string>
<string name=signin>Sign In</string>
<string name=signup>Sign Up For SnapStack</string>
<string name=forgot_password>Forgot Your Password?</string>
<string name=username_hint>Enter your username</string>
<string name=password_hint>Enter your password</string>
<string name=email_hint>Enter your email address</string>
<string name=join>Join SnapStack</string>
<string name=choose_photo>Make profile picture</string>
<string name=contentDescriptionChoosePhoto>Choose your profile picture</string>
<string name=toggle_turn_on>Show Map</string>
<string name=toggle_turn_off>Show List</string>
<string name=contentDescriptionProfileImage>This is the profile picture</string>
<string name=contentDescriptionImageView>This is an image</string>
<string name=share_photo>Share Photo</string>
<string name=comments>Comments</string>
<string name=signout>Sign Out</string>
<string name=delete>Delete</string>
<string name=comment_hint>Type your comment here</string>
<string name=share_comment>Share Comment</string>
<string name=comment>Comment</string>
<string name=forgot_password_button>Email a temporary password</string>
<string name=forgot_password_textview>Enter your username, and we\ll email you a temporary
password.</string>
<string name=change_password_hint>Enter temporary password</string>
</resources>

www.stackmob.com

10

Adding 3rd party libraries


Download Android Pull to Refresh and add it to your project:

1. Right-click the project and choose Import

2. Choose Existing Android Code Into Workspace as an import source.

3. Select only the library and click


Finish.Make sure you have
Copy projects into Workspace
checked.

www.stackmob.com

11

4. Right-click the project, and select Properties:


5. Navigate to the Android menu and click Add:

6. Select the library and click OK.

Download Android Universal


Image Loader and add it to your
project. Copy the jar file into
your libs folder.

www.stackmob.com

12

Adding the necessary assets


Weve included the assets needed for this project, including drawables and XML layouts. Download and
unzip the assets for this project.

1. Drag the entire drawable folder into the project, under the res folder. Make sure you have Copy
files selected.

2. Copy the contents of the drawable-hdpi folder into the corresponding directory in your project.
Make sure you have Copy files selected.

3. Copy the contents of the layout folder into the corresponding directory in your project. Make sure
you have Copy files selected.

4. Finally, copy the contents of the menu folder into the corresponding directory in your project. Make
sure you have Copy files selected.

SnapStack
Application
In our app well create an
Application class. This is
where well store our User
object, for use throughout
the app. Create a class called
SnapStackApplication.java.
Make sure it subclasses the
Application class.

www.stackmob.com

13

package com.stackmob.snapstack;
import android.app.Application;
import android.content.Context;
import
import
import
import

com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator;
com.nostra13.universalimageloader.core.ImageLoader;
com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
com.nostra13.universalimageloader.core.assist.QueueProcessingType;

public class SnapStackApplication extends Application {



private User user;


private Snap snap;

@Override

public void onCreate() {
super.onCreate();
initImageLoader(getApplicationContext());
}

public User getUser() {
return user;
}

public void setUser(User user) {

this.user = user;
}

public Snap getSnap() {
return snap;
}

public void setSnap(Snap snap) {

this.snap = snap;
}

public static void initImageLoader(Context context) {

ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(

context).threadPriority(Thread.NORM_PRIORITY - 2)
.denyCacheImageMultipleSizesInMemory()
.discCacheFileNameGenerator(new Md5FileNameGenerator())

.tasksProcessingOrder(QueueProcessingType.LIFO).enableLogging()
.build();

// Initialize ImageLoader with configuration.

ImageLoader.getInstance().init(config);
}
}

This application class contains two instance variables, user and snap, as well as getters and setters for
both of them. Well use these two extensively throughout the project. Well also be utilizing ImageLoader
throughout the project, and we initialize it here.

www.stackmob.com

14

AndroidManifest
Update your projects AndroidManifest.xml file to look like this:
<?xml version=1.0 encoding=utf-8?>
<manifest xmlns:android=http://schemas.android.com/apk/res/android
package=com.stackmob.snapstack
android:versionCode=1
android:versionName=1.0 >
<uses-feature
android:glEsVersion=0x00020000
android:required=true />
<permission
android:name=com.stackmob.snapstack.permission.MAPS_RECEIVE
android:protectionLevel=signature />
<uses-permission
<uses-permission
<uses-permission
<uses-permission
<uses-permission
<uses-permission
<uses-permission
<uses-permission
<uses-permission

android:name=com.stackmob.snapstack.permission.MAPS_RECEIVE />
android:name=android.permission.INTERNET />
android:name=android.permission.CAMERA />
android:name=android.permission.WRITE_EXTERNAL_STORAGE />
android:name=com.google.android.providers.gsf.permission.READ_GSERVICES />
android:name=android.permission.ACCESS_COARSE_LOCATION />
android:name=android.permission.ACCESS_FINE_LOCATION />
android:name=android.permission.ACCESS_NETWORK_STATE />
android:name=android.permission.ACCESS_WIFI_STATE />

<uses-feature
android:name=android.hardware.camera
android:required=true />
<uses-sdk
android:minSdkVersion=14
android:targetSdkVersion=17 />
<application
android:name=.SnapStackApplication
android:allowBackup=true
android:icon=@drawable/app_icon
android:label=@string/app_name
android:theme=@style/AppTheme >
<meta-data
android:name=com.google.android.maps.v2.API_KEY
android:value=YOUR_API_KEY />
<activity
android:name=com.stackmob.snapstack.MainActivity
android:screenOrientation=portrait >
<intent-filter>
<action android:name=android.intent.action.MAIN />
<category android:name=android.intent.category.LAUNCHER />
</intent-filter>
</activity>
<activity
android:name=.SignUpActivity
android:screenOrientation=portrait >
</activity>
<activity
android:name=.SignInActivity
android:screenOrientation=portrait >
</activity>
<activity

www.stackmob.com

15

android:name=.ChoosePhotoActivity
android:screenOrientation=portrait >
</activity>
<activity
android:name=.MasterActivity
android:screenOrientation=portrait >
</activity>
<activity
android:name=.ProfileActivity
android:screenOrientation=portrait >
</activity>
<activity
android:name=.SharePhotoActivity
android:screenOrientation=portrait >
</activity>
<activity
android:name=.DetailViewActivity
android:screenOrientation=portrait >
</activity>
<activity
android:name=.PhotoViewActivity
android:screenOrientation=portrait >
</activity>
<activity
android:name=.CommentViewActivity
android:screenOrientation=portrait>
</activity>
<activity
android:name=.ShareCommentActivity
android:screenOrientation=portrait >
</activity>
<activity
android:name=.ForgotPasswordActivity
android:screenOrientation=portrait >
</activity>
<activity
android:name=.ChangePasswordActivity
android:screenOrientation=portrait >
</activity>
</application>
</manifest>

The project now includes all the necessary permissions and activity references for our app.

Sanity check
Be sure to build your project to double check that it is free of errors.

Congrats!
Weve reached the end of Part 1 and have completed the majority of the grunt work. All the pieces are
in place: StackMob, S3 integration, XML layouts and more. We can focus on simply the code from here
on out.
In Part 2 , well focus on creating and uploading Snaps, as well as the profile and map view.

www.stackmob.com

16

SnapStack Android Bootcamp

Part 2
What well cover
In this part well build the sign up/sign in flow for the app, allowing users to create profiles on SnapStack.
Well also implement forgot password functionality in our app.

Creating the MasterActivity class


Create a class that extends Activity, named MasterActivity.java. Most of our app will run through
MasterActivity. For now, we wont add any logic behind it:
package com.stackmob.snapstack;
import android.app.Activity;
import android.os.Bundle;
public class MasterActivity extends Activity {

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

The signup flow


Lets build out the signup flow for SnapStack. Our signup process will be straightforward: well have
users create accounts by providing a username, password and email. Well also require users to upload a
profile picture.
For photos, well make use of the standard Android Camera library. Users will have the option to take a
photo or select from their gallery, and afterwards, crop the photo into a square.
Add the following classes, which weve borrowed from this image cropping example on Github.
CropOption.java:

www.stackmob.com

17

package com.stackmob.snapstack;
import android.content.Intent;
import android.graphics.drawable.Drawable;
public class CropOption {
public CharSequence title;

public Drawable icon;

public Intent appIntent;
}

CropOptionAdapter.java:
package com.stackmob.snapstack;
import java.util.ArrayList;
import
import
import
import
import
import
import

android.content.Context;
android.view.LayoutInflater;
android.view.View;
android.view.ViewGroup;
android.widget.ArrayAdapter;
android.widget.ImageView;
android.widget.TextView;

public class CropOptionAdapter extends ArrayAdapter<CropOption> {


private ArrayList<CropOption> mOptions;

private LayoutInflater mInflater;


public CropOptionAdapter(Context context, ArrayList<CropOption> options) {

super(context, R.layout.crop_selector, options);


mOptions
= options;


mInflater
= LayoutInflater.from(context);
}

@Override

public View getView(int position, View convertView, ViewGroup group) {

if (convertView == null)

convertView = mInflater.inflate(R.layout.crop_selector, null);


CropOption item = mOptions.get(position);


if (item != null) {

((ImageView) convertView.findViewById(R.id.iv_icon)).setImageDrawable(item.
icon);

((TextView) convertView.findViewById(R.id.tv_name)).setText(item.title);

return convertView;
}

return null;
}
}

www.stackmob.com

18

Next, add an Activity named ChoosePhotoActivity, with the following code:


package com.stackmob.snapstack;
import
import
import
import

java.io.ByteArrayOutputStream;
java.io.File;
java.util.ArrayList;
java.util.List;

import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import

android.app.Activity;
android.app.AlertDialog;
android.app.ProgressDialog;
android.app.AlertDialog.Builder;
android.content.ActivityNotFoundException;
android.content.ComponentName;
android.content.DialogInterface;
android.content.Intent;
android.content.pm.ResolveInfo;
android.graphics.Bitmap;
android.graphics.BitmapFactory;
android.graphics.drawable.BitmapDrawable;
android.net.Uri;
android.os.Bundle;
android.os.Environment;
android.provider.MediaStore;
android.view.View;
android.widget.ArrayAdapter;
android.widget.Button;
android.widget.ImageView;
android.widget.Toast;

import com.stackmob.sdk.api.StackMobFile;
import com.stackmob.sdk.callback.StackMobModelCallback;
import com.stackmob.sdk.exception.StackMobException;
public class ChoosePhotoActivity extends Activity {








private
private
private
private
private

SnapStackApplication snapStackApplication;
Uri imageCaptureUri;
ImageView choose_photo_imageview;
Button choose_photo_button;
ProgressDialog progressDialog;

private static final int PICK_FROM_CAMERA = 1;


private static final int CROP_FROM_CAMERA = 2;
private static final int PICK_FROM_FILE = 3;

@Override

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

setContentView(R.layout.activity_choose_photo);


snapStackApplication = (SnapStackApplication) getApplication();

final String[] items = new String[] { Take from camera,

Select from gallery };

ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,

android.R.layout.select_dialog_item, items);

AlertDialog.Builder builder = new AlertDialog.Builder(this);

builder.setTitle(Select Image);

builder.setAdapter(adapter, new DialogInterface.OnClickListener() {

public void onClick(DialogInterface dialog, int item) { // pick from

// camera

if (item == 0) {

www.stackmob.com

19

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

imageCaptureUri = Uri.fromFile(new File(Environment



.getExternalStorageDirectory(), tmp_avatar_

+ String.valueOf(System.currentTimeMillis())

+ .jpg));

intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT,
imageCaptureUri);
try {

intent.putExtra(return-data, true);


startActivityForResult(intent, PICK_FROM_CAMERA);
} catch (ActivityNotFoundException e) {

e.printStackTrace();
}

} else { // pick from file
Intent intent = new Intent();

intent.setType(image/*);
intent.setAction(Intent.ACTION_GET_CONTENT);


startActivityForResult(Intent.createChooser(intent,

Complete action using), PICK_FROM_FILE);
}
}
});

final AlertDialog dialog = builder.create();


choose_photo_button = (Button) findViewById(R.id.choose_photo_button);
choose_photo_button.setEnabled(false);


choose_photo_imageview = (ImageView) findViewById(R.id.choose_photo_imageview);

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.default_
avatar);

choose_photo_imageview.setImageBitmap(bitmap);


choose_photo_imageview.setOnClickListener(new View.OnClickListener() {
@Override

public void onClick(View v) {
dialog.show();
}
});



choose_photo_button.setOnClickListener(new View.OnClickListener() {
@Override

public void onClick(View v) {

progressDialog = ProgressDialog.show(

ChoosePhotoActivity.this, Uploading photo,

Uploading your profile pic, true);


Bitmap bitmap = ((BitmapDrawable) choose_photo_imageview.
getDrawable()).getBitmap();

ByteArrayOutputStream stream = new ByteArrayOutputStream();

bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);

byte[] image = stream.toByteArray();

User user = snapStackApplication.getUser();

user.setPhoto(new StackMobFile(image/jpeg, profile_picture.jpg,
image));

user.save(new StackMobModelCallback() {

@Override

www.stackmob.com

20


public void success() {



progressDialog.dismiss();



int callingActivity = getIntent().getIntExtra(calling_
activity, 0);



if (callingActivity == 666) {


setResult(RESULT_OK, null);

finish();

}

else if (callingActivity == 333) {


Intent intent = new Intent(

ChoosePhotoActivity.this,

MasterActivity.class);


intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);

startActivity(intent);


finish();


}

}


@Override

public void failure(StackMobException e) {



progressDialog.dismiss();



runOnUiThread(new Runnable() {
@Override
public void run() {

Builder builder = new AlertDialog.
Builder(ChoosePhotoActivity.this);

builder.setTitle(Uh oh...);
builder.setCancelable(true);

builder.setMessage(There was an error
saving your photo.);
AlertDialog dialog = builder.create();

dialog.show();
}

});


}
});
}
});

dialog.show();
}
@Override

protected void onActivityResult(int requestCode, int resultCode, Intent data) {

if (resultCode != RESULT_OK)
return;

switch (requestCode) {

case PICK_FROM_CAMERA:
doCrop();
break;

case PICK_FROM_FILE:
imageCaptureUri = data.getData();
doCrop();
break;

case CROP_FROM_CAMERA:

www.stackmob.com

21

Bundle extras = data.getExtras();


if (extras != null) {

Bitmap photo = extras.getParcelable(data);

choose_photo_imageview.setImageBitmap(photo);
choose_photo_button.setEnabled(true);
}

File f = new File(imageCaptureUri.getPath());

if (f.exists())
f.delete();
break;
}
}

private void doCrop() {

final ArrayList<CropOption> cropOptions = new ArrayList<CropOption>();

Intent intent = new Intent(com.android.camera.action.CROP);


intent.setType(image/*);


List<ResolveInfo> list = getPackageManager().queryIntentActivities(
intent, 0);

int size = list.size();


if (size == 0) {

Toast.makeText(this, Can not find image crop app,

Toast.LENGTH_SHORT).show();
return;

} else {
intent.setData(imageCaptureUri);





intent.putExtra(outputX, 200);
intent.putExtra(outputY, 200);
intent.putExtra(aspectX, 1);
intent.putExtra(aspectY, 1);
intent.putExtra(scale, true);
intent.putExtra(return-data, true);


if (size == 1) {

Intent i = new Intent(intent);

ResolveInfo res = list.get(0);
i.setComponent(new ComponentName(res.activityInfo.packageName,
res.activityInfo.name));

startActivityForResult(i, CROP_FROM_CAMERA);
} else {

for (ResolveInfo res : list) {

final CropOption co = new CropOption();
co.title = getPackageManager().getApplicationLabel(
res.activityInfo.applicationInfo);
co.icon = getPackageManager().getApplicationIcon(
res.activityInfo.applicationInfo);
co.appIntent = new Intent(intent);
co.appIntent
.setComponent(new ComponentName(
res.activityInfo.packageName,
res.activityInfo.name));

www.stackmob.com

22

cropOptions.add(co);
}

CropOptionAdapter adapter = new CropOptionAdapter(
getApplicationContext(), cropOptions);

AlertDialog.Builder builder = new AlertDialog.Builder(this);

builder.setTitle(Choose Crop App);
builder.setAdapter(adapter,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int
item) {

startActivityForResult(
cropOptions.get(item).
appIntent,

CROP_FROM_CAMERA);
}
});
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
if (imageCaptureUri != null) {

getContentResolver().delete(imageCaptureUri,
null,
null);
imageCaptureUri = null;
}
}
});
AlertDialog alert = builder.create();
alert.show();
}
}
}
}

The doCrop method makes use of the CropOption and CropOptionAdapter classes. For more
information, check out this blog post explaining the code.
Once a photo is chosen, we save it to StackMob and present MasterActivity.
Finally, add an Activity named SignUpActivity:
package com.stackmob.snapstack;
import
import
import
import
import
import
import
import
import
import
import
import

android.app.Activity;
android.app.AlertDialog;
android.app.AlertDialog.Builder;
android.app.ProgressDialog;
android.content.Context;
android.content.Intent;
android.os.Bundle;
android.os.Handler;
android.view.View;
android.view.inputmethod.InputMethodManager;
android.widget.Button;
android.widget.EditText;

www.stackmob.com

23

import com.stackmob.sdk.callback.StackMobModelCallback;
import com.stackmob.sdk.exception.StackMobException;
public class SignUpActivity extends Activity {







private
private
private
private
private
private
private
private

SnapStackApplication snapStackApplication;
EditText username_edittext;
EditText password_edittext;
EditText email_edittext;
Button join_button;
ProgressDialog progressDialog;
Handler handler = new Handler();
User user;

@Override

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

setContentView(R.layout.activity_signup);

snapStackApplication = (SnapStackApplication) getApplication();

// Find our views


username_edittext = (EditText) findViewById(R.id.username_edittext);
password_edittext = (EditText) findViewById(R.id.password_edittext);
email_edittext = (EditText) findViewById(R.id.email_edittext);
join_button = (Button) findViewById(R.id.join_button);

join_button.setOnClickListener(new View.OnClickListener() {


public void onClick(View v) {

InputMethodManager imm = (InputMethodManager)getSystemService(Context.
INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(username_edittext.getWindowToken(), 0);
imm.hideSoftInputFromWindow(password_edittext.getWindowToken(), 0);
imm.hideSoftInputFromWindow(email_edittext.getWindowToken(), 0);

progressDialog = ProgressDialog.show(

SignUpActivity.this, Signing up,

Signing up for SnapStack, true);


String username = username_edittext.getText().toString().trim();


String password = password_edittext.getText().toString().trim();
String email = email_edittext.getText().toString().trim();


if (foundError(username, password, email)) {
progressDialog.dismiss();
return;
}

User user = new User(username, password, email);

snapStackApplication.setUser(user);
user.save(new StackMobModelCallback() {
@Override
public void success() {


handler.post(new UserLogin());
}
@Override
public void failure(StackMobException e) {

www.stackmob.com

24

progressDialog.dismiss();


runOnUiThread(new Runnable() {
@Override
public void run() {

Builder builder = new AlertDialog.Builder(
SignUpActivity.this);

builder.setTitle(Uh oh...);
builder.setCancelable(true);

builder.setMessage(There was an error
signing up.);
AlertDialog dialog = builder.create();
dialog.show();
}
});
}
});
}
});
}

public boolean foundError(String username, String password, String email) {


Builder builder = new AlertDialog.Builder(this);

builder.setTitle(Oops);
builder.setCancelable(true);

if (username.equals()) {

builder.setMessage(Dont forget to enter a username!);

AlertDialog dialog = builder.create();
dialog.show();
return true;
}

else if (password.equals()) {

builder.setMessage(Dont forget to enter a password!);

AlertDialog dialog = builder.create();
dialog.show();
return true;
}

else if (email.equals()) {

builder.setMessage(Dont forget to enter an email);

AlertDialog dialog = builder.create();
dialog.show();
return true;
}

else if (!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) {

builder.setMessage(Please enter a valid email address.);

AlertDialog dialog = builder.create();
dialog.show();
return true;
}
return false;
}


private class UserLogin implements Runnable{
public UserLogin(){

}

www.stackmob.com

25

public void run(){




user = snapStackApplication.getUser();


user.login(new StackMobModelCallback() {


@Override

public void success() {



progressDialog.dismiss();

snapStackApplication.setUser(user);
Intent intent = new Intent(SignUpActivity.this,
ChoosePhotoActivity.class);

intent.putExtra(calling_activity, 333);
startActivity(intent);

finish();

}


@Override

public void failure(StackMobException e) {



progressDialog.dismiss();



runOnUiThread(new Runnable() {
@Override
public void run() {

Builder builder = new AlertDialog.Builder(
SignUpActivity.this);

builder.setTitle(Uh oh...);
builder.setCancelable(true);

builder.setMessage(There was an error logging
in.);
AlertDialog dialog = builder.create();
dialog.show();
}
});

}
});

}
}

The signup view accepts a username/password/email combo and creates an account for the user. We do
this by creating a new User object and calling save on it. When the save is successful, we use a Handler
to call the UserLogin runnable, which in its run method signs the User in. The foundError method
checks for any empty or incorrect fields and notifies the user. If the save is successful, we present the
ChoosePhotoActivity.

Implementing forgot password


Its a good idea to have a way for a user to recover their account, in case they ever lose their password.
StackMob provides this feature with all User schemas. Earlier, we specified what field to use for the
forgot password email. With the Android SDK, we can call a method that will trigger the process to allow
the user to reclaim their account.

www.stackmob.com

26

First, create an Activity named ChangePasswordActivity, and add the following code:
package com.stackmob.snapstack;
import com.stackmob.sdk.callback.StackMobModelCallback;
import com.stackmob.sdk.exception.StackMobException;
import
import
import
import
import
import
import
import
import
import
import

android.app.Activity;
android.app.AlertDialog;
android.app.ProgressDialog;
android.app.AlertDialog.Builder;
android.content.Context;
android.content.Intent;
android.os.Bundle;
android.view.View;
android.view.inputmethod.InputMethodManager;
android.widget.Button;
android.widget.EditText;

public class ChangePasswordActivity extends Activity {



private SnapStackApplication snapStackApplication;

private EditText username_edittext;

private EditText temporary_password_edittext;

private EditText password_edittext;

private Button signin_button;

private ProgressDialog progressDialog;

@Override

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

setContentView(R.layout.activity_change_password);


snapStackApplication = (SnapStackApplication) getApplication();

// Find our views

username_edittext = (EditText) findViewById(R.id.username_edittext);

password_edittext = (EditText) findViewById(R.id.password_edittext);

temporary_password_edittext = (EditText) findViewById(R.id.temporary_password_
edittext);

signin_button = (Button) findViewById(R.id.signin_button);


signin_button.setOnClickListener(new View.OnClickListener() {

SERVICE);

public void onClick(View v) {



InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_

imm.hideSoftInputFromWindow(username_edittext.getWindowToken(), 0);
imm.hideSoftInputFromWindow(temporary_password_edittext.getWindowToken(), 0);
imm.hideSoftInputFromWindow(password_edittext.getWindowToken(), 0);

progressDialog = ProgressDialog.show(

ChangePasswordActivity.this, Signing in,

Signing into SnapStack, true);

String username = username_edittext.getText().toString().trim();
String temp = temporary_password_edittext.getText().toString().trim();

String password = password_edittext.getText().toString().trim();



if (foundError(username, password)) {

progressDialog.dismiss();

return;

}

User user = new User(username, temp, null);

www.stackmob.com

27

snapStackApplication.setUser(user);
user.loginResettingTemporaryPassword(password,new StackMobModelCallback() {
@Override
public void success() {


progressDialog.dismiss();



Intent intent = new Intent(


ChangePasswordActivity.this,

MasterActivity.class);

startActivity(intent);


}


@Override

public void failure(StackMobException e) {



progressDialog.dismiss();



runOnUiThread(new Runnable() {
@Override
public void run() {



Builder builder = new AlertDialog.Builder(ChangePasswordActivity.this);

builder.setTitle(Uh oh...);

builder.setCancelable(true);

builder.setMessage(There was an error signing in.);

AlertDialog dialog = builder.create();

dialog.show();

}
});

}

});

}
});
}

public boolean foundError(String username, String password) {


Builder builder = new AlertDialog.Builder(this);

builder.setTitle(Oops);
builder.setCancelable(true);


if (username.equals()){

builder.setMessage(Dont forget to enter a username!);

AlertDialog dialog = builder.create();

dialog.show();

return true;
}


else if (password.equals()){

builder.setMessage(Dont forget to enter a password!);

AlertDialog dialog = builder.create();

dialog.show();

return true;
}
return false;
}
}

www.stackmob.com

28

The activity contains a special sign in flow which allows the user to enter the temporary password, along
with a new password. The method loginResettingTemporaryPassword makes this happen seamlessly.
On a successful call, the user is signed into the master activity.
Next, add the Activity ForgotPasswordActivity:
package com.stackmob.snapstack;
import
import
import
import
import
import
import
import
import

android.app.Activity;
android.app.AlertDialog;
android.app.ProgressDialog;
android.app.AlertDialog.Builder;
android.content.Intent;
android.os.Bundle;
android.view.View;
android.widget.Button;
android.widget.EditText;

import com.stackmob.sdk.callback.StackMobModelCallback;
import com.stackmob.sdk.exception.StackMobException;
import com.stackmob.sdk.model.StackMobUser;
public class ForgotPasswordActivity extends Activity {

private EditText username_edittext;

private Button forgot_password_button;

private ProgressDialog progressDialog;

@Override

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

setContentView(R.layout.activity_forgot_password);


username_edittext = (EditText) findViewById(R.id.username_edittext);

forgot_password_button = (Button) findViewById(R.id.forgot_password_button);


forgot_password_button.setOnClickListener( new View.OnClickListener() {
@Override

public void onClick(View arg0) {




String username = username_edittext.getText().toString();

if (username.trim().length() != 0){

progressDialog = ProgressDialog.show(


ForgotPasswordActivity.this, Saving,

Sending email, true);

StackMobUser.sentForgotPasswordEmail(username, new
StackMobModelCallback() {

@Override public void success() {


progressDialog.dismiss();



Intent intent = new Intent(ForgotPasswordActivity.this,
ChangePasswordActivity.class);
startActivity(intent);



}


@Override public void failure(StackMobException e) {


progressDialog.dismiss();

www.stackmob.com

29




runOnUiThread(new Runnable() {
@Override
public void run() {

Builder builder = new AlertDialog.
Builder(

ForgotPasswordActivity.this);

builder.setTitle(Uh oh...);
builder.setCancelable(true);

builder.setMessage(Unable to
recover password.);
AlertDialog dialog = builder.
create();
dialog.show();
}
});

}
});
}
}
});
}

This is where users enter their username and request a temporary password. The
sentForgotPasswordEmail method causes a temporary password to be sent to the user via email. That
password is valid for 24 hours.

SignInActivity
Create a new Activity named SignInActivity.java, with the following code:
package com.stackmob.snapstack;
import
import
import
import
import
import
import
import
import
import
import
import

android.app.Activity;
android.app.AlertDialog;
android.app.AlertDialog.Builder;
android.app.ProgressDialog;
android.content.Context;
android.content.Intent;
android.os.Bundle;
android.view.View;
android.view.inputmethod.InputMethodManager;
android.widget.Button;
android.widget.EditText;
android.widget.TextView;

import com.stackmob.sdk.callback.StackMobModelCallback;
import com.stackmob.sdk.exception.StackMobException;
public class SignInActivity extends Activity {







private
private
private
private
private
private
private

SnapStackApplication snapStackApplication;
EditText username_edittext;
EditText password_edittext;
Button signin_button;
TextView forgot_password;
ProgressDialog progressDialog;
User user;

www.stackmob.com

30

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

snapStackApplication = (SnapStackApplication) getApplication();


// Find our views

username_edittext = (EditText) findViewById(R.id.username_edittext);

password_edittext = (EditText) findViewById(R.id.password_edittext);

signin_button = (Button) findViewById(R.id.signin_button);

forgot_password = (TextView) findViewById(R.id.forgot_password);


forgot_password.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Intent intent = new Intent(


SignInActivity.this,
ForgotPasswordActivity.class);
startActivity(intent);
}
});


signin_button.setOnClickListener(new View.OnClickListener() {

SERVICE);

public void onClick(View v) {



InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_

imm.hideSoftInputFromWindow(username_edittext.getWindowToken(), 0);
imm.hideSoftInputFromWindow(password_edittext.getWindowToken(), 0);

progressDialog = ProgressDialog.show(

SignInActivity.this, Signing in,

Signing into SnapStack, true);

String username = username_edittext.getText().toString().trim();

String password = password_edittext.getText().toString().trim();



if (foundError(username, password)) {

progressDialog.dismiss();

return;

}










user = new User(username, password, null);

user.login(new StackMobModelCallback() {
@Override
public void success() {


progressDialog.dismiss();
snapStackApplication.setUser(user);


Intent intent = new Intent(


SignInActivity.this,

MasterActivity.class);

intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);

startActivity(intent);

finish();


}

www.stackmob.com

31


@Override

public void failure(StackMobException e) {



progressDialog.dismiss();



runOnUiThread(new Runnable() {
@Override
public void run() {



Builder builder = new AlertDialog.Builder(SignInActivity.this);

builder.setTitle(Uh oh...);

builder.setCancelable(true);

builder.setMessage(There was an error signing in.);

AlertDialog dialog = builder.create();

dialog.show();

}
});

}

});

}
});
}

public boolean foundError(String username, String password) {


Builder builder = new AlertDialog.Builder(this);

builder.setTitle(Oops);
builder.setCancelable(true);


if (username.equals()){

builder.setMessage(Dont forget to enter a username!);

AlertDialog dialog = builder.create();

dialog.show();

return true;
}


else if (password.equals()){

builder.setMessage(Dont forget to enter a password!);

AlertDialog dialog = builder.create();

dialog.show();

return true;
}
return false;
}
}

The SignInActivity accepts a username and a password; the login attempts to sign in the user. Our
helper method, foundError, notifies the user if theyre missing their username or password. We
present the option to recover password if necessary, linking to ForgotPasswordActivity. If the sign in is
successful, we present MasterActivity; if not, we present an error dialog.

www.stackmob.com

32

Adding GPSTracker
Our app will leverage the Users location to find Snaps nearby, and to add geopoints to the Snaps they
upload. Add a class named GPSTracker.java:
package com.stackmob.snapstack;
import
import
import
import
import
import
import
import
import
import
import
import

android.app.AlertDialog;
android.app.Service;
android.content.Context;
android.content.DialogInterface;
android.content.Intent;
android.location.Location;
android.location.LocationListener;
android.location.LocationManager;
android.os.Bundle;
android.os.IBinder;
android.provider.Settings;
android.util.Log;

public class GPSTracker extends Service implements LocationListener {


private final Context mContext;
// flag for GPS status
boolean isGPSEnabled = false;
// flag for network status
boolean isNetworkEnabled = false;
// flag for GPS status
boolean canGetLocation = false;
Location location; // location
double latitude; // latitude
double longitude; // longitude
// The minimum distance to change Updates in meters
private static final long MIN_DISTANCE_CHANGE_FOR_UPDATES = 10; // 10 meters
// The minimum time between updates in milliseconds
private static final long MIN_TIME_BW_UPDATES = 1000 * 60 * 1; // 1 minute
// Declaring a Location Manager
protected LocationManager locationManager;
public GPSTracker(Context context) {
this.mContext = context;
getLocation();
}
public Location getLocation() {
try {
locationManager = (LocationManager) mContext
.getSystemService(LOCATION_SERVICE);
// getting GPS status
isGPSEnabled = locationManager
.isProviderEnabled(LocationManager.GPS_PROVIDER);
// getting network status
isNetworkEnabled = locationManager
.isProviderEnabled(LocationManager.NETWORK_PROVIDER);

www.stackmob.com

33

if (!isGPSEnabled && !isNetworkEnabled) {


// no network provider is enabled
} else {
this.canGetLocation = true;
// First get location from Network Provider
if (isNetworkEnabled) {
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
MIN_TIME_BW_UPDATES,
MIN_DISTANCE_CHANGE_FOR_UPDATES, this);
Log.d(Network, Network);
if (locationManager != null) {
location = locationManager
.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
if (location != null) {
latitude = location.getLatitude();
longitude = location.getLongitude();
}
}
}
// if GPS Enabled get lat/long using GPS Services
if (isGPSEnabled) {
if (location == null) {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
MIN_TIME_BW_UPDATES,
MIN_DISTANCE_CHANGE_FOR_UPDATES, this);
Log.d(GPS Enabled, GPS Enabled);
if (locationManager != null) {
location = locationManager
.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (location != null) {
latitude = location.getLatitude();
longitude = location.getLongitude();
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

return location;

/**
* Stop using GPS listener
* Calling this function will stop using GPS in your app
* */
public void stopUsingGPS(){
if(locationManager != null){
locationManager.removeUpdates(GPSTracker.this);
}
}
/**
* Function to get latitude
* */
public double getLatitude(){
if(location != null){
latitude = location.getLatitude();
}
// return latitude

www.stackmob.com

34

return latitude;

/**
* Function to get longitude
* */
public double getLongitude(){
if(location != null){
longitude = location.getLongitude();
}

// return longitude
return longitude;

/**
* Function to check GPS/wifi enabled
* @return boolean
* */
public boolean canGetLocation() {
return this.canGetLocation;
}
/**
* Function to show settings alert dialog
* On pressing Settings button will lauch Settings Options
* */
public void showSettingsAlert(){
AlertDialog.Builder alertDialog = new AlertDialog.Builder(mContext);
// Setting Dialog Title
alertDialog.setTitle(GPS is settings);
// Setting Dialog Message
alertDialog.setMessage(GPS is not enabled. Do you want to go to settings menu?);
// On pressing Settings button
alertDialog.setPositiveButton(Settings, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,int which) {
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
mContext.startActivity(intent);
}
});
// on pressing cancel button
alertDialog.setNegativeButton(Cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});

// Showing Alert Message


alertDialog.show();

@Override
public void onLocationChanged(Location location) {
}
@Override
public void onProviderDisabled(String provider) {
}
@Override
public void onProviderEnabled(String provider) {
}

www.stackmob.com

35

@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public IBinder onBind(Intent arg0) {
return null;
}
}

Well utilize this Service to handle all of our location needs. For more information, check out this tutorial.

Editing MainActivity
Lets add the logic for our main view. Edit MainActivity to look like this:
package com.stackmob.snapstack;
import java.util.List;
import
import
import
import
import
import
import

android.app.Activity;
android.app.ProgressDialog;
android.content.Intent;
android.os.Bundle;
android.view.View;
android.widget.Button;
android.widget.Toast;

import
import
import
import

com.stackmob.android.sdk.common.StackMobAndroid;
com.stackmob.sdk.api.StackMob;
com.stackmob.sdk.callback.StackMobQueryCallback;
com.stackmob.sdk.exception.StackMobException;

public



class MainActivity extends Activity {


private Button sign_up;
private Button sign_in;
private SnapStackApplication snapStackApplication;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
StackMobAndroid.init(getApplicationContext(), 1, YOUR_PUBLIC_KEY);
StackMob.getStackMob().getSession().getLogger().setLogging(true);
snapStackApplication = (SnapStackApplication) getApplication();
GPSTracker gps = new GPSTracker(this);
if(!gps.canGetLocation()){

gps.showSettingsAlert();
}
// Find our buttons
sign_up = (Button) findViewById(R.id.signup_button);
sign_in = (Button) findViewById(R.id.signin_button);

www.stackmob.com

36

// Set an OnClickListener for the sign_up button


sign_up.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Intent intent = new Intent(
MainActivity.this,
SignUpActivity.class);
startActivity(intent);
}
});
// Set an OnClickListener for the sign_in button
sign_in.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Intent intent = new Intent(
MainActivity.this,
SignInActivity.class);
startActivity(intent);
}
});
}
@Override
protected void onResume() {
super.onResume();
if(StackMob.getStackMob().isLoggedIn()) {


final ProgressDialog progressDialog = ProgressDialog.show(

MainActivity.this, Signing in,

Signing back in, true);

User.getLoggedInUser(User.class, new StackMobQueryCallback<User>() {
@Override
public void success(List<User> list) {


progressDialog.dismiss();

User user = list.get(0);
snapStackApplication.setUser(user);
Intent intent = new Intent(
MainActivity.this,
MasterActivity.class);

startActivity(intent);
}
@Override
public void failure(StackMobException e) {


progressDialog.dismiss();

Toast.makeText(MainActivity.this, Couldnt sign back in, Toast.LENGTH_LONG).

show();
}

});

}
}

www.stackmob.com

37

In MainActivity, we initialize the StackMob SDK. Copy your public key from the Dashboard and use it in
the init method.
The MainActivity simply presents two buttons that link to Sign Up and Sign In, respectively. At this point
weve fully built out our sign up/sign in flows.

Sanity Check
Build and run the app to check for errors. Youll be greeted with MainActivity. Create an account and sign in.

Congrats!
Youve finished Part 2. In this part, we laid out the skeleton for our app; we setup the sign up and sign in
flows and added forgot password functionality to our app.
In Part 3, well focus on the foundation of our app, MasterActivity.

www.stackmob.com

38

SnapStack Android Bootcamp

Part 3
What well cover
In this chapter well add MasterActivity, the core of our app. It consists of a pull-to-refresh list view
layered over a map view, and will serve as the main view in the app. Well also add a Profile view. Finally,
well build the feature to allow users to take and upload Snaps.

Adding DetailViewActivity
Add an Activity called DetailViewActivity. This activity will serve as host to an individual Snap object.
Well add more code to it in the next tutorial; for now, keep it as an empty view:
package com.stackmob.snapstack;
import android.app.Activity;
public class DetailViewActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_detail);

}

SnapAdapter
Well display our Snaps in list views. To help with that, well create a reusable ListAdapter as a helper
class. Create a file named SnapAdapter, and add the following code:
package com.stackmob.snapstack;
import java.util.List;
import
import
import
import
import
import
import

android.content.Context;
android.graphics.Bitmap;
android.graphics.BitmapFactory;
android.view.LayoutInflater;
android.view.View;
android.view.ViewGroup;
android.widget.ArrayAdapter;

www.stackmob.com

39

import android.widget.ImageView;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
public class SnapAdapter extends ArrayAdapter<Snap> {



private Context context;


private List<Snap> objects;
private DisplayImageOptions options;
protected ImageLoader imageLoader = ImageLoader.getInstance();


public SnapAdapter(Context context, List<Snap> objects) {

super(context, R.layout.listview_snap_item, objects);

this.objects = objects;
this.context = context;


options = new DisplayImageOptions.Builder()

.showStubImage(R.drawable.placeholder)

.showImageForEmptyUri(R.drawable.placeholder)

.showImageOnFail(R.drawable.placeholder).cacheInMemory()

.cacheOnDisc().bitmapConfig(Bitmap.Config.RGB_565).build();
}
@Override

public View getView(int position, View convertView, ViewGroup parent) {

View view = convertView;

if (view == null) {

LayoutInflater inflater = (LayoutInflater) context

.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

view = inflater.inflate(R.layout.listview_snap_item, null);
}

if (objects != null) {

Snap snap = objects.get(position);

ImageView snap_item_profile_image = (ImageView) view

.findViewById(R.id.snap_item_profile_image);


if (snap.getCreator().getPhoto() != null) {
imageLoader.displayImage(snap.getCreator().getPhoto().getS3Url(),

snap_item_profile_image, options);
} else {

Bitmap bitmap = BitmapFactory.decodeResource(

context.getResources(), R.drawable.default_avatar);

snap_item_profile_image.setImageBitmap(bitmap);
}

TextView user_name = (TextView) view

.findViewById(R.id.snap_item_username);

user_name.setText(snap.getCreator().getUsername());

ImageView snap_item_image = (ImageView) view

.findViewById(R.id.snap_item_image);
imageLoader.displayImage(snap.getPhoto()
.getS3Url(), snap_item_image, options);
}
return view;
}
}

www.stackmob.com

40

Adding a Profile
Create an Activity named ProfileActivity.java, with the following code:
package com.stackmob.snapstack;
import java.util.ArrayList;
import java.util.List;
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import

android.app.Activity;
android.content.Intent;
android.graphics.Bitmap;
android.graphics.BitmapFactory;
android.os.Bundle;
android.os.Handler;
android.view.Menu;
android.view.MenuInflater;
android.view.MenuItem;
android.view.View;
android.view.animation.AlphaAnimation;
android.view.animation.Animation;
android.view.animation.DecelerateInterpolator;
android.widget.AdapterView;
android.widget.AdapterView.OnItemClickListener;
android.widget.ImageView;
android.widget.ListView;
android.widget.TextView;
android.widget.Toast;

import
import
import
import
import
import
import
import
import
import

com.handmark.pulltorefresh.library.PullToRefreshBase;
com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener;
com.handmark.pulltorefresh.library.PullToRefreshListView;
com.nostra13.universalimageloader.core.DisplayImageOptions;
com.nostra13.universalimageloader.core.ImageLoader;
com.stackmob.sdk.api.StackMobOptions;
com.stackmob.sdk.api.StackMobQuery;
com.stackmob.sdk.callback.StackMobNoopCallback;
com.stackmob.sdk.callback.StackMobQueryCallback;
com.stackmob.sdk.exception.StackMobException;

public class ProfileActivity extends Activity {



SnapStackApplication snapStackApplication;

private PullToRefreshListView pull_refresh_list;

private List<Snap> snaps = new ArrayList<Snap>();

private Handler handler = new Handler();

private SnapAdapter adapter;

private User user;

private TextView profile_username;

private ImageView profile_photo_imageview;

private DisplayImageOptions options;

protected ImageLoader imageLoader = ImageLoader.getInstance();


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_profile);
options = new DisplayImageOptions.Builder()


.showStubImage(R.drawable.default_avatar)

.showImageForEmptyUri(R.drawable.default_avatar)

.showImageOnFail(R.drawable.default_avatar)
.cacheInMemory()
.cacheOnDisc()

www.stackmob.com

41


.bitmapConfig(Bitmap.Config.RGB_565)
.build();
snapStackApplication = (SnapStackApplication) getApplication();
user = snapStackApplication.getUser();
profile_photo_imageview = (ImageView) findViewById(R.id.profile_photo_imageview);
if (user.getPhoto() != null) {
imageLoader.displayImage(user.getPhoto().getS3Url(), profile_photo_imageview, options);
}
else {

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.default_

avatar);

profile_photo_imageview.setImageBitmap(bitmap);

profile_photo_imageview.setOnClickListener(new View.OnClickListener() {
@Override

public void onClick(View v) {

Intent intent = new Intent(ProfileActivity.this,
ChoosePhotoActivity.class);

intent.putExtra(calling_activity, 666);

startActivityForResult(intent, 0);
}
});
profile_username = (TextView) findViewById(R.id.profile_username);
profile_username.setText(user.getUsername());
pull_refresh_list = (PullToRefreshListView) findViewById(R.id.pull_refresh_list);
pull_refresh_list.setOnRefreshListener(new OnRefreshListener<ListView>() {
@Override

public void onRefresh(PullToRefreshBase<ListView> refreshView) {
loadObjects();


}
});
pull_refresh_list.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view, int position, long id){
Snap snap = snaps.get(position - 1);
Intent intent = new Intent(

ProfileActivity.this,
DetailViewActivity.class);
snapStackApplication.setSnap(snap);

});

startActivity(intent);

loadObjects();
}


private class ListUpdater implements Runnable{
public ListUpdater(){
}

www.stackmob.com

42

show();

public void run(){




if (snaps.size() == 0) {

Toast.makeText(ProfileActivity.this, No Snaps found, Toast.LENGTH_LONG).

}
adapter = new SnapAdapter(ProfileActivity.this, snaps);
pull_refresh_list.onRefreshComplete();
pull_refresh_list.setAdapter(adapter);
Animation fadeIn = new AlphaAnimation(0, 1);
fadeIn.setInterpolator(new DecelerateInterpolator()); //add this
fadeIn.setDuration(1000);
pull_refresh_list.setAnimation(fadeIn);

}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.signout_menu, menu);
}

return true;


@Override
public boolean onOptionsItemSelected(MenuItem item) {

switch (item.getItemId()) {

case R.id.signOut:



SnapStackApplication snapStackApplication = (SnapStackApplication) this.
getApplication();



snapStackApplication.getUser().logout(new StackMobNoopCallback());





Intent myIntent = new Intent(this, MainActivity.class);

myIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);

startActivity(myIntent);

return true;

default:

return super.onOptionsItemSelected(item);

}
}


private void loadObjects() {

StackMobQuery query = new StackMobQuery();

query.fieldIsOrderedBy(createddate, StackMobQuery.Ordering.DESCENDING);

query.fieldIsEqualTo(creator, user.getUsername());


Snap.query(Snap.class, query, StackMobOptions.depthOf(1), new
StackMobQueryCallback<Snap>() {
@Override

public void success(List<Snap> result) {

snaps = result;
handler.post(new ListUpdater());

}


@Override

public void failure(StackMobException e) {



handler.post(new ListUpdater());

www.stackmob.com

43


}
});
}

@Override

public void onActivityResult(int requestCode, int resultCode, Intent data) {

super.onActivityResult(requestCode, resultCode, data);


if (resultCode == Activity.RESULT_OK) {



imageLoader.clearDiscCache();

imageLoader.clearMemoryCache();



user = snapStackApplication.getUser();

imageLoader.displayImage(user.getPhoto().getS3Url(), profile_photo_imageview,
options);

}

}
}

The profile displays a users picture and username, as well as a list view of their Snaps. In
onCreateOptionsMenu, weve added a sign out menu option to fully complete the flow for users. When
onOptionsItemSelected is called, we sign the user out and bring them back to MainActivity. The
loadObjects method queries for Snaps created by the user. We feed the objects to our SnapAdapter,
which we pair with our list view.

Adding SharePhotoActivity
One of the central features of the app is the share photo feature, where users take a photo and pair it
with their location to create a Snap. Well walkthrough building that feature. Create an Activity called
SharePhotoActivity, with the following code:
package com.stackmob.snapstack;
import
import
import
import

java.io.ByteArrayOutputStream;
java.io.File;
java.util.ArrayList;
java.util.List;

import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import

android.app.Activity;
android.app.AlertDialog;
android.app.ProgressDialog;
android.app.AlertDialog.Builder;
android.content.ActivityNotFoundException;
android.content.ComponentName;
android.content.Context;
android.content.DialogInterface;
android.content.Intent;
android.content.pm.ResolveInfo;
android.graphics.Bitmap;
android.graphics.BitmapFactory;
android.graphics.drawable.BitmapDrawable;
android.net.Uri;
android.os.Bundle;
android.os.Environment;
android.provider.MediaStore;

www.stackmob.com

44

import
import
import
import
import

android.view.View;
android.widget.ArrayAdapter;
android.widget.Button;
android.widget.ImageView;
android.widget.Toast;

import com.stackmob.sdk.api.StackMobFile;
import com.stackmob.sdk.callback.StackMobModelCallback;
import com.stackmob.sdk.exception.StackMobException;
public class SharePhotoActivity extends Activity {




SnapStackApplication snapStackApplication;
private Uri imageCaptureUri;
private ImageView share_photo_imageview;
private Button share_photo_button;
private ProgressDialog progressDialog;

private static final int PICK_FROM_CAMERA = 1;


private static final int CROP_FROM_CAMERA = 2;
private static final int PICK_FROM_FILE = 3;

@Override

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

setContentView(R.layout.activity_share_photo);

snapStackApplication = (SnapStackApplication) getApplication();


final String[] items = new String[] { Take from camera,

Select from gallery };

ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,

android.R.layout.select_dialog_item, items);

AlertDialog.Builder builder = new AlertDialog.Builder(this);

builder.setTitle(Select Image);

builder.setAdapter(adapter, new DialogInterface.OnClickListener() {

public void onClick(DialogInterface dialog, int item) { // pick from

// camera

if (item == 0) {

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
imageCaptureUri = Uri.fromFile(new File(Environment

.getExternalStorageDirectory(), tmp_avatar_

+ String.valueOf(System.currentTimeMillis())

+ .jpg));

intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT,
imageCaptureUri);
try {

intent.putExtra(return-data, true);


startActivityForResult(intent, PICK_FROM_CAMERA);
} catch (ActivityNotFoundException e) {

e.printStackTrace();
}

} else { // pick from file
Intent intent = new Intent();

intent.setType(image/*);
intent.setAction(Intent.ACTION_GET_CONTENT);


startActivityForResult(Intent.createChooser(intent,

Complete action using), PICK_FROM_FILE);
}

www.stackmob.com

45

}
});

final AlertDialog dialog = builder.create();
dialog.show();

share_photo_button = (Button) findViewById(R.id.share_photo_button);
share_photo_button.setEnabled(false);

share_photo_imageview = (ImageView) findViewById(R.id.share_photo_imageview);

Bitmap bitmap = BitmapFactory.decodeResource(getResources(),

R.drawable.placeholder);

share_photo_imageview.setImageBitmap(bitmap);

share_photo_imageview.setOnClickListener(new View.OnClickListener() {
@Override

public void onClick(View v) {
dialog.show();
}
});

share_photo_button.setOnClickListener(new View.OnClickListener() {
@Override

public void onClick(View v) {

progressDialog = ProgressDialog.show(

SharePhotoActivity.this, Saving...,

Saving your snap, true);


Bitmap bitmap = ((BitmapDrawable) share_photo_imageview

.getDrawable()).getBitmap();

ByteArrayOutputStream stream = new ByteArrayOutputStream();

bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);

byte[] image = stream.toByteArray();

Snap snap = snapStackApplication.getSnap();


snap.setPhoto(new StackMobFile(image/jpeg,

profile_picture.jpg, image));
snap.save(new StackMobModelCallback() {
@Override
public void success() {
progressDialog.dismiss();

threadAgnosticDialog(SharePhotoActivity.this, Your photo
was shared to SnapStack!);

setResult(RESULT_OK, null);

finish();

}
@Override
public void failure(StackMobException e) {
progressDialog.dismiss();

threadAgnosticDialog(SharePhotoActivity.this, There was
an error saving your photo.);
}
});
}
});
}
@Override

protected void onActivityResult(int requestCode, int resultCode, Intent data) {

if (resultCode != RESULT_OK)
return;

switch (requestCode) {

www.stackmob.com

46


case PICK_FROM_CAMERA:
doCrop();
break;

case PICK_FROM_FILE:
imageCaptureUri = data.getData();
doCrop();
break;

case CROP_FROM_CAMERA:

Bundle extras = data.getExtras();

if (extras != null) {

Bitmap photo = extras.getParcelable(data);

share_photo_imageview.setImageBitmap(photo);
share_photo_button.setEnabled(true);
}

File f = new File(imageCaptureUri.getPath());

if (f.exists())
f.delete();
break;
}
}

private void doCrop() {

final ArrayList<CropOption> cropOptions = new ArrayList<CropOption>();

Intent intent = new Intent(com.android.camera.action.CROP);


intent.setType(image/*);


List<ResolveInfo> list = getPackageManager().queryIntentActivities(
intent, 0);

int size = list.size();


if (size == 0) {

Toast.makeText(this, Can not find image crop app,

Toast.LENGTH_SHORT).show();
return;

} else {
intent.setData(imageCaptureUri);





intent.putExtra(outputX, 200);
intent.putExtra(outputY, 200);
intent.putExtra(aspectX, 1);
intent.putExtra(aspectY, 1);
intent.putExtra(scale, true);
intent.putExtra(return-data, true);


if (size == 1) {

Intent i = new Intent(intent);

ResolveInfo res = list.get(0);
i.setComponent(new ComponentName(res.activityInfo.packageName,
res.activityInfo.name));

startActivityForResult(i, CROP_FROM_CAMERA);
} else {

www.stackmob.com

47


for (ResolveInfo res : list) {

final CropOption co = new CropOption();
co.title = getPackageManager().getApplicationLabel(
res.activityInfo.applicationInfo);
co.icon = getPackageManager().getApplicationIcon(
res.activityInfo.applicationInfo);
co.appIntent = new Intent(intent);
co.appIntent
.setComponent(new ComponentName(
res.activityInfo.packageName,
res.activityInfo.name));
cropOptions.add(co);
}

CropOptionAdapter adapter = new CropOptionAdapter(
getApplicationContext(), cropOptions);

AlertDialog.Builder builder = new AlertDialog.Builder(this);

builder.setTitle(Choose Crop App);
builder.setAdapter(adapter,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int
item) {

startActivityForResult(
cropOptions.get(item).
appIntent,

CROP_FROM_CAMERA);
}
});
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
if (imageCaptureUri != null) {

getContentResolver().delete(imageCaptureUri,
null,
null);
imageCaptureUri = null;
}
}
});
AlertDialog alert = builder.create();
alert.show();
}
}
}


private void threadAgnosticDialog(final Context ctx, final String txt) {

runOnUiThread(new Runnable() {

@Override public void run() {

Builder builder = new AlertDialog.Builder(ctx);

builder.setTitle(Share Photo);
builder.setCancelable(true);
builder.setMessage(txt);
AlertDialog dialog = builder.create();
dialog.show();
}
});
}

}

www.stackmob.com

48

This activity works much like the ChoosePhotoActivity; we call the same Camera intent and use the
same doCrop method to crop our images. Once we have a photo, we attach it to the Snap object stored in
SnapStackApplication, and call save. If the save is successful, we call finish on SharePhotoActivity.

The MasterActivity
In the last part of the tutorial, we left MasterActivity blank. Add the following code to MasterActivity:
package com.stackmob.snapstack;
import java.util.ArrayList;
import java.util.List;
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import

android.app.Activity;
android.app.AlertDialog;
android.app.AlertDialog.Builder;
android.app.Dialog;
android.content.Context;
android.content.DialogInterface;
android.content.Intent;
android.graphics.Bitmap;
android.location.Location;
android.os.Bundle;
android.os.Handler;
android.view.LayoutInflater;
android.view.View;
android.view.animation.AccelerateInterpolator;
android.view.animation.AlphaAnimation;
android.view.animation.Animation;
android.view.animation.DecelerateInterpolator;
android.widget.AdapterView;
android.widget.AdapterView.OnItemClickListener;
android.widget.CompoundButton;
android.widget.ImageButton;
android.widget.ImageView;
android.widget.LinearLayout;
android.widget.ListView;
android.widget.Toast;
android.widget.ToggleButton;

import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import

com.google.android.gms.common.ConnectionResult;
com.google.android.gms.common.GooglePlayServicesUtil;
com.google.android.gms.maps.CameraUpdateFactory;
com.google.android.gms.maps.GoogleMap;
com.google.android.gms.maps.GoogleMap.OnMarkerClickListener;
com.google.android.gms.maps.MapFragment;
com.google.android.gms.maps.model.LatLng;
com.google.android.gms.maps.model.LatLngBounds;
com.google.android.gms.maps.model.Marker;
com.google.android.gms.maps.model.MarkerOptions;
com.handmark.pulltorefresh.library.PullToRefreshBase;
com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener;
com.handmark.pulltorefresh.library.PullToRefreshListView;
com.nostra13.universalimageloader.core.DisplayImageOptions;
com.nostra13.universalimageloader.core.ImageLoader;
com.stackmob.sdk.api.StackMobGeoPoint;
com.stackmob.sdk.api.StackMobOptions;
com.stackmob.sdk.api.StackMobQuery;
com.stackmob.sdk.callback.StackMobQueryCallback;
com.stackmob.sdk.exception.StackMobException;

www.stackmob.com

49

public class MasterActivity extends Activity {





private SnapStackApplication snapStackApplication;

private ImageButton profile_button;

private ImageButton camera_button;

private GoogleMap map;

private LinearLayout transparent_cover;

private PullToRefreshListView pull_refresh_list;

private ToggleButton toggle_button;

private List<Snap> snaps = new ArrayList<Snap>();

private Handler handler = new Handler();

private SnapAdapter adapter;

private GPSTracker gps;



@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_master);
gps = new GPSTracker(this);
snapStackApplication = (SnapStackApplication) getApplication();
// Getting Google Play availability status
int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(getBaseContext());
// Showing status
if(status!=ConnectionResult.SUCCESS){ // Google Play Services are not available
int requestCode = 10;
Dialog dialog = GooglePlayServicesUtil.getErrorDialog(status, this, requestCode);
dialog.show();
}
profile_button = (ImageButton) findViewById(R.id.profile_button);
profile_button.setOnClickListener(new View.OnClickListener() {
@Override

public void onClick(View v) {

Intent intent = new Intent(
MasterActivity.this,

ProfileActivity.class);
startActivity(intent);
}
});

map = ((MapFragment) getFragmentManager().findFragmentById(R.id.map))


.getMap();
map.setMyLocationEnabled(true);
map.getUiSettings().setAllGesturesEnabled(false);
camera_button = (ImageButton) findViewById(R.id.camera_button);
camera_button.setOnClickListener(new View.OnClickListener() {
@Override

public void onClick(View v) {


if (gps.getLocation() == null) {

Builder builder = new AlertDialog.Builder(MasterActivity.this);

builder.setTitle(Oh snap!);

builder.setCancelable(true);

builder.setMessage(Couldnt get your location.);

www.stackmob.com

50


AlertDialog dialog = builder.create();

dialog.show();

return;
}

Location location = gps.getLocation();

StackMobGeoPoint point = new StackMobGeoPoint(location.getLongitude(),
location.getLatitude());
User user = snapStackApplication.getUser();

Snap snap = new Snap(user, point);
snapStackApplication.setSnap(snap);


Intent intent = new Intent(
MasterActivity.this,
SharePhotoActivity.class);

startActivityForResult(intent, 0);
}
});
transparent_cover = (LinearLayout) findViewById(R.id.transparent_cover);
pull_refresh_list = (PullToRefreshListView) findViewById(R.id.pull_refresh_list);
adapter = new SnapAdapter(MasterActivity.this, snaps);
pull_refresh_list.setAdapter(adapter);
pull_refresh_list.setRefreshing(true);
pull_refresh_list.setOnRefreshListener(new OnRefreshListener<ListView>() {
@Override

public void onRefresh(PullToRefreshBase<ListView> refreshView) {

loadObjects();
}
});
pull_refresh_list.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view, int position, long id){
Snap snap = snaps.get(position - 1);
Intent intent = new Intent(
MasterActivity.this,
DetailViewActivity.class);
snapStackApplication.setSnap(snap);

});

startActivityForResult(intent, 0);

toggle_button = (ToggleButton) findViewById(R.id.toggle_button);


toggle_button.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
// Save the state here

if (isChecked) {


Animation fadeOut = new AlphaAnimation(1, 0);

fadeOut.setInterpolator(new AccelerateInterpolator()); //and this

fadeOut.setDuration(500);


transparent_cover.setAnimation(fadeOut);

pull_refresh_list.setAnimation(fadeOut);


transparent_cover.setVisibility(View.GONE);

www.stackmob.com

51


pull_refresh_list.setVisibility(View.GONE);


map.getUiSettings().setAllGesturesEnabled(true);
}
else {

transparent_cover.setVisibility(View.VISIBLE);

pull_refresh_list.setVisibility(View.VISIBLE);

});










}

}

Animation fadeIn = new AlphaAnimation(0, 1);


fadeIn.setInterpolator(new DecelerateInterpolator());
fadeIn.setDuration(500);
transparent_cover.setAnimation(fadeIn);
pull_refresh_list.setAnimation(fadeIn);
map.getUiSettings().setAllGesturesEnabled(false);

map.setOnMarkerClickListener(new OnMarkerClickListener() {
@Override

public boolean onMarkerClick(final Marker marker) {


AlertDialog.Builder builder = new AlertDialog.Builder(MasterActivity.
this);
LayoutInflater inflater =

(LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflater.inflate(R.layout.activity_photo, null);
builder.setView(v);

int i = Integer.parseInt(marker.getSnippet());
Snap snap = snaps.get(i);
ImageView imageView = (ImageView) v.findViewById(R.id.photo_image);
DisplayImageOptions options = new DisplayImageOptions.Builder()


.showStubImage(R.drawable.placeholder)

.showImageForEmptyUri(R.drawable.placeholder)

.showImageOnFail(R.drawable.placeholder)

.cacheInMemory()

.cacheOnDisc()

.bitmapConfig(Bitmap.Config.RGB_565)

.build();
ImageLoader imageLoader = ImageLoader.getInstance();
imageLoader.displayImage(snap.getPhoto().getS3Url(), imageView, options);
imageView.setAdjustViewBounds(true);
imageView.setMaxHeight(150);
imageView.setMaxWidth(150);
imageView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v)
{
int i = Integer.parseInt(marker.getSnippet());
Snap snap = snaps.get(i);
Intent intent = new Intent(
MasterActivity.this,
DetailViewActivity.class);
snapStackApplication.setSnap(snap);

www.stackmob.com

52

});

startActivity(intent);

builder.setPositiveButton(Show Details, new DialogInterface.OnClickListener() {


@Override
public void onClick(DialogInterface dialog, int id) {
int i = Integer.parseInt(marker.getSnippet());
Snap snap = snaps.get(i);
Intent intent = new Intent(
MasterActivity.this,
DetailViewActivity.class);

}

snapStackApplication.setSnap(snap);
startActivity(intent);

});
builder.setNegativeButton(Close, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
Dialog dialog = builder.create();
dialog.show();

return true;
}

});
loadObjects();
}


private void setMarkers () {


if (snaps == null || snaps.size() == 0)
return;


LatLngBounds.Builder builder = new LatLngBounds.Builder();


for (int i = 0; i < snaps.size(); i++) {

Snap snap = snaps.get(i);

LatLng point = new LatLng(snap.getLocation().getLatitude(), snap.
getLocation().getLongitude());
builder.include(point);

map.addMarker(new MarkerOptions().position(point)

.snippet(+i));
}


if (gps.canGetLocation) {


LatLng location = new LatLng(gps.latitude, gps.longitude);
builder.include(location);
}


LatLngBounds bounds = builder.build();

map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 30));
}

www.stackmob.com

53

private class ListUpdater implements Runnable{


public ListUpdater(){
}

public void run(){




if (snaps.size() == 0) {

Toast.makeText(MasterActivity.this, Couldnt find any Snaps nearby, Toast.
LENGTH_LONG).show();

}


adapter = new SnapAdapter(MasterActivity.this, snaps);

pull_refresh_list.onRefreshComplete();

pull_refresh_list.setAdapter(adapter);

setMarkers();


Animation fadeIn = new AlphaAnimation(0, 1);

fadeIn.setInterpolator(new DecelerateInterpolator()); //add this

fadeIn.setDuration(1000);


pull_refresh_list.setAnimation(fadeIn);

}
}


private void loadObjects() {


Location location = gps.getLocation();


if (location == null) {


pull_refresh_list.onRefreshComplete();

Toast.makeText(this, Couldnt get your location, Toast.LENGTH_LONG).show();
return;
}


LatLng currentLocation = new LatLng(location.getLatitude(), location.getLongitude());

// Move the camera instantly to the current location with a zoom of 15.
map.animateCamera(CameraUpdateFactory.newLatLngZoom(currentLocation, 15));


StackMobGeoPoint point = new StackMobGeoPoint(location.getLongitude(), location.
getLatitude());
StackMobQuery query = new StackMobQuery();

query.fieldIsNear(location, point);

query.fieldIsOrderedBy(createddate, StackMobQuery.Ordering.DESCENDING);

query.isInRange(0, 50);


Snap.query(Snap.class, query, StackMobOptions.depthOf(1), new
StackMobQueryCallback<Snap>() {
@Override

public void success(List<Snap> result) {

snaps = result;
handler.post(new ListUpdater());

}


@Override

public void failure(StackMobException e) {



handler.post(new ListUpdater());

}
});

www.stackmob.com

54

}

@Override

public void onActivityResult(int requestCode, int resultCode, Intent data) {

super.onActivityResult(requestCode, resultCode, data);


if (resultCode == Activity.RESULT_OK) {



loadObjects();


}

}
}

MasterActivity handles a lot at once. We have a map fragment, using the Google Maps Android v2 API.
Layered on top of it is a pull-to-refresh list view for Snap objects. Beneath the list view is a toggle button
to switch between the map and the list view; each time, the list view is faded out or in with an animation.
The loadObjects method grabs the users location from the map, and queries for Snaps nearby. If a query
is successful, the list view is refreshed using the run method from our private ListUpdater. The list view
uses SnapAdapter to build its list items.
Also in the run method, we update the map fragment, using the setMarkers. This method plots the
Snaps as annotations using their locations, and focuses the map camera to fit them all. The map uses an
OnMarkerClickListener to build a custom dialog window for the annotations; when an annotation is
selected, the image associated with the Snap is shown.
Finally the MasterActivity has links to the ProfileActivity and SharePhotoActivity we just built.

Congrats!
Youve finished Part 3. We added the ability to take and upload Snap. We also added a Profile with sign
out. Finally, we added the biggest piece of our app, MasterActivity.
In Part 4, well wrap up development on SnapStack.

www.stackmob.com

55

SnapStack Android Bootcamp

Part 4
What well cover
In this chapter, well focus on the detail view and comment view to our app. Well also add the ability to
delete Snaps. Finally, well finish with the ability to add comments to Snaps.

PhotoViewActivity
Add an Activity named PhotoViewActivity:
package com.stackmob.snapstack;
import
import
import
import
import
import
import
import
import
import

android.app.Activity;
android.app.AlertDialog;
android.app.ProgressDialog;
android.app.AlertDialog.Builder;
android.graphics.Bitmap;
android.os.Bundle;
android.view.Menu;
android.view.MenuInflater;
android.view.MenuItem;
android.widget.ImageView;

import
import
import
import

com.nostra13.universalimageloader.core.DisplayImageOptions;
com.nostra13.universalimageloader.core.ImageLoader;
com.stackmob.sdk.callback.StackMobModelCallback;
com.stackmob.sdk.exception.StackMobException;

public class PhotoViewActivity extends Activity {




private Snap snap;

SnapStackApplication snapStackApplication;


DisplayImageOptions options;

protected ImageLoader imageLoader = ImageLoader.getInstance();

private ProgressDialog progressDialog;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_photo);
options = new DisplayImageOptions.Builder()
.showStubImage(R.drawable.placeholder)

.showImageForEmptyUri(R.drawable.placeholder)

.showImageOnFail(R.drawable.placeholder)
.cacheInMemory()
.cacheOnDisc()

.bitmapConfig(Bitmap.Config.RGB_565)
.build();

www.stackmob.com

56

snapStackApplication = (SnapStackApplication) getApplication();


snap = snapStackApplication.getSnap();

ImageView imageView = (ImageView) findViewById(R.id.photo_image);


imageLoader.displayImage(snap.getPhoto().getS3Url(), imageView, options);

}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.delete_menu, menu);
String username = snap.getCreator().getUsername();
User user = snapStackApplication.getUser();
if (username.equals(user.getUsername())) {

return true;
}
}

return false;


@Override
public boolean onOptionsItemSelected(MenuItem item) {


progressDialog = ProgressDialog.show(

PhotoViewActivity.this, Deleting...,

Deleting your snap, true);


snap.destroy(new StackMobModelCallback() {

@Override

public void success() {

// the call succeeded

progressDialog.dismiss();

setResult(RESULT_OK, null);

finish();

}


@Override

public void failure(StackMobException e) {

// the call failed

progressDialog.dismiss();

runOnUiThread(new Runnable() {
@Override public void run() {

Builder builder = new AlertDialog.
Builder(PhotoViewActivity.this);

builder.setTitle(Uh oh...);
builder.setCancelable(true);

builder.setMessage(Couldnt delete snap);
AlertDialog dialog = builder.create();
dialog.show();
}
});

}
});

return true;

www.stackmob.com

57

PhotoViewActivity is very basic; it displays the photo from the Snap selected. The
onCreateOptionsMenu presents the option to delete the Snap, if it was created by the user. When
onOptionsItemSelected is called, we use the destroy method on the Snap object. If the delete is
successful, we call finish on the activity.

ShareCommentActivity
In SnapStack, users can add comments to Snaps. Add an Activity named ShareCommentActivity:
package com.stackmob.snapstack;
import
import
import
import
import
import
import
import
import

android.app.Activity;
android.app.AlertDialog;
android.app.AlertDialog.Builder;
android.app.ProgressDialog;
android.content.Context;
android.os.Bundle;
android.view.View;
android.widget.Button;
android.widget.EditText;

import com.stackmob.sdk.callback.StackMobModelCallback;
import com.stackmob.sdk.exception.StackMobException;
public class ShareCommentActivity extends Activity {

private SnapStackApplication snapStackApplication;

private EditText comment_edittext;

private Button share_comment_button;

private ProgressDialog progressDialog;

@Override

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

setContentView(R.layout.activity_share_comment);


snapStackApplication = (SnapStackApplication) getApplication();


comment_edittext = (EditText) findViewById(R.id.comment_edittext);

share_comment_button = (Button) findViewById(R.id.share_comment_button);


share_comment_button.setOnClickListener( new View.OnClickListener() {
@Override

public void onClick(View arg0) {


String commentText = comment_edittext.getText().toString();


if (commentText.trim().length() != 0){

progressDialog = ProgressDialog.show(


ShareCommentActivity.this, Saving,

Saving comment, true);

User user = snapStackApplication.getUser();


final Snap snap = snapStackApplication.getSnap();


Comment comment = new Comment(user, commentText, snap);

www.stackmob.com

58


comment.save(new StackMobModelCallback() {

@Override

public void success() {

// the call succeeded


progressDialog.dismiss();

setResult(RESULT_OK, null);

finish();

}


@Override

public void failure(StackMobException e) {


progressDialog.dismiss();

// the call failed

threadAgnosticDialog(ShareCommentActivity.this, There
was an error saving your comment.);

}
});


}
}

});

}


private void threadAgnosticDialog(final Context ctx, final String txt) {

runOnUiThread(new Runnable() {

@Override public void run() {

Builder builder = new AlertDialog.Builder(ctx);
builder.setCancelable(true);
builder.setMessage(txt);
AlertDialog dialog = builder.create();
dialog.show();
}
});
}
}

This activity presents a simple EditText for users to type comments. Once the share button is clicked,
we create a Comment object, complete with the User who created it, the text of the comment and the
associated Snap. After we call save, if its successful, we finish the activity.

CommentViewActivity
Now that weve built the feature to add comments, well make an Activity to display them. Add an
Activity named CommentViewActivity:
package com.stackmob.snapstack;
import java.util.ArrayList;
import java.util.List;
import
import
import
import

android.app.Activity;
android.app.AlertDialog;
android.app.ProgressDialog;
android.app.AlertDialog.Builder;

www.stackmob.com

59

import
import
import
import
import
import
import
import
import
import
import
import

android.content.Context;
android.graphics.Bitmap;
android.graphics.BitmapFactory;
android.os.Bundle;
android.os.Handler;
android.view.LayoutInflater;
android.view.View;
android.view.ViewGroup;
android.widget.ArrayAdapter;
android.widget.ImageView;
android.widget.ListView;
android.widget.TextView;

import
import
import
import
import
import

com.nostra13.universalimageloader.core.DisplayImageOptions;
com.nostra13.universalimageloader.core.ImageLoader;
com.stackmob.sdk.api.StackMobOptions;
com.stackmob.sdk.api.StackMobQuery;
com.stackmob.sdk.callback.StackMobQueryCallback;
com.stackmob.sdk.exception.StackMobException;

public class CommentViewActivity extends Activity {






List<Comment> comments;
ListView listView;
private SnapStackApplication snapStackApplication;
private Handler handler = new Handler();
CommentAdapter adapter;

DisplayImageOptions options;
protected ImageLoader imageLoader = ImageLoader.getInstance();
private ProgressDialog progressDialog;

@Override

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

setContentView(R.layout.activity_comment);

options = new DisplayImageOptions.Builder()

.showStubImage(R.drawable.placeholder)

.showImageForEmptyUri(R.drawable.placeholder)

.showImageOnFail(R.drawable.placeholder).cacheInMemory()

.cacheOnDisc().bitmapConfig(Bitmap.Config.RGB_565).build();

listView = (ListView) findViewById(R.id.comment_listview);

comments = new ArrayList<Comment>();

snapStackApplication = (SnapStackApplication) getApplication();


adapter = new CommentAdapter(CommentViewActivity.this, comments);
listView.setAdapter(adapter);
loadComments();
}

public void loadComments() {


progressDialog = ProgressDialog.show(CommentViewActivity.this,

Loading..., Loading comments, true);


Snap snap = snapStackApplication.getSnap();


StackMobQuery query = new StackMobQuery();
query.fieldIsEqualTo(snap, snap.getID());


Comment.query(Comment.class, query, StackMobOptions.depthOf(1),
new StackMobQueryCallback<Comment>() {
@Override
public void success(List<Comment> result) {

www.stackmob.com

60

progressDialog.dismiss();
comments = result;
handler.post(new ListUpdater());
}
@Override
public void failure(StackMobException e) {
progressDialog.dismiss();
handler.post(new ListUpdater());

Builder builder = new AlertDialog.Builder(
CommentViewActivity.this);

builder.setTitle(Uh oh...);
builder.setCancelable(true);

builder.setMessage(There was an error loading
comments.);
AlertDialog dialog = builder.create();
dialog.show();
}
});
}

private class ListUpdater implements Runnable {

public ListUpdater() {

}

public void run() {

adapter = new CommentAdapter(CommentViewActivity.this, comments);
listView.setAdapter(adapter);
}
}

private class CommentAdapter extends ArrayAdapter<Comment> {

private List<Comment> objects;


public CommentAdapter(Context context, List<Comment> objects) {

super(context, R.layout.listview_comment_item, objects);
this.objects = objects;
}
@Override

public View getView(int position, View convertView, ViewGroup parent) {

View view = convertView;

if (view == null) {

LayoutInflater inflater = (LayoutInflater) CommentViewActivity.this

.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

view = inflater.inflate(R.layout.listview_comment_item, null);
}

if (objects != null) {
Comment comment = objects.get(position);

TextView user_name = (TextView) view

.findViewById(R.id.comment_username);

user_name.setText(comment.getCreator().getUsername());

ImageView comment_item_profile_image = (ImageView) view

.findViewById(R.id.comment_item_profile_image);

www.stackmob.com

61

if (comment.getCreator().getPhoto() != null) {

imageLoader.displayImage(comment.getCreator().getPhoto()

.getS3Url(), comment_item_profile_image, options);
} else {

Bitmap bitmap = BitmapFactory.decodeResource(

getResources(), R.drawable.default_avatar);

comment_item_profile_image.setImageBitmap(bitmap);
}

TextView text = (TextView) view.findViewById(R.id.comment_text);


text.setText(comment.getText());

}
return view;
}
}
}

In CommentViewActivity weve built a ListAdapter similar to SnapAdapter. In the OnCreate method, we


call loadComments, which queries for any Comments associated with the Snap.

DetailViewActivity
Add an Activity named DetailViewActivity, which simply ties all of previous Activities together:
package com.stackmob.snapstack;
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import

android.app.Activity;
android.content.Intent;
android.graphics.Bitmap;
android.graphics.BitmapFactory;
android.os.Bundle;
android.view.Menu;
android.view.MenuInflater;
android.view.MenuItem;
android.view.View;
android.view.View.OnClickListener;
android.view.animation.AlphaAnimation;
android.view.animation.Animation;
android.view.animation.DecelerateInterpolator;
android.widget.Button;
android.widget.ImageView;
android.widget.TextView;

import
import
import
import
import

com.nostra13.universalimageloader.core.DisplayImageOptions;
com.nostra13.universalimageloader.core.ImageLoader;
com.stackmob.sdk.api.StackMobQuery;
com.stackmob.sdk.callback.StackMobCountCallback;
com.stackmob.sdk.exception.StackMobException;

public class DetailViewActivity extends Activity {



private Snap snap;


SnapStackApplication snapStackApplication;

www.stackmob.com

62

private Button commentsButton;

DisplayImageOptions options;
protected ImageLoader imageLoader = ImageLoader.getInstance();

@Override

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

setContentView(R.layout.activity_detail);

options = new DisplayImageOptions.Builder()

.showStubImage(R.drawable.placeholder)

.showImageForEmptyUri(R.drawable.placeholder)

.showImageOnFail(R.drawable.placeholder).cacheInMemory()

.cacheOnDisc().bitmapConfig(Bitmap.Config.RGB_565).build();

snapStackApplication = (SnapStackApplication) getApplication();

snap = snapStackApplication.getSnap();

TextView user_name = (TextView) findViewById(R.id.detail_username);


user_name.setText(snap.getCreator().getUsername());

ImageView detail_profile_image = (ImageView) findViewById(R.id.detail_profile_image);

if (snap.getCreator().getPhoto() != null) {

imageLoader.displayImage(snap.getCreator().getPhoto().getS3Url(),

detail_profile_image, options);

} else {

Bitmap bitmap = BitmapFactory.decodeResource(getResources(),

R.drawable.default_avatar);

detail_profile_image.setImageBitmap(bitmap);
}

ImageView imageView = (ImageView) findViewById(R.id.detail_image);
imageLoader
.displayImage(snap.getPhoto().getS3Url(), imageView, options);

imageView.setOnClickListener(new OnClickListener() {
@Override

public void onClick(View v) {

Intent intent = new Intent(DetailViewActivity.this,
PhotoViewActivity.class);

}
});

startActivityForResult(intent, 0);


commentsButton = (Button) findViewById(R.id.detail_comments);

commentsButton.setOnClickListener(new OnClickListener() {
@Override

public void onClick(View v) {

Intent intent = new Intent(DetailViewActivity.this,
CommentViewActivity.class);

startActivityForResult(intent, 0);
}
});

commentsButton.setVisibility(View.GONE);
countComments();
}


private void countComments(){

www.stackmob.com

63



Snap snap = snapStackApplication.getSnap();

StackMobQuery query = new StackMobQuery();

query.fieldIsEqualTo(snap, snap.getID());


Comment.count(Comment.class, query, new StackMobCountCallback(){
@Override

public void success(long count) {

// TODO Auto-generated method stub
String commentLabel;


if (count == 0) {
return;
}

else if (count > 1) {

commentLabel = Comments;
}
else {

commentLabel = Comment;
}


final String label = + (int)count + commentLabel;


runOnUiThread(new Runnable() {
@Override
public void run() {


commentsButton.setText(label);

commentsButton.setVisibility(View.VISIBLE);
Animation fadeIn = new AlphaAnimation(0, 1);


fadeIn.setInterpolator(new DecelerateInterpolator());


fadeIn.setDuration(500);




commentsButton.setAnimation(fadeIn);

}
});
}
@Override

public void failure(StackMobException arg0) {

// TODO Auto-generated method stub

}

});
}
@Override

public void onActivityResult(int requestCode, int resultCode, Intent data) {

super.onActivityResult(requestCode, resultCode, data);

if (resultCode == Activity.RESULT_OK && requestCode == 0) {

setResult(RESULT_OK, null);

finish();
}

if (resultCode == Activity.RESULT_OK && requestCode == 1) {

setResult(RESULT_OK, null);
countComments();
}
}
@Override

www.stackmob.com

64


public boolean onCreateOptionsMenu(Menu menu) {

MenuInflater inflater = getMenuInflater();

inflater.inflate(R.menu.comment_menu, menu);
return true;
}
@Override

public boolean onOptionsItemSelected(MenuItem item) {

Intent intent = new Intent(DetailViewActivity.this,
ShareCommentActivity.class);

startActivityForResult(intent, 1);

return true;
}
}

Build and run


Build an run, and you have a complete app on your hands!

Congrats!
After completing this part, our app is operating and fully functional.
In Part 5, well detail the steps to submit our finished app to the Google Play Store.

www.stackmob.com

65

SnapStack Android Bootcamp

Part 5
What well cover
In this chapter, well talk about testing on our app, push to production and learn how to submit to the
Play Store.

Testing your app


Although we strive for our code to be perfect, usually it ends up far from it. Its not a question of if your
application has bugs, but when and how many. Its very important to test your app as much as possible
before releasing to the Google Play Store. Fortunately, there are a few resources available to help in this
process.

Android emulator
The Android Emulator comes packaged with the Android SDK, and allows for the developers to create
many different virtual devices. Using the emulator, a developer can test against a large selection of
Android API levels.

Ad-hoc distribution
With Android, its very easy to build and
distribute your app to potential testers.
Simply export and APK file of the project,
and you can install it on anyones phone.
To export a project into an APK, right click
the project and select export:

www.stackmob.com

66

Under Android, select Export


Android Application and click
next:

In the keystore selection screen,


navigate to wherever your
production keystore is located.
If you dont have a production
keystore, you can create one.
Enter your keystore password
and click next.

Select your keystore key alias,


enter your password and click
next.

www.stackmob.com

67

Finally, choose a place to save


your APK and click finish.

Developers can send out their


APKs to groups of testers
and have them install right
from email. This makes ad-hoc
distribution and coordination
less painful.

Deploy to Production
Now that youve finished building and testing your app, its ready to be released, right? Not so fast.
Before we showcase our creation to the world, we need to setup our production environment. During
the development process, everything about your app is in the development stage. This allows you to
test and experiment. Once your app goes public, youll want to separate everything into two separate
environments: Development and Production. This provides you the opportunity to test changes and
improvements in your app without directly affecting live users.
To accomplish this on StackMob, head to the deploy section of your dashboard. Check Deploy API
and click Deploy. Enter a version number, and all of your schemas will be copied over into a Production
environment.

Tip: If its your first time deploying to production, use the version number 1.0.
Back in Eclipse, update where we instantiate StackMob in LoginActivity; add your Production keys and
change your version number to 1.

www.stackmob.com

68

Preparing for release


To submit apps to the Google Play store, youll need to be registered for a Google Play publisher account.
Check out this tutorial on how to register to be a Google Play publisher.
Once youve registered, youll be provided with a developer console to view the status of your apps.
Google provides an excellent walkthrough of the Google Play developer console.
Finally, the time comes to submit to the Google Play store. Make sure you follow the checklist for submitting
to the Play store, a detailed tutorial provided by Google for developers in preparation for release.

Congrats!
Youve successfully completed our Android bootcamp. We went through the different stages of
development a pieced together a complete app. Using StackMob allowed us to rapidly build in the
features for this app. Lets recap the benefits brought by utilizing the StackMob platform and our
Android SDK:
User Authentication
Access Control Lists (ACL)
CRUD API
S3 Integration
GeoPoints
Development and Production Environments
Imagine if you had to create that backend entirely by yourself!
Through this series, youve seen the power of the StackMob platform, as well as its flexibility. The
possibilities are endless for developers who utilize StackMob with their apps and services. In the final
chapter, well finish out our SnapStack series with a final post about monitoring the performance of your
app.
If youre new to StackMob, its completely free to get started. Sign up today!

www.stackmob.com

69

You might also like