Android Activity Flow - From Base Superclass to Subclass to Baseclass again - java

As a N00bie to android, I'm trying to build a simple map-app. I started out doing everything in the same class, but for obvious reasons that got out of hand, fast. So I wanted to split the class, with the base class as the main flow for the activity, and the subclass as the 'utility' class.
So I instantiate an Subclass object, and in the subclass's onCreate I start calling methods. These methods never run though. What am I doing wrong? As soon as I create the subclass object, the sub's onCreate should fire, no? And, is it even the smart way of doing this in a subclass, instead of a whole other class?
Thanks in advance!
Base class:
package com.example.TestMap;
import android.app.Activity;
import android.location.Location;
import android.os.Bundle;
import android.util.Log;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapFragment;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
public class MyActivity extends Activity {
private GoogleMap mMap;
#Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
LocationClass locationClass = new LocationClass();
}
public void setMap(){
Log.i("TestMap", "setMap");
mMap = ((MapFragment) getFragmentManager().findFragmentById(R.id.map)).getMap();
mMap.setMyLocationEnabled(true);
}
public void setCamera(Location location) {
Log.i("TestMap", "setCamera");
final LatLng locationLatLng = new LatLng( (location.getLatitude() ), location.getLongitude() );
CameraPosition cameraPosition = new CameraPosition.Builder()
.target(locationLatLng) // Sets the center of the map to Mountain View
.zoom(17) // Sets the zoom
.bearing(90) // Sets the orientation of the camera to east
.tilt(30) // Sets the tilt of the camera to 30 degrees
.build(); // Creates a CameraPosition from the builder
mMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
}
}
Subclass
package com.example.TestMap;
import android.content.Context;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import java.util.List;
public class LocationClass extends MyActivity implements LocationListener {
private LocationManager locationManager;
private String provider;
private List<String> providers;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i("TestMap", "LocationClass OnCreate");
GetProivder();
}
public void GetProivder (){
Log.i("TestMap", "LocationClass GetProivder");
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
Criteria criteria = new Criteria();
criteria.setAccuracy(2);
provider = locationManager.getBestProvider(criteria, false);
providers = locationManager.getProviders(true);
Location location = locationManager.getLastKnownLocation(provider);
Log.i("TestMap", "providerlist = " + providers);
Log.i("TestMap", "getBestProvider = " + provider);
Log.i("TestMap", "Location = " + location);
if (location != null) {
Log.i("TestMap", "Provider " + provider + " has been selected.");
super.setCamera(location);
super.setMap();
} else {
Log.i("TestMap", "location is null.");
}
}
#Override
public void onLocationChanged(Location location) {
Log.i("TestMap", "onLocationChanged");
}
#Override
protected void onResume() {
super.onResume();
locationManager.requestLocationUpdates(provider, 400, 1, this);
}
#Override
protected void onPause() {
super.onPause();
locationManager.removeUpdates(this);
}
#Override
public void onStatusChanged(String provider, int status, Bundle extras) {
Log.i("TestMap", "onStatusChanged");
}
#Override
public void onProviderEnabled(String provider) {
Toast.makeText(this, "Enabled new provider " + provider,
Toast.LENGTH_SHORT).show();
}
#Override
public void onProviderDisabled(String provider) {
Log.i("TestMap", "onProviderDisabled");
Toast.makeText(this, "Disabled provider " + provider,
Toast.LENGTH_SHORT).show();
}
}

Your code
LocationClass locationClass = new LocationClass(); does not create it. It just makes an object of it, but it does not tie to the lifecycle and call the methods.
You need to start it with an intent to make it appear (and take the entire screen based on skimming your code). Android will fire the appropriate method calls when you do so.
Like this:
Intent intent = new Intent(this, LocationClass.class);
startActivity(intent);
More information can be found here: http://developer.android.com/training/basics/firstapp/starting-activity.html#StartActivity
It must be defined in your manifest too or it will crash. There are other noticeable oddities things in your code, do you want LocationClass to extend MyActivity and not Activity? Your LocationClass also does not call setContentView() in onCreate, so you're not going to see a UI (as far as I can tell), unless you wanted it through the extends part.
EDIT :
If you extend subclass and put this intent code in onCreate, you're probably going to crash, as it will call super() in MyActivity (calling onCreate() again as it's the superclass), and will keep making more intents to start the activity. You should not subclass MyActivity if that's what the 'parent' class is.
You should only subclass Activity or a global parent activity (e.g. in my project right now, I extend SpiceActivity, as they all use common components related to Spice).

The subclass' onCreate method should be called when it is created by Android (see Understanding the Lifecycle Callbacks), which for example happens when an Intent to that Activity is issued.
Edit: I just saw #Mgamerz answer and realised that line in your superclass Activity was where you were trying to make Android create the subclass Activity. This next paragraph is somewhat irrelevant now, but note that you do still need to add the subclass activity to the manifest file.
Are you sure you're application is actually starting the subclass Activity, or is it still starting the superclass Activity? You might have to have a look at your project's "AndroidManifest.xml" file and check that there's an <activity /> element corresponding to the subclass Activity.
I think splitting the class into a superclass and a subclass is sensible if the superclass has functionality which can/will be re-used by multiple subclass Activities. For example you might have subclass activities like DirectionsActivity and SearchActivity which have some common map-related activity provided by their superclass MapActivity. Even if you have only one subclass Acitivty now, it may still make sense to have a superclass and a subclass if you think you're likely to write additional map-related activities later on. I my opinion it's not sensible to split the class into a superclass and a subclass just because the single class was getting too long. If you do just want a helper class and you don't expect to have more MapActivity subclasses in the future, you could make a MapUtils class in the same package as the Activity class, which would define some static helper methods. For example, you could put your GetProvider method into such a helper class. Sketch example (note the package-private access of MapHelper):
class MapHelper {
static Location getProvider() {
// ...
return location;
}
// Other helper methods here
}
public class MyActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
// ...
setCamera(MapHelper.getProvider());
setMap();
}
// Other activity methods here
}

Related

Android Studio: Getters for editText

I want to use the value of 2 editTexts from one activity in another. Here is my code so far. I am getting:
java.lang.NullPointerException.
The Code:
public class AddJob extends AppCompatActivity{
// vars
private BottomNavigationView bottomNavigationView;
private EditText editTextLat, editTextLng;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_job);
TextView textView = findViewById(R.id.activityTitleAddJob);
textView.setText("Add a Job");
editTextLat = findViewById(R.id.editTextLat);
editTextLng = findViewById(R.id.editTextLng);
}
public int getLatitude() {
return new Integer(editTextLat.getText().toString());
}
public int getLongitude() {
return new Integer(editTextLng.getText().toString());
}
}
The Stack Trace:
Here is the code snippet from the map class:
AddJob aj = new AddJob();
int lat = aj.getLatitude();
int lng = aj.getLongitude();
Toast.makeText(aj, lat + " " + lng, Toast.LENGTH_LONG).show();
Please read about the Activity lifecycle. You should never
create an Activity directly with
new MyActivity()
This will not launch any of the lifecycle events (onCreate, etc...) or bind it to a context, set a view hierarchy on it, or do any of the regular Activity things you may be expecting. Your program is returning null because onCreate is never called on the activity, and if you were to simply try to call it yourself it would likely crash.
If you want data from one activity to be available in another activity, an easy way to achieve this is to save the data in SharedPreferences in the AddJob activity (whenever the values are updated) and access it in MapActivity from the SharedPreferences. You can also pass the data from one Activity to the next by adding data to the Intent when you launch it.
One advantage to using SharedPreferences here is that the user's choices will be saved from one app session to the next, and if you have multiple things that can launch the MapActivity they don't have to keep passing that data to it.
hello would it not be better using an Intent
Intent i = new Intent(MainActivity.this, NEXTActivity.class);
i.putExtra("latitude", lat);
i.putExtra("longitude", lng);
startActivity(i);
NEXTActivity
Bundle extras = getIntent().getExtras();
double latitude = extras.getDouble("latitude");
double longitude = extras.getDouble("longitude");
You shouldn't create activity objects, you can start activity with Intent and pass data through it. Check out the relevant answer in the link below:
https://stackoverflow.com/a/2091482/10116426
It seems that you are instantiating the AddJob class. This may raise problems on the back-stack of android, which may create lifecycle management problems. So, as mentioned by #NoobAndroid, it is better to use the recommended way not to run into unexpected errors.
use this code .
package com.example.cloudanalogy.introscreen;
import android.support.design.widget.BottomNavigationView;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private BottomNavigationView bottomNavigationView;
private EditText editTextLat, editTextLng;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.activityTitleAddJob);
textView.setText("Add a Job");
editTextLat = findViewById(R.id.editTextLat);
editTextLng = findViewById(R.id.editTextLng);
}
public int getLatitude() {
return Integer.parseInt(editTextLat.getText().toString());
}
public int getLongitude() {
return Integer.parseInt(editTextLng.getText().toString());
}
public void onclick(View view) {
int lat = getLatitude();
int lng = getLongitude();
Toast.makeText(this, ""+lat+" and "+lng, Toast.LENGTH_SHORT).show();
}
}

java.lang.IllegalStateException: System services not available to Activities before onCreate() When trying to store users location as a variable

I have created a distance calculator for my final year project. The calculator should display the users current location and then display a marker on the map when pressed. The distance will be displayed from the users current location to the marker.
I have retrieved the users location and stored as a variable which I use in my code but I get thrown with the java.lang.IllegalStateException: System services not available to Activities before onCreate() error. I've tried placing my code from the start in the onCreate() method but this doesn't work either. Any help would be greatly appreciated. I have been trying for hours to get it working but no luck. When I try to place the (LocationManager)getSystemService(Context.LOCATION_SERVICE); in the onCreate() it requires a permission and I've tried everything.
Here is my code
package com.example.matthewmcnabb.moyola;
import android.Manifest;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.AsyncTask;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentActivity;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.view.View;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.Polyline;
import com.google.android.gms.maps.model.PolylineOptions;
import java.io.IOException;
import java.util.List;
public class MapsActivity extends FragmentActivity {
// the Google Map object
private GoogleMap mMap;
private LocationManager locationManager;
private Location mCurrentLocation;
LocationManager lm = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
public Location location = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER);
private double longitude = location.getLongitude();
private double latitude = location.getLatitude();
private LatLng STARTING_MARKER_POSITION =new LatLng(longitude, latitude);
private LatLng distanceFrom = STARTING_MARKER_POSITION;
// line will be drawn at the click event
private Polyline line=null;
// A Geocoder can transform a pair of latitude/longitude into a street address and viceversa.
// We'll use it in the listener
private static Geocoder geocoder=null;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// we set the layout for the Activity
setContentView(R.layout.activity_maps);
// the geocoder is instantiated for the first time
geocoder=new Geocoder(this);
// if there isn't a map, it will be created
setUpMapIfNeeded();
}
private GoogleMap.OnMapClickListener clickListener=new GoogleMap.OnMapClickListener() {
#Override
public void onMapClick(final LatLng pos) {
// this method is called when the user taps the map
// if a line already appears, it's removed
if (line!=null)
line.remove();
// a new line is created
line = mMap.addPolyline(new PolylineOptions()
.add(distanceFrom, pos)
.width(5) // width of the line
.color(Color.RED)); // line color
// call the converter object for geocoding invocation and distance calculation
new AddressConverter().execute(distanceFrom, pos);
}
};
#Override
protected void onResume() {
super.onResume();
// the availability of the GoogleMap will be checked before the Activity starts interacting with the user
setUpMapIfNeeded();
}
private void setUpMapIfNeeded() {
// the map is created only it has not been initialized
if (mMap == null) {
// the map is located in the layout
mMap = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map)).getMap();
// if a map exists, we proceed with initialization
if (mMap != null) {
setUpMap();
}
}
}
// Now it's time to configure the map. We can add markers, shapes, event handlers and so on
private void setUpMap() {
// the camera will be positioned according to the new coordinates
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(STARTING_MARKER_POSITION, 16));
// we choose the type of the map: Satellite in this case
mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
// markerOptions describes the marker we want to place
MarkerOptions markerOptions=new MarkerOptions()
.position(STARTING_MARKER_POSITION)
.draggable(true);
// the marker has to be draggable as we'll move it
// the marker is rendered on the map
mMap.addMarker(markerOptions);
// we define the object to invoke when the marker is dragged
mMap.setOnMarkerDragListener(new GoogleMap.OnMarkerDragListener()
{
#Override
public void onMarkerDragStart(Marker arg0)
{
// this method is called when the drag starts
// the operation we need is the cancellation of a preexisting line
if (line!=null)
line.remove();
}
#Override
public void onMarkerDragEnd(final Marker pos)
{
// we get the final position of the marker
distanceFrom=pos.getPosition();
}
#Override
public void onMarkerDrag(Marker arg0)
{
// operations performed during the movement. Nothing to do
}
});
// the callback to invoke is set
mMap.setOnMapClickListener(clickListener);
}
// we want to know which address corresponds to this location
// we use AsyncTask to perform slower operations on a separate thread
private class AddressConverter extends AsyncTask<LatLng,Void,String>
{
// The ProgressDialog window we'll show during the calculation
private ProgressDialog progress=null;
// this method is called before the background job starts. It works on the main thread
#Override
protected void onPreExecute() {
// ProgressDialog is shown
progress= ProgressDialog.show(MapsActivity.this,"Distance calculator","We are calcultating the distance...", true,false);
}
// this method works on a separate thread
// it performs geocoding operations to retrieve the address of the points and calculates the distance in meters between them
#Override
protected String doInBackground(LatLng... params) {
float[] distance=new float[1];
try {
// the Location class contains what we need to calculate distances
Location.distanceBetween(params[0].latitude,params[0].longitude,params[1].latitude,params[1].longitude,distance);
// geocoding operations
List<Address> fromResult=geocoder.getFromLocation(params[0].latitude,params[0].longitude,1);
List<Address> toResult=geocoder.getFromLocation(params[1].latitude,params[1].longitude,1);
// the message informs the user about the distance from the marker to the point selected with the click
// if we have got both the addresses, we use them to compose the message, otherwise we show only the distance
if (fromResult.size()>0 && toResult.size()>0)
{
return "The distance is " + Math.round(distance[0]) + " meters";
}
else
return "The distance is " + Math.round(distance[0]) + " meters";
}
catch (IOException e) {
return "The distance is " + Math.round(distance[0]) + " meters";
}
}
#Override
protected void onPostExecute(String message)
{
if (progress!=null)
progress.dismiss();
// The builder of the window is instantiated
AlertDialog.Builder builder=new AlertDialog.Builder(MapsActivity.this);
builder.setTitle("Distance");
builder.setMessage(message);
// the Alert dialog appears
builder.show();
}
}
// this method only formats the message with addresses
private String getAddressDescription(Address a)
{
String city=a.getLocality();
String address=a.getAddressLine(0);
return "'"+address+"' ("+city+")";
}
}
The error thrown
FATAL EXCEPTION: main
Process: com.example.matthewmcnabb.moyola, PID: 27349
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.example.matthewmcnabb.moyola/com.example.matthewmcnabb.moyola.MapsActivity}: java.lang.IllegalStateException: System services not available to Activities before onCreate()
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2515)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2723)
at android.app.ActivityThread.access$900(ActivityThread.java:172)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1422)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:145)
at android.app.ActivityThread.main(ActivityThread.java:5832)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1399)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1194)
Caused by: java.lang.IllegalStateException: System services not available to Activities before onCreate()
at android.app.Activity.getSystemService(Activity.java:5259)
at com.example.matthewmcnabb.moyola.MapsActivity.<init>(MapsActivity.java:51)
at java.lang.reflect.Constructor.newInstance(Native Method)
at java.lang.Class.newInstance(Class.java:1650)
at android.app.Instrumentation.newActivity(Instrumentation.java:1079)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2505)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2723) 
at android.app.ActivityThread.access$900(ActivityThread.java:172) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1422) 
at android.os.Handler.dispatchMessage(Handler.java:102) 
at android.os.Looper.loop(Looper.java:145) 
at android.app.ActivityThread.main(ActivityThread.java:5832) 
at java.lang.reflect.Method.invoke(Native Method) 
at java.lang.reflect.Method.invoke(Method.java:372) 
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1399) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1194) 
I get thrown with the java.lang.IllegalStateException: System services not available to Activities before onCreate() error.
That is because you are trying to call methods inherited from Activity, like getSystemService(), from a field initializer. This will not work. You need to wait until onCreate(), and usually until after super.onCreate(), before calling methods like getSystemService().
Ive tried placing my code from the start in the onCreate() method but this doesn't work either.
In this sample app, I get the LocationManager in onCreate() of a fragment:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
template=getActivity().getString(R.string.url);
mgr=
(LocationManager)getActivity().getSystemService(Context.LOCATION_SERVICE);
}
The same principle will hold with onCreate() of an activity.
When I try to place the (LocationManager)getSystemService(Context.LOCATION_SERVICE); in the onCreate() it requires a permission
You need to have a <uses-permission> element in the manifest for ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION, depending on whether you plan on using GPS_PROVIDER or NETWORK_PROVIDER.
On Android 6.0+, if your targetSdkVersion is 23 or higher, you need to implement runtime permissions, as those permissions are dangerous.
You try to get contexts and services in a constructor. This is wrong.
The constructor is executed when the object is created, before it is attached to the Android framework.
Just move member initialization to onCreate().

Android: onCreate boolean not changing

In an android application that I am developing Im using a thread, and to make sure I dont get the "java.lang.IllegalStateException: System services not available to Activities before onCreate()" I use a boolean called donecreate. Problem is that Android studio says I have a "java.lang.NullPointerException at picLoop.run(picLoop.java:24)"
Code main class:
import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.media.AudioManager;
import android.os.Bundle;
import android.view.Display;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
public class main extends Activity {
public Boolean donecreate;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(new eyeCanvas(this));
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
docreate();
}
public void docreate(){
donecreate = true;
}
public void checkHead(){
AudioManager am = (AudioManager)getSystemService(AUDIO_SERVICE);
if(am.isWiredHeadsetOn()){
Toast.makeText(getApplicationContext(), "HEADPHONES", Toast.LENGTH_LONG).show();
}
}
}
Code: pic loop
import android.graphics.Canvas;
//**Threading
public class picLoop extends Thread {
private eyeCanvas eye;
private main main = new main();
public picLoop(eyeCanvas eye) {
this.eye = eye;
}
#Override
public void run(){
Canvas c = null;
while(true) {
if(main.donecreate){ //<-- where error is
main.checkHead();
}
try {
// head.onCreate(Bundle savedInstanceState);
c = eye.getHolder().lockCanvas();
synchronized (eye.getHolder()) {
eye.onDraw(c);
}
} finally {
if (c != null) {
eye.getHolder().unlockCanvasAndPost(c);
}
}
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Also if you guys could give me feedback on how I submitted, It would help :)
You can't create activities like you're trying to do. You can NEVER EVER do 'new Activity()', as the activity needs to be launched by the system to get set up properly and go through its lifecycle as intended.
So remove the line private main main = new main();.
To do what you're trying, make the boolean a static variable.
Change
public Boolean donecreate;
to
public static Boolean donecreate;
Then you can access it like you're trying to do, without creating an instance of main Activity.
There are a large number of things wrong with the assumptions you're making. Firstly, if your Thread requires your Activity to be created, don't start it until your Activity is created. Manage the lifecycle of this object within the Activity itself, i.e.:
#Override
public void onStart() {
super.onStart();
// Start your work here
}
#Override
public void onStop() {
super.onStop();
// Stop your work here
}
Secondly, please don't use the static access approach being recommended -- that makes the assumption that there is only one Activity instance (which is wrong immediately on a configuration change, and/or if you start another instance of that Activity in the task). And even if that assumption were true, you would need to set it back to false in onDestroy() (still, don't do that).
try setting donecreate to false initially
public class main extends Activity {
public Boolean donecreate = false;
#Override
protected void onCreate(Bundle savedInstanceState) {
....
You can request system service on application context, look at this answer.
So create a static variable in application class, initialize it like instance = this; in onCreate of Application class and then you'll be able to get app context whenever you want.

Why must a callback to a method in an Android activity be static?

In a barebones Android application, I want to call a method in the MainActivity instance, from an asynchronous callback in the instance of another class. Why does Java force me to call a static method in the class, rather than a non-static method in the instance itself?
My app has a MainActivity class and a TextToSpeech (TTS) class. From the main activity, I instantiate the class, passing a pointer to the MainActivity instance. Instantiation of the TTS engine is an asynchronous operation. I cannot interact with the TTS instance until it has triggered an onInit() method.
Below is code that works. However, I had imagined that I would be able to call a non-static method in the MainActivity instance, and this appears not to be possible. The code below uses a static call to the MainActivity class itself, so no instance variables are available.
Here are the changes I made to a basic Hello World application in Android Studio.
//MainActivity.java
package com.example.callback;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends ActionBarActivity {
private static TTS tts; // apparently this has to be static
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onResume() {
super.onResume();
tts = new TTS();
Log.d("onResume", this.toString());
// D/Main﹕ com.example.callback.MainActivity#4a014e50
tts.init(this);
}
// Apparently this method has to be static
public static void ttsReady() {
tts.speakText("Hello world");
}
}
Custom class.
// TTS.java
package com.example.callback;
import android.app.Activity;
import android.content.Context;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import java.util.HashMap;
import java.util.Locale;
public class TTS implements TextToSpeech.OnInitListener {
private TextToSpeech tts;
private Activity activity;
public void init(Activity currentActivity) {
activity = currentActivity;
Context context = activity.getApplicationContext();
tts = new android.speech.tts.TextToSpeech(context, this);
}
#Override
public void onInit(int status) {
if (status == TextToSpeech.SUCCESS) {
tts.setLanguage(Locale.UK);
Log.d("onInit", activity.toString());
// D/onInit﹕ com.example.callback.MainActivity#4a014e50
// activity.ttsReady();
// The line above does not compile. ttsReady() is not recognized as a
// method of activity, regardless of whether the tts variable and the
// ttsReady() method in the MainActivity class are made static or not,
// Making the activity variable static here has no effect either.
// The commented line below throws Error:(31, 35)
// error: non-static method toString() cannot be referenced from a static context
// So presumably MainActivity is a static variable.
// Log.d("onInit", MainActivity.toString())
// This works, if the tts variable and the ttsReady() method in the
// MainActivity class are made static. Is there a non-static alternative?
MainActivity.ttsReady();
}
}
// Deprecated signature for speak() used for compatibility with API 20
// and earlier
public void speakText(String toSpeak) {
int mode = android.speech.tts.TextToSpeech.QUEUE_FLUSH;
// Object hashMap = null; // Causes a "no suitable method error".
// How is HashMap null not the same as Object null? Using just plain null
// instead of hashMap also works with no problems.
HashMap hashMap = null;
tts.speak(toSpeak, mode, hashMap);
}
}
#Override
public void onInit(int status) {
if (status == TextToSpeech.SUCCESS) {
tts.setLanguage(Locale.UK);
Log.d("onInit", activity.toString());
((MainActivity)activity).ttsReady();
}
}
You are not referring to the instance of MainActivity when you say MainActivity.ttsReady(). That is why you can only access static methods. You need to make a call to the instance itself which you have stored in the activity variable.
Edit:
For a complete solution based on what you're trying to accomplish, I would probably set it up this way:
// TTSActivity.java
public abstract class TTSActivity extends Activity implements TextToSpeech.OnInitListener {
private TextToSpeech tts;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
tts = new TextToSpeech(this, this);
}
#Override
public void onInit(int status) {
if (status == TextToSpeech.SUCCESS) {
tts.setLanguage(Locale.UK);
ttsReady();
}
}
public void speakText(String toSpeak) {
int mode = android.speech.tts.TextToSpeech.QUEUE_FLUSH;
tts.speak(toSpeak, mode, null);
}
protected TextToSpeech getTts() {
return tts;
}
protected abstract void ttsReady();
}
then MainActivity becomes:
// MainActivity.java
public class MainActivity extends TTSActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
#Override
protected void ttsReady() {
speakText("Hello world");
}
}
Any Activity you want to use TTS in, all you have to do is extend TTSActivity and implements ttsReady().
As you can see, this is much more concise and easier to understand. However, your exact implementation will of course be dependent upon all the requirements you have in your particular application.
#zaventh's answer helped me to realize that a Java instance needs to be cast to the right class or interface for its methods to be found in the inheritance hierarchy. I have now rewritten my barebones project to include an interface that declares the ttsReady() method.
Here is the solution that allows me to access the methods of the MainActivity instance in a generic way.
TTSUser Interface
package com.example.callback;
interface TTSUser {
void ttsReady();
}
MainActivity
package com.example.callback;
import android.app.Activity;
import android.os.Bundle;
// TTUser Interface ensures the existence of the ttsReady() method in every instance
public class MainActivity extends Activity implements TTSUser {
private TTS tts; // non-static
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onResume() {
super.onResume();
tts = new TTS();
tts.init(this);
}
public void ttsReady() { // non-static
tts.speakText("Hello world");
}
}
TTS Class
package com.example.callback;
import android.app.Activity;
import android.content.Context;
import android.speech.tts.TextToSpeech;
import java.util.Locale;
public class TTS implements TextToSpeech.OnInitListener {
private TextToSpeech tts;
private TTSUser activity;
// Use the TTSUser interface in the signature
public void init(TTSUser activity) {
this.activity = activity;
// Cast to generic Activity, to access .getApplicationContext()
Context context = ((Activity) activity).getApplicationContext();
tts = new android.speech.tts.TextToSpeech(context, this);
}
#Override
public void onInit(int status) {
if (status == TextToSpeech.SUCCESS) {
tts.setLanguage(Locale.UK);
activity.ttsReady(); // accessible through the TTSUser interfaceg
}
}
// Deprecated signature for speak() used for compatibility with API 20
// and earlier
public void speakText(String toSpeak) {
int mode = android.speech.tts.TextToSpeech.QUEUE_FLUSH;
tts.speak(toSpeak, mode, null);
}
}

Outsource GPS functionality in android app to a separate class

I followed http://developer.android.com/guide/topics/location/obtaining-user-location.html and this worked fine when being in the activity in the onCreate-method.
Then I wanted to outsource this functionality in a separate class called LocationHelper.
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
public class LocationHelper {
public Context mContext;
public Location loc;
public LocationHelper (Context mContext){
this.mContext = mContext;
// Acquire a reference to the system Location Manager
LocationManager locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
// Define a listener that responds to location updates
LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
// Called when a new location is found by the network location provider.
setLocation(location);
}
public void onStatusChanged(String provider, int status, Bundle extras) {}
public void onProviderEnabled(String provider) {}
public void onProviderDisabled(String provider) {}
};
// Register the listener with the Location Manager to receive location updates
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListener);
}
public void setLocation(Location location) {
this.loc = location;
}
public Location getLocation() {
return this.loc;
}
}
In the activity I do this; basically I want to pull (for testing purposes!) the GPS coordinates from my helper class and display it. Problem being, the location always is null.
public class GraffitiWall extends Activity {
private TextView tv;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
tv = new TextView(this);
LocationHelper gpsie = new LocationHelper(this);
while (true){
makeUseOfNewLocation(gpsie.getLocation());
}
}
public void makeUseOfNewLocation(Location loc){
if (loc == null){return;}
tv.setText("" + loc.getLatitude());
setContentView(tv);
}
}
What am I missing and doing wrong?
Putting an infinite loop in your onCreate method is a bad idea. Your problem is most likely being caused because onCreate is never completing and passing control back to the OS. I wouldn't be surprised if this caused Force Close errors.
Maybe what you need to do is create a service which will do your location monitoring and update your activity from there.

Categories