Trying to detect ActionMode memory leak - java

I have been trying to find the source of ActionMode memory leak for days now without luck. I have an activity with several fragments and when I leave the fragment having ActionMode (while auto cancelling it), LeakCanary detects a memory leak.
I have nulled both ActionMode and ActionMode.Callback on destroy() and even tried doing it on onDestroyActionMode().
Here is my LeakCanary screenshot:
https://i.imgur.com/RUbdqj3.png
I hope someone points me in the right direction.
P.S. I have suspected it has something to do with ActionMode.Callback. Though, I could not find any methods for the CallBack that destroys it. I start the ActionMode using startSupportActionMode(mActionModeCallback). I have tried to find a method to remove the mActionModeCallback from that, too, but no methods.
Here is my full ActionMode code:
private ActionMode mActionMode;
private ActionMode.Callback mActionModeCallback;
public void startCAB()
{
if (mActionMode == null)
mActionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(mActionModeCallback);
}
private void buildActionModeCallBack()
{
mActionModeCallback = new ActionMode.Callback() {
#Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.menu_cab, menu);
return true;
}
#Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
#Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
... Some Code ...
}
}
#Override
public void onDestroyActionMode(ActionMode mode) {
mActionMode = null;
mActionModeCallback = null; // Tried with and without this.
}
};
}
public void finishActionMode()
{
mActionMode.finish();
}
#Override
public void onDestroy()
{
super.onDestroy();
mActionMode = null;
mActionModeCallback = null;
}
Parent Activity containing fragments:
#Override
public void onTabUnselected(TabLayout.Tab tab)
{
clearCAB();
}
private void clearCAB()
{
int index = mPagerAdapter.getCurrentFragmentIndex();
FragmentOne fragmentOne = (FragmentOne) mPagerAdapter.instantiateItem(mViewPager, index);
fragmentOne.finishActionMode();
}

According to my experience, if your ActionMode.Callback object use the Anonymous inner class it may cause your fragment memory leak.
Maybe you can create a new class and implements ActionMode.Callback then use it to put in startSupportActionMode() parameter:
public class YourFragment extends skip implements skip, ActionMode.Callback {
private ActionMode mActionMode;
public void startCAB()
{
if (mActionMode == null)
mActionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(new SafeActionModeCallback(this));
}
public void finishActionMode()
{
mActionMode.finish();
}
#Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.menu_cab, menu);
return true;
}
#Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
#Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
// ... Some Code ...
}
}
#Override
public void onDestroyActionMode(ActionMode mode) {
mActionMode = null;
}
}
SafeActionModeCallback:
public class SafeActionModeCallback implements ActionMode.Callback {
// you can also use the WeakReference
private ActionMode.Callback callback;
public SafeActionModeCallback(ActionMode.Callback callback) {
this.callback = callback;
}
#Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return callback.onCreateActionMode(mode, menu);
}
#Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return callback.onPrepareActionMode(mode, menu);
}
#Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return callback.onActionItemClicked(mode, item);
}
#Override
public void onDestroyActionMode(ActionMode mode) {
callback.onDestroyActionMode(mode);
callback = null;
}
}

It seems the ActionMode in the activity has a reference to the fragment's layout which is causing the memory leak and preventing the fragment from getting GC'ed. I couldn't find a way to remove the reference.
In my use case, I'm using a ListView inside the fragment that was activating the activity's ActionMode (via listener.setMultiChoiceModeListener).
My hacky solution: In the fragment's onDestroyView, remove the listView (or whichever view activated the ActionMode) from the layout and remove all listeners for the list view. I made a kotlin extension method for it:
fun ListView.removeViewAndClearListeners() {
setMultiChoiceModeListener(null)
setOnScrollListener(null)
onItemClickListener = null
(parent as? ViewGroup)?.removeView(this)
}
After doing this, the leak is gone.

I am still wondering why you are relying on ActionMode.Callback. I had an application where I was supposed to create a Custom Menu on long press and I wasted almost 2 months on this issue :
ActionModeCallback does not work
I am not sure If you are aware of this or not, The ActionMode Callback barely works on all devices. After a lot of research, I came to know that devices who are focusing too much on battery consumption and optimization will not let your background services and some callbacks work as expected.
Try testing your code on MI or Oppo/Vivo devices. It will jump directly to onDestroyActionMode instead of calling onActionItemClicked

Related

How to inflate SupportActionBar menu from outside Activity

I decoupled view from my Activity and I have a separate class for all UI related operations. One operation I want to implement there is to inflate menu for Action Bar. I try doing things like these, but none of it works.
fun displayMenu() {
rootView.toolbar.inflateMenu(R.menu.my_menu)
rootView.toolbar.setOnMenuItemClickListener { item ->
if (item.itemId == R.id.action_one) {
listener.onActionOne()
}
true
}
}
I tried this:
activity.menuInflater.inflate(R.menu.my_menu, rootView.toolbar.menu)
and this:
rootView.toolbar.inflateMenu(R.menu.my_menu)
But none of these gets the job done. How can I inflate this menu?
It was all my silly mistake.
activity.menuInflater.inflate(R.menu.my_menu, rootView.toolbar.menu)
Works perfectly fine. Just remember to call it during or after Activity.onCreateOptionMenu. Complete example to make it works is something like:
public class MyActivity extends AppCompatActivity {
#Inject
MyView myView;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(myView.getRootView());
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
myView.displayMenu();
return super.onCreateOptionsMenu(menu);
}
}
public class MyViewImpl implements MyView {
#Override
public void displayMenu() {
activity.getMenuInflater().inflate(
R.menu.categories_modification_menu,
rootView.findViewById(R.id.toolbar).getMenu()
)
}
}
I used displayMenu() from Activity.onCreate before... Sorry for wasting your time, especially I did not post this part as I thought it is irrelevant to the question...
I don't know how to code in Kotlin.
This is how I do in Java. Just Override onCreateOptionsMenu() method in your Activity class.
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.my_menu, menu);
}

Android WebView - Text Selection Listener in 2018

I will create an app that user will be enter the website url to my app. Then I am showing this page in my app using WebView.
As you know, when user clicks the any text in the context a little bit long, android cursor will appear then we can select text as many as we want.
After selection, we will see that "COPY, SHARE, SELECT ALL" etc..
My question is that when user selects text, I want to show them different options. Let's say "MyCOPY, SendTwitter, SendMessage".
How can i do that?
What I did so far?
I am just creating bar at the top of the app. But I don't want this.
Here is the code:
private WebView view;
private final String TAG = MainActivity.class.getSimpleName();
private ActionMode actionMode;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.view = findViewById(R.id.webView);
view.loadUrl("https://stackoverflow.com/questions/28385768/android-how-to-check-for-successful-load-of-url-when-using-webview-loadurl");
view.setWebViewClient(new MyWebViewClient());
Log.d(TAG, view.getUrl());
view.setOnLongClickListener((v) -> {
if (actionMode != null)
return false;
actionMode = startSupportActionMode(actionCallBack);
return true;
});
}
Where startSupportActionMode(actionCallBack) is
private ActionMode.Callback actionCallBack = new ActionMode.Callback() {
#Override
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
actionMode.getMenuInflater().inflate(R.menu.custommenu, menu);
actionMode.setTitle("Choose");
return true;
}
#Override
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
return false;
}
#Override
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
switch (menuItem.getItemId()){
case R.id.example_item_1:
Toast.makeText(MainActivity.this, "Option 1 selected", Toast.LENGTH_SHORT).show();
actionMode.finish();
return true;
case R.id.example_item_2 :
Toast.makeText(MainActivity.this, "Option 2 selected", Toast.LENGTH_SHORT).show();
actionMode.finish();
return true;
default:
return false;
}
}
#Override
public void onDestroyActionMode(ActionMode actionMode) {
actionMode = null;
}
};
You can implement the ActionMode.Callback interface to create your own menu upon selection.
An action mode's lifecycle is as follows:
onCreateActionMode(ActionMode, Menu) once on initial creation
onPrepareActionMode(ActionMode, Menu) after creation and any time the
ActionMode is invalidated
onActionItemClicked(ActionMode, MenuItem)
any time a contextual action button is clicked
onDestroyActionMode(ActionMode) when the action mode is closed
just make sure that your text views allow for text selection (android:textIsSelectable="true")
private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
// Called when the action mode is created; startActionMode() was called
#Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Inflate a menu resource providing context menu items
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.context_menu, menu);
return true;
}
// Called each time the action mode is shown. Always called after onCreateActionMode, but
// may be called multiple times if the mode is invalidated.
#Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false; // Return false if nothing is done
}
// Called when the user selects a contextual menu item
#Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_share:
shareCurrentItem();
mode.finish(); // Action picked, so close the CAB
return true;
default:
return false;
}
}
// Called when the user exits the action mode
#Override
public void onDestroyActionMode(ActionMode mode) {
mActionMode = null;
}
};
then call startActionMode() to enable the contextual action mode when appropriate (source), such as inside a setOnLongClickListener

menu share across multiple activities [duplicate]

This question already has answers here:
Android: Best way to share code between activities? [closed]
(2 answers)
Closed 4 years ago.
I have multiple activities sharing the same options menu so in my every activity, I am doing
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.settings:
Intent opensettings = new Intent(this, SettingsActivity.class);
startActivity(opensettings);
return true;
case R.id.help:
...others
default:
return super.onOptionsItemSelected(item);
}
}
Is there a way to share the above code amongst different activities?
I have tried adding a class
class MenuHelper{
Context ctx;
public MenuHelper(Context context){
ctx=context
}
public boolean openMenuItems(Menu item){
switch(item.getItemId()) //here .getItemId() doesnt work{
case R.id.settings: //R.id.settings not found
}
}
}
But am stuck in my helper class. How do I proceed so that in my different activities I only have to
MenuHelper menuitems = new MenuHelper(this);
menuitems.openMenuItems(menu)
You can have a BaseActivity where you can put the common implementation across your activities and then have other activities extend the BaseActivity
public class BaseActivity extends AppCompatActivity {
// Any other common methods
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.settings:
Intent opensettings = new Intent(this, SettingsActivity.class);
startActivity(opensettings);
return true;
case R.id.help:
...others
default:
return super.onOptionsItemSelected(item);
}
}
}
You can now create your activities extending BaseActivity:
public class MainActivity extends BaseActivity {
}
Why not just create a super class for your common Activities? If you create a super class, like so:
public class MySharedMenuActivity extends Activity {
#Override
public boolean onOptionsItemSelected(MenuItem item) { ... }
}
Then, if you extend that class for the activities you want, you will be able to access the shared menu.
Inheritance
As the other responses suggest, you could use inheritance to provide this sort of functionality. That does break the "favor composition over inheritance rule", but may be the practical solution for simple applications.
Composition
I think you are on the right path with creating a "menu helper" of sorts. I'd prefer a name such as OptionsMenuHandler and would probably write it like this:
public class OptionsMenuHandler {
private final Activity activity;
public OptionsMenuHandler(Activity activity) {
this.activity = activity;
}
public boolean onCreateOptionsMenu(Menu menu) {
// do menu inflation here.
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.settings:
Intent openSettings = new Intent(activity, SettingsActivity.class);
activity.startActivity(openSettings);
return true;
case R.id.help:
// others
default:
return false;
}
}
public boolean onPrepareOptionsMenu(Menu menu) {
// do menu preparation here.
}
}
and use it like this:
public class TestActivity extends AppCompatActivity {
private final OptionsMenuHandler optionsMenuHandler = new OptionsMenuHandler(this);
#Override
public boolean onCreateOptionsMenu(Menu menu) {
return optionsMenuHandler.onCreateOptionsMenu(menu) ||
super.onCreateOptionsMenu(menu);
}
#Override
public boolean onPrepareOptionsMenu(Menu menu) {
return optionsMenuHandler.onPrepareOptionsMenu(menu) ||
super.onPrepareOptionsMenu(menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
return optionsMenuHandler.onOptionsItemSelected(item) ||
super.onOptionsItemSelected(item);
}
}
This does require extra boiler plate in each Activity. It also creates abstraction. The abstraction is justified because it keeps the code DRY. I also like the fact that business logic isn't tucked away and invisible inside a parent class somewhere... the composition makes the location of the business logic a lot more obvious.
Base Activity that Supports Composition
Another option would be to support composition in a base Activity as follows...
Create a well defined abstraction:
public interface OptionsMenuHandler {
boolean onCreateOptionsMenu(Menu menu);
boolean onOptionsItemSelected(MenuItem item);
boolean onPrepareOptionsMenu(Menu menu);
}
Create an implementation for the abstraction:
public class DefaultOptionsMenuHandler implements OptionsMenuHandler {
private final Activity activity;
public DefaultOptionsMenuHandler(Activity activity) {
this.activity = activity;
}
public boolean onCreateOptionsMenu(Menu menu) {
// do menu inflation here.
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.settings:
Intent openSettings = new Intent(activity, SettingsActivity.class);
activity.startActivity(openSettings);
return true;
case R.id.help:
// others
default:
return false;
}
}
public boolean onPrepareOptionsMenu(Menu menu) {
// do menu preparation here.
}
}
Support composition in the base class (ie base class has a setter):
public class BaseActivity extends AppCompatActivity {
#Nullable
private OptionsMenuHandler optionsMenuHandler;
protected void setOptionsMenuHandler(OptionsMenuHandler optionsMenuHandler) {
this.optionsMenuHandler = optionsMenuHandler;
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
return optionsMenuHandler != null
? optionsMenuHandler.onCreateOptionsMenu(menu)
: super.onCreateOptionsMenu(menu);
}
#Override
public boolean onPrepareOptionsMenu(Menu menu) {
return optionsMenuHandler != null
? optionsMenuHandler.onPrepareOptionsMenu(menu)
: super.onPrepareOptionsMenu(menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
return optionsMenuHandler != null
? optionsMenuHandler.onOptionsItemSelected(item)
: super.onOptionsItemSelected(item);
}
}
Set the implementation in the Activity that needs the functionality.
public class TestActivity extends BaseActivity {
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setOptionsMenuHandler(new DefaultOptionsMenuHandler(this));
}
}
The net benefit here is that you write the main boilerplate once and support it through all of your activities. You can also continue to keep your business logic defined in the top level activity - where it goes with the other various logic for that particular activity.
Most non-trivial apps would benefit from something along these lines. I typically do something even more robust that supports zero or more OptionsMenuHandlers being set in any given activity where each handler supports a specific type of functionality. The code for this is fairly long and many considerations are needed, so I won't produce it here.

Set search widget back button functionality

I have implemented the Android search widget in my navigation drawer based app. I have set it to open the keyboard and focus the editText when clicking on the search icon. I want to set the back button (up button) to hide the keyboard. I have searched the web for the R.id of the up button, and found this android.R.id.home. So I have set it to be:
#Override
public boolean onOptionsItemSelected(MenuItem item) {
...
case android.R.id.home:
hideKeyboard();
break;
...
}
I debugged the code and noticed that clicking on the navigation bar icon fires up the android.R.id.home, but hitting the up button of the search widget doesn't even enter the onOptionsItemSelected(MenuItem item) function.
I have also tried this:
searchView.setOnFocusChangeListener(new OnFocusChangeListener() {
#Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
hideKeyboard();
}
}
});
But didn't work.
How can I hide the keyboard when pressing the back (up) button?
Setting the search view:
private void setSearchView(Menu menu) {
// Get the SearchView and set the searchable configuration
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
searchView = (SearchView) menu.findItem(R.id.search).getActionView();
// Assumes current activity is the searchable activity
searchView.setSearchableInfo(searchManager
.getSearchableInfo(getComponentName()));
searchView.setIconifiedByDefault(false);
SearchView.OnQueryTextListener queryTextListener = new SearchView.OnQueryTextListener() {
public boolean onQueryTextChange(String newText) {
Home.getFilter().filter(newText);
return true;
}
public boolean onQueryTextSubmit(String query) {
return true;
}
};
searchView.setOnQueryTextListener(queryTextListener);
}
The following code should work:
searchView = (SearchView) menu.findItem(R.id.search).getActionView();
searchView.setOnCloseListener(new OnCloseListener() {
#Override
public bool OnClose() {
searchView.clearFocus();
return true;
});
However this didn't work for me for some reason. :-/
I found the workaround below here:
searchView = (SearchView) menu.findItem(R.id.search).getActionView();
searchView.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
#Override
public void onViewDetachedFromWindow(View v) {
searchView.clearFocus();
}
#Override
public void onViewAttachedToWindow(View v) {
}
});
I don't think that using android.R.id.home will work since I think that onOptionsItemSelected(android.R.id.home) will only be called once the SearchView has been closed.

Is my activity being leaked - and is it the ActionBar that's doing it?

I have an Activity named PhotoSelectorActivity. It inherits from a BaseActivity that looks like this:
public class BaseActivity
extends ActionBarActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(this.getClass().getSimpleName(),
"onCreate("+Integer.toHexString(System.identityHashCode(this))+")");
}
#Override
protected void onDestroy() {
super.onDestroy();
getSupportActionBar().setCustomView(null);
Log.d(this.getClass().getSimpleName(),
"onDestroy("+Integer.toHexString(System.identityHashCode(this))+")");
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
return onNavigateUp(item);
case R.id.menu_item_settings:
startActivity(new Intent(this, PreferencesActivity.class));
return true;
default:
return super.onOptionsItemSelected(item);
}
}
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
protected ActionBar setupActionBar(boolean enableBackButton) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
getSupportActionBar().setDisplayHomeAsUpEnabled(enableBackButton);
}
ActionBar actionBar = getSupportActionBar();
actionBar.setCustomView(R.layout.action_bar);
actionBar.setDisplayShowTitleEnabled(false);
actionBar.setDisplayShowCustomEnabled(true);
return null;
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
The purpose of this BaseActivity is to provide the same menu and actionbar to each one of my activities. You'll notice the getSupportActionBar().setCustomView(null) in the onDestroy() method, that's there to try and combat the problem that I may be having.
When i get an orientation change event, i notice in DDMS that i end up with 2 instances of my activity. One of them may be leaking, but I'm not certain. Here's a screen shot from DDMS:
So the object at the top is the Activity in question: PhotoSelectorActivity. The instance shown here is the previous instance (onDestroy() has already been called on it). Yet it remains in memory even after a forced GC via DDMS.
Another bit of information is that this only seems to happen after using a dialog. That is, when the Activity is initially displayed and before the user performs and action I can do back to back orientation changes without the # of activities climbing above 1. After I've used the following dialog i seem to get the extra Activity in memory:
public class PhotoSourceDialog
extends DialogFragment
implements DialogInterface.OnClickListener {
public static interface PhotoSourceDialogListener {
void onPhotoSourceSelected(String result);
}
private PhotoSourceDialogListener listener;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (!PhotoSourceDialogListener.class.isInstance(activity)) {
throw new IllegalStateException(
"Activity must implement PhotoSourceDialogListener");
}
listener = PhotoSourceDialogListener.class.cast(activity);
}
#Override
public void onDetach() {
super.onDetach();
listener = null;
}
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.photo_source)
.setItems(R.array.photo_sources, this).create();
}
#Override
public void onClick(DialogInterface dialog, int which) {
String choice = getResources().getStringArray(
R.array.photo_sources)[which];
if (listener!=null) {
listener.onPhotoSourceSelected(choice);
}
}
}
and to invoke it i do this in my activity:
PhotoSourceDialog dialog = new PhotoSourceDialog();
dialog.show(getSupportFragmentManager(), PhotoSourceDialog.class.getName());
So my question is this: Should I be worried? Is this just something that is hanging around for a bit but will eventually be GCd? I would think that if there was a leak it would grow higher than 2.
I'm closing this question. Someone at google has responded with the following:
OK, in that case then it's not an AppCompat bug since the standard
Action Bar implementation is used on ICS+.
Looking at that MAT screenshot, the framework's ActionMenuItemView is
being referenced from a clipboard event which is being finalized,
hence about to be GC'd. The LayoutInflater is probably the
LayoutInflater that the Activity keeps itself (getLayoutInflater()).

Categories