A question on activity lifecycle and resource deletion/allocation - android

I've been puzzled very much by this lifecycle thing, so I did a little experiement. Long story short: The result shows that when a process is created after having been destroyed, UI objects allocated in the last session were all gone and need be re-created (which is expected). But other memory space allocated in the last session are still available for this session.
The surprise to me is: system's UI objects (like ListView) and memory space allocated by me are not destroyed at the same time. Why they don't die (or stay alive) at the same time???
See the experiement here:
public class PracticeActivity extends ListActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// If there is left-over value in G.count[99], then do not populate
// the ListView.
if (G.count[99] == 0) {
ListView lv = getListView();
lv.setAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, m_Starbucks));
}
Log.d("TAG", MessageFormat.format("Created, count = {0,number}", G.count[99]));
Log.d("TAG", MessageFormat.format("Starbucks = {0}", m_Starbucks[0]));
G.count[99]++; // increment the count
m_Starbucks[0] = "Coffee Frappuccino"; // and change the menu
}
#Override public void onRestart() { super.onRestart(); Log.d("TAG", "Restarted"); }
#Override public void onStart() { super.onStart(); Log.d("TAG", "Started"); }
#Override public void onResume() { super.onResume(); Log.d("TAG", "Resumed"); }
#Override public void onPause() { super.onPause(); Log.d("TAG", "Paused"); }
#Override public void onStop() { super.onStop(); Log.d("TAG", "Stopped"); }
#Override public void onDestroy() {
super.onDestroy();
if (isFinishing())
Log.d("TAG", "Destroyed -- someone finished me");
else
Log.d("TAG", "Destroyed -- system needs resources");
}
private static final String[] m_Starbucks = {
"Latte", "Cappuccino", "Caramel Macchiato", "Americano", "Mocha",
"White Mocha", "Mocha Valencia", "Cinnamon Spice Mocha",
"Toffee Nut Latte", "Espresso", "Espresso Macchiato",
"Espresso Con Panna"
};
}
Here is the class G, defined in G.java file:
public class G {
public static int[] count = new int[100];
}
Running this test produced the following results:
Created, count = 0
Starbucks = Latte
Started
Resumed
Paused
Stopped
Destroyed -- someone finished me
Created, count = 1
Starbucks = Coffee Frappuccino
Started
Resumed
In the first session, count[99]'s value was 0, so the program went to populate the ListView, so everything was fine.
In the 2nd session, count[99] still holds the value left over from the first session, so the program did not populate the ListView, in hope that the ListView would also be available too. But it's not, the result is a black screen. This means G.count[] was retained (and so is m_Starbucks[]) from last session, but the ListView didn't survive.
It's apparent that there's only one instance of PracticeActivity in the system, when this instance dies, both PracticeActivity and G classes should die too. But they didn't, they still retain values from the last session.
QUESTIONS:
If count[] and m_Starbucks[] are
still available, then this means
PracticeActivity and G are both
alive too. Then why the ListView is
gone? Shouldn't all of them die or
live at the same time?
When I see some of my classes'
members hold their old values from
last session, can I trust that
all of my classes' members are also valid??? I.e., does Android
kill my resources in an all-or-none
fashion? Or, it can delete some and
leave some others? (This question
shouldn't have existed in the first
place, but seeing the result of the
experiement, one starts to wonder.)
Can anybody shed some light on this? Much appreciated.

Static class members live as long as JVM (DVM) lives - which may be (and certainly is) longer than
your activity lifecycle. Your activity could be destroyed, but static fields survive it.

If count[] and m_Starbucks[] are still
available, then this means
PracticeActivity and G are both alive
too.
No. count and m_Starbucks are both declared static. Per Java documentation:
"Class Variables (Static Fields) A
class variable is any field declared
with the static modifier; this tells
the compiler that there is exactly one
copy of this variable in existence,
regardless of how many times the class
has been instantiated"
So say you do the following: (pretend this isn't an activity and you can conveniently just construct it)...
PracticeActivity example1 = new PracticeActivity();
PracticeActivity example2 = new PracticeActivity();
Then you do not have example1.m_Starbucks[0] and example2.m_Starbucks[0] as distinct variables. Instead, you just have PracticeActivity.m_Starbucks[0] and any specific instance of that class has the same variable. Therefore it is unaffected by (unrelated to!) the destruction of the actual instance of your Activity. And in fact, they exist, even if you have never constructed an instance of the class that contains them.
Also, if you change example1.m_Starbucks[0], you will find that example2.m_Starbucks[0] also has changed -- because, again, there's only one array.
The simple answer here is that you shouldn't be using static variables for this type of storage. It's safe to use static for constants and some other special cases, but never as member variables that you expect to hold attributes of a given instance of a class, that make that instance uniquely different from other classes.

Related

OnSaveInstanceState/ RestCalls

I am relatively new to Android development, and I have a question about onSaveInstanceState(). I am currently working on a login Activity for an app. To check to see if the user can login to their account, I perform a rest call to a server and, based on the response-code, see if I should grant access to the user. The root of my question is based on the fact that I am trying to avoid passing the Activity's Context to my rest-call class. To do this, I create a boolean field in my login Activity representing whether or not the rest-call was successful and a runnable that updates said boolean that I pass to the rest-call class. I know this goes against the idea of an AsyncTask, but I can't find any alternative to simply putting up a dialog box telling the user to wait while this happens. My questions are below.
1) If I use savedInstanceState() in the onCreate method, how do I instantiate this boolean field for the first time barring null checking an Object boolean? What I mean by this is that after the Activity is destroyed for whatever reason (such as orientation change, etc...) I will use the boolean value stored in my overriden onSaveInstanceState method; however, when it is created for the first time, it has no reference to a boolean value so it has to create one.
2) Does this Runnable even help? I did it so that I wouldn't have to pass the context, but if the Activity is going to be deleted before the RestCall(AsyncTask) is complete, does it really matter whether you pass the context or a Runnable affecting a field of the Activity? The more I think about this, the more I believe it is not going to make much of a difference as it will still result in it pointing to a non-existent object. I am trying to avoid using a Singleton design as I have gathered it is not optimal, but because of the potential lag in time with an AsyncTask, I am beginning to think that it may not be avoidable.
I know onSaveInstanceState() is a topic that has been brought up a lot on StackOverflow, however, I could not find an answer to these questions. I apologize if there has already been a thread for this, but any help or guidance on this would be greatly appreciated! Thank You!
Login Activities' setup:
public class LoginActivity extends Activity implements View.OnClickListener {
private EditText username_et;
private EditText password_et;
private Button login_b;
private boolean login_success = true;
private Runnable run;
/**
* Instances created when app starts
*/
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.view_login);
// login_success = false;
login_success = savedInstanceState.getBoolean("login_success");
username_et = (EditText) findViewById(R.id.username_text);
username_et.setOnClickListener(LoginActivity.this);
password_et = (EditText) findViewById(R.id.password_text);
password_et.setOnClickListener(LoginActivity.this);
login_b = (Button) findViewById(R.id.login_button);
login_b.setOnClickListener(LoginActivity.this);
run = new Runnable() {
#Override
public void run() {
login_success = true;
}
};
}
#Override
public void onSaveInstanceState(Bundle savedInstanceState){
super.onSaveInstanceState(savedInstanceState);
savedInstanceState.putBoolean("login_success", login_success);
}
Congratulations. You just discovered Android's dirty little secret.
AsyncTask has an inherent design flaw. It doesn't deal well with configuration changes that happen during background task execution because of exactly the problem you mentioned. It needs to hold a reference to the activity, but there's no guarantee that the reference will still be valid by the time the background task completes.
Here are two ways to overcome this problem:
I refer you to Alex Lockwood's excellent blog post on using hidden fragments with setRetainInstance(true) to span activity destruction and recreation. This is a more involved solution than the next one, but this solution has the advantage that you can still report progress with callbacks. If you were intending to call publishProgress() in your AsyncTask, then this is the method you should use.
Use a Loader. Loaders were designed around database data retrieval in the background, but the fact is that they can also be used to handle remote server access in the background as well. I use a Loader for the majority of my remote server tasks.
Here's an example:
public static class ResetPasswordLoader extends AsyncTaskLoader<Pair<CharSequence, Exception>> {
private static final String TAG = "ResetPasswordLoader ";
private String mEmail;
public ResetPasswordLoader(Context context, String email) {
super(context);
mEmail = email;
// set the content-changed flag
onContentChanged();
}
#Override
protected void onStartLoading() {
// only start the load if the content-changed flag is set
// takeContentChanged() returns the value of the flag before it is cleared
if (takeContentChanged()) {
forceLoad();
}
}
#Override
public Pair<CharSequence, Exception> loadInBackground() {
CharSequence result = null;
Exception exc = null;
try {
result = Service.getInstance().resetPassword(mEmail);
} catch (RemoteServiceException e) {
exc = e;
Log.e(TAG, "loadInBackground(), email = " + mEmail, e);
}
return new Pair<>(result, exc);
}
}
Also, in my onLoadFinished() override I make sure to call loaderManager.destroyLoader() on the loader's id.
Again, Alex Lockwood's blog has some great articles on loaders as well.
For the UI, something I do frequently is put up a indeterminate progress bar over the UI upon calling loaderManager.initLoader(). I also set a boolean like mProgressShown. This boolean gets saved in onSaveInstanceState, so when the activity/fragment is created again, I restore the boolean value which tells me to show the progress bar immediately. Some time later onLoadFinished will be called and I clear mProgressShown and hide the progress bar.

Is it necessary to save singletons?

Problem:
I'm saving some data in a singleton class... Sometimes it happens, that this singleton returns null data, from which I derive, that it was destroyed...
My idea/thoughts:
Actually, I thought, the singleton will live as long as the application lives and as long as the application remembers anything else like fragments state for example, my singleton will exist with it's last data too. Isn't this correct?
concrete problem:
My case is following: I go from my main fragment to a sub fragment and save an selected object in my singleton... I stop using my app and come back after some time. My app remembers it's state and recreates the fragments, my fragment want to get the selected object from my singleton and get's null.
I thought, a singleton should exist as long as the application exists and therefore needs no saving... Whenever the application is ended, nothing will be restored anyway and my app won't remember anything, so that's ok anyway. Is that a wrong assumption?
I need an answer to this question, because if I'm sure, that above thoughts are correct, I at least know, that I have to search for the problem somewhere else...
Here is a short summury of what I've found out (or have had forgotten)
Activitys can be recreated, although the application was destroyed
Singletons can be garbage collected if not referenzed from somewhere
So you HAVE TO SAVE your singletons! Otherwise, whenever your phone is on low memory, it may kill the application and create a NEW application, but RECREATE the activities...
For me, as I'm actually always use a single activity with fragments, it is easy to solve the problem:
when I create an activity, I call a static restore function (BEFORE calling get!!!)
in the onSaveInstanceState of the activity a always save the singleton to the bundle
=> so my singleton looks like following (base structure)
public class DataCache implements Parcelable
{
private static final String TAG = DataCache.class.getName();
private static DataCache mCache = null;
public static synchronized final DataCache get()
{
if (mCache == null)
mCache = new DataCache();
return mCache;
}
private DataCache()
{
// init...
}
public void save(Bundle outState)
{
outState.putParcelable(TAG, this);
}
public static void restore(Bundle savedInstanceState)
{
if (savedInstanceState != null && savedInstanceState.containsKey(TAG))
{
// only restore, if necessary, i.e. if application was destroyed but activity saved our last cache
if (mCache == null)
mCache = savedInstanceState.getParcelable(TAG);
}
}
}

Can I have a multi-process global variable?

I have a real problem using my app that involve 2 processes. One process its executing a Service (p1) and the other the GUI (p2).
I have a class in p2 that implements the use of an object (iThing) that is custom memory managed (and its static). It has to be like this bacause of Android OS implementation of destroying the views whenever he wants.
public class Connections{
public static int iGlobalCounter=0;
public static Object iThing;
public static void start(){
iGlobalCounter++;
Log.d("PROCESS", "UP: "+iGlobalCounter);
if (iGlobalCounter<=1){
//Create the object "iThing"
}
}
public static int stop(){
iGlobalCounter--;
Log.d("PROCESS", "DOWN: "+iGlobalCounter);
if (iGlobalCounter<=0){
//Destroy the object "iThing"
}
}
}
The main GUI (in p2), starts and stops the variable on the onCreate / onDestroy (for all views in my app)
public class MyMainClass extends Activity{
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Connections.start();
}
#Override
public void onDestroy(){
super.onDestroy();
Connections.stop();
}
}
Finally in p1 I have the service, which also needs the variable, so, it does the same
public class MyMainService extends Service{
#Override
public void onCreate() {
super.onCreate();
Connections.start();
}
#Override
public void onDestroy(){
super.onDestroy();
Connections.stop();
}
}
The problem is that if I use only p2 (GUI), it goes all well, but when I execute the service (in p1), the counter doesn't increment from the last state, but from 0, resulting in destroying the object when leaving the service, not the app.
if do this navigation, I get the following counters:
MyMainClass (1) --> OtherClass (2) --> AnotherClass (3) --> MyMainService (1)
My question is if there is a way of having a multi-process global variable? As it seems that every process takes its own static variables and are not "real static". A solution could be using SharedPreferences to save the state, but not really nice solution, as it hasn't to be saved when leaving the app.
Thanks,
PAU
I think that you should extend Application class and put your globalVariable there.
You can store your global data in shared memory (see MemoryFile).
To synchronize access to the file, I think the best approach is to implement some sort of spinlock using the same memory file.
In and case, I don't know a simply way of doing this.
You have the following options which you can look into for sharing data between different processes,
Message Queue,
Named Pipes,
Memory mapped files
WCF on Named Pipes or MSMQ

Recycling attributes from destroyed activities?

I have a doubt about destruction of activities and objects.
While I attach & detach the activity from the AsyncTask I do not change the ArrayAdapter from the asynctask (see code). So, what I get is multiple activities being attached & detached (ought to orientation changes) and just one task running and modifying ONE adapter, which in turn is the one from the first activity that created the task. So, when I attach the task in the onCreate() I just set the adapter with the one which holds the task, which in turn has all the values processed (in the example just a dummie list of numbers).
How can this be possible? I thought that onDestroy() would erase the activity itself and its attributes, and therefore I would get a null pointer exception or something like that while trying to access the ArrayAdapter of the original activity from the AsynkTask, but the code below works!
private static class TestingTask extends AsyncTask<Void, Integer, Void> {
private TestingActivity mActivity; // extends ListActivity
private ArrayAdapter<String> mAdapter;
private boolean mIsFinished;
private TestingTask(Context activity) {
attach(activity);
mAdapter = (ArrayAdapter<String>)mActivity.getListAdapter();
mIsFinished = false;
}
private void attach(Context activity) {
mActivity = (TestingActivity)activity;
}
private void detach() {
mActivity = null;
}
protected Void doInBackground(Void... params) {
for (int i = 0; i < 100000; i++) {
publishProgress(i);
}
return null;
}
protected void onProgressUpdate(Integer... values) {
if (!isCancelled()) {
mAdapter.add(values[0].toString());
}
}
// ...
}
Is this because the task keeps an active reference to the ArrayAdapter object, and therefore it is not deleted? Or is it something else?
I also experienced another "similar case" in which I returned an Activity's attribute from onRetainNonConfigurationInstance() let's say A a, that had visibility over B b (which is another attribute of the Activity). Then, when trying to access b instance through a, there is no problem and I thought I would need a wrapper to hold the two instances (a and b), or else I would get an exception when trying to access b (which I do not actually save). I do not know if it is related width the previous case in which the objects that I supposed not to be available actually are there, maybe because of the active reference to them that causes no deletion?
Thank you!
I think I have found the answer to these questions and as I was wondering... it is related to the Garbage Collector and the use of strong references.
In Understanding weak references article it is said that:
if an object is reachable via a chain of strong references (strongly reachable), it is not eligible for garbage collection. As you don't want the garbage collector destroying objects you're working on, this is normally exactly what you want
In another article How Gargabe Collection works it is explained that:
if an object holds reference of another object and when you set container object's reference null, child or contained object automatically becomes eligible for garbage collection.
So, my conclusion is that:
In the first case: As I am setting activity to null in detach() there is no memory leak and all objects can be garbage collected unless the adapter, which has a strong reference. So, I understand that the activity and all other objects contained by it are deleted unless the adapter, this is what I actually want.
In the second case: As I am returning the container object (A a) in onRetainNonConfigurationInstance() and it has a strong reference to (B b), b instance is accessible too, because it can be reachable via a chain of strong references.
Hope this will be helpful. If anyone else wants to give his/her opinion it will be welcome!

Google Analytics in Android app - dealing with multiple activities

I was pretty excited to see how easy it is to set up Google Analytics with my app, but the lack of documentation has me sitting with a few questions. The only information that I can find is right from the documentation here, which only looks at reporting PageViews and Events from one Activity. I want to report PageViews and Events across multiple Activities in my app.
Right now in the onCreate() of all of my activities, I am calling:
tracker = GoogleAnalyticsTracker.getInstance();
tracker.start("UA-xxxxxxxxx", this);
And in the onDestroy() of all of my activities:
tracker.stop();
I then track PageViews and Events as needed, and Dispatch them along with another HTTP request I am performing. But I'm not so sure this is the best way. Should I be calling start() and stop() in each activity, or should I only call start() and stop() in my main launcher activity?
The problem with calling start()/stop() in every activity (as suggested by Christian) is that it results in a new "visit" for every activity your user navigates to. If this is okay for your usage, then that's fine, however, it's not the way most people expect visits to work. For example, this would make comparing android numbers to web or iphone numbers very difficult, since a "visit" on the web and iphone maps to a session, not a page/activity.
The problem with calling start()/stop() in your Application is that it results in unexpectedly long visits, since Android makes no guarantees to terminate the application after your last activity closes. In addition, if your app does anything with notifications or services, these background tasks can start up your app and result in "phantom" visits. UPDATE: stefano properly points out that onTerminate() is never called on a real device, so there's no obvious place to put the call to stop().
The problem with calling start()/stop() in a single "main" activity (as suggested by Aurora) is that there's no guarantee that the activity will stick around for the duration that your user is using your app. If the "main" activity is destroyed (say to free up memory), your subsequent attempts to write events to GA in other activities will fail because the session has been stopped.
In addition, there's a bug in Google Analytics up through at least version 1.2 that causes it to keep a strong reference to the context you pass in to start(), preventing it from ever getting garbage collected after its destroyed. Depending on the size of your context, this can be a sizable memory leak.
The memory leak is easy enough to fix, it can be solved by calling start() using the Application instead of the activity instance itself. The docs should probably be updated to reflect this.
eg. from inside your Activity:
// Start the tracker in manual dispatch mode...
tracker.start("UA-YOUR-ACCOUNT-HERE", getApplication() );
instead of
// Start the tracker in manual dispatch mode...
tracker.start("UA-YOUR-ACCOUNT-HERE", this ); // BAD
Regarding when to call start()/stop(), you can implement a sort of manual reference counting, incrementing a count for each call to Activity.onCreate() and decrementing for each onDestroy(), then calling GoogleAnalyticsTracker.stop() when the count reaches zero.
The new EasyTracker library from Google will take care of this for you.
Alternately, if you can't subclass the EasyTracker activities, you can implement this manually yourself in your own activity base class:
public abstract class GoogleAnalyticsActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Need to do this for every activity that uses google analytics
GoogleAnalyticsSessionManager.getInstance(getApplication()).incrementActivityCount();
}
#Override
protected void onResume() {
super.onResume();
// Example of how to track a pageview event
GoogleAnalyticsTracker.getInstance().trackPageView(getClass().getSimpleName());
}
#Override
protected void onDestroy() {
super.onDestroy();
// Purge analytics so they don't hold references to this activity
GoogleAnalyticsTracker.getInstance().dispatch();
// Need to do this for every activity that uses google analytics
GoogleAnalyticsSessionManager.getInstance().decrementActivityCount();
}
}
public class GoogleAnalyticsSessionManager {
protected static GoogleAnalyticsSessionManager INSTANCE;
protected int activityCount = 0;
protected Integer dispatchIntervalSecs;
protected String apiKey;
protected Context context;
/**
* NOTE: you should use your Application context, not your Activity context, in order to avoid memory leaks.
*/
protected GoogleAnalyticsSessionManager( String apiKey, Application context ) {
this.apiKey = apiKey;
this.context = context;
}
/**
* NOTE: you should use your Application context, not your Activity context, in order to avoid memory leaks.
*/
protected GoogleAnalyticsSessionManager( String apiKey, int dispatchIntervalSecs, Application context ) {
this.apiKey = apiKey;
this.dispatchIntervalSecs = dispatchIntervalSecs;
this.context = context;
}
/**
* This should be called once in onCreate() for each of your activities that use GoogleAnalytics.
* These methods are not synchronized and don't generally need to be, so if you want to do anything
* unusual you should synchronize them yourself.
*/
public void incrementActivityCount() {
if( activityCount==0 )
if( dispatchIntervalSecs==null )
GoogleAnalyticsTracker.getInstance().start(apiKey,context);
else
GoogleAnalyticsTracker.getInstance().start(apiKey,dispatchIntervalSecs,context);
++activityCount;
}
/**
* This should be called once in onDestrkg() for each of your activities that use GoogleAnalytics.
* These methods are not synchronized and don't generally need to be, so if you want to do anything
* unusual you should synchronize them yourself.
*/
public void decrementActivityCount() {
activityCount = Math.max(activityCount-1, 0);
if( activityCount==0 )
GoogleAnalyticsTracker.getInstance().stop();
}
/**
* Get or create an instance of GoogleAnalyticsSessionManager
*/
public static GoogleAnalyticsSessionManager getInstance( Application application ) {
if( INSTANCE == null )
INSTANCE = new GoogleAnalyticsSessionManager( ... ,application);
return INSTANCE;
}
/**
* Only call this if you're sure an instance has been previously created using #getInstance(Application)
*/
public static GoogleAnalyticsSessionManager getInstance() {
return INSTANCE;
}
}
The SDK now has a external library which takes care of all of this. Its called EasyTracker. You can just import it and extend the provided Activity or ListActivity, create a string resource with your code and you are done.
The tracker will only track the activity where it's executed. So, why don't you subclass an Activity which start it every time on onCreate:
public class GAnalyticsActivity extends Activity{
public void onCreate(Bundle icicle){
super.onCreate(icile);
tracker = GoogleAnalyticsTracker.getInstance();
tracker.start("UA-xxxxxxxxx", this);
}
// same for on destroy
}
Then, you extends that class for every activity you use:
public class YourActivity extends GAnalyticsActivity{
public void onCreate(Bundle icicle){
super.onCreate(icile);
// whatever you do here you can be sure
// that the tracker has already been started
}
}
The approach I am using is to use a Bound Service (I happen to be using one already so was spared the creation of extra boiler plate code.)
A Bound Service will only last as long as there are Activities bound to it. All the activities in my app bind to this service, so it lasts only as long as the user is actively using my application - therefore very much a real 'session'.
I start the tracker with a singleton instance of Application which I have extended and added a static getInstance() method to retrieve the instance:
// Non-relevant code removed
public IBinder onBind(Intent intent) {
tracker = GoogleAnalyticsTracker.getInstance();
tracker.startNewSession(PROPERTY_ID, MyApp.getInstance());
}
public boolean onUnbind(Intent intent) {
tracker.stopSession();
}
See: http://developer.android.com/guide/topics/fundamentals/bound-services.html
I did a time based split between visits in my app, working like this:
I've build a wrapper singleton Tracker object for the GoogleAnalyticsTracker where i keep the last time something got tracked. If that time's more then x seconds i treat it as a new visit.
Of course this is only useful if you track everything in your app, and may not be the best solution in every situation, works well for my app though.
It only supports trackPageView, but setCustomVar and trackEvent should be easily implemented..
Anywhere you need to track something just add the line:
Tracker.getInstance(getApplicationContext()).trackPageView("/HelloPage");
I usually do it in the onResume of an activity
Tracker gist
You will need something like this: http://mufumbo.wordpress.com/2011/06/13/google-analytics-lags-on-android-how-to-make-it-responsive/
That's on the previous version and used to work very well. Now I'm in the same struggle as you, as V2 doesn't seems to be very consistent.
I wonder if this is something that could be done using AOP.
Android can only use compile-time AOP methods so maybe something like AspectJ?
There's a little more info on using AspectJ in Android in this thread. The main issue being that you would still need to declare on classes you own.

Categories

Resources