merge heads

This commit is contained in:
Jimmy Shih
2012-01-28 16:13:12 -08:00
parent 22e9687aff
commit 6bb87e1db9
38 changed files with 444 additions and 783 deletions
-7
View File
@@ -1738,13 +1738,6 @@ limitations under the License.
Screenshot: settings_recording_top.png">
Voice
</string>
<string name="settings_recording_voice_not_available"
translation_description="In the 'Recording' settings, the summary for the 'Voice'
explaining that this option is not available on your Android phone.
Screenshot: settings_recording_voice_not_available.png">
Voice announcement is not available on your Android phone.
</string>
<string name="settings_recording_voice_summary"
translation_description="In the 'Recording' settings, the summary for the 'Voice'
dropdown option explaining that it configures the time or distance between two voice
@@ -26,10 +26,9 @@ import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
@@ -40,6 +39,7 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.location.Location;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
@@ -116,9 +116,7 @@ public class ChartActivity extends Activity implements TrackDataListener {
super.onCreate(savedInstanceState);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.mytracks_charts);
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
@@ -63,8 +63,7 @@ class CheckUnits {
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = useMetricPreferences.edit();
String key = context.getString(R.string.metric_units_key);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(
editor.putBoolean(key, metric));
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor.putBoolean(key, metric));
}
});
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@@ -76,7 +75,7 @@ class CheckUnits {
}
private static void recordCheckPerformed(SharedPreferences preferences) {
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(
preferences.edit().putBoolean(CHECK_UNITS_PREFERENCE_KEY, true));
}
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
@@ -66,7 +66,7 @@ public class DeleteAllTracks extends Handler {
Editor editor = prefs.edit();
// TODO: Go through data manager
editor.putLong(context.getString(R.string.selected_track_key), -1);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
if (done != null) {
Handler h = new Handler();
h.post(done);
@@ -19,7 +19,7 @@ import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.UriUtils;
import com.google.android.maps.mytracks.R;
@@ -110,7 +110,7 @@ public class DeleteTrack extends Activity
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
if (preferences.getLong(selectedKey, -1) == deleteTrackId) {
Editor editor = preferences.edit().putLong(selectedKey, -1);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
}
}
@@ -27,9 +27,8 @@ import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.SaveActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.SendType;
import com.google.android.apps.mytracks.io.sendtogoogle.UploadServiceChooserActivity;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.GeoRect;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.maps.GeoPoint;
@@ -44,6 +43,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.provider.Settings;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
@@ -153,9 +153,7 @@ public class MapActivity extends com.google.android.maps.MapActivity
super.onCreate(bundle);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
// We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -28,8 +28,8 @@ import com.google.android.apps.mytracks.io.file.TempFileCleaner;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.EulaUtil;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.apps.mytracks.util.UriUtils;
@@ -48,6 +48,7 @@ import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
@@ -141,9 +142,8 @@ public class MyTracks extends TabActivity implements OnTouchListener {
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "MyTracks.onCreate");
super.onCreate(savedInstanceState);
ApiFeatures apiFeatures = ApiFeatures.getInstance();
if (!SystemUtils.isRelease(this)) {
apiFeatures.getApiAdapter().enableStrictMode();
ApiAdapterFactory.getApiAdapter().enableStrictMode();
}
tracker = GoogleAnalyticsTracker.getInstance();
@@ -159,10 +159,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
menuManager = new MenuManager(this);
serviceConnection = new TrackRecordingServiceConnection(this, serviceBindCallback);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
setVolumeControlStream(volumeStream);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
// We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -20,13 +20,13 @@ import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.io.backup.BackupActivityHelper;
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
import com.google.android.apps.mytracks.services.sensors.ant.AntUtils;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.BluetoothDeviceUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.bluetooth.BluetoothAdapter;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
@@ -44,6 +44,7 @@ import android.preference.PreferenceCategory;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.provider.Settings;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.widget.Toast;
@@ -102,10 +103,7 @@ public class SettingsActivity extends PreferenceActivity {
super.onCreate(icicle);
// The volume we want to control is the Text-To-Speech volume
ApiFeatures apiFeatures = ApiFeatures.getInstance();
int volumeStream =
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
setVolumeControlStream(volumeStream);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
// Tell it where to read/write preferences
PreferenceManager preferenceManager = getPreferenceManager();
@@ -113,23 +111,13 @@ public class SettingsActivity extends PreferenceActivity {
preferenceManager.setSharedPreferencesMode(0);
// Set up automatic preferences backup
backupListener = apiFeatures.getApiAdapter().getBackupPreferencesListener(this);
backupListener = ApiAdapterFactory.getApiAdapter().getBackupPreferencesListener(this);
preferences = preferenceManager.getSharedPreferences();
preferences.registerOnSharedPreferenceChangeListener(backupListener);
// Load the preferences to be displayed
addPreferencesFromResource(R.xml.preferences);
// Disable voice announcement if not available
if (!apiFeatures.hasTextToSpeech()) {
IntegerListPreference announcementFrequency =
(IntegerListPreference) findPreference(
getString(R.string.announcement_frequency_key));
announcementFrequency.setEnabled(false);
announcementFrequency.setValue(TASK_FREQUENCY_OFF);
announcementFrequency.setSummary(R.string.settings_recording_voice_not_available);
}
setRecordingIntervalOptions();
setAutoResumeTimeoutOptions();
@@ -557,20 +545,17 @@ public class SettingsActivity extends PreferenceActivity {
* Configures preference actions related to bluetooth.
*/
private void configureBluetoothPreferences() {
if (BluetoothDeviceUtils.isBluetoothMethodSupported()) {
// Populate the list of bluetooth devices
populateBluetoothDeviceList();
// Make the pair devices preference go to the system preferences
findPreference(getString(R.string.bluetooth_pairing_key))
.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
startActivity(settingsIntent);
return false;
}
});
}
// Populate the list of bluetooth devices
populateBluetoothDeviceList();
// Make the pair devices preference go to the system preferences
findPreference(getString(R.string.bluetooth_pairing_key)).setOnPreferenceClickListener(
new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
startActivity(settingsIntent);
return false;
}
});
}
/**
@@ -582,7 +567,10 @@ public class SettingsActivity extends PreferenceActivity {
List<String> entryValues = new ArrayList<String>();
// The actual devices
BluetoothDeviceUtils.getInstance().populateDeviceLists(entries, entryValues);
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter != null) {
BluetoothDeviceUtils.populateDeviceLists(bluetoothAdapter, entries, entryValues);
}
CharSequence[] entriesArray = entries.toArray(new CharSequence[entries.size()]);
CharSequence[] entryValuesArray = entryValues.toArray(new CharSequence[entryValues.size()]);
@@ -685,7 +673,7 @@ public class SettingsActivity extends PreferenceActivity {
SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
Editor editor = prefs.edit();
editor.putString(getString(id), metricspeed);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
/**
@@ -23,8 +23,7 @@ import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
@@ -32,6 +31,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Window;
@@ -105,9 +105,7 @@ public class StatsActivity extends Activity implements TrackDataListener {
utils = new StatsUtilities(this);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
// We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -16,7 +16,6 @@
package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R;
import android.content.ContentProvider;
@@ -417,12 +416,9 @@ public class MyTracksProvider extends ContentProvider {
} else {
throw new IllegalArgumentException("Unknown URL " + url);
}
if (ApiFeatures.getInstance().canReuseSQLiteQueryBuilder()) {
Log.i(Constants.TAG,
"Build query: " + qb.buildQuery(projection, selection, selectionArgs,
null, null, sortOrder, null));
}
Log.i(Constants.TAG, "Build query: "
+ qb.buildQuery(projection, selection, selectionArgs, null, null, sortOrder, null));
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null,
sortOrder);
c.setNotificationUri(getContext().getContentResolver(), url);
@@ -28,7 +28,7 @@ import com.google.android.apps.mytracks.content.MyTracksProviderUtils.DoubleBuff
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.TrackDataListener.ProviderState;
import com.google.android.apps.mytracks.content.TrackDataListeners.ListenerRegistration;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.maps.mytracks.R;
@@ -399,7 +399,7 @@ public class TrackDataHub {
// Save the selection to memory and flush.
selectedTrackId = trackId;
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(
preferences.edit().putLong(SELECTED_TRACK_KEY, trackId));
// Force it to reload data from the beginning.
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
@@ -106,7 +106,7 @@ class PreferenceBackupHelper {
byte typeId = reader.readByte();
readAndSetPreference(name, typeId, reader, editor);
}
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
/**
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.io.sendtogoogle;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import android.accounts.Account;
@@ -153,7 +153,7 @@ public class AccountChooserActivity extends Activity {
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
Editor editor = prefs.edit();
editor.putString(getString(R.string.preferred_account_key), account.name);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
Intent intent = new Intent();
intent.putExtra(ACCOUNT, account);
@@ -7,7 +7,7 @@ import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
@@ -108,7 +108,7 @@ public class SendFusionTablesAsyncTask extends AsyncTask<Void, Integer, Boolean>
context = activity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
HttpTransport transport = ApiFeatures.getInstance().getApiAdapter().getHttpTransport();
HttpTransport transport = ApiAdapterFactory.getApiAdapter().getHttpTransport();
httpRequestFactory = transport.createRequestFactory(new MethodOverride());
canRetry = true;
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.io.sendtogoogle;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
@@ -218,7 +218,7 @@ public class UploadServiceChooserActivity extends Activity {
editor.putBoolean(getString(R.string.send_to_fusion_tables_key), sendFusionTables());
editor.putBoolean(getString(R.string.send_to_docs_key), sendDocs());
}
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
/**
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import android.content.Context;
@@ -174,19 +174,19 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener {
public void setAutoResumeTrackCurrentRetry(int retryAttempts) {
Editor editor = sharedPreferences.edit();
editor.putInt(autoResumeTrackCurrentRetryKey, retryAttempts);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
public void setRecordingTrack(long id) {
Editor editor = sharedPreferences.edit();
editor.putLong(recordingTrackKey, id);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
public void setSelectedTrack(long id) {
Editor editor = sharedPreferences.edit();
editor.putLong(selectedTrackKey, id);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
public void shutdown() {
@@ -36,14 +36,12 @@ import com.google.android.apps.mytracks.services.tasks.SplitTask;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiLevelAdapter;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentValues;
@@ -80,7 +78,6 @@ public class TrackRecordingService extends Service {
static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;
private NotificationManager notificationManager;
private LocationManager locationManager;
private WakeLock wakeLock;
@@ -243,8 +240,6 @@ public class TrackRecordingService extends Service {
super.onCreate();
Log.d(TAG, "TrackRecordingService.onCreate");
providerUtils = MyTracksProviderUtils.Factory.get(this);
notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
setUpTaskExecutors();
@@ -369,7 +364,6 @@ public class TrackRecordingService extends Service {
// Make sure we have no indirect references to this service.
locationManager = null;
notificationManager = null;
providerUtils = null;
binder.detachFromService();
binder = null;
@@ -485,8 +479,6 @@ public class TrackRecordingService extends Service {
* Shows the notification message and icon in the notification bar.
*/
private void showNotification() {
final ApiLevelAdapter apiLevelAdapter =
ApiFeatures.getInstance().getApiAdapter();
if (isRecording) {
Notification notification = new Notification(
R.drawable.arrow_320, null /* tickerText */,
@@ -497,16 +489,24 @@ public class TrackRecordingService extends Service {
notification.setLatestEventInfo(this, getString(R.string.my_tracks_app_name),
getString(R.string.track_record_notification), contentIntent);
notification.flags += Notification.FLAG_NO_CLEAR;
apiLevelAdapter.startForeground(this, notificationManager, 1,
notification);
startForegroundService(notification);
} else {
apiLevelAdapter.stopForeground(this, notificationManager, 1);
stopForegroundService();
}
}
@VisibleForTesting
protected void startForegroundService(Notification notification) {
startForeground(1, notification);
}
@VisibleForTesting
protected void stopForegroundService() {
stopForeground(true);
}
private void setUpTaskExecutors() {
announcementExecutor = new PeriodicTaskExecutor(
this, new StatusAnnouncerFactory(ApiFeatures.getInstance()));
announcementExecutor = new PeriodicTaskExecutor(this, new StatusAnnouncerFactory());
splitExecutor = new PeriodicTaskExecutor(this, new SplitTask.Factory());
}
@@ -18,7 +18,7 @@ package com.google.android.apps.mytracks.services.sensors;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@@ -31,8 +31,6 @@ import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.UUID;
/**
@@ -44,8 +42,9 @@ import java.util.UUID;
* @author Sandor Dornbush
*/
public class BluetoothConnectionManager {
// Unique UUID for this application
private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
// Unique Bluetooth UUID for My Tracks
public static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private MessageParser parser;
@@ -254,41 +253,13 @@ public class BluetoothConnectionManager {
// Get a BluetoothSocket for a connection with the
// given BluetoothDevice
try {
tmp = getSocket();
tmp = ApiAdapterFactory.getApiAdapter().getBluetoothSocket(device);
} catch (IOException e) {
Log.e(Constants.TAG, "create() failed", e);
}
socket = tmp;
}
private BluetoothSocket getSocket() throws IOException {
if (ApiFeatures.getInstance().hasBluetoothDeviceCreateInsecureRfcommSocketToServiceRecord()) {
try {
return device.createInsecureRfcommSocketToServiceRecord(SPP_UUID);
} catch (IOException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
}
} else {
try {
Class<? extends BluetoothDevice> c = device.getClass();
Method insecure = c.getMethod("createInsecureRfcommSocket", Integer.class);
insecure.setAccessible(true);
return (BluetoothSocket) insecure.invoke(device, 1);
} catch (SecurityException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (NoSuchMethodException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (IllegalArgumentException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (IllegalAccessException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (InvocationTargetException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
}
}
return device.createRfcommSocketToServiceRecord(SPP_UUID);
}
@Override
public void run() {
Log.d(Constants.TAG, "BEGIN mConnectThread");
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.services.sensors;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import java.util.Arrays;
@@ -62,8 +62,7 @@ public class ZephyrMessageParser implements MessageParser {
// 0x1A00316550003162 produces erroneous values for Cadence and needs
// a workaround based on the stride counter.
// Firmware values range from field 3 to 10 (inclusive) of the byte buffer.
byte[] hardwareFirmwareId = ApiFeatures.getInstance().getApiAdapter()
.copyByteArray(buffer, 3, 11);
byte[] hardwareFirmwareId = ApiAdapterFactory.getApiAdapter().copyByteArray(buffer, 3, 11);
Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder();
if (Arrays.equals(hardwareFirmwareId, CADENCE_BUG_FW_ID)) {
@@ -22,7 +22,7 @@ import com.dsi.ant.AntMesg;
import com.dsi.ant.exception.AntInterfaceException;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import android.content.Context;
@@ -135,7 +135,7 @@ public class AntDirectSensorManager extends AntSensorManager {
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(context.getString(R.string.ant_heart_rate_sensor_id_key), deviceNumberHRM);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
private void handleMessageResponse(byte[] rawMessage) {
@@ -22,7 +22,7 @@ import com.dsi.ant.AntMesg;
import com.dsi.ant.exception.AntInterfaceException;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
@@ -199,7 +199,7 @@ public class AntSrmBridgeSensorManager extends AntSensorManager {
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(context.getString(R.string.ant_srm_bridge_sensor_id_key), deviceNumber);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
private void handleMessageResponse(byte[] rawMessage) {
@@ -15,10 +15,9 @@
*/
package com.google.android.apps.mytracks.services.tasks;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import android.content.Context;
import android.media.AudioManager;
/**
* Factory which wraps construction and setup of text-to-speech announcements in
@@ -28,30 +27,11 @@ import android.media.AudioManager;
*/
public class StatusAnnouncerFactory implements PeriodicTaskFactory {
private final boolean hasTts;
public StatusAnnouncerFactory(ApiFeatures apiFeatures) {
this.hasTts = apiFeatures.hasTextToSpeech();
public StatusAnnouncerFactory() {
}
@Override
public PeriodicTask create(Context context) {
if (hasTts) {
return ApiFeatures.getInstance().getApiAdapter().getStatusAnnouncerTask(context);
} else {
return null;
}
}
/**
* Returns the appropriate volume stream for controlling announcement
* volume.
*/
public int getVolumeStream() {
if (hasTts) {
return StatusAnnouncerTask.getVolumeStream();
} else {
return AudioManager.USE_DEFAULT_STREAM_TYPE;
}
return ApiAdapterFactory.getApiAdapter().getStatusAnnouncerTask(context);
}
}
@@ -331,13 +331,6 @@ public class StatusAnnouncerTask implements PeriodicTask {
}
}
/**
* Returns the volume stream to use for controlling announcement volume.
*/
public static int getVolumeStream() {
return TextToSpeech.Engine.DEFAULT_STREAM;
}
/**
* Gets a string to announce the time.
*
@@ -0,0 +1,44 @@
/*
* Copyright 2012 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.util;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.services.sensors.BluetoothConnectionManager;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.util.Log;
import java.io.IOException;
/**
* API level 10 specific implementation of the {@link ApiAdapter}.
*
* @author Jimmy Shih
*/
public class Api10Adapter extends Api9Adapter {
@Override
public BluetoothSocket getBluetoothSocket(BluetoothDevice bluetoothDevice) throws IOException {
try {
return bluetoothDevice.createInsecureRfcommSocketToServiceRecord(
BluetoothConnectionManager.SPP_UUID);
} catch (IOException e) {
Log.d(Constants.TAG, "Unable to create insecure connection", e);
}
return bluetoothDevice.createRfcommSocketToServiceRecord(BluetoothConnectionManager.SPP_UUID);
};
}
@@ -15,50 +15,32 @@
*/
package com.google.android.apps.mytracks.util;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
import com.google.android.apps.mytracks.services.sensors.BluetoothConnectionManager;
import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.apache.ApacheHttpTransport;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.util.Log;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* API level 3 specific implementation of the {@link ApiLevelAdapter}.
* API level 7 specific implementation of the {@link ApiAdapter}.
*
* @author Bartlomiej Niechwiej
*/
public class ApiLevel3Adapter implements ApiLevelAdapter {
public class Api7Adapter implements ApiAdapter {
@Override
public void startForeground(Service service,
NotificationManager notificationManager, int id,
Notification notification) {
setServiceForeground(service, true);
notificationManager.notify(id, notification);
}
@Override
public void stopForeground(Service service,
NotificationManager notificationManager, int id) {
setServiceForeground(service, false);
if (id != -1) {
notificationManager.cancel(id);
}
}
@Override
public PeriodicTask getStatusAnnouncerTask(Context context) {
return new StatusAnnouncerTask(context);
@@ -74,24 +56,6 @@ public class ApiLevel3Adapter implements ApiLevelAdapter {
};
}
private void setServiceForeground(Service service, boolean foreground) {
// setForeground has been completely removed in API level 11, so we use reflection.
try {
Method setForegroundMethod = Service.class.getMethod("setForeground", boolean.class);
setForegroundMethod.invoke(service, foreground);
} catch (SecurityException e) {
Log.e(TAG, "Unable to set service foreground state", e);
} catch (NoSuchMethodException e) {
Log.e(TAG, "Unable to set service foreground state", e);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Unable to set service foreground state", e);
} catch (IllegalAccessException e) {
Log.e(TAG, "Unable to set service foreground state", e);
} catch (InvocationTargetException e) {
Log.e(TAG, "Unable to set service foreground state", e);
}
}
@Override
public void applyPreferenceChanges(Editor editor) {
editor.commit();
@@ -114,4 +78,25 @@ public class ApiLevel3Adapter implements ApiLevelAdapter {
public HttpTransport getHttpTransport() {
return new ApacheHttpTransport();
}
@Override
public BluetoothSocket getBluetoothSocket(BluetoothDevice bluetoothDevice) throws IOException {
try {
Class<? extends BluetoothDevice> c = bluetoothDevice.getClass();
Method insecure = c.getMethod("createInsecureRfcommSocket", Integer.class);
insecure.setAccessible(true);
return (BluetoothSocket) insecure.invoke(bluetoothDevice, 1);
} catch (SecurityException e) {
Log.d(Constants.TAG, "Unable to create insecure connection", e);
} catch (NoSuchMethodException e) {
Log.d(Constants.TAG, "Unable to create insecure connection", e);
} catch (IllegalArgumentException e) {
Log.d(Constants.TAG, "Unable to create insecure connection", e);
} catch (IllegalAccessException e) {
Log.d(Constants.TAG, "Unable to create insecure connection", e);
} catch (InvocationTargetException e) {
Log.d(Constants.TAG, "Unable to create insecure connection", e);
}
return bluetoothDevice.createRfcommSocketToServiceRecord(BluetoothConnectionManager.SPP_UUID);
}
}
@@ -1,3 +1,18 @@
/*
* Copyright 2012 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.util;
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
@@ -8,11 +23,11 @@ import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
import android.content.Context;
/**
* API level 8 specific implementation of the {@link ApiLevelAdapter}.
* API level 8 specific implementation of the {@link ApiAdapter}.
*
* @author Jimmy Shih
*/
public class ApiLevel8Adapter extends ApiLevel5Adapter {
public class Api8Adapter extends Api7Adapter {
@Override
public PeriodicTask getStatusAnnouncerTask(Context context) {
@@ -1,3 +1,18 @@
/*
* Copyright 2012 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.util;
import com.google.android.apps.mytracks.Constants;
@@ -11,11 +26,11 @@ import android.util.Log;
import java.util.Arrays;
/**
* API level 9 specific implementation of the {@link ApiLevelAdapter}.
* API level 9 specific implementation of the {@link ApiAdapter}.
*
* @author Rodrigo Damazio
*/
public class ApiLevel9Adapter extends ApiLevel8Adapter {
public class Api9Adapter extends Api8Adapter {
@Override
public void applyPreferenceChanges(Editor editor) {
@@ -0,0 +1,101 @@
/*
* Copyright 2010 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.util;
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
import com.google.api.client.http.HttpTransport;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.content.SharedPreferences;
import java.io.IOException;
/**
* A set of methods that may be implemented differently depending on the Android
* API level.
*
* @author Bartlomiej Niechwiej
*/
public interface ApiAdapter {
/**
* Gets a status announcer task.
* <p>
* Due to changes in API level 8.
*
* @param context the context
*/
public PeriodicTask getStatusAnnouncerTask(Context context);
/**
* Gets a {@link BackupPreferencesListener}.
* <p>
* Due to changes in API level 8.
*
* @param context the context
*/
public BackupPreferencesListener getBackupPreferencesListener(Context context);
/**
* Applies all the changes done to a given preferences editor. Changes may or
* may not be applied immediately.
* <p>
* Due to changes in API level 9.
*
* @param editor the editor
*/
public void applyPreferenceChanges(SharedPreferences.Editor editor);
/**
* Enables strict mode where supported, only if this is a development build.
* <p>
* Due to changes in API level 9.
*/
public void enableStrictMode();
/**
* Copies elements from an input byte array into a new byte array, from
* indexes start (inclusive) to end (exclusive). The end index must be less
* than or equal to the input length.
* <p>
* Due to changes in API level 9.
*
* @param input the input byte array
* @param start the start index
* @param end the end index
* @return a new array containing elements from the input byte array.
*/
public byte[] copyByteArray(byte[] input, int start, int end);
/**
* Gets a {@link HttpTransport}.
* <p>
* Due to changes in API level 9.
*/
public HttpTransport getHttpTransport();
/**
* Gets a {@link BluetoothSocket}.
* <p>
* Due to changes in API level 10.
*
* @param bluetoothDevice
*/
public BluetoothSocket getBluetoothSocket(BluetoothDevice bluetoothDevice) throws IOException;
}
@@ -0,0 +1,50 @@
/*
* Copyright 2010 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.util;
import android.os.Build;
/**
* A factory to get the {@link ApiAdapter} for the current device.
*
* @author Rodrigo Damazio
*/
public class ApiAdapterFactory {
private static ApiAdapter apiAdapter;
/**
* Gets the {@link ApiAdapter} for the current device.
*/
public static ApiAdapter getApiAdapter() {
if (apiAdapter == null) {
if (Build.VERSION.SDK_INT >= 10) {
apiAdapter = new Api10Adapter();
return apiAdapter;
} else if (Build.VERSION.SDK_INT >= 9) {
apiAdapter = new Api9Adapter();
return apiAdapter;
} else if (Build.VERSION.SDK_INT >= 8) {
apiAdapter = new Api8Adapter();
return apiAdapter;
} else {
apiAdapter = new Api7Adapter();
return apiAdapter;
}
}
return apiAdapter;
}
}
@@ -1,122 +0,0 @@
/*
* Copyright 2010 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.util;
import com.google.android.apps.mytracks.Constants;
import android.os.Build;
import android.util.Log;
/**
* Utility class for determining if newer-API features are available on the
* current device.
*
* @author Rodrigo Damazio
*/
public class ApiFeatures {
/**
* The API level of the Android version we are being run under.
*/
private static final int ANDROID_API_LEVEL = Integer.parseInt(
Build.VERSION.SDK);
private static ApiFeatures instance;
/**
* The API level adapter for the Android version we are being run under.
*/
private ApiLevelAdapter apiLevelAdapter;
/**
* Returns the singleton instance of this class.
*/
public static ApiFeatures getInstance() {
if (instance == null) {
instance = new ApiFeatures();
}
return instance;
}
/**
* Injects a specific singleton instance, to be used for unit tests.
*/
@SuppressWarnings("hiding")
public static void injectInstance(ApiFeatures instance) {
ApiFeatures.instance = instance;
}
/**
* Allow subclasses for mocking, but no direct instantiation.
*/
protected ApiFeatures() {
// It is safe to import unsupported classes as long as we only actually
// load the class when supported.
if (getApiLevel() >= 9) {
apiLevelAdapter = new ApiLevel9Adapter();
} else if (getApiLevel() >= 8) {
apiLevelAdapter = new ApiLevel8Adapter();
} else if (getApiLevel() >= 5) {
apiLevelAdapter = new ApiLevel5Adapter();
} else {
apiLevelAdapter = new ApiLevel3Adapter();
}
Log.i(Constants.TAG, "Using API level adapter " + apiLevelAdapter.getClass());
}
public ApiLevelAdapter getApiAdapter() {
return apiLevelAdapter;
}
// API Level 4 Changes
/**
* Returns whether text-to-speech is available.
*/
public boolean hasTextToSpeech() {
return getApiLevel() >= 4;
}
// API Level 5 Changes
/**
* There's a bug (#1587) in Cupcake and Donut which prevents you from
* using a SQLiteQueryBuilder twice. That is, if you call buildQuery
* on a given instance (to log the statement for debugging), and then
* call query on the same instance to make it actually do the query,
* it'll regenerate the query for the second call, and will screw it
* up. Specifically, it'll add extra parens which don't belong.
*/
public boolean canReuseSQLiteQueryBuilder() {
return getApiLevel() >= 5;
}
// API Level 10 changes
/**
* Returns true if BluetoothDevice.createInsecureRfcommSocketToServiceRecord
* is available.
*/
public boolean hasBluetoothDeviceCreateInsecureRfcommSocketToServiceRecord() {
return getApiLevel() >= 10;
}
// Visible for testing.
protected int getApiLevel() {
return ANDROID_API_LEVEL;
}
}
@@ -1,113 +0,0 @@
/*
* Copyright 2010 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.util;
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
import com.google.api.client.http.HttpTransport;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.SharedPreferences;
/**
* A set of methods that may be implemented differently depending on the Android API level.
*
* @author Bartlomiej Niechwiej
*/
public interface ApiLevelAdapter {
/**
* Puts the specified service into foreground.
*
* Due to changes in API level 5.
*
* @param service the service to be put in foreground.
* @param notificationManager the notification manager used to post the given
* notification.
* @param id the ID of the notification, unique within the application.
* @param notification the notification to post.
*/
void startForeground(Service service, NotificationManager notificationManager,
int id, Notification notification);
/**
* Puts the given service into background.
*
* Due to changes in API level 5.
*
* @param service the service to put into background.
* @param notificationManager the notification manager to user when removing
* notifications.
* @param id the ID of the notification to be remove, or -1 if the
* notification shouldn't be removed.
*/
void stopForeground(Service service, NotificationManager notificationManager,
int id);
/**
* Gets a status announcer task.
*
* Due to changes in API level 8.
*/
PeriodicTask getStatusAnnouncerTask(Context context);
/**
* Gets a {@link BackupPreferencesListener}.
*
* Due to changes in API level 8.
*/
BackupPreferencesListener getBackupPreferencesListener(Context context);
/**
* Applies all changes done to the given preferences editor.
* Changes may or may not be applied immediately.
*
* Due to changes in API level 9.
*/
void applyPreferenceChanges(SharedPreferences.Editor editor);
/**
* Enables strict mode where supported, only if this is a development build.
*
* Due to changes in API level 9.
*/
void enableStrictMode();
/**
* Copies elements from the input byte array into a new byte array, from
* indexes start (inclusive) to end (exclusive). The end index must be less
* than or equal to input.length.
*
* Due to changes in API level 9.
*
* @param input the input byte array
* @param start the start index
* @param end the end index
* @return a new array containing elements from the input byte array
*/
byte[] copyByteArray(byte[] input, int start, int end);
/**
* Gets a {@link HttpTransport}.
*
* Due to changes in API level 9.
*/
HttpTransport getHttpTransport();
}
@@ -15,186 +15,49 @@
*/
package com.google.android.apps.mytracks.util;
import com.google.android.apps.mytracks.Constants;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.os.Build;
import android.util.Log;
import java.util.List;
import java.util.Set;
/**
* Utilities for dealing with bluetooth devices.
* This can be used safely even in systems that don't support bluetooth,
* in which case a dummy implementation will be used.
*
* @author Rodrigo Damazio
*/
public abstract class BluetoothDeviceUtils {
public static final String ANY_DEVICE = "any";
private static BluetoothDeviceUtils instance;
public class BluetoothDeviceUtils {
private BluetoothDeviceUtils() {}
/**
* Dummy implementation, for systems that don't support bluetooth.
*/
private static class DummyImpl extends BluetoothDeviceUtils {
@Override
public void populateDeviceLists(List<String> deviceNames, List<String> deviceAddresses) {
// Do nothing - no devices to add
}
@Override
public BluetoothDevice findDeviceMatching(String targetDeviceAddress) {
return null;
}
}
/**
* Real implementation, for systems that DO support bluetooth.
*/
private static class RealImpl extends BluetoothDeviceUtils {
private final BluetoothAdapter bluetoothAdapter;
public RealImpl() {
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
throw new IllegalStateException("Unable to get bluetooth adapter");
}
}
@Override
public void populateDeviceLists(List<String> deviceNames, List<String> deviceAddresses) {
ensureNotDiscovering();
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
for (BluetoothDevice device : pairedDevices) {
BluetoothClass bluetoothClass = device.getBluetoothClass();
if (bluetoothClass != null) {
// Not really sure what we want, but I know what we don't want.
switch(bluetoothClass.getMajorDeviceClass()) {
case BluetoothClass.Device.Major.COMPUTER:
case BluetoothClass.Device.Major.PHONE:
break;
default:
deviceAddresses.add(device.getAddress());
deviceNames.add(device.getName());
}
}
}
}
@Override
public BluetoothDevice findDeviceMatching(String targetDeviceAddress) {
if (targetDeviceAddress.equals(ANY_DEVICE)) {
return findAnyDevice();
} else {
return findDeviceByAddress(targetDeviceAddress);
}
}
/**
* Finds and returns the first suitable bluetooth sensor.
*/
private BluetoothDevice findAnyDevice() {
ensureNotDiscovering();
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
for (BluetoothDevice device : pairedDevices) {
// Look for the first paired computer device
if (isSuitableDevice(device)) {
return device;
}
}
return null;
}
/**
* Finds and returns a device with the given address, or null if it's not
* a suitable sensor.
*/
private BluetoothDevice findDeviceByAddress(String targetDeviceAddress) {
ensureNotDiscovering();
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(targetDeviceAddress);
if (isSuitableDevice(device)) {
return device;
}
return null;
}
/**
* Ensures the bluetooth adapter is not in discovery mode.
*/
private void ensureNotDiscovering() {
// If it's in discovery mode, cancel that for now.
bluetoothAdapter.cancelDiscovery();
}
/**
* Checks whether the given device is a suitable sensor.
*
* @param device the device to check
* @return true if it's suitable, false otherwise
*/
private boolean isSuitableDevice(BluetoothDevice device) {
// Check that the device is bonded
if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
return false;
}
return true;
}
}
/**
* Returns the proper (singleton) instance of this class.
*/
public static BluetoothDeviceUtils getInstance() {
if (instance == null) {
if (!isBluetoothMethodSupported()) {
Log.d(Constants.TAG, "Using dummy bluetooth utils");
instance = new DummyImpl();
} else {
Log.d(Constants.TAG, "Using real bluetooth utils");
try {
instance = new RealImpl();
} catch (IllegalStateException ise) {
Log.w(Constants.TAG, "Oops, I mean, using dummy bluetooth utils", ise);
instance = new DummyImpl();
}
}
}
return instance;
}
/**
* Populates the given lists with the names and addresses of all suitable
* Populates the device names and the device addresses with all the suitable
* bluetooth devices.
*
* @param deviceNames the list to populate with user-visible names
* @param deviceAddresses the list to populate with device addresses
* @param bluetoothAdapter the bluetooth adapter
* @param deviceNames list of device names
* @param deviceAddresses list of device addresses
*/
public abstract void populateDeviceLists(List<String> deviceNames, List<String> deviceAddresses);
public static void populateDeviceLists(
BluetoothAdapter bluetoothAdapter, List<String> deviceNames, List<String> deviceAddresses) {
// Ensure the bluetooth adapter is not in discovery mode.
bluetoothAdapter.cancelDiscovery();
/**
* Finds the bluetooth device with the given address.
*
* @param targetDeviceAddress the address of the device, or
* {@link #ANY_DEVICE} for using the first suitable device
* @return the device's descriptor, or null if not found
*/
public abstract BluetoothDevice findDeviceMatching(String targetDeviceAddress);
/**
* @return whether the bluetooth method is supported on this device
*/
public static boolean isBluetoothMethodSupported() {
return Integer.parseInt(Build.VERSION.SDK) >= 5;
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
for (BluetoothDevice device : pairedDevices) {
BluetoothClass bluetoothClass = device.getBluetoothClass();
if (bluetoothClass != null) {
// Not really sure what we want, but I know what we don't want.
switch (bluetoothClass.getMajorDeviceClass()) {
case BluetoothClass.Device.Major.COMPUTER:
case BluetoothClass.Device.Major.PHONE:
break;
default:
deviceAddresses.add(device.getAddress());
deviceNames.add(device.getName());
}
}
}
}
}
@@ -45,7 +45,7 @@ public class EulaUtil {
EULA_PREFERENCE_FILE, Context.MODE_PRIVATE);
Editor editor = preferences.edit();
editor.putBoolean(EULA_PREFERENCE_KEY, true);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
public static String getEulaMessage(Context context) {
@@ -0,0 +1,64 @@
// Copyright 2012 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.services;
import android.app.Notification;
import android.app.Service;
import android.test.ServiceTestCase;
import android.util.Log;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* A {@link TrackRecordingService} that can be used with {@link ServiceTestCase}.
* {@link ServiceTestCase} throws a null pointer exception when the service
* calls {@link Service#startForeground(int, android.app.Notification)} and
* {@link Service#stopForeground(boolean)}.
* <p>
* See http://code.google.com/p/android/issues/detail?id=12122
* <p>
* Wrap these two methods in wrappers and override them.
*
* @author Jimmy Shih
*/
public class TestRecordingService extends TrackRecordingService {
private static final String TAG = TestRecordingService.class.getSimpleName();
@Override
protected void startForegroundService(Notification notification) {
try {
Method setForegroundMethod = Service.class.getMethod("setForeground", boolean.class);
setForegroundMethod.invoke(this, true);
} catch (SecurityException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (NoSuchMethodException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (IllegalAccessException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (InvocationTargetException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
}
}
@Override
protected void stopForegroundService() {
try {
Method setForegroundMethod = Service.class.getMethod("setForeground", boolean.class);
setForegroundMethod.invoke(this, false);
} catch (SecurityException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (NoSuchMethodException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (IllegalAccessException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (InvocationTargetException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
}
}
}
@@ -24,7 +24,6 @@ import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R;
import android.content.BroadcastReceiver;
@@ -57,8 +56,7 @@ import java.util.List;
* is called, which prevents from testing multiple scenarios in a single
* test (see runFunctionTest for more details).
*/
public class TrackRecordingServiceTest
extends ServiceTestCase<TrackRecordingService> {
public class TrackRecordingServiceTest extends ServiceTestCase<TestRecordingService> {
private Context context;
private MyTracksProviderUtils providerUtils;
@@ -74,7 +72,7 @@ public class TrackRecordingServiceTest
private Intent serviceIntent;
public TrackRecordingServiceTest() {
super(TrackRecordingService.class);
super(TestRecordingService.class);
}
/**
@@ -96,19 +94,6 @@ public class TrackRecordingServiceTest
}
}
/**
* A mock class that forces API level < 5 to make sure we can workaround a bug
* in ServiceTestCase (throwing a NPE).
* See http://code.google.com/p/android/issues/detail?id=12122 for more
* details.
*/
private static class MockApiFeatures extends ApiFeatures {
@Override
protected int getApiLevel() {
return 4;
}
}
@Override
protected IBinder bindService(Intent intent) {
if (getService() != null) {
@@ -138,8 +123,6 @@ public class TrackRecordingServiceTest
protected void setUp() throws Exception {
super.setUp();
ApiFeatures.injectInstance(new MockApiFeatures());
MockContentResolver mockContentResolver = new MockContentResolver();
RenamingDelegatingContext targetContext = new RenamingDelegatingContext(
getContext(), getContext(), "test.");
@@ -15,13 +15,6 @@
*/
package com.google.android.apps.mytracks.services.tasks;
import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.android.apps.mytracks.util.ApiFeatures;
import android.media.AudioManager;
import android.speech.tts.TextToSpeech;
import android.test.AndroidTestCase;
/**
@@ -31,62 +24,9 @@ import android.test.AndroidTestCase;
* @author Rodrigo Damazio
*/
public class StatusAnnouncerFactoryTest extends AndroidTestCase {
/**
* Mock version of the {@link ApiFeatures} class.
*/
private class MockApiFeatures extends ApiFeatures {
private boolean hasTts;
public void setHasTextToSpeech(boolean hasTts) {
this.hasTts = hasTts;
}
@Override
public boolean hasTextToSpeech() {
return hasTts;
}
}
private MockApiFeatures apiFeatures;
@Override
protected void setUp() throws Exception {
super.setUp();
apiFeatures = new MockApiFeatures();
}
public void testCreate() {
apiFeatures.setHasTextToSpeech(true);
PeriodicTaskFactory factory =
new StatusAnnouncerFactory(apiFeatures);
PeriodicTaskFactory factory = new StatusAnnouncerFactory();
PeriodicTask task = factory.create(getContext());
assertTrue(task instanceof StatusAnnouncerTask);
}
public void testCreate_notAvailable() {
apiFeatures.setHasTextToSpeech(false);
PeriodicTaskFactory factory =
new StatusAnnouncerFactory(apiFeatures);
PeriodicTask task = factory.create(getContext());
assertNull(task);
}
public void testGetVolumeStream() {
apiFeatures.setHasTextToSpeech(true);
StatusAnnouncerFactory factory =
new StatusAnnouncerFactory(apiFeatures);
assertEquals(
TextToSpeech.Engine.DEFAULT_STREAM,
factory.getVolumeStream());
}
public void testGetVolumeStream_notAvailable() {
apiFeatures.setHasTextToSpeech(false);
StatusAnnouncerFactory factory =
new StatusAnnouncerFactory(apiFeatures);
assertEquals(
AudioManager.USE_DEFAULT_STREAM_TYPE,
factory.getVolumeStream());
}
}
@@ -15,27 +15,15 @@
*/
package com.google.android.apps.mytracks.util;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import junit.framework.TestCase;
/**
* API level 5 specific implementation of the {@link ApiLevelAdapter}.
* Tests {@link ApiAdapterFactory}.
*
* @author Bartlomiej Niechwiej
* @author Rodrigo Damazio
*/
public class ApiLevel5Adapter extends ApiLevel3Adapter {
@Override
public void startForeground(Service service,
NotificationManager notificationManager, int id,
Notification notification) {
service.startForeground(id, notification);
}
@Override
public void stopForeground(Service service,
NotificationManager notificationManager, int id) {
service.stopForeground(id != -1);
public class ApiAdapterFactoryTest extends TestCase {
public void testGetApiLevelAdapter() {
assertNotNull(ApiAdapterFactory.getApiAdapter());
}
}
@@ -1,62 +0,0 @@
/*
* Copyright 2010 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.util;
import junit.framework.TestCase;
/**
* Tests the API feature detection code in {@link ApiFeatures}.
* This test requires Froyo+ to run.
*
* @author Rodrigo Damazio
*/
public class ApiFeaturesTest extends TestCase {
private TestableApiFeatures features;
private class TestableApiFeatures extends ApiFeatures {
private int apiLevel;
public void setApiLevel(int apiLevel) {
this.apiLevel = apiLevel;
}
@Override
protected int getApiLevel() {
return apiLevel;
}
}
@Override
protected void setUp() throws Exception {
super.setUp();
features = new TestableApiFeatures();
}
public void testHasTextToSpeech() {
features.setApiLevel(3);
assertFalse(features.hasTextToSpeech());
for (int i = 4; i <= 8; i++) {
features.setApiLevel(i);
assertTrue(features.hasTextToSpeech());
}
}
public void testGetApiAdapter() {
assertNotNull(features.getApiAdapter());
}
}