diff --git a/MyTracks/res/values-fr/strings.xml b/MyTracks/res/values-fr/strings.xml index f2e634c30..a1c00f1fc 100644 --- a/MyTracks/res/values-fr/strings.xml +++ b/MyTracks/res/values-fr/strings.xml @@ -160,7 +160,7 @@ limitations under the License. Partage Fréquence des annonces - Temps minimum entre eux annonces vocales + Temps minimum entre deux annonces vocales Distance min. entre 2 points Plus petite distance entre deux enregistrements de votre position diff --git a/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java b/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java index 57c107a97..679685edf 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java @@ -39,11 +39,9 @@ public class DialogManager { public static final int DIALOG_IMPORT_PROGRESS = 2; public static final int DIALOG_PROGRESS = 3; public static final int DIALOG_SEND_TO_GOOGLE = 4; - public static final int DIALOG_WRITE_PROGRESS = 5; private ProgressDialog progressDialog; private ProgressDialog importProgressDialog; - private ProgressDialog writeProgressDialog; private SendDialog sendToGoogleDialog; private ChartSettingsDialog chartSettingsDialog; @@ -79,15 +77,6 @@ public class DialogManager { case DIALOG_SEND_TO_GOOGLE: sendToGoogleDialog = new SendDialog(activity); return sendToGoogleDialog; - case DIALOG_WRITE_PROGRESS: - writeProgressDialog = new ProgressDialog(activity); - writeProgressDialog.setIcon(android.R.drawable.ic_dialog_info); - writeProgressDialog.setTitle( - activity.getString(R.string.progress_title)); - writeProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); - writeProgressDialog.setMessage( - activity.getString(R.string.write_progress_message)); - return writeProgressDialog; } return null; } @@ -213,4 +202,19 @@ public class DialogManager { }); } + /** + * The equivalent of {@link #dismissDialogSafely(int)}, but for a specific + * dialog instance. + */ + public static void dismissDialogSafely(Activity activity, final Dialog dialog) { + activity.runOnUiThread(new Runnable() { + public void run() { + try { + dialog.dismiss(); + } catch (IllegalArgumentException e) { + // This will be thrown if this dialog was not shown before. + } + } + }); + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index 2fd020d57..7f9c84766 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -19,7 +19,6 @@ import static com.google.android.apps.mytracks.Constants.TAG; import static com.google.android.apps.mytracks.DialogManager.DIALOG_IMPORT_PROGRESS; import static com.google.android.apps.mytracks.DialogManager.DIALOG_PROGRESS; import static com.google.android.apps.mytracks.DialogManager.DIALOG_SEND_TO_GOOGLE; -import static com.google.android.apps.mytracks.DialogManager.DIALOG_WRITE_PROGRESS; import com.google.android.accounts.Account; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; @@ -1227,17 +1226,15 @@ public class MyTracks extends TabActivity implements OnTouchListener, * @param trackId The id of the track to be sent */ public void saveTrack(long trackId, TrackFileFormat format) { - dialogManager.showDialogSafely(DIALOG_WRITE_PROGRESS); - final TrackWriter writer = - TrackWriterFactory.newWriter(this, providerUtils, trackId, format); - writer.setOnCompletion(new Runnable() { - public void run() { - dialogManager.dismissDialogSafely(DIALOG_WRITE_PROGRESS); - dialogManager.showMessageDialog(writer.getErrorMessage(), - writer.wasSuccess()); + TrackWriter writer = TrackWriterFactory.newWriter(this, providerUtils, trackId, format); + WriteProgressController controller = new WriteProgressController(this, writer); + controller.setOnCompletionListener(new WriteProgressController.OnCompletionListener() { + @Override + public void onComplete(TrackWriter writer) { + dialogManager.showMessageDialog(writer.getErrorMessage(), writer.wasSuccess()); } }); - writer.writeTrackAsync(); + controller.startWrite(); } /** @@ -1248,9 +1245,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, * @param trackId The id of the track to be sent */ public void sendTrack(long trackId, final TrackFileFormat format) { - dialogManager.showDialogSafely(DIALOG_WRITE_PROGRESS); - final TrackWriter writer = - TrackWriterFactory.newWriter(this, providerUtils, trackId, format); + TrackWriter writer = TrackWriterFactory.newWriter(this, providerUtils, trackId, format); FileUtils fileUtils = new FileUtils(); String extension = format.getExtension(); @@ -1258,28 +1253,31 @@ public class MyTracks extends TabActivity implements OnTouchListener, File dir = new File(dirName); writer.setDirectory(dir); - writer.setOnCompletion(new Runnable() { - public void run() { - dialogManager.dismissDialogSafely(DIALOG_WRITE_PROGRESS); + + WriteProgressController controller = new WriteProgressController(this, writer); + controller.setOnCompletionListener(new WriteProgressController.OnCompletionListener() { + @Override + public void onComplete(TrackWriter writer) { if (!writer.wasSuccess()) { - dialogManager.showMessageDialog(writer.getErrorMessage(), - writer.wasSuccess()); - } else { - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_SUBJECT, - getResources().getText(R.string.send_track_subject).toString()); - shareIntent.putExtra(Intent.EXTRA_TEXT, - getResources().getText(R.string.send_track_body_format) - .toString()); - shareIntent.setType(format.getMimeType()); - Uri u = Uri.fromFile(new File(writer.getAbsolutePath())); - shareIntent.putExtra(Intent.EXTRA_STREAM, u); - startActivity(Intent.createChooser(shareIntent, - getResources().getText(R.string.share_track).toString())); + dialogManager.showMessageDialog(writer.getErrorMessage(), writer.wasSuccess()); + return; } + + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, + getResources().getText(R.string.send_track_subject).toString()); + shareIntent.putExtra(Intent.EXTRA_TEXT, + getResources().getText(R.string.send_track_body_format) + .toString()); + shareIntent.setType(format.getMimeType()); + Uri u = Uri.fromFile(new File(writer.getAbsolutePath())); + shareIntent.putExtra(Intent.EXTRA_STREAM, u); + startActivity(Intent.createChooser(shareIntent, + getResources().getText(R.string.share_track).toString())); } }); - writer.writeTrackAsync(); + + controller.startWrite(); } public AccountChooser getAccountChooser() { diff --git a/MyTracks/src/com/google/android/apps/mytracks/WriteProgressController.java b/MyTracks/src/com/google/android/apps/mytracks/WriteProgressController.java new file mode 100644 index 000000000..514f2fc02 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/WriteProgressController.java @@ -0,0 +1,113 @@ +/* + * Copyright 2011 Google Inc. + * + * 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 com.google.android.apps.mytracks; + +import com.google.android.apps.mytracks.io.file.TrackWriter; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.app.ProgressDialog; + +/** + * Given a {@link TrackWriter}, this class manages the process of writing the + * data in the track represented by the writer. This includes the display of + * a progress dialog, updating the progress bar in said dialog, and notifying + * interested parties when the write completes. + * + * @author Matthew Simmons + */ +class WriteProgressController { + /** + * This listener is used to notify interested parties when the write has + * completed. + */ + public interface OnCompletionListener { + /** + * When this method is invoked, the write has completed, and the progress + * dialog has been dismissed. Whether the write succeeded can be + * determined by examining the {@link TrackWriter}. + */ + public void onComplete(TrackWriter writer); + } + + private final Activity activity; + private final TrackWriter writer; + private final ProgressDialog dialog; + + private OnCompletionListener onCompletionListener; + + /** + * @param activity the activity associated with this write + * @param writer the writer which writes the track to disk. Note that this + * class will use the writer's completion listener. If callers are + * interested in notification upon completion of the write, they should + * use {@link #setOnCompletionListener}. + */ + public WriteProgressController(Activity activity, TrackWriter writer) { + this.activity = activity; + this.writer = writer; + + dialog = new ProgressDialog(activity); + dialog.setIcon(android.R.drawable.ic_dialog_info); + dialog.setTitle(activity.getString(R.string.progress_title)); + dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + dialog.setMessage(activity.getString(R.string.write_progress_message)); + dialog.setIndeterminate(true); + + writer.setOnCompletionListener(writerCompleteListener); + writer.setOnWriteListener(writerWriteListener); + } + + /** Set a listener to be invoked when the write completes. */ + public void setOnCompletionListener(OnCompletionListener onCompletionListener) { + this.onCompletionListener = onCompletionListener; + } + + /** Initiate an asynchronous write. */ + public void startWrite() { + DialogManager.showDialogSafely(activity, dialog); + writer.writeTrackAsync(); + } + + /** VisibleForTesting */ + ProgressDialog getDialog() { + return dialog; + } + + private final TrackWriter.OnCompletionListener writerCompleteListener = + new TrackWriter.OnCompletionListener() { + @Override + public void onComplete() { + DialogManager.dismissDialogSafely(activity, dialog); + + if (onCompletionListener != null) { + onCompletionListener.onComplete(writer); + } + } + }; + + private final TrackWriter.OnWriteListener writerWriteListener = + new TrackWriter.OnWriteListener() { + @Override + public void onWrite(int number, int max) { + if (number % 500 == 0) { + dialog.setIndeterminate(false); + dialog.setMax(max); + dialog.setProgress(Math.min(number, max)); + } + } + }; +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriter.java index 4b1e7340d..411ae5061 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriter.java @@ -27,12 +27,31 @@ import java.io.File; * @author Rodrigo Damazio */ public interface TrackWriter { + /** This listener is used to signal completion of track write */ + public interface OnCompletionListener { + public void onComplete(); + } + + /** This listener is used to signal track writes. */ + public interface OnWriteListener { + /** + * This method is invoked whenever a location within a track is written. + * @param number the location number + * @param max the maximum number of locations, for calculation of + * completion percentage + */ + public void onWrite(int number, int max); + } + /** - * Sets a completion callback. - * - * @param onCompletion Runnable that will be executed when finished + * Sets listener to be invoked when the writer has finished. */ - void setOnCompletion(Runnable onCompletion); + void setOnCompletionListener(OnCompletionListener onCompletionListener); + + /** + * Sets a listener to be invoked for each location writer. + */ + void setOnWriteListener(OnWriteListener onWriteListener); /** * Sets a custom directory where the file will be written. diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java index 25b24311e..5e0adc968 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java @@ -45,17 +45,18 @@ import java.io.OutputStream; * @author Sandor Dornbush * @author Rodrigo Damazio */ -public class TrackWriterImpl implements TrackWriter { +class TrackWriterImpl implements TrackWriter { private final Context context; private final MyTracksProviderUtils providerUtils; private final Track track; private final TrackFormatWriter writer; private final FileUtils fileUtils; - private Runnable onCompletion = null; private boolean success = false; private int errorMessage = -1; private File directory = null; private File file = null; + private OnCompletionListener onCompletionListener; + private OnWriteListener onWriteListener; TrackWriterImpl(Context context, MyTracksProviderUtils providerUtils, Track track, TrackFormatWriter writer) { @@ -67,8 +68,13 @@ public class TrackWriterImpl implements TrackWriter { } @Override - public void setOnCompletion(Runnable onCompletion) { - this.onCompletion = onCompletion; + public void setOnCompletionListener(OnCompletionListener onCompletionListener) { + this.onCompletionListener = onCompletionListener; + } + + @Override + public void setOnWriteListener(OnWriteListener onWriteListener) { + this.onWriteListener = onWriteListener; } @Override @@ -121,8 +127,13 @@ public class TrackWriterImpl implements TrackWriter { */ private void finished() { - if (onCompletion != null) { - runOnUiThread(onCompletion); + if (onCompletionListener != null) { + runOnUiThread(new Runnable() { + @Override + public void run() { + onCompletionListener.onComplete(); + } + }); return; } } @@ -289,9 +300,12 @@ public class TrackWriterImpl implements TrackWriter { Log.w(Constants.TAG, "Unable to get any points to write"); return; } + int pointNumber = 0; while (it.hasNext()) { Location loc = it.next(); + pointNumber++; + boolean isValid = LocationUtils.isValidLocation(loc); boolean validSegment = isValid && isLastValid; if (!wroteFirst && validSegment) { @@ -312,6 +326,9 @@ public class TrackWriterImpl implements TrackWriter { // Write the current point writer.writeLocation(loc); + if (onWriteListener != null) { + onWriteListener.onWrite(pointNumber, track.getNumberOfPoints()); + } } else { if (segmentOpen) { writer.writeCloseSegment(); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/DialogManagerTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/DialogManagerTest.java index 92cf13598..1b23a0315 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/DialogManagerTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/DialogManagerTest.java @@ -69,13 +69,4 @@ public class DialogManagerTest assertNotNull(d); assertTrue(d instanceof SendDialog); } - - public void test_onCreateWriteProgress() { - Dialog d = - getActivity().onCreateDialog( - DialogManager.DIALOG_WRITE_PROGRESS, null); - assertNotNull(d); - assertTrue(d instanceof ProgressDialog); - } - } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/WriteProgressControllerTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/WriteProgressControllerTest.java new file mode 100644 index 000000000..2f4349b32 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/WriteProgressControllerTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2011 Google Inc. + * + * 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 com.google.android.apps.mytracks; + +import com.google.android.apps.mytracks.io.file.MockTrackWriter; +import com.google.android.apps.mytracks.io.file.TrackWriter; + +import android.app.ProgressDialog; +import android.test.ActivityInstrumentationTestCase2; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Tests {@link WriteProgressController}. + * + * @author Matthew Simmons + */ +public class WriteProgressControllerTest extends ActivityInstrumentationTestCase2 { + public WriteProgressControllerTest() { + super(MyTracks.class); + } + + private static void assertProgress(ProgressDialog dialog, int expectedProgress, + int expectedMax) { + assertEquals(expectedProgress, dialog.getProgress()); + assertEquals(expectedMax, dialog.getMax()); + } + + public void testSimple() throws Exception { + final AtomicReference dialogRef = new AtomicReference(); + final AtomicReference controllerDoneRef = new AtomicReference(); + final Semaphore writerDone = new Semaphore(0); + + TrackWriter mockWriter = new MockTrackWriter() { + @Override + public void writeTrackAsync() { + onWriteListener.onWrite(1000, 10000); + assertProgress(dialogRef.get(), 1000, 10000); + + onCompletionListener.onComplete(); + writerDone.release(); + } + }; + + WriteProgressController controller = new WriteProgressController( + getActivity(), mockWriter); + controller.setOnCompletionListener(new WriteProgressController.OnCompletionListener() { + @Override + public void onComplete(TrackWriter writer) { + controllerDoneRef.set(true); + } + }); + + dialogRef.set(controller.getDialog()); + + controller.startWrite(); + + // wait for the writer to finish + writerDone.acquire(); + + assertFalse(dialogRef.get().isShowing()); + assertTrue(controllerDoneRef.get()); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/MockTrackWriter.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/MockTrackWriter.java new file mode 100644 index 000000000..c5fbb18a3 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/MockTrackWriter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2011 Google Inc. + * + * 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 com.google.android.apps.mytracks.io.file; + +import java.io.File; + +/** + * A simple, fake {@link TrackWriter} subclass with all methods mocked out. + * Tests are expected to override {@link #writeTrack} and/or + * {@link #writeTrackAsync}, at the very least. + * + * @author Matthew Simmons + * + */ +public class MockTrackWriter implements TrackWriter { + public OnCompletionListener onCompletionListener; + public OnWriteListener onWriteListener; + + @Override + public void setOnCompletionListener(OnCompletionListener onCompletionListener) { + this.onCompletionListener = onCompletionListener; + } + + @Override + public void setOnWriteListener(OnWriteListener onWriteListener) { + this.onWriteListener = onWriteListener; + } + + @Override + public void setDirectory(File directory) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public String getAbsolutePath() { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void writeTrackAsync() { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void writeTrack() { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public boolean wasSuccess() { + return false; + } + + @Override + public int getErrorMessage() { + return 0; + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackWriterTest.java index d066ce06b..1f74dd500 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackWriterTest.java @@ -6,9 +6,9 @@ import static org.easymock.EasyMock.expect; import com.google.android.apps.mytracks.content.MyTracksProvider; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; import com.google.android.apps.mytracks.io.file.TrackFormatWriter; import com.google.android.apps.mytracks.io.file.TrackWriterImpl; import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext; @@ -146,7 +146,7 @@ public class TrackWriterTest extends AndroidTestCase { setContext(context); providerUtils = MyTracksProviderUtils.Factory.get(context); oldProviderUtilsFactory = TestingProviderUtilsFactory.installWithInstance(providerUtils); - + mocksControl = EasyMock.createStrictControl(); formatWriter = mocksControl.createMock(TrackFormatWriter.class); expect(formatWriter.getExtension()).andStubReturn(EXTENSION); @@ -166,12 +166,13 @@ public class TrackWriterTest extends AndroidTestCase { writer = new WriteTracksTrackWriter(getContext(), providerUtils, track, formatWriter, true); - // Expect the completion callback to be run - Runnable completionCallback = mocksControl.createMock(Runnable.class); - completionCallback.run(); + // Expect the completion listener to be run + TrackWriter.OnCompletionListener completionListener + = mocksControl.createMock(TrackWriter.OnCompletionListener.class); + completionListener.onComplete(); mocksControl.replay(); - writer.setOnCompletion(completionCallback); + writer.setOnCompletionListener(completionListener); writer.writeTrack(); assertEquals(1, writeDocumentCalls); @@ -183,19 +184,20 @@ public class TrackWriterTest extends AndroidTestCase { writer = new WriteTracksTrackWriter(getContext(), providerUtils, track, formatWriter, false); - // Expect the completion callback to be run - Runnable completionCallback = mocksControl.createMock(Runnable.class); - completionCallback.run(); + // Expect the completion listener to be run + TrackWriter.OnCompletionListener completionListener + = mocksControl.createMock(TrackWriter.OnCompletionListener.class); + completionListener.onComplete(); mocksControl.replay(); - writer.setOnCompletion(completionCallback); + writer.setOnCompletionListener(completionListener); writer.writeTrack(); assertEquals(0, writeDocumentCalls); assertEquals(1, openFileCalls); mocksControl.verify(); } - + public void testOpenFile() { final ByteArrayOutputStream stream = new ByteArrayOutputStream(); writer = new OpenFileTrackWriter( @@ -260,7 +262,7 @@ public class TrackWriterTest extends AndroidTestCase { // Make location 3 invalid locs[2].setLatitude(100); - + assertEquals(locs.length, providerUtils.bulkInsertTrackPoints(locs, locs.length, TRACK_ID)); for (int i = 0; i < wps.length; ++i) { Waypoint wpt = wps[i]; @@ -315,7 +317,7 @@ public class TrackWriterTest extends AndroidTestCase { return wpt.getId() == wpt2.getId(); } - + @Override public void appendTo(StringBuffer buffer) { buffer.append("wptEq("); @@ -325,7 +327,7 @@ public class TrackWriterTest extends AndroidTestCase { }); return null; } - + private static Location locEq(final Location loc) { EasyMock.reportMatcher(new IArgumentMatcher() { @Override @@ -345,7 +347,7 @@ public class TrackWriterTest extends AndroidTestCase { && loc.getLongitude() == loc2.getLongitude() && loc.getTime() == loc2.getTime(); } - + @Override public void appendTo(StringBuffer buffer) { buffer.append("locEq("); @@ -355,7 +357,7 @@ public class TrackWriterTest extends AndroidTestCase { }); return null; } - + private void fillLocations(Location... locs) { assertTrue(locs.length < 90); for (int i = 0; i < locs.length; i++) {