Professional Documents
Culture Documents
Photo
Sharing
App in a
Day
Step by Step Guide
for Android
www.stackmob.com
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.
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.
www.stackmob.com
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.
www.stackmob.com
www.stackmob.com
www.stackmob.com
Setting up S3
Follow the tutorial to create and add S3 credentials to your StackMob app.
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.
www.stackmob.com
www.stackmob.com
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;
}
}
package com.stackmob.snapstack;
import com.stackmob.sdk.model.StackMobModel;
public class Comment extends StackMobModel {
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
www.stackmob.com
11
www.stackmob.com
12
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;
@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
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.
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;
www.stackmob.com
18
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;
@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.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);
}
}
});
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
if (extras != null) {
Bitmap photo = extras.getParcelable(data);
choose_photo_imageview.setImageBitmap(photo);
choose_photo_button.setEnabled(true);
}
if (f.exists())
f.delete();
break;
}
}
private void doCrop() {
final ArrayList<CropOption> cropOptions = new ArrayList<CropOption>();
List<ResolveInfo> list = getPackageManager().queryIntentActivities(
intent, 0);
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);
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);
if (foundError(username, password, email)) {
progressDialog.dismiss();
return;
}
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();
}
});
}
});
}
});
}
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
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.
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;
SERVICE);
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;
}
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);
// 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);
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.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;
www.stackmob.com
33
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();
}
});
@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
@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
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
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> {
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;
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();
}
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;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_share_photo);
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);
}
if (f.exists())
f.delete();
break;
}
}
private void doCrop() {
final ArrayList<CropOption> cropOptions = new ArrayList<CropOption>();
List<ResolveInfo> list = getPackageManager().queryIntentActivities(
intent, 0);
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
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);
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);
});
}
}
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);
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
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
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;
www.stackmob.com
56
}
@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;
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();
adapter = new CommentAdapter(CommentViewActivity.this, comments);
listView.setAdapter(adapter);
loadComments();
}
progressDialog = ProgressDialog.show(CommentViewActivity.this,
Loading..., Loading comments, true);
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();
}
});
}
public ListUpdater() {
}
public void run() {
adapter = new CommentAdapter(CommentViewActivity.this, comments);
listView.setAdapter(adapter);
}
}
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);
}
}
return view;
}
}
}
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;
www.stackmob.com
62
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();
snap = snapStackApplication.getSnap();
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;
}
}
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
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.
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
www.stackmob.com
67
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
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