Java tutorial
/* * Copyright (C) 2013 ohmage * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ohmage.app; import android.accounts.Account; import android.accounts.AccountManager; import android.app.AlertDialog; import android.app.Dialog; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.location.Location; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.Parcelable; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.AsyncTaskLoader; import android.support.v4.content.Loader; import android.support.v4.view.PagerAdapter; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.Button; import android.widget.Toast; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesClient.ConnectionCallbacks; import com.google.android.gms.common.GooglePlayServicesClient.OnConnectionFailedListener; import com.google.android.gms.location.LocationClient; import com.google.android.gms.location.LocationListener; import com.google.android.gms.location.LocationRequest; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.viewpagerindicator.CirclePageIndicator; import org.ohmage.auth.AuthUtil; import org.ohmage.condition.Condition; import org.ohmage.condition.NoResponse; import org.ohmage.dagger.InjectedActionBarActivity; import org.ohmage.fragments.InstallDependenciesDialog; import org.ohmage.log.AppLogEntry; import org.ohmage.log.AppLogManager; import org.ohmage.log.AppLogSyncAdapter; import org.ohmage.models.ApkSet; import org.ohmage.prompts.AnswerablePrompt; import org.ohmage.prompts.Prompt; import org.ohmage.prompts.PromptFragment; import org.ohmage.prompts.SurveyItemFragment; import org.ohmage.provider.OhmageContract; import org.ohmage.provider.OhmageContract.Surveys; import org.ohmage.provider.ResponseContract; import org.ohmage.provider.ResponseContract.Responses; import org.ohmage.reminders.glue.TriggerFramework; import org.ohmage.streams.StreamPointBuilder; import org.ohmage.widget.VerticalViewPager; import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.AbstractMap; import java.util.ArrayList; import java.util.BitSet; import java.util.HashMap; import java.util.Map; import javax.inject.Inject; /** * Created by cketcham on 12/18/13. */ public class SurveyActivity extends InjectedActionBarActivity implements LoaderCallbacks<ArrayList<Prompt>>, ConnectionCallbacks, OnConnectionFailedListener { private static final String TAG = SurveyActivity.class.getSimpleName(); @Inject Gson gson; @Inject AccountManager am; /** * The pager widget, which handles animation and allows swiping horizontally to access previous * and next wizard steps. */ private VerticalViewPager mPager; /** * The pager adapter, which provides the pages to the view pager widget. */ private PromptFragmentAdapter mPagerAdapter; private CirclePageIndicator indicator; /** * Location client to get accurate location */ private LocationClient mLocationClient; /** * Tracks if survey is in progress */ private boolean isFinished = false; private static final LocationRequest REQUEST = LocationRequest.create().setInterval(5000) // 5 seconds .setFastestInterval(16) // 16ms = 60fps .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); private SurveyStateFragment mState; public static final int MSG_SHOW_INSTALL_DEPENDENCIES = 0; private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == MSG_SHOW_INSTALL_DEPENDENCIES) { FragmentManager fm = getSupportFragmentManager(); InstallDependenciesDialog fragment = (InstallDependenciesDialog) fm.findFragmentByTag("install"); if (fragment == null) { fragment = InstallDependenciesDialog.getInstance((ApkSet) msg.obj, true); } fragment.show(fm, "install"); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_survey); // Instantiate a ViewPager and a PagerAdapter. mPager = (VerticalViewPager) findViewById(R.id.pager); mPager.setPageMargin(getResources().getDimensionPixelSize(R.dimen.gutter)); //Bind the title indicator to the adapter indicator = (CirclePageIndicator) findViewById(R.id.titles); if (savedInstanceState == null) { mState = new SurveyStateFragment(); getSupportFragmentManager().beginTransaction().add(mState, "state").commit(); } else { mState = (SurveyStateFragment) getSupportFragmentManager().findFragmentByTag("state"); } if (mState.prompts != null) { setPromptFragmentAdapter(); } else { getSupportLoaderManager().initLoader(0, null, this); } AppLogManager.getInstance().logInfo(this, "SurveyStarted", "User started the survey: " + Surveys.getId(getIntent().getData())); } @Override protected void onResume() { super.onResume(); setUpLocationClientIfNeeded(); mLocationClient.connect(); AppLogManager.getInstance().logInfo(this, "SurveyResumed", "User resumed the survey: " + Surveys.getId(getIntent().getData())); } @Override public void onPause() { super.onPause(); if (mLocationClient != null) { mLocationClient.disconnect(); } } @Override public void onStop() { super.onStop(); if (!isFinished) { AppLogManager.getInstance().logInfo(this, "SurveyStopped", "User left survey without submitting or discarding: " + Surveys.getId(getIntent().getData())); } } private void setUpLocationClientIfNeeded() { if (mLocationClient == null) { mLocationClient = new LocationClient(getApplicationContext(), this, // ConnectionCallbacks this); // OnConnectionFailedListener } } @Override public void onBackPressed() { // If nothing has been answered just go back if (mPagerAdapter == null || mPagerAdapter.getAnsweredCount() == 0) { discardSurvey(); return; } // Otherwise show a dialog so they don't lose their responses FragmentManager fm = getSupportFragmentManager(); CancelResponseDialogFragment fragment = (CancelResponseDialogFragment) fm.findFragmentByTag("cancel"); if (fragment == null) { fragment = new CancelResponseDialogFragment(); } fragment.show(fm, "cancel"); } private void discardSurvey() { isFinished = true; if (mPagerAdapter != null) mPagerAdapter.clearExtras(); AppLogManager.getInstance().logInfo(this, "SurveyDiscarded", "User discarded the survey: " + Surveys.getId(getIntent().getData())); super.onBackPressed(); } @Override public SurveyPromptLoader onCreateLoader(int id, Bundle args) { return new SurveyPromptLoader(this, getIntent().getData()); } @Override public void onLoadFinished(Loader<ArrayList<Prompt>> loader, ArrayList<Prompt> data) { if (data == null) { Toast.makeText(this, R.string.survey_not_found, Toast.LENGTH_SHORT).show(); finish(); return; } if (mPagerAdapter == null) { setPrompts(data); // Check for remote activity prompts and make sure they all exist if (data != null) { ApkSet appItems = ApkSet.fromPromptsIgnoreSkippable(data); appItems.clearInstalled(this); if (!appItems.isEmpty()) { Message msg = handler.obtainMessage(MSG_SHOW_INSTALL_DEPENDENCIES); msg.obj = appItems; msg.sendToTarget(); } } } } @Override public void onLoaderReset(Loader<ArrayList<Prompt>> loader) { } public void setPromptFragmentAdapter() { mPagerAdapter = new PromptFragmentAdapter(getSupportFragmentManager(), mState.prompts); mPager.setAdapter(mPagerAdapter); indicator.setViewPager(mPager); } public void setPrompts(ArrayList<Prompt> data) { mState.prompts = new Prompts(data); setPromptFragmentAdapter(); } /* * Submit the survey. First tell reminders that the survey was taken, then insert the response * to the content provider, and finally show the Toast and return to the previous activity */ public synchronized void submit() { isFinished = true; // Tell reminders that the survey was taken TriggerFramework.notifySurveyTaken(this, Surveys.getId(getIntent().getData())); ContentValues values = new ContentValues(); values.put(Responses.SURVEY_ID, Surveys.getId(getIntent().getData())); values.put(Responses.SURVEY_VERSION, Surveys.getVersion(getIntent().getData())); if (mLocationClient != null && mLocationClient.isConnected()) { values.put(Responses.RESPONSE_METADATA, new StreamPointBuilder().now().withId() .withLocation(mLocationClient.getLastLocation()).getMetadata()); } else { values.put(Responses.RESPONSE_METADATA, new StreamPointBuilder().now().withId().getMetadata()); } mPagerAdapter.buildResponse(values); getContentResolver().insert(Responses.CONTENT_URI, values); Toast.makeText(this, "The response has been submitted. Thank you!", Toast.LENGTH_SHORT).show(); AppLogManager.getInstance().logInfo(this, "SurveySubmitted", "User submitted the survey: " + Surveys.getId(getIntent().getData())); // Force a sync to upload data Bundle settingsBundle = new Bundle(); settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); Account[] accounts = am.getAccountsByType(AuthUtil.ACCOUNT_TYPE); if (accounts.length == 1) { Account account = accounts[0]; getContentResolver().requestSync(account, ResponseContract.CONTENT_AUTHORITY, settingsBundle); getContentResolver().requestSync(account, AppLogSyncAdapter.CONTENT_AUTHORITY, settingsBundle); } finish(); } @Override public void onConnected(Bundle bundle) { mLocationClient.requestLocationUpdates(REQUEST, new LocationListener() { @Override public void onLocationChanged(Location location) { } }); } @Override public void onDisconnected() { } @Override public void onConnectionFailed(ConnectionResult connectionResult) { } public PromptFragmentAdapter getPagerAdapter() { return mPagerAdapter; } public class PromptFragmentAdapter extends FragmentStatePagerAdapter { private final Prompts prompts; private final FragmentManager mFragmentManager; /** * Keeps track of what fragments have been created */ private BitSet mFragments = new BitSet(); /** * Keeps track of any prompts which have just changed due to an answer being updated. */ int mLastUpdate = 0; public PromptFragmentAdapter(FragmentManager fm, Prompts prompts) { super(fm); mFragmentManager = fm; this.prompts = prompts; } @Override public SurveyItemFragment getItem(final int position) { Prompt prompt = prompts.getPromptAt(position); SurveyItemFragment fragment = null; if (prompt == null) { fragment = new SubmitResponseFragment(); } else { fragment = prompt.getFragment(); fragment.showButtons(position == getCount() - 1 ? View.VISIBLE : View.GONE); } if (fragment != null) { mFragments.set(position); } return fragment; } @Override public void destroyItem(ViewGroup container, int position, Object object) { mFragments.clear(position); super.destroyItem(container, position, object); } @Override public int getCount() { return prompts.getAnsweredCount() + 1; } @Override public float getPageWidth(int position) { if (position == prompts.size()) { return 1.0f; } return 0.1f; } @Override public Parcelable saveState() { Parcelable parent = super.saveState(); Bundle state = new Bundle(); state.putParcelable("parent", parent); return state; } @Override public void restoreState(Parcelable state, ClassLoader loader) { Bundle bundle = (Bundle) state; state = bundle.getBundle("parent"); // Hijack the fragment state from our parent if (state != null) { bundle = (Bundle) state; mFragments.clear(); Iterable<String> keys = bundle.keySet(); for (String key : keys) { if (key.startsWith("f")) { int index = Integer.parseInt(key.substring(1)); Fragment f = mFragmentManager.getFragment(bundle, key); if (f != null) { mFragments.set(index, true); } else { Log.w(TAG, "Bad fragment at key " + key); } } } } super.restoreState(state, loader); } public void buildResponse(ContentValues values) { Log.d(TAG, gson.toJson(prompts.answers)); values.put(Responses.RESPONSE_DATA, gson.toJson(prompts.answers)); values.put(Responses.RESPONSE_EXTRAS, gson.toJson(prompts.extras)); } public void updateAnswer(PromptFragment promptFragment) { promptFragment.showButtons(View.GONE); Prompt prompt = promptFragment.getPrompt(); boolean alreadyAnswered = prompts.isAnswered(prompt); mLastUpdate = prompts.updateAnswer(prompt); notifyDataSetChanged(); if (!alreadyAnswered) { moveToLastPrompt(); } } /** * Moves to the last prompt, waits until it has been added to the view */ private void moveToLastPrompt() { final int position = getCount() - 1; mPager.post(new Runnable() { @Override public void run() { mPager.bringPositionUpOnScreen(position); } }); } @Override public int getItemPosition(Object object) { if (object instanceof PromptFragment) { if (prompts.positionOf(((PromptFragment) object).getPrompt()) >= mLastUpdate) { return PagerAdapter.POSITION_NONE; } return POSITION_UNCHANGED; } return PagerAdapter.POSITION_NONE; } public int getPosition(SurveyItemFragment fragment) { if (fragment instanceof PromptFragment) { return prompts.positionOf(((PromptFragment) fragment).getPrompt()); } else if (fragment instanceof SubmitResponseFragment) { return prompts.size(); } return -1; } public SurveyItemFragment getPromptFragmentAt(int position) { if (mFragments.get(position)) { return getItem(position); } return null; } public int getAnsweredCount() { return prompts.getAnsweredCount(); } public void clearExtras() { prompts.clearExtras(); } } public static class SubmitResponseFragment extends SurveyItemFragment { boolean submitted = false; @Override public View onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.prompt_submit, container, false); final Button submit = (Button) view.findViewById(R.id.submit); submit.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { submit.setEnabled(false); if (!submitted) { submitted = true; ((SurveyActivity) getActivity()).submit(); } } }); return view; } } /** * Static library support version of the framework's {@link android.content.CursorLoader}. * Used to write apps that run on platforms prior to Android 3.0. When running * on Android 3.0 or above, this implementation is still used; it does not try * to switch to the framework's implementation. See the framework SDK * documentation for a class overview. */ public static class SurveyPromptLoader extends AsyncTaskLoader<ArrayList<Prompt>> { @Inject Gson gson; Uri mUri; String[] mProjection; String mSelection; String[] mSelectionArgs; Cursor mCursor; private ArrayList<Prompt> mPrompts; /* Runs on a worker thread */ @Override public ArrayList<Prompt> loadInBackground() { Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection, mSelectionArgs, null); if (cursor != null && cursor.moveToFirst()) { mPrompts = gson.fromJson(cursor.getString(0), new TypeToken<ArrayList<Prompt>>() { }.getType()); cursor.close(); } return mPrompts; } /* Runs on the UI thread */ @Override public void deliverResult(ArrayList<Prompt> result) { if (isReset()) { // An async query came in while the loader is stopped mPrompts = null; return; } mPrompts = result; if (isStarted()) { super.deliverResult(result); } } /** * Creates a Loader for prompt items */ public SurveyPromptLoader(Context context, String surveyId, long surveyVersion) { super(context); ((SurveyActivity) context).inject(this); mUri = OhmageContract.Surveys.CONTENT_URI; mProjection = new String[] { OhmageContract.Surveys.SURVEY_ITEMS }; mSelection = OhmageContract.Surveys.SURVEY_ID + "=? AND " + OhmageContract.Surveys.SURVEY_VERSION + "=?"; mSelectionArgs = new String[] { surveyId, String.valueOf(surveyVersion) }; } public SurveyPromptLoader(Context context, Uri surveyUri) { super(context); ((SurveyActivity) context).inject(this); mUri = surveyUri; mProjection = new String[] { OhmageContract.Surveys.SURVEY_ITEMS }; } /** * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks * will be called on the UI thread. If a previous load has been completed and is still valid * the result may be passed to the callbacks immediately. * <p/> * Must be called from the UI thread */ @Override protected void onStartLoading() { if (mPrompts != null) { deliverResult(mPrompts); } if (takeContentChanged() || mCursor == null) { forceLoad(); } } /** * Must be called from the UI thread */ @Override protected void onStopLoading() { // Attempt to cancel the current load task if possible. cancelLoad(); } @Override protected void onReset() { super.onReset(); // Ensure the loader is stopped onStopLoading(); mPrompts = null; } @Override public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { super.dump(prefix, fd, writer, args); writer.print(prefix); writer.print("mPrompts="); writer.println(mPrompts); } } public static class SurveyStateFragment extends Fragment { public Prompts prompts; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } } /** * This class keeps track of all the prompts and how the user answered them. It provides the * functions {@link #answer(AnswerablePrompt)}, {@link #ignore(Prompt)}, and * {@link #skip(Prompt)} based on the users actions. {@link #getPromptAt(int)} will return the * next prompt ignoring all prompts which weren't displayed. */ public static class Prompts { final ArrayList<Prompt> prompts; // Answers final public HashMap<String, Object> answers; final public HashMap<String, String> extras; // List of the skipped prompts final ArrayList<String> mSkipped; // List of not displayed prompts final ArrayList<String> mNotDisplayed; public Prompts(ArrayList<Prompt> data) { this.prompts = data; answers = new HashMap<String, Object>(); extras = new HashMap<String, String>(); mSkipped = new ArrayList<String>(); mNotDisplayed = new ArrayList<String>(); } /** * Returns the prompt at the given position ignoring prompts which are not displayed * * @param position * @return */ public Prompt getPromptAt(int position) throws IllegalStateException { Prompt prompt = null; Map<String, Object> prev = null; int index = promptIndex(position); while (prompt == null && index < prompts.size()) { prompt = prompts.get(index); if (prompt.getCondition() != null) { if (prev == null) { prev = getPreviousResponses(index); } Condition condition = new Condition(prompt.getCondition()); if (!condition.evaluate(prev)) { ignore(prompt); updatePrevious(prompt, prev); prompt = null; index++; } } } return prompt; } /** * Returns the index into {@link #prompts} given the position of shown prompts * * @param position * @return */ private int promptIndex(int position) { return position + positionOffset(position); } private int positionOffset(int index) { int count = 0; for (int i = 0; i <= index && i < prompts.size(); i++) { if (isIgnored(prompts.get(i))) { count++; index++; } } return count; } /** * Count the number of prompts that have been ignored before the given position * * @param index * @return */ private int ignoredPromptsBefore(int index) { int count = 0; for (int i = 0; i < index && i < prompts.size(); i++) { if (isIgnored(prompts.get(i))) { count++; } } return count; } /** * Gets the map of all answers before the current answer to be used to evaluate the * the condition for the prompt at the given position * * @return a map of all previous responses */ private Map<String, Object> getPreviousResponses(int index) { AbstractMap prev = new HashMap<String, Object>(); for (int i = 0; i < index; i++) { updatePrevious(prompts.get(i), prev); } return prev; } /** * Convenience method to update the previous responses map for the given prompt * * @param prompt * @param prev */ private void updatePrevious(Prompt prompt, Map<String, Object> prev) { if (answers.keySet().contains(prompt.getId())) { prev.put(prompt.getId(), answers.get(prompt.getId())); } else if (mSkipped.contains(prompt.getId())) { prev.put(prompt.getId(), NoResponse.SKIPPED); } else if (mNotDisplayed.contains(prompt.getId())) { prev.put(prompt.getId(), NoResponse.NOT_DISPLAYED); } } /** * Calculates the number of prompts that were actively answered or skipped. * * @return */ public int getAnsweredCount() { return answers.size() + mSkipped.size(); } /** * Will add an answer given the state of the prompt item * * @param prompt * @return the index of the first position which changed due to conditions */ public int updateAnswer(Prompt prompt) { if (prompt instanceof AnswerablePrompt) { if (((AnswerablePrompt) prompt).hasValidResponse()) { answer((AnswerablePrompt) prompt); } else if (prompt.isSkippable()) { skip(prompt); } else { // If this prompt is no longer valid remove all answers after it int invalidPrompt = prompts.indexOf(prompt); removeAnswersAfter(invalidPrompt); return invalidPrompt - ignoredPromptsBefore(invalidPrompt) + 1; } } else { answer(prompt); } return resetFutureAnswers(prompt); } private void answer(Prompt prompt) { remove(prompt); answers.put(prompt.getId(), null); } private void answer(AnswerablePrompt prompt) { remove(prompt); answers.put(prompt.getId(), prompt.getAnswer()); extras.put(prompt.getId(), prompt.getAnswerExtras()); } private void ignore(Prompt prompt) { remove(prompt); mNotDisplayed.add(prompt.getId()); } private void skip(Prompt prompt) { remove(prompt); mSkipped.add(prompt.getId()); } private void remove(Prompt prompt) { mNotDisplayed.remove(prompt.getId()); mSkipped.remove(prompt.getId()); answers.remove(prompt.getId()); extras.remove(prompt.getId()); } /** * Calculates if a prompt was answered * * @param prompt * @return true if it was answered or skipped */ public boolean isAnswered(Prompt prompt) { return answers.containsKey(prompt.getId()) || mSkipped.contains(prompt.getId()); } /** * Calculates if a prompt was ignored due to conditions * * @param prompt * @return true if it was ignored */ public boolean isIgnored(Prompt prompt) { return mNotDisplayed.contains(prompt.getId()); } public int ignoredPromptsSize() { return mNotDisplayed.size(); } public int size() { return prompts.size() - mNotDisplayed.size(); } public int positionOf(Prompt prompt) { int idx = prompts.indexOf(prompt); return idx - ignoredPromptsBefore(idx); } private void removeAnswersAfter(int index) { for (int i = index; i < prompts.size(); i++) { Prompt prompt = prompts.get(i); String promptId = prompt.getId(); if (mSkipped.contains(promptId)) { mSkipped.remove(promptId); } else if (answers.keySet().contains(promptId)) { answers.remove(promptId); if (extras.keySet().contains(promptId)) { extras.remove(promptId); } } else if (mNotDisplayed.contains(promptId)) { mNotDisplayed.remove(promptId); } } } public void clearExtras() { for (int i = 0; i < prompts.size(); i++) { String fileName = extras.get(prompts.get(i).getId()); if (fileName != null) { new File(fileName).delete(); } } } /** * Since the answer changed for this response we should re-calculate all of the next responses * * @param prompt */ private int resetFutureAnswers(Prompt prompt) throws IllegalStateException { int pivot = prompts.indexOf(prompt); Map<String, Object> prev = null; int lastUpdate = size(); // Calculate the first prompt which no longer passes the condition test int after = prompts.size(); for (int i = pivot + 1; i < getAnsweredCount() + ignoredPromptsSize() + 1 && i < prompts.size(); i++) { prompt = prompts.get(i); if (prev == null) { prev = getPreviousResponses(i); } if (prompt.getCondition() != null) { Condition condition = new Condition(prompt.getCondition()); // The first prompt which is not the same as it used to be and is shown boolean show = condition.evaluate(prev); if (show == isIgnored(prompt)) { lastUpdate = Math.min(lastUpdate, i); if (!show) { ignore(prompt); } else { after = i; break; } } } updatePrevious(prompt, prev); } // Remove all responses after the last valid one removeAnswersAfter(after); return lastUpdate - ignoredPromptsBefore(lastUpdate + 1); } } public static class CancelResponseDialogFragment extends DialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(getString(R.string.discard_survey_title)) .setMessage(getString(R.string.discard_survey_message)) .setPositiveButton(R.string.discard, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ((SurveyActivity) getActivity()).discardSurvey(); } }).setNegativeButton(R.string.cancel, null); return builder.create(); } } }