I am building an app that detects when the car is moving or parking by detecting its speed.
I have some code that detects the current speed, but I don't know how to write that code so if the speed is over 20 km/h so do anything, and if the speed is 0 km/h so do anything.
Here is my code:
public class DeviceSpeedDemoActivity extends Activity implements GPSCallback{
private GPSManager gpsManager = null;
private double speed = 0.0;
private int measurement_index = Constants.INDEX_KM;
private AbsoluteSizeSpan sizeSpanLarge = null;
private AbsoluteSizeSpan sizeSpanSmall = null;
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
gpsManager = new GPSManager();
gpsManager.startListening(getApplicationContext());
gpsManager.setGPSCallback(this);
((TextView)findViewById(R.id.info_message)).setText(getString(R.string.info));
measurement_index = AppSettings.getMeasureUnit(this);
}
#Override
public void onGPSUpdate(Location location)
{
location.getLatitude();
location.getLongitude();
speed = location.getSpeed();
String speedString = "" + roundDecimal(convertSpeed(speed),2);
String unitString = measurementUnitString(measurement_index);
setSpeedText(R.id.info_message,speedString + " " + unitString);
}
#Override
protected void onDestroy() {
gpsManager.stopListening();
gpsManager.setGPSCallback(null);
gpsManager = null;
super.onDestroy();
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
boolean result = true;
switch(item.getItemId())
{
case R.id.menu_about:
{
displayAboutDialog();
break;
}
case R.id.unit_km:
{
measurement_index = 0;
AppSettings.setMeasureUnit(this, 0);
break;
}
case R.id.unit_miles:
{
measurement_index = 1;
AppSettings.setMeasureUnit(this, 1);
break;
}
default:
{
result = super.onOptionsItemSelected(item);
break;
}
}
return result;
}
private double convertSpeed(double speed){
return ((speed * Constants.HOUR_MULTIPLIER) * Constants.UNIT_MULTIPLIERS[measurement_index]);
}
private String measurementUnitString(int unitIndex){
String string = "";
switch(unitIndex)
{
case Constants.INDEX_KM: string = "km/h"; break;
case Constants.INDEX_MILES: string = "mi/h"; break;
}
return string;
}
private double roundDecimal(double value, final int decimalPlace)
{
BigDecimal bd = new BigDecimal(value);
bd = bd.setScale(decimalPlace, RoundingMode.HALF_UP);
value = bd.doubleValue();
return value;
}
private void setSpeedText(int textid,String text)
{
Spannable span = new SpannableString(text);
int firstPos = text.indexOf(32);
span.setSpan(sizeSpanLarge, 0, firstPos,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
span.setSpan(sizeSpanSmall, firstPos + 1, text.length(),Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
TextView tv = ((TextView)findViewById(textid));
tv.setText(span);
}
private void displayAboutDialog()
{
final LayoutInflater inflator = LayoutInflater.from(this);
final View settingsview = inflator.inflate(R.layout.about, null);
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.app_name));
builder.setView(settingsview);
builder.setPositiveButton(android.R.string.ok, new OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
}
});
builder.create().show();
}
}
You can use a simple if-elseif block. When you are getting your speed, i.e. onGPSUpdate, just add the following:
if(convertSpeed(speed) > 20.0){
//Code for Speed Over 20
}
else{
//code for speed less than 20
}
By the way when using location.getLongitude() use a try-catch block, as they don't work well always, and you don't want to annoy the user with a force-close.
I solved a similar problem for a flight logging application. I wanted to detect the begin of taxiing to the runway, the take-off, landing, and reaching the parking position, plus some more complex things as touch-and-goes.
A useful pattern here is a state machine (or the state pattern): Your app maintains a state (e.g. stopped, or running; in my example parking, taxi-for-take-off, in-flight, and so on). If in a certain state you look for different events: E.g. if in state "stopped" you look if the speed exceeds a certain threshold, e.g. 5 km/h or if the location changes and certain distance threshold is exceeded. If in state "driving" you check for the speed dropping below a certain threshold and if that happens you look if it stays below that thereshold for some time (if you want to avoid detecting a short stop like taking a turn). And so on.
Thus your app is in a certain state which determines the interpretation of sensor events. Your apps evaluates the sensor events wrt. to the current state and switches state if the evaluations supports a hypothesis that the state should be changed.
In your method onGPSUpdate(), you should be able to determine this criteria.
#Override
public void onGPSUpdate(Location location)
{
location.getLatitude();
location.getLongitude();
speed = location.getSpeed();
double speedKPH = convertSpeed(speed);
if(speedKPH > 20.0)
{
//Do something, Speed Over 20
}else if(speedKPH == 0)
{
//Do something else, speed 0
}
String speedString = "" + roundDecimal(convertSpeed(speed),2);
String unitString = measurementUnitString(measurement_index);
setSpeedText(R.id.info_message,speedString + " " + unitString);
}
Related
I am reframing my last question, which is unanswered, and I have rewritten the problem following Google's BasicLocation.
My main activity is defined as:
public class MainActivity extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener {
// private LocationCallback locationCallback;
// private FusedLocationProviderClient mFusedLocationClient;
private FusedLocationProviderClient mFusedLocationClient;
protected Location mLastLocation;
private static final int REQUEST_PERMISSIONS_REQUEST_CODE = 34;
private static final String TAG = MainActivity.class.getSimpleName();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.drawer_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
SectionsPagerAdapter sectionsPagerAdapter = new SectionsPagerAdapter(this, getSupportFragmentManager());
ViewPager viewPager = findViewById(R.id.view_pager);
viewPager.setAdapter(sectionsPagerAdapter);
DrawerLayout drawer = findViewById(R.id.drawer_layout);
NavigationView navigationView = findViewById(R.id.nav_view);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
drawer.addDrawerListener(toggle);
toggle.syncState();
navigationView.setNavigationItemSelectedListener(this);
ImageButton leftNav = findViewById(R.id.left_nav);
ImageButton rightNav = findViewById(R.id.right_nav);
leftNav.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
int tab = viewPager.getCurrentItem();
if (tab > 0) {
tab--;
viewPager.setCurrentItem(tab);
} else if (tab == 0) {
viewPager.setCurrentItem(tab);
}
}
});
rightNav.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
int tab = viewPager.getCurrentItem();
tab++;
viewPager.setCurrentItem(tab);
}
});
}
#Override
public void onStart() {
super.onStart();
if (!checkPermissions()) {
requestPermissions();
} else {
getLastLocation();
}
}
with latlang.[Lat,Lang] is in a seperate file:
public class latlang {
public static double Lat;
public static double Lang;
}
and the location file, which is the first fragment in the viewpager is defined as:
public class SunFragment extends Fragment {
List<SunSession> sunsList;
Typeface sunfont;
//to be called by the MainActivity
public SunFragment() {
// Required empty public constructor
}
// Keys for storing activity state.
// private static final String KEY_CAMERA_POSITION = "camera_position";
private static final String KEY_LOCATION_NAME = "location_name";
public String location;//="No location name found";
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Retrieve location and camera position from saved instance state.
if (savedInstanceState != null) {
location = savedInstanceState.getCharSequence(KEY_LOCATION_NAME).toString();
System.out.println("OnCreate location "+location);
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View rootView = inflater.inflate(R.layout.fragment_sun, container, false);
onSaveInstanceState(new Bundle());
//SecondFragment secondFragment = new SecondFragment();
//secondFragment.getDeviceLocation();
RecyclerView rv = rootView.findViewById(R.id.rv_recycler_view);
rv.setNestedScrollingEnabled(false);
rv.setHasFixedSize(true);
//MyAdapter adapter = new MyAdapter(new String[]{"Today", "Golden Hour", "Blue Hour", "Civil Twilight", "Nautical Twilight", "Astronomical Twilight", "Hello", "World"});
//rv.setAdapter(adapter);
LinearLayoutManager llm = new LinearLayoutManager(getActivity());
rv.setLayoutManager(llm);
System.out.println("location "+location);
/*
Reversegeocoding location
*/
String location="No location name found";
String errorMessage = "";
List<Address> addresses = null;
Geocoder geocoder = new Geocoder(getContext(), Locale.getDefault());
try {
addresses = geocoder.getFromLocation(
latlang.Lat,
latlang.Lang,
1);
} catch (IOException ioException) {
// Catch network or other I/O problems.
errorMessage = getString(R.string.service_not_available);
// Log.e(TAG, errorMessage, ioException);
if (getView() != null){
Snackbar.make(getView(), errorMessage, Snackbar.LENGTH_LONG).show();
}
} catch (IllegalArgumentException illegalArgumentException) {
// Catch invalid latitude or longitude values.
errorMessage = getString(R.string.invalid_lat_long_used);
if (getView() != null){
Snackbar.make(getView(),
"Illegal Latitude = " + latlang.Lat + ", Longitude = " +
latlang.Lang, Snackbar.LENGTH_LONG).show();
}
}
if (addresses == null || addresses.size() == 0) {
if (errorMessage.isEmpty()) {
System.out.println("Adress Empty No Address Found");// Snackbar.LENGTH_LONG).show();
location = "Lat:"+latlang.Lat+" Lang: "+latlang.Lang;
}
} else {
location = addresses.get(0).getAddressLine(0);//+", "+ addresses.get(0).getLocality();
/* for(int i = 0; i <= addresses.get(0).getMaxAddressLineIndex(); i++) {
location = addresses.get(0).getAddressLine(i);
}*/
}
The problem with this is evident from the logcat:
I/System.out: location null
I/Google Maps Android API: Google Play services package version: 17785022
I/Choreographer: Skipped 31 frames! The application may be doing too much work on its main thread.
I/System.out: Position:0
I/System.out: Position:1
I/System.out: Position:2
I/zygote: Do full code cache collection, code=202KB, data=177KB
I/zygote: After code cache collection, code=129KB, data=91KB
I/zygote: JIT allocated 56KB for compiled code of void android.view.View.<init>(android.content.Context, android.util.AttributeSet, int, int)
I/zygote: Background concurrent copying GC freed 44415(2MB) AllocSpace objects, 7(136KB) LOS objects, 49% free, 3MB/6MB, paused 294us total 102.458ms
I/System.out: Position:3
I/System.out: Position:4
I/zygote: Do partial code cache collection, code=193KB, data=126KB
I/zygote: After code cache collection, code=193KB, data=126KB
I/zygote: Increasing code cache capacity to 1024KB
I/zygote: JIT allocated 71KB for compiled code of void android.widget.TextView.<init>(android.content.Context, android.util.AttributeSet, int, int)
I/zygote: Compiler allocated 4MB to compile void android.widget.TextView.<init>(android.content.Context, android.util.AttributeSet, int, int)
E/MainActivity: Latit: 37.42342342342342
This shows, at the start, location is null,
I/System.out: location null
then the recyclerview of the sunfragment is created
I/System.out: Position:0
I/System.out: Position:1
I/System.out: Position:2
and after that I am getting the location:
E/MainActivity: Latit: 37.42342342342342
Link of the complete code:https://drive.google.com/file/d/1pMl_3Lf76sy82C0J4b-9ta4jbSHonJ2y/view?usp=sharing
Is it somehow possible to get the location first before creating the sunfragment's oncreateview?
I found something wrong about your code (I may be wrong):
Why fields of latlang are static? It doesn't looks like they should.
At SunFragment.onCreate() you are reading location if savedInstanceState != null. savedInstanceState is not null only if activity that holds this fragment was restored from saved state. It may not happen at all.
You should use fragment's arguments (Bundle) to pass initial data to fragment
You should implement Parcelable interface for latlang to be able to pass custom class thru Bundle
I think that's not everything but for me it seems like enough for this code to not work as you expected
As stated by the previous answer there are a lot of issues in your code.
Apart from that understand that last known location may not always return a value. Gps basically has two data types: Ephemeris (precise nav data) and Almanac (coarse data). Now when the receiver is cold started ie after gps has been off for more than 8-10 mins, there is basically no last known location (the duration may vary based on the device but the basic idea is this).
So when you do not get last known location, fetch the actual live location using the fused client. Also since you are saving the data in shared preference and fetching it in your fragment, i believe your fragment is going to heavily rely on this data. So i would suggest either of the following two approaches to get correct result
1) do not fetch the location in the nesting activity at all. Just do it in the fragment where it is needed. This will not work if other fragments in your viewpager also need the location.
2) If you must have the location in your activity and it is a dependency in the container fragments, you can use two approaches here as well. My approaches rely on event bus.. Event bus, otto or rxbus anything will do
2a) do not add anything to the viewpager. basically fetch the location first fully and then add stuff to the viewpager once you get the location callback.
2b) Add stuff to the viewpager from the start. In the activity once you get the location, use the event bus to inform the fragments of the same and on getting the event in the fragments, actually start what you need to do.
I have previously used both these approaches and everything works. Now it is entirely up to your use case to use what suits you. Either ways it is too long a code and too complicated to post everything here.
Right now you are using like this:
#Override
public void onStart() {
super.onStart();
if (!checkPermissions()) {
requestPermissions();
} else {
getLastLocation();
}
}
In your else statement check if getLastLocation() is not null. if not null, replace the fragment.
#Override
public void onStart() {
super.onStart();
if (!checkPermissions()) {
requestPermissions();
} else {
if(getLastLocation() != null){
//replace your fragment
}else{//Null Location}
}
}
Use a service class to handle location service precisely. Here, i'm giving you a custom LocationService class which i used in many projects to collect the location data from background continuously
LocationService.kt
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.Looper
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
class LocationService : Service() {
//Custom Location Binder class
inner class LocationBinder : Binder() {
val locationService: LocationService
get() = this#LocationService
}
companion object {
val TAG = "LocationService_123"
private val UPDATE_INTERVAL = (4 * 1000).toLong() /* 4 secs */
private val FASTEST_INTERVAL: Long = 2000 /* 2 sec */
private var INSTANCE: LocationService? = null
fun isInstanceCreated(): Boolean {
return (INSTANCE != null)
}
}
private var mFusedLocationClient: FusedLocationProviderClient? = null
private var mLocationListener: LocationListener? = null
private var mLocationBinder = LocationBinder()
fun setLocationListener(locationListener: LocationListener) {
this.mLocationListener = locationListener
}
override fun onBind(intent: Intent): IBinder? {
return mLocationBinder
}
override fun onCreate() {
INSTANCE = this
super.onCreate()
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
if (Build.VERSION.SDK_INT >= 26) {
val CHANNEL_ID = "ostad_gps"
val channel = NotificationChannel(
CHANNEL_ID,
"Ostad GPS",
NotificationManager.IMPORTANCE_DEFAULT)
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(channel)
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("")
.setContentText("").build()
startForeground(1, notification)
}
}
override fun onDestroy() {
INSTANCE = null
super.onDestroy()
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand: called.")
getLocation()
return Service.START_NOT_STICKY
}
private fun getLocation() {
// ---------------------------------- LocationRequest ------------------------------------
// Create the location request to start receiving updates
val mLocationRequestHighAccuracy = LocationRequest()
mLocationRequestHighAccuracy.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
mLocationRequestHighAccuracy.interval = UPDATE_INTERVAL
mLocationRequestHighAccuracy.fastestInterval = FASTEST_INTERVAL
// new Google API SDK v11 uses getFusedLocationProviderClient(this)
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
Log.d(TAG, "getLocation: stopping the location mitchService.")
stopSelf()
return
}
Log.d(TAG, "getLocation: getting location information.")
mFusedLocationClient!!.requestLocationUpdates(
mLocationRequestHighAccuracy, object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult?) {
val location = locationResult!!.lastLocation
Log.d(TAG, "onLocationResult: got location result: $location")
if (location != null) {
if (mLocationListener != null)
mLocationListener!!.onLocationChanged(location)
}
}
},
Looper.myLooper()
) // Looper.myLooper tells this to repeat forever until thread is destroyed
}
}
add this in your AndroidManifest file and start the LocationService from you MainActivity.
By looking at the code, it seems you do not need a very accurate location, you will be fine with last known location. This value might be null in some cases, like you have already experienced. Simple answer to your question is no, you cannot get not null location before creating SunFragment. Following steps is to load location in background and update UI once found.
Request last known location in MainActivity
Keep a reference of last location in cache for easy loading and better user
experience
If last location is null, request location updates until you get a good fix
Have a listener in SunFragment to track location updates
Here are some code you need (Please do read them)
Use the LocationUtil to handle location related events (I prefer LocationManager over FusedLocationProviderClient);
public class LocationUtil {
public static void updateLastKnownLocation(Context context) {
LocationManager lm = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
if(hasSelfPermission(context, new String[]{
Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION})) {
try {
Location currentBestLocation;
Location gpsLocation = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER);
Location lbsLocation = lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
if(isBetterLocation(lbsLocation, gpsLocation)) {
currentBestLocation = lbsLocation;
} else {
currentBestLocation = gpsLocation;
}
if(currentBestLocation == null) {
requestLocationUpdates(lm);
} else {
updateCacheLocation(currentBestLocation);
}
} catch (SecurityException se) {
// unlikely as permission checks
se.printStackTrace();
} catch (Exception e) {
// unexpected
e.printStackTrace();
}
}
}
private static void updateCacheLocation(Location location) {
if(location == null) return;
LocationLite temp = new LocationLite();
temp.lat = location.getLatitude();
temp.lon = location.getLongitude();
Gson gson = new Gson();
String locationString = gson.toJson(temp);
AppCache.setLastLocation(locationString);
}
#SuppressLint("MissingPermission")
private static void requestLocationUpdates(LocationManager lm) {
try {
lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 0.0F, new LocationListener() {
#Override
public void onLocationChanged(Location location) {
updateCacheLocation(location);
lm.removeUpdates(this);
}
#Override
public void onStatusChanged(String s, int i, Bundle bundle) {
// doing nothing
}
#Override
public void onProviderEnabled(String s) {
// doing nothing
}
#Override
public void onProviderDisabled(String s) {
// doing nothing
}
});
}catch (Exception e) {
e.printStackTrace();
}
}
private static boolean isBetterLocation(Location location, Location currentBestLocation) {
int TWO_MINUTES = 1000 * 60 * 2;
if (currentBestLocation == null) {
// A new location is always better than no location
return true;
}
if (location == null) {
// A new location is always better than no location
return false;
}
// Check whether the new location fix is newer or older
long timeDelta = location.getTime() - currentBestLocation.getTime();
boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
boolean isNewer = timeDelta > 0;
// If it's been more than two minutes since the current location, use the new location
// because the user has likely moved
if (isSignificantlyNewer) {
return true;
// If the new location is more than two minutes older, it must be worse
} else if (isSignificantlyOlder) {
return false;
}
// Check whether the new location fix is more or less accurate
int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
boolean isLessAccurate = accuracyDelta > 0;
boolean isMoreAccurate = accuracyDelta < 0;
boolean isSignificantlyLessAccurate = accuracyDelta > 200;
// Check if the old and new location are from the same provider
boolean isFromSameProvider = isSameProvider(location.getProvider(), currentBestLocation.getProvider());
// Determine location quality using a combination of timeliness and accuracy
if (isMoreAccurate) {
return true;
} else if (isNewer && !isLessAccurate) {
return true;
} else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
return true;
}
return false;
}
private static boolean isSameProvider(String provider1, String provider2) {
if (provider1 == null) {
return provider2 == null;
}
return provider1.equals(provider2);
}
public static boolean hasSelfPermission(Context context, String[] permissions) {
// Below Android M all permissions are granted at install time and are already available.
if (!(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)) {
return true;
}
// Verify that all required permissions have been granted
for (String permission : permissions) {
if (context.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
}
Use AppCache to store last location;
public class AppCache {
public static final String KEY_LAST_LOCATION = "_key_last_location";
private static SharedPreferences mPreference;
static {
mPreference = PreferenceManager.getDefaultSharedPreferences(App.getApp().getApplicationContext());
}
public static String getLastLocation() {
return mPreference.getString(KEY_LAST_LOCATION, null);
}
public static String getLastLocation(String defaultValue) {
return mPreference.getString(KEY_LAST_LOCATION, defaultValue);
}
public static void setLastLocation(String lastLocation) {
mPreference.edit().putString(KEY_LAST_LOCATION, lastLocation).commit();
}
public static void registerPreferenceChangeListener(SharedPreferences.OnSharedPreferenceChangeListener listener) {
mPreference.registerOnSharedPreferenceChangeListener(listener);
}
public static void unregisterPreferenceChangeListener(SharedPreferences.OnSharedPreferenceChangeListener listener) {
mPreference.unregisterOnSharedPreferenceChangeListener(listener);
}
}
Put the following code into your MainActivity onCreate() This will call locationManager to get last known location and update app cache.
LocationUtil.updateLastKnownLocation(MainActivity.this);
Also replace fetchLocation(); in onRequestPermissionsResult method with above line of code, so it will look like;
#Override
public void onRequestPermissionsResult(...){
switch (requestCode) {
case 101:
{
...
// permission was granted
//fetchLocation();
LocationUtil.updateLastKnownLocation(MainActivity.this);
} else {
// Show some error
}
return;
}
}
}
I did not use your latlang class. (Please make sure all class names follow Java coding standards) Instead use LocationLite to store location in cache. Also I used GSON google library to convert and restore pojo to JSON and backward.
public class LocationLite {
public double lat;
public double lon;
public String address;
}
Final changes in SunFragment.
Make SunAdapter as a member variable, and SharedPreferences.OnSharedPreferenceChangeListener to listen to any changes on location value.
SunAdapter mAdapter;
SharedPreferences.OnSharedPreferenceChangeListener mPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
#Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if(AppCache.KEY_LAST_LOCATION.equalsIgnoreCase(key)) {
// location value has change, update data-set
SunSession sunSession = sunsList.get(0);
sunSession.setId(sharedPreferences.getString(key, "No Location"));
sunsList.add(0, sunSession);
mAdapter.notifyDataSetChanged();
}
}
};
Start listening to preference changes in onStart() and unregister in onStop()
#Override
public void onStart() {
super.onStart();
AppCache.registerPreferenceChangeListener(mPreferenceChangeListener);
}
#Override
public void onStop() {
super.onStop();
AppCache.unregisterPreferenceChangeListener(mPreferenceChangeListener);
}
Finally when populating first SunSession use the following instead location local variable. So It will look like following;
sunsList.add(
new SunSession(
AppCache.getLastLocation("Searching location..."),
"",
sun_rise,
"",
sun_set,
"",
moon_rise,
"",
moon_set));
That's all. Feel free to ask anything you do not understand.
I'm trying to detect ibeacon using an android smartphone. I bought ibeacon devices from a company that has provided me android library to make them work (this library is much like the android beacon library for AltBeacon, such as the code that I used). Here is the MainActivity
public class MainActivity extends Activity implements IBeaconConsumer {
private static final String TAG = "BB-EXAPP";
// iBeacon bluetooth scanning parameters
private static final int FOREGROUND_SCAN_PERIOD = 1000;
private static final int FOREGROUND_BETWEEN_SCAN_PERIOD = 1000;
private static final int BACKGROUND_SCAN_PERIOD = 250;
private static final int BACKGROUND_BETWEEN_SCAN_PERIOD = 2000;
// iBeacon Library Stuff
private static final Region blueupRegion = new Region("BlueUp", "acfd065e-c3c0-11e3-9bbe-1a514932ac01", null, null);
private IBeaconManager iBeaconManager = IBeaconManager.getInstanceForApplication(this);
private Intent iBeaconService;
private boolean isMonitoring = false;
private boolean isRanging = false;
// Android BLE Stuff
private BluetoothAdapter mBluetoothAdapter;
private static final int REQUEST_ENABLE_BT = 1;
// UI Stuff
private List<IBeacon> beacons;
private ListView listView;
private IBeaconListAdapter listAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Initializes Bluetooth adapter
final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
// Ensures Bluetooth is available on the device and it is enabled. If not,
// displays a dialog requesting user permission to enable Bluetooth.
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
// Initializes iBeacon Service
iBeaconService = new Intent(getApplicationContext(), IBeaconService.class);
// Start the iBeacon Service
Log.d(TAG, "Starting service: iBeaconService");
startService(iBeaconService);
// Set desired scan periods
iBeaconManager.setForegroundScanPeriod(FOREGROUND_SCAN_PERIOD);
iBeaconManager.setForegroundBetweenScanPeriod(FOREGROUND_BETWEEN_SCAN_PERIOD);
iBeaconManager.setBackgroundScanPeriod(BACKGROUND_SCAN_PERIOD);
iBeaconManager.setBackgroundBetweenScanPeriod(BACKGROUND_BETWEEN_SCAN_PERIOD);
// Bind the iBeacon Service
iBeaconManager.bind(this);
//
// UI Initialization
//
// Create Empty IBeacons List
beacons = new ArrayList<IBeacon>();
// Create List Adapter
listAdapter = new IBeaconListAdapter(this, beacons);
// Get ListView
listView = (ListView)findViewById(R.id.listView);
// Set ListAdapter
listView.setAdapter(listAdapter);
}
#Override
public void onIBeaconServiceConnect() {
Log.d(TAG, "onIBeaconServiceConnect");
// Set Monitor Notifier
iBeaconManager.setMonitorNotifier(new MonitorNotifier() {
#Override
public void didExitRegion(Region region) {
Log.d(TAG, "didExitRegion: region = " + region.toString());
}
#Override
public void didEnterRegion(Region region) {
Log.d(TAG, "didEnterRegion: region = " + region.toString());
// Set Range Notifier
iBeaconManager.setRangeNotifier(new RangeNotifier() {
#Override
public void didRangeBeaconsInRegion(Collection<IBeacon> iBeacons, Region region) {
// Update UI iBeacons List
beacons = new ArrayList<IBeacon>(iBeacons);
listAdapter.notifyDataSetChanged();
// Log found iBeacons
Log.d(TAG, "didRangeBeaconsInRegion: region = " + region.toString());
if (!iBeacons.isEmpty()) {
int j = 0;
for (IBeacon beacon : iBeacons) {
Log.d(TAG, " [" + j + "] (Major = " + beacon.getMajor() + ", Minor = " + beacon.getMinor() + ", RSSI = " + beacon.getRssi() + ", Accuracy = " + beacon.getAccuracy() + ")");
j++;
}
}
}
});
// Start Ranging
try {
iBeaconManager.startRangingBeaconsInRegion(blueupRegion);
isRanging = true;
Log.d(TAG, "startRangingBeaconsInRegion: region = " + blueupRegion.toString());
} catch (RemoteException e) {
Log.d(TAG, "startRangingBeaconsInRegion [RemoteException]");
e.printStackTrace();
}
}
#Override
public void didDetermineStateForRegion(int state, Region region) {
Log.d(TAG, "didDetermineStateForRegion: state = " + state + ", region = " + region.toString());
}
});
// Start Monitoring
try {
iBeaconManager.startMonitoringBeaconsInRegion(blueupRegion);
isMonitoring = true;
Log.d(TAG, "startMonitoringBeaconsInRegion: region = " + blueupRegion.toString());
} catch (RemoteException e) {
Log.d(TAG, "startMonitoringBeaconsInRegion [RemoteException]");
e.printStackTrace();
}
}
#Override
public void onResume() {
Log.d(TAG, "onResume");
super.onResume();
if (iBeaconManager.isBound(this)) {
iBeaconManager.setBackgroundMode(this, false);
Log.d(TAG, "iBeaconManager.setBackgroundMode = false");
}
}
#Override
public void onStop() {
Log.d(TAG, "onStop");
if (iBeaconManager.isBound(this)) {
iBeaconManager.setBackgroundMode(this, true);
Log.d(TAG, "iBeaconManager.setBackgroundMode = true");
}
super.onStop();
}
#Override
public void onDestroy() {
Log.d(TAG, "onDestroy");
if (isRanging) {
try {
iBeaconManager.stopRangingBeaconsInRegion(blueupRegion);
Log.d(TAG, "stopRangingBeaconsInRegion: region = " + blueupRegion.toString());
} catch (RemoteException e) {
Log.d(TAG, "stopRangingBeaconsInRegion [RemoteException]");
e.printStackTrace();
}
}
if (isMonitoring) {
try {
iBeaconManager.stopMonitoringBeaconsInRegion(blueupRegion);
Log.d(TAG, "stopMonitoringBeaconsInRegion: region = " + blueupRegion.toString());
} catch (RemoteException e) {
Log.d(TAG, "stopMonitoringBeaconsInRegion [RemoteException]");
e.printStackTrace();
}
}
if (iBeaconManager.isBound(this)) {
Log.d(TAG, "Unbinding iBeaconManager");
iBeaconManager.unBind(this);
}
Log.d(TAG, "Stopping service: iBeaconService");
stopService(iBeaconService);
super.onDestroy();
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
There's a second activity to make the list:
public class IBeaconListAdapter extends BaseAdapter {
private Activity activity;
private List<IBeacon> beacons;
private static LayoutInflater inflater = null;
public IBeaconListAdapter(Activity _activity, List<IBeacon> _beacons) {
this.activity = _activity;
this.beacons = _beacons;
inflater = (LayoutInflater)activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
#Override
public int getCount() {
return beacons.size();
}
#Override
public Object getItem(int position) {
return beacons.get(position);
}
#Override
public long getItemId(int position) {
/*IBeacon beacon = beacons.get(position);
if (beacon != null) {
return beacon.hashCode();
}*/
return position;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (convertView == null) {
view = inflater.inflate(R.layout.beacon_list_row, null);
}
TextView majorTextView = (TextView)view.findViewById(R.id.majorValue);
TextView minorTextView = (TextView)view.findViewById(R.id.minorValue);
TextView rssiTextView = (TextView)view.findViewById(R.id.rssiValue);
TextView accuracyTextView = (TextView)view.findViewById(R.id.accuracyValue);
IBeacon beacon = beacons.get(position);
if (beacon != null) {
DecimalFormat df = new DecimalFormat("#.0");
majorTextView.setText(beacon.getMajor());
minorTextView.setText(beacon.getMinor());
rssiTextView.setText(beacon.getRssi() + " dBm");
accuracyTextView.setText(df.format(beacon.getAccuracy()) + " m");
}
return view;
}
}
When I run the app on the phone, from the LogCat I can see that
it detects the devices, while on the phone I can't see anything but a blank page! I noticed from the LogCat that once the onIBeaconServiceConnect is called, than the app jumps to the startMonitoringBeaconsInRegion and never calls the didEnterRegion method, which includes the didRangeBeaconsInRegion and the code to fill the list.
I couldn't find other answers to a question similar to mine and I really don't know where is my mistake.
A few thoughts:
Make sure your beacons are really transmitting and have the identifiers you think they do. Try downloading an off-the-shelf app like Locate and make sure it sees them.
Your code sets up a region that is looking for a beacon with a UUID of acfd065e-c3c0-11e3-9bbe-1a514932ac01. Are you sure this is exactly right? Try replacing the region definition with Region("BlueUp", null, null, null); so it detects any region regardless of identifier.
If you plan to do significant development, I would strongly recommend you upgrade to the Android Beacon Library 2.0. The old 0.x library version that library is based off of is no longer supported or maintained, and it will be more difficult to find help when problems like this come up.
These are the significant lines in the LogCat.
D/BB-EXAPP(7756): onIBeaconServiceConnect
D/BB-EXAPP(7756): startMonitoringBeaconsInRegion: region = proximityUuid: acfd065e-c3c0-11e3-9bbe-1a514932ac01 major: null minor:null
D/AbsListView(7756): unregisterIRListener() is called
I/IBeaconService(7756): start monitoring received
D/BluetoothAdapter(7756): startLeScan(): null
D/BluetoothAdapter(7756): onClientRegistered() - status=0 clientIf=4
I/IBeaconService(7756): Adjusted scanStopTime to be Sat Jan 17 12:09:58 CET 2015
D/AbsListView(7756): unregisterIRListener() is called
D/BluetoothAdapter(7756): onScanResult() - Device=DA:0D:22:4B:40:17 RSSI=-59
D/Callback(7756): attempting callback via intent: ComponentInfo{com.android.appbeacon/com.blueup.libbeacon.IBeaconIntentProcessor}
D/BluetoothAdapter(7756): onScanResult() - Device=DA:0D:22:4B:40:17 RSSI=-64
I/IBeaconService(7756): iBeacon detected multiple times in scan cycle :acfd065e-c3c0-11e3-9bbe-1a514932ac01 0 0 accuracy: 0.7351236323265571 proximity: 2
D/BluetoothAdapter(7756): stopLeScan()
I am working on an app that should get accurate user location continuously if user is inside a specific zone.
The location updates should be saved on a server (Parse.com) so other users can get the current location of the user.
The flow is:
Application onCreate -->
start Service with LocationListener -->
onLocationChanged -->
save new location on Parse.com -->
Parse.com Cloud afterSave method send push notification to users -->
other users get notifications via broadcast reciever -->
update user marker on map <--
Questions:
I am not sure about how to implement step 4, should I save the new location on parse inside the onLocationChanged method? should I pass it to a BroadcastReciever and save it on parse there? or should I pass it to a static method in my CurrentUser class that will perform the save on server?
I have noticed the requestLocationUpdates documentation says:
"This method is suited for the foreground use cases, more specifically
for requesting locations while being connected to LocationClient. For
background use cases, the PendingIntent version of the method is
recommended."
In my case, which method should I use?
What values should I use for getting almost real time location without consuming more than 5% of battery power per hour?
My Service Class:
public class UserTracker extends Service implements
GooglePlayServicesClient.ConnectionCallbacks,
GooglePlayServicesClient.OnConnectionFailedListener, LocationListener{
static int GEOFENCE_STATUS;
final static int INSIDE_GEOFENCE = 9001, OUTSIDE_GEOFENCE = 9002;
static final int MINIMUM_ACCURACY = 26;
final static int GEOFENCE_RADIUS = 2000;
static final int NULL_LOCATION_ERROR = 7001;
static final int INSIDE_INTERVAL = 10000, INSIDE_FASTEST_INTERVAL = 1000, INSIDE_SMALLEST_DISPLACEMENT = 10;
static final int OUTSIDE_INTERVAL = 300000, OUTSIDE_FASTEST_INTERVAL = 60000, OUTSIDE_SMALLEST_DISPLACEMENT = 100;
static Location eventLocation;
LocationClient lc;
#Override
public void onCreate() {
super.onCreate();
eventLocation = new Location("Test");
lc = new LocationClient(getApplicationContext(), this, this);
}
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
double eventLat = intent.getDoubleExtra("Latitude", -1);
double eventLon = intent.getDoubleExtra("Longitude", -1);
if (eventLat == -1 || eventLon == -1) {
stopSelf();
return START_REDELIVER_INTENT;
}
eventLocation.setLatitude(eventLat);
eventLocation.setLongitude(eventLon);
lc.connect();
return START_STICKY;
}
public int getGeofenceStatus(Location currentLocation) {
if (currentLocation == null) {
return NULL_LOCATION_ERROR;
}
if (currentLocation.distanceTo(eventLocation) > GEOFENCE_RADIUS) {
Log.d(TAG, "User is outside geofence");
return OUTSIDE_GEOFENCE;
} else {
Log.d(TAG, "User is inside geofence");
return INSIDE_GEOFENCE;
}
}
public void requestLocationUpdates(int status) {
GEOFENCE_STATUS = status;
LocationRequest request = LocationRequest.create();
switch (status) {
case NULL_LOCATION_ERROR:
stopSelf();
break;
case INSIDE_GEOFENCE:
request.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
request.setInterval(INSIDE_INTERVAL);
request.setFastestInterval(INSIDE_FASTEST_INTERVAL);
request.setSmallestDisplacement(INSIDE_SMALLEST_DISPLACEMENT);
break;
case OUTSIDE_GEOFENCE:
request.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
request.setInterval(OUTSIDE_INTERVAL);
request.setFastestInterval(OUTSIDE_FASTEST_INTERVAL);
request.setSmallestDisplacement(OUTSIDE_SMALLEST_DISPLACEMENT);
break;
}
lc.requestLocationUpdates(request, this);
}
#Override
public void onLocationChanged(Location location) {
int newStatus = getGeofenceStatus(location);
int accuracy = (int) location.getAccuracy();
String message = "Geofence status: " + newStatus +
"\nAccuracy: " + accuracy +
"\nDistance to event: " + location.distanceTo(eventLocation);
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
if (newStatus == INSIDE_GEOFENCE && accuracy < MINIMUM_ACCURACY) {
Log.d(TAG, "Accuracy is good");
// do the save on server procees
}
if (GEOFENCE_STATUS != newStatus) {
requestLocationUpdates(newStatus);
}
}
#Override
public void onConnectionFailed(ConnectionResult result) {
Log.d(TAG, "LocationClient connection failed: " + result.getErrorCode());
}
#Override
public void onConnected(Bundle arg0) {
Location currentLocation = lc.getLastLocation();
requestLocationUpdates(getGeofenceStatus(currentLocation));
}
#Override
public void onDisconnected() {
Log.d(TAG, "LocationClient disconnected");
}
#Override
public IBinder onBind(Intent intent) {
return null;
}
}
Android question:
When I set text to a view, when the view will update the UI?
Here is my case 1:
for(int i=0;i< 2000; i++){
aTextView.setText("a"+i);
}
aTextView is a MyTextView, that extends from TextView. I overwirte the onDraw as:
public static int counterP = 0;
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
counterP++;
Log.d("MYButton", "onDraw: " + counterP);
}
From the log, I can see, the printed counterP is not consequent. In the loop, the onDraw method is called only 2-4 times.
I did another test, case 2:
boolean notInit = true;
List<String> cmdList = null;
long stSelfRefresh = 0;
String contentStr;
TextView selfTv;
public void onSelfRefreshTv(View v) {
if (cmdList == null || cmdList.isEmpty()) {
Log.e(TAG, "empty now, reset");
notInit = true;
}
if (notInit) {
cmdList = new ArrayList<String>();
for (int i = 0; i < 2000; i++) {
cmdList.add("a" + i);
}
stSelfRefresh = SystemClock.uptimeMillis();
notInit = false;
selfTv = (MyTextView) findViewById(R.id.mytv);
}
contentStr = cmdList.remove(0);
Log.d(TAG, "contentStr = " + contentStr);
selfTv.setText(contentStr);
if (!cmdList.isEmpty()) {
if (!mHandler.sendEmptyMessage(99)) {
Log.e(TAG, "SKIP my self");
}
} else {
Log.d(TAG, "Cost time: " + (SystemClock.uptimeMillis() - stSelfRefresh));
}
}
private Handler mHandler = new Handler() {
#Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 99:
onSelfRefreshTv(null);
break;
default:
break;
}
}
};
The result of case 2 is the counterP is printed out consequent. 1-2000.
I don't know why the textView is updated for each time?
Do you have any idea for this?
Thanks in advance.
******add case 3***********
for(int i=0;i< 2000;i++){
contentStr = cmdList.remove(0);
mHandler.sendEmptyMessage(100);
}
private Handler mHandler = new Handler() {
#Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 100:
selfTv.setText(contentStr);
break;
default:
break;
}
}
};
******end of case 3**********
The test result for case 3 is similar with case 1.
It looks like you are running your code on the UI Thread.
This means: as long as your code is running, the drawing code can not run. The onDraw() is called after your loop finished counting and only the last value is actually drawn on the display.
It's as simple as that.
If you want something count up on your display, you can use a AsyncTask to move the counting part into a non-UI Thread:
new AsyncTask<Void,Integer, Void>() {
#Override
protected Void doInBackground(Void... voids) {
for (int i =0; i<2000; i++) {
publishProgress(i);
}
return null;
}
#Override
protected void onProgressUpdate(Integer... values) {
aTextView.setText("a"+values[0]);
}
};
Your second case is like this:
You prepare the list with all 2000 elements.
Set the first element of the list to the TextView and remove the item from the list.
Send yourself a message to run on the UI Thread as soon as the system thinks it is appropriate.
Let go the control of the UI Thread.
Your code is actually finished and the system calls onDraw().
The message from 4 gets processed and you start again with 2.
You see that the system gets control over the UI Thread in the middle of your loop. That's why it's counting visibly.
Case 3 is different from 2:
You are sending 2000 messages in on loop without letting the system handle it.
I'm playing text with android TTS - android.speech.tts.TextToSpeech
I use: TextToSpeech.speak to speak and .stop to stop. Is there a way to pause the text also?
The TTS SDK doesn't have any pause functionality that I know of. But you could use synthesizeToFile() to create an audio file that contains the TTS output. Then, you would use a MediaPlayer object to play, pause, and stop playing the file. Depending on how long the text string is, it might take a little longer for audio to be produced because the synthesizeToFile() function would have to complete the entire file before you could play it, but this delay should be acceptable for most applications.
I used splitting of string and used playsilence() like below:
public void speakSpeech(String speech) {
HashMap<String, String> myHash = new HashMap<String, String>();
myHash.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "done");
String[] splitspeech = speech.split("\\.");
for (int i = 0; i < splitspeech.length; i++) {
if (i == 0) { // Use for the first splited text to flush on audio stream
textToSpeech.speak(splitspeech[i].toString().trim(),TextToSpeech.QUEUE_FLUSH, myHash);
} else { // add the new test on previous then play the TTS
textToSpeech.speak(splitspeech[i].toString().trim(), TextToSpeech.QUEUE_ADD,myHash);
}
textToSpeech.playSilence(750, TextToSpeech.QUEUE_ADD, null);
}
}
You can make the TTS pause between sentences, or anywhere you want by adding up to three periods (".") all followed by a single space " ". The example below has a long pause at the beginning, and again before the message body. I'm not sure that is what you are after though.
private final BroadcastReceiver SMScatcher = new BroadcastReceiver() {
#Override
public void onReceive(final Context context, final Intent intent) {
if (intent.getAction().equals(
"android.provider.Telephony.SMS_RECEIVED")) {
// if(message starts with SMStretcher recognize BYTE)
StringBuilder sb = new StringBuilder();
/*
* The SMS-Messages are 'hiding' within the extras of the
* Intent.
*/
Bundle bundle = intent.getExtras();
if (bundle != null) {
/* Get all messages contained in the Intent */
Object[] pdusObj = (Object[]) bundle.get("pdus");
SmsMessage[] messages = new SmsMessage[pdusObj.length];
for (int i = 0; i < pdusObj.length; i++) {
messages[i] = SmsMessage
.createFromPdu((byte[]) pdusObj[i]);
}
/* Feed the StringBuilder with all Messages found. */
for (SmsMessage currentMessage : messages) {
// periods are to pause
sb.append("... Message From: ");
/* Sender-Number */
sb.append(currentMessage.getDisplayOriginatingAddress());
sb.append(".. ");
/* Actual Message-Content */
sb.append(currentMessage.getDisplayMessageBody());
}
// Toast.makeText(application, sb.toString(),
// Toast.LENGTH_LONG).show();
if (mTtsReady) {
try {
mTts.speak(sb.toString(), TextToSpeech.QUEUE_ADD,
null);
} catch (Exception e) {
Toast.makeText(application, "TTS Not ready",
Toast.LENGTH_LONG).show();
e.printStackTrace();
}
}
}
}
}
};
If you omit the space after the last period it will (or may) not work as expected.
In the absence of a pause option, you can add silence for the duration of when you want to delay the TTS Engine speaking. This of course would have to be a predetermined 'pause' and wouldn't help to include functionality of a pause button, for example.
For API < 21 : public int playSilence (long durationInMs, int queueMode, HashMap params)
For > 21 : public int playSilentUtterance (long durationInMs, int queueMode, String utteranceId)
Remember to use TextToSpeech.QUEUE_ADD rather than TextToSpeech.QUEUE_FLUSH otherwise it will clear the previously started speech.
I used a different approach.
Seperate your text into sentences
Speak every sentence one by one and keep track of the spoken sentence
pause will stop the text instantly
resume will start at the beginning of the last spoken sentence
Kotlin code:
class VoiceService {
private lateinit var textToSpeech: TextToSpeech
var sentenceCounter: Int = 0
var myList: List<String> = ArrayList()
fun resume() {
sentenceCounter -= 1
speakText()
}
fun pause() {
textToSpeech.stop()
}
fun stop() {
sentenceCounter = 0
textToSpeech.stop()
}
fun speakText() {
var myText = "This is some text to speak. This is more text to speak."
myList =myText.split(".")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
textToSpeech.speak(myList[sentenceCounter], TextToSpeech.QUEUE_FLUSH, null, utteranceId)
sentenceCounter++
} else {
var map: HashMap<String, String> = LinkedHashMap<String, String>()
map[TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID] = utteranceId
textToSpeech.speak(myList[sentenceCounter], TextToSpeech.QUEUE_FLUSH, map)
sentenceCounter++
}
}
override fun onDone(p0: String?) {
if (sentenceCounter < myList.size) {
speakText()
} else {
speakNextText()
}
}
}
I haven't yet tried this, but I need to do the same thing. My thinking is to first split your speech text into an array of words.
Then create a recursive function that plays the next word after the current word is finished, while keeping a counter of the current word.
divide the messages into parts and listen for last utterance by using onutteranceprogress listener
tts.playSilence(1250, TextToSpeech.QUEUE_ADD, null);
It seems that if you put a period after a word AND start the next word with a capital letter, just like a new sentence, like this:
after we came home. We ate dinner.
the "home. We" will then have a pause in it.
This becomes a grammatically strange way of writing it.
So far I have only tested this in my own language, Swedish.
It might be important that the space is there.
Also, an escaped quote (\") seems to have it pause somewhat as well - at least, if you put it around a word it adds space around the word.
This solution is not perfect, but an alternative to #Aaron C's solution may be to create a custom text to speech class like the below. This solution may work well enough if your text is relatively short and spoken words per minute is accurate enough for the language you are using.
private class CustomTextToSpeech extends TextToSpeech {
private static final double WORDS_PER_MS = (double)190/60/1000;
long startTimestamp = 0;
long pauseTimestamp = 0;
private Handler handler;
private Runnable speakRunnable;
StringBuilder textToSpeechBuilder;
private boolean isPaused = false;
public CustomTextToSpeech(Context context, OnInitListener initListener){
super(context, initListener);
setOnUtteranceProgressListener(new UtteranceProgressListener() {
#Override
public void onDone(String arg0) {
Log.d(TAG, "tts done. " + arg0);
startTimestamp = 0;
pauseTimestamp = 0;
handler.postDelayed(speakRunnable, TTS_INTERVAL_MS);
}
#Override
public void onError(String arg0) {
Log.e(TAG, "tts error. " + arg0);
}
#Override
public void onStart(String arg0) {
Log.d(TAG, "tts start. " + arg0);
setStartTimestamp(System.currentTimeMillis());
}
});
handler = new Handler();
speakRunnable = new Runnable() {
#Override
public void run() {
speak();
}
};
textToSpeechBuilder = new StringBuilder(getResources().getString(R.string.talkback_tips));
}
public void setStartTimestamp(long timestamp) {
startTimestamp = timestamp;
}
public void setPauseTimestamp(long timestamp) {
pauseTimestamp = timestamp;
}
public boolean isPaused(){
return (startTimestamp > 0 && pauseTimestamp > 0);
}
public void resume(){
if(handler != null && isPaused){
if(startTimestamp > 0 && pauseTimestamp > 0){
handler.postDelayed(speakRunnable, TTS_SETUP_TIME_MS);
} else {
handler.postDelayed(speakRunnable, TTS_INTERVAL_MS);
}
}
isPaused = false;
}
public void pause(){
isPaused = true;
if (handler != null) {
handler.removeCallbacks(speakRunnable);
handler.removeMessages(1);
}
if(isSpeaking()){
setPauseTimestamp(System.currentTimeMillis());
}
stop();
}
public void utter(){
if(handler != null){
handler.postDelayed(speakRunnable, TTS_INTERVAL_MS);
}
}
public void speak(){
Log.d(TAG, "textToSpeechBuilder: " + textToSpeechBuilder.toString());
if(isPaused()){
String[] words = textToSpeechBuilder.toString().split(" ");
int wordsAlreadySpoken = (int)Math.round((pauseTimestamp - startTimestamp)*WORDS_PER_MS);
words = Arrays.copyOfRange(words, wordsAlreadySpoken-1, words.length);
textToSpeechBuilder = new StringBuilder();
for(String s : words){
textToSpeechBuilder.append(s);
textToSpeechBuilder.append(" ");
}
} else {
textToSpeechBuilder = new StringBuilder(getResources().getString(R.string.talkback_tips));
}
if (tts != null && languageAvailable)
speak(textToSpeechBuilder.toString(), TextToSpeech.QUEUE_FLUSH, new Bundle(), "utter");
}
}