/*------------------------------------------------------------------------------ ** Ident: Sogeti Smart Mobile Solutions ** Author: rene ** Copyright: (c) Apr 24, 2011 Sogeti Nederland B.V. All Rights Reserved. **------------------------------------------------------------------------------ ** Sogeti Nederland B.V. | No part of this file may be reproduced ** Distributed Software Engineering | or transmitted in any form or by any ** Lange Dreef 17 | means, electronic or mechanical, for the ** 4131 NJ Vianen | purpose, without the express written ** The Netherlands | permission of the copyright holder. *------------------------------------------------------------------------------ * * This file is part of OpenGPSTracker. * * OpenGPSTracker is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * OpenGPSTracker is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenGPSTracker. If not, see <http://www.gnu.org/licenses/>. * */ package nl.sogeti.android.gpstracker.actions; import android.app.*; import android.app.AlertDialog.Builder; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnDismissListener; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.widget.SimpleCursorAdapter; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.*; import nl.sogeti.android.gpstracker.R; import nl.sogeti.android.gpstracker.db.GPStracking.Stations; import nl.sogeti.android.gpstracker.db.GPStracking.Tracks; import java.util.Calendar; //To use LoaderManager and cursorLoader classes on pre honeycomb /** * Empty Activity that pops up the dialog to name the track * * @version $Id: NameTrack.java 1132 2011-10-09 18:52:59Z rcgroot $ * @author rene (c) Jul 27, 2010, Sogeti B.V. */ public class NameTrack extends FragmentActivity //Compatibility requirement : instead of Activity implements LoaderManager.LoaderCallbacks<Cursor>, TextWatcher //Required for LoaderManager, Used to watch user text //input and restart cursorLoaders accordingly { private static final int DIALOG_TRACKNAME = 23; protected static final String TAG = "OGT.NameTrack"; private EditText mTrackNameView; //F8F BEGIN private RadioGroup mHelmetRadioGroup; private Spinner mOriginReasonSpinner; private Spinner mDestinationReasonSpinner; private ArrayAdapter<CharSequence> mReasonAdapter; private RatingBar mServiceRatingBar; private AutoCompleteTextView mStartStationAutoCompleteTextView; private AutoCompleteTextView mEndStationAutoCompleteTextView; //To deprecate : this is used in the textbook version of autocompletetextview //(filled from a resource file) private ArrayAdapter<String> mStationAdapter; /////////////////////////////////////////////////////////////////// private SimpleCursorAdapter mStartStationSimpleCursorAdapter; private SimpleCursorAdapter mEndStationSimpleCursorAdapter; //Used to pass around current user input in suggestion boxes String mCurStartStationNameFilter; String mCurEndStationNameFilter; /////////////////////////////////////////////////////////////////// private static final int STARTSTATIONCURSOR_LOADER = 0; private static final int ENDSTATIONCURSOR_LOADER = 1; //private CursorAdapter //F8F END private boolean paused; Uri mTrackUri; private final DialogInterface.OnClickListener mTrackNameDialogListener = new DialogInterface.OnClickListener() { public void onClick( DialogInterface dialog, int which ) { switch( which ) { case DialogInterface.BUTTON_POSITIVE: updateTrack(); clearNotification(); break; case DialogInterface.BUTTON_NEUTRAL: startDelayNotification(); break; case DialogInterface.BUTTON_NEGATIVE: clearNotification(); break; default: Log.e( TAG, "Unknown option ending dialog:"+which ); break; } finish(); } }; //Some hack to prevent autocomplete tips being displayed again after item selection private final TextWatcher mStartStationTextWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { //To change body of implemented methods use File | Settings | File Templates. } @Override public void onTextChanged(CharSequence charSequence, int start, int count, int after) { if (count > 1) { mCurStartStationNameFilter = charSequence.toString(); } } @Override public void afterTextChanged(Editable editable) { //NOTE : the activity is also listening because cursorloader restart function needs to //know where to find the callbacks, hence being passed the activity itself } }; private final TextWatcher mEndStationTextWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { //To change body of implemented methods use File | Settings | File Templates. } @Override public void onTextChanged(CharSequence charSequence, int start, int count, int after) { if (count > 1 ) { mCurEndStationNameFilter = charSequence.toString(); } } @Override public void afterTextChanged(Editable editable) { //NOTE : the activity is also listening because cursorloader restart function needs to //know where to find the callbacks, hence being passed the activity itself } }; //End of hack private void updateTrack() { String trackName = null; trackName = mTrackNameView.getText().toString(); ContentValues values = new ContentValues(); values.put( Tracks.NAME, trackName ); if (mHelmetRadioGroup.getCheckedRadioButtonId() == -1) { values.put(Tracks.WITH_HELMET, "-1"); } else if (mHelmetRadioGroup.getCheckedRadioButtonId() == R.id.helmetRadio_yes) { values.put(Tracks.WITH_HELMET, "1"); } else //mHelmetRadioGroup.getCheckedRadioButtonId() == R.id.helmetRadio_no { values.put(Tracks.WITH_HELMET, "0"); } String startReason = mOriginReasonSpinner.getSelectedItem().toString(); values.put(Tracks.START_REASON, startReason); String endReason = mDestinationReasonSpinner.getSelectedItem().toString(); values.put(Tracks.END_REASON, endReason); if (mServiceRatingBar.getRating() > 0) // 0 = unrated track, minimum valid rating being 1 { values.put(Tracks.SERVICE_RATING, (int)mServiceRatingBar.getRating()); } values.put(Tracks.START_STATION_NAME, mStartStationAutoCompleteTextView.getText().toString()); values.put(Tracks.END_STATION_NAME, mEndStationAutoCompleteTextView.getText().toString()); getContentResolver().update( mTrackUri, values, null, null ); } private void clearNotification() { NotificationManager noticationManager = (NotificationManager) this.getSystemService( Context.NOTIFICATION_SERVICE );; noticationManager.cancel( R.layout.namedialog ); } private void startDelayNotification() { int resId = R.string.dialog_routename_title; int icon = R.drawable.ic_maps_indicator_current_position; CharSequence tickerText = getResources().getString( resId ); long when = System.currentTimeMillis(); Notification nameNotification = new Notification( icon, tickerText, when ); nameNotification.flags |= Notification.FLAG_AUTO_CANCEL; CharSequence contentTitle = getResources().getString( R.string.app_name ); CharSequence contentText = getResources().getString( resId ); Intent notificationIntent = new Intent( this, NameTrack.class ); notificationIntent.setData( mTrackUri ); PendingIntent contentIntent = PendingIntent.getActivity( this, 0, notificationIntent, Intent.FLAG_ACTIVITY_NEW_TASK ); nameNotification.setLatestEventInfo( this, contentTitle, contentText, contentIntent ); NotificationManager noticationManager = (NotificationManager) this.getSystemService( Context.NOTIFICATION_SERVICE ); noticationManager.notify( R.layout.namedialog, nameNotification ); } @Override protected void onCreate( Bundle savedInstanceState ) { super.onCreate( savedInstanceState ); //This activity hides itself, the TrackList activity switches it's visible state this.setVisible( false ); paused = false; mTrackUri = this.getIntent().getData(); mCurStartStationNameFilter = ""; mCurEndStationNameFilter = ""; getSupportLoaderManager().initLoader(STARTSTATIONCURSOR_LOADER, null, this); getSupportLoaderManager().initLoader(ENDSTATIONCURSOR_LOADER, null, this); String[] uiBindFrom = { Tracks.NAME }; int[] uiBindTo = { android.R.id.text1 }; mStartStationSimpleCursorAdapter = new SimpleCursorAdapter( this, android.R.layout.simple_dropdown_item_1line, null, uiBindFrom, uiBindTo, 0); mEndStationSimpleCursorAdapter = new SimpleCursorAdapter( this, android.R.layout.simple_dropdown_item_1line, null, uiBindFrom, uiBindTo, 0); // Set the CursorToStringConverter, to provide the labels for the // choices to be displayed in the AutoCompleteTextView. mStartStationSimpleCursorAdapter.setCursorToStringConverter(new SimpleCursorAdapter.CursorToStringConverter() { public String convertToString(android.database.Cursor cursor) { // Get the label for this row out of the "state" column final int columnIndex = cursor.getColumnIndexOrThrow(Tracks.NAME); final String str = cursor.getString(columnIndex); return str; } }); //TODO : Maybe I should have this in an external object I'd reuse instead of two anonymous objects ? mEndStationSimpleCursorAdapter.setCursorToStringConverter(new SimpleCursorAdapter.CursorToStringConverter() { public String convertToString(android.database.Cursor cursor) { // Get the label for this row out of the "state" column final int columnIndex = cursor.getColumnIndexOrThrow(Tracks.NAME); final String str = cursor.getString(columnIndex); return str; } }); //DO NOT BE TEMPTED, THE FOLLOWING CODE MAKE THE QUERY FROM THE UI THREAD I THINK, AND THIS IS BAD, VERY BAD ! // Set the FilterQueryProvider, to run queries for choices // that match the specified input. /*mStartStationSimpleCursorAdapter.setFilterQueryProvider(new FilterQueryProvider() { public Cursor runQuery(CharSequence constraint) { // Search for states whose names begin with the specified letters. Cursor cursor = mDbHelper.getMatchingStates( (constraint != null ? constraint.toString() : null)); return cursor; } });*/ } @Override protected void onPause() { super.onPause(); paused = true; } /* * (non-Javadoc) * @see com.google.android.maps.MapActivity#onPause() */ @Override protected void onResume() { super.onResume(); if( mTrackUri != null ) { showDialog( DIALOG_TRACKNAME ); } else { Log.e(TAG, "Naming track without a track URI supplied." ); finish(); } } @Override protected Dialog onCreateDialog( int id ) { Dialog dialog = null; LayoutInflater factory = null; View view = null; Builder builder = null; switch (id) { case DIALOG_TRACKNAME: builder = new AlertDialog.Builder( this ); factory = LayoutInflater.from( this ); view = factory.inflate( R.layout.namedialog, null ); mTrackNameView = (EditText) view.findViewById( R.id.nameField ); mHelmetRadioGroup = (RadioGroup) view.findViewById(R.id.helmetRadioGroup); mServiceRatingBar = (RatingBar) view.findViewById(R.id.serviceRatingBar); mOriginReasonSpinner = (Spinner) view.findViewById(R.id.originReasonSpinner); mDestinationReasonSpinner = (Spinner) view.findViewById(R.id.destinationReasonSpinner); mReasonAdapter = ArrayAdapter.createFromResource(this, R.array.Reason_choices, android.R.layout.simple_spinner_item); mReasonAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); mOriginReasonSpinner.setAdapter(mReasonAdapter); mDestinationReasonSpinner.setAdapter(mReasonAdapter); mStartStationAutoCompleteTextView = (AutoCompleteTextView) view.findViewById(R.id.startStationAutocomplete); mStartStationAutoCompleteTextView.setAdapter(mStartStationSimpleCursorAdapter); mStartStationAutoCompleteTextView.addTextChangedListener(this); mStartStationAutoCompleteTextView.addTextChangedListener(mStartStationTextWatcher); mEndStationAutoCompleteTextView = (AutoCompleteTextView) view.findViewById(R.id.endStationAutocomplete); mEndStationAutoCompleteTextView.setAdapter(mEndStationSimpleCursorAdapter); mEndStationAutoCompleteTextView.addTextChangedListener(this); mEndStationAutoCompleteTextView.addTextChangedListener(mEndStationTextWatcher); builder .setTitle(R.string.dialog_routename_title) //.setMessage( R.string.dialog_routename_message ) .setIcon( android.R.drawable.ic_dialog_alert ) .setPositiveButton( R.string.btn_okay, mTrackNameDialogListener ) .setNeutralButton( R.string.btn_skip, mTrackNameDialogListener ) .setNegativeButton( R.string.btn_cancel, mTrackNameDialogListener ) .setView( view ); dialog = builder.create(); dialog.setOnDismissListener( new OnDismissListener() { public void onDismiss( DialogInterface dialog ) { if( !paused ) { finish(); } } }); return dialog; default: return super.onCreateDialog( id ); } } @Override protected void onPrepareDialog( int id, Dialog dialog ) { switch (id) { case DIALOG_TRACKNAME: Cursor trackCursor = null; //TODO: I feel kinda bad now I'm using CursorLoader for start/end station, though here I always request 1 ROW AT MOST (AND HOPEFULLY) //trackCursor = TrackList.this.getContentResolver().query(mDialogUri, new String[] { Tracks._ID, Tracks.WITH_HELMET, Tracks.START_REASON}, null, null,null); trackCursor = getContentResolver().query(mTrackUri, null, null, null,null); //This should always contains at most 1 row, given we request for a particular track ID, contained in the URI if (trackCursor.moveToFirst()) { String trackName = trackCursor.getString(trackCursor.getColumnIndex(Tracks.NAME)); if (trackName == null || trackName.isEmpty()) { Calendar c = Calendar.getInstance(); trackName = String.format( getString( R.string.dialog_routename_default ), c, c, c, c, c ); } mTrackNameView.setText( trackName ); mTrackNameView.setSelection( 0, trackName.length() ); //boolean nullHelmet = trackCursor.isNull(trackCursor.getColumnIndex(Tracks.WITH_HELMET)); int helmetValue = trackCursor.getInt(trackCursor.getColumnIndex(Tracks.WITH_HELMET)); if (/*nullHelmet ||*/ helmetValue == -1) { mHelmetRadioGroup.clearCheck(); } else if (helmetValue == 1) { mHelmetRadioGroup.check(R.id.helmetRadio_yes); } else if (helmetValue == 0) { mHelmetRadioGroup.check(R.id.helmetRadio_no); } int startReasonIdx = trackCursor.getColumnIndex(Tracks.START_REASON); String startReason = trackCursor.getString(startReasonIdx); //beurk ! mOriginReasonSpinner.setSelection(mReasonAdapter.getPosition(startReason)); int endReasonIdx = trackCursor.getColumnIndex(Tracks.END_REASON); String endReason = trackCursor.getString(endReasonIdx); //beurk ! mDestinationReasonSpinner.setSelection(mReasonAdapter.getPosition(endReason)); mServiceRatingBar.setRating(trackCursor.getInt(trackCursor.getColumnIndex(Tracks.SERVICE_RATING))); String startStationName = trackCursor.getString(trackCursor.getColumnIndex(Tracks.START_STATION_NAME)); if (startStationName == null) { startStationName = ""; } mStartStationAutoCompleteTextView.setText(startStationName); String endStationName = trackCursor.getString(trackCursor.getColumnIndex(Tracks.END_STATION_NAME)); if (endStationName == null) { endStationName = ""; } mEndStationAutoCompleteTextView.setText(endStationName); } trackCursor.close(); //Yeah that's where it is suspect, I should now use Loader to avoid managing cursor myself break; default: super.onPrepareDialog( id, dialog ); break; } } ////////////////////////////////////////////////////////////////////////// //CursorLoader interface @Override public Loader<Cursor> onCreateLoader(int loaderID, Bundle bundle) { //?? // NOTE: // If wildcards are to be used in a rawQuery, they must appear // in the query parameters, and not in the query string proper. // See http://code.google.com/p/android/issues/detail?id=3153 //constraint = constraint.trim() + "%"; //--> Ok got it, though I wonder what dictates where I put the '%' character //: in the select clause or concatenated at the end of the corresponding selectArgs[] item //So here goes, I put the % in selectArgs, though I don't feel like I use a 'rawQuery' String[] stationProjection = {Stations._ID, Stations.NAME}; String stationSelect = Stations.NAME + " LIKE ?"; //Assuming mCurStartStationNameFilter/mCurEndStationNameFilter has been trimmed String[] stationSelectArgs = null; if (loaderID == STARTSTATIONCURSOR_LOADER) { stationSelectArgs = new String[]{"%" + mCurStartStationNameFilter + "%"}; } else { stationSelectArgs = new String[]{"%" + mCurEndStationNameFilter + "%"}; } //String[] selectArgs = {"sim%"}; String stationSortOrder = Stations.NAME + " COLLATE LOCALIZED ASC"; CursorLoader cursorLoader = null; if (loaderID == STARTSTATIONCURSOR_LOADER && mCurStartStationNameFilter.isEmpty() || loaderID == ENDSTATIONCURSOR_LOADER && mCurEndStationNameFilter.isEmpty()) { //I don't feel good requesting every rows here as the precise point of using an autocompletetextview //is to avoid it. Though it seems the GUI element can't function with an empty Loader created with the empty constructor //even if suggestion are supposed to appear after a few characters are in. I could format a request such as getting an //empty cursor ? cursorLoader = new CursorLoader(this, // Tracks.CONTENT_URI, projection, select, selectArgs, sortOrder); Stations.CONTENT_URI, null, null, null, null); } else { cursorLoader = new CursorLoader(this, Stations.CONTENT_URI, stationProjection, stationSelect, stationSelectArgs, stationSortOrder); //Tracks.CONTENT_URI, null, null, null, null); } return cursorLoader; } @Override public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) { if (cursorLoader.getId() == STARTSTATIONCURSOR_LOADER) { mStartStationSimpleCursorAdapter.swapCursor(cursor); } else { mEndStationSimpleCursorAdapter.swapCursor(cursor); } } @Override public void onLoaderReset(Loader<Cursor> cursorLoader) { if (cursorLoader.getId() == STARTSTATIONCURSOR_LOADER) { mStartStationSimpleCursorAdapter.swapCursor(null); } else { mEndStationSimpleCursorAdapter.swapCursor(null); } } ////////////////////////////////////////////////////////////////////////// //TextWatcher interface for activity @Override public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { } @Override public void onTextChanged(CharSequence charSequence, int start, int count, int after) { } @Override public void afterTextChanged(Editable editable) { if(mStartStationAutoCompleteTextView.enoughToFilter()) { String newFilter = editable.toString(); if (!newFilter.equalsIgnoreCase(mCurStartStationNameFilter)) { mCurStartStationNameFilter = newFilter; getSupportLoaderManager().restartLoader(STARTSTATIONCURSOR_LOADER, null, this); } } if (mEndStationAutoCompleteTextView.enoughToFilter()) { String newFilter = editable.toString(); if (!newFilter.equalsIgnoreCase(mCurEndStationNameFilter)) { mCurEndStationNameFilter = newFilter; getSupportLoaderManager().restartLoader(ENDSTATIONCURSOR_LOADER, null, this); } } } }