Leak canary is showing a leak in my activity, but I don't understand the cause of this leak.
I tried to remove parts from the activity to check where the leak comes from, but it didn't work. I also removed everything from the activity and I still got the same results. After these tests and many others, I'm assuming that there is something wrong in the activity that sends me to the PostDetailActivity
; or am I wrong?
I'm a novice. Sorry for how the activity is implemented.
public class PostDetailActivity extends AppCompatActivity {
//details of user and post
String myUid, myName, myProfilPic, postId, pLikes, pDislike, pTitle, pTime, pImage, pUserPic, pUserName;
private DatabaseReference likesRef;
private DatabaseReference dislikeRef;
boolean mProcessLike = false;
boolean mProcessDislike = false;
Integer position, dislike, like;
//post views
ImageView userPicture, postImage;
TextView userName, postTime, postTitle;
Button likeBtn, dislikeBtn, shareBtn;
LinearLayout profileLayout, emptyRecycler, commentExtraSpace;
public RecyclerView recyclerView;
NestedScrollView scrollView;
public static List<Comment> commentList;
CommentAdapter commentAdapter;
//add comments views;
EditText commentEt;
ImageButton sendCommentBtn;
ImageView userCommentImage;
Comment replyComment;
@Override
protected void onCreate(Bundle savedInstanceState) {
// DARK/LIGHT THEME CODE START
if (DarkThem) {
setTheme(R.style.DarkTheme);
} else {
setTheme(R.style.LightTheme);
}
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_post_detail);
myUid = FirebaseAuth.getInstance().getCurrentUser().getUid();
likesRef = FirebaseDatabase.getInstance().getReference().child("Likes");
dislikeRef = FirebaseDatabase.getInstance().getReference().child("Dislikes");
Toolbar toolbar = findViewById(R.id.toolBar);
setSupportActionBar(toolbar);
getSupportActionBar().setTitle("");
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PostFragment.relogPost(position);
onBackPressed();
}
});
//getting post info with intent
Intent intent = getIntent();
postId = intent.getStringExtra("postId");
pLikes = intent.getStringExtra("pUp");
pDislike = intent.getStringExtra("pDown");
pTitle = intent.getStringExtra("pTitle");
pTime = intent.getStringExtra("pTime");
pImage = intent.getStringExtra("pImage");
pUserPic = intent.getStringExtra("uDp");
pUserName = intent.getStringExtra("uName");
try {
String positionList = intent.getStringExtra("position");
position = Integer.parseInt(positionList);
} catch (Exception e) {
}
//initializing views post
userPicture = findViewById(R.id.userPic);
postImage = findViewById(R.id.pImage);
userName = findViewById(R.id.userNamePost);
postTime = findViewById(R.id.pTime);
postTitle = findViewById(R.id.pTitle);
likeBtn = findViewById(R.id.upBtn);
dislikeBtn = findViewById(R.id.dwnBtn);
shareBtn = findViewById(R.id.shareBtn);
profileLayout = findViewById(R.id.profileLayout);
recyclerView = findViewById(R.id.recyclerView);
scrollView = findViewById(R.id.scrollView);
emptyRecycler = findViewById(R.id.emptyRecycler);
commentExtraSpace = findViewById(R.id.commentExtraSpace);
//initializing views comment
commentEt = findViewById(R.id.commentEt);
sendCommentBtn = findViewById(R.id.sendComment);
userCommentImage = findViewById(R.id.userCommentImage);
//convert time to dd//mm//yyyy hh:mm am/pm
Calendar calendar = Calendar.getInstance(Locale.getDefault());
calendar.setTimeInMillis(Long.parseLong(pTime));
String pTimeshown = DateFormat.format("dd/MM/yyyy hh:mm aa", calendar).toString();
//set data for PostDetails
userName.setText(pUserName);
postTime.setText(pTimeshown);
postTitle.setText(pTitle);
likeBtn.setText(pLikes);
dislikeBtn.setText(pDislike);
//Set user pic
if (pUserPic.equals("default")) {
userPicture.setImageResource(R.mipmap.ic_launcher_round);
} else {
try {
Glide.with(this).load(pUserPic).into(userPicture);
} catch (Exception e) {
}
}
//Setting postPicture
if (pImage.equals("default")) {
postImage.setImageResource(R.drawable.sunset);
} else {
try {
Glide.with(this).load(pImage).into(postImage);
} catch (Exception e) {
}
}
setLikes(postId);
setDislike(postId);
loadComments();
sendCommentBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendComment();
commentList.add(replyComment);
commentAdapter.notifyDataSetChanged();
scrollView.post(new Runnable() {
@Override
public void run() {
scrollView.scrollTo(0, recyclerView.getBottom());
}
});
}
});
dislikeBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pDislike = dislikeBtn.getText().toString();
pLikes = likeBtn.getText().toString();
mProcessDislike = true;
dislikeRef.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
if (mProcessDislike) {
if (dataSnapshot.child(postId).hasChild(myUid)) {
//already disliked ,so remove dislike
dislikeRef.child(postId).child(myUid).removeValue();
mProcessDislike = false;
dislikeBtn.setText(MessageFormat.format("{0}", Integer.parseInt(pDislike) - 1));
postList.get(position).setpDown("" + (Integer.parseInt(pDislike) - 1));
dislike = Integer.parseInt(pDislike) - 1;
postList.get(position).setpDown("" + dislike);
dislikeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_downvote_black, 0, 0, 0);
} else {
//not liked
likesRef.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
//verifying is user has liked the post
if (dataSnapshot.child(postId).hasChild(myUid)) {
likesRef.child(postId).child(myUid).removeValue();
dislikeRef.child(postId).child(myUid).setValue("-1");
likeBtn.setText(MessageFormat.format("{0}", Integer.parseInt(pLikes) - 1));
postList.get(position).setpUp("" + (Integer.parseInt(pLikes) - 1));
dislike = Integer.parseInt(pDislike) + 1;
dislikeBtn.setText(MessageFormat.format("{0}", dislike));
postList.get(position).setpDown("" + dislike);
} else {
dislikeRef.child(postId).child(myUid).setValue("-1");
dislike = Integer.parseInt(pDislike) + 1;
dislikeBtn.setText(MessageFormat.format("{0}", dislike));
postList.get(position).setpDown("" + dislike);
}
mProcessDislike = false;
dislikeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_disliked, 0, 0, 0);
likeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_upvote_black, 0, 0, 0);
}
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
}
});
}
}
}
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
}
});
}
});
likeBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pDislike = dislikeBtn.getText().toString();
pLikes = likeBtn.getText().toString();
mProcessLike = true;
likesRef.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
if (mProcessLike) {
if (dataSnapshot.child(postId).hasChild(myUid)) {
//already liked ,so remove like
likesRef.child(postId).child(myUid).removeValue();
like = Integer.parseInt(pLikes) - 1;
mProcessLike = false;
likeBtn.setText(MessageFormat.format("{0}", like));
postList.get(position).setpUp("" + like);
likeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_upvote_black, 0, 0, 0);
} else {
//not liked
dislikeRef.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
//verifying is user has dislkied the post
if (dataSnapshot.child(postId).hasChild(myUid)) {
dislikeRef.child(postId).child(myUid).removeValue();
likesRef.child(postId).child(myUid).setValue("1");
dislikeBtn.setText(MessageFormat.format("{0}", Integer.parseInt(pDislike) - 1));
postList.get(position).setpDown("" + (Integer.parseInt(pDislike) - 1));
like = Integer.parseInt(pLikes) + 1;
likeBtn.setText(MessageFormat.format("{0}", like));
postList.get(position).setpUp("" + like);
dislikeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_downvote_black, 0, 0, 0);
} else {
likesRef.child(postId).child(myUid).setValue("1");
like = Integer.parseInt(pLikes) + 1;
likeBtn.setText(MessageFormat.format("{0}", like));
postList.get(position).setpUp("" + like);
}
mProcessLike = false;
likeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_liked, 0, 0, 0);
}
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
}
});
}
}
}
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
}
});
}
});
scrollView.post(new Runnable() {
@Override
public void run() {
scrollView.scrollTo(0, commentExtraSpace.getBottom());
}
});
DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference().child("Users").child(myUid);
databaseReference.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
myProfilPic = "" + dataSnapshot.child("imageURL").getValue();
myName = "" + dataSnapshot.child("username").getValue();
}
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
}
});
}
private void sendComment() {
String comment = commentEt.getText().toString().trim();
//checking if empty
if (TextUtils.isEmpty(comment)) {
Toast.makeText(this, getString(R.string.EmptyComment), Toast.LENGTH_SHORT).show();
return;
}
String timeStamp = String.valueOf(System.currentTimeMillis());
DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference("Comments");
HashMap<String, String> hashMap = new HashMap<>();
//put info in hashmap
hashMap.put("commentId", timeStamp);
hashMap.put("comment", comment);
hashMap.put("timeStamp", timeStamp);
hashMap.put("userId", myUid);
hashMap.put("userPicture", myProfilPic);
hashMap.put("userName", myName);
hashMap.put("like", "0");
hashMap.put("dislike", "0");
hashMap.put("replies", "no");
hashMap.put("liked", "no");
hashMap.put("disliked", "no");
databaseReference.child(postId).child(timeStamp).setValue(hashMap);
replyComment = new Comment(timeStamp, comment, timeStamp, "noid", myProfilPic, myName, "0", "0", "no", "no", "no");
commentEt.setText("");
}
private void loadComments() {
//linear layout for recyclerView
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getApplicationContext());
//set layout to recyclerView
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(linearLayoutManager);
//init comments list
commentList = new ArrayList<>();
//path of the post,to get comments;
DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference("Comments").child(postId);
databaseReference.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
commentList.clear();
for (DataSnapshot snapshot : dataSnapshot.getChildren()) {
Comment comment = snapshot.getValue(Comment.class);
if (snapshot.child("comments").getChildrenCount() >= 1) {
comment.setReplies("" + snapshot.child("comments").getChildrenCount());
} else {
comment.setReplies("no");
}
if (snapshot.child("likes").hasChild(myUid)) {
comment.setLiked("yes");
} else {
comment.setLiked("no");
}
if (snapshot.child("dislikes").hasChild(myUid)) {
comment.setDisliked("yes");
} else {
comment.setDisliked("no");
}
comment.setLike(String.valueOf(snapshot.child("likes").getChildrenCount()));
comment.setDislike(String.valueOf(snapshot.child("dislikes").getChildrenCount()));
commentList.add(comment);
//setup adapter
}
commentAdapter = new CommentAdapter(PostDetailActivity.this, commentList, postId);
//set adapter
if (commentAdapter.getItemCount() > 0) {
recyclerView.setAdapter(commentAdapter);
emptyRecycler.setVisibility(View.GONE);
} else {
recyclerView.setVisibility(View.GONE);
emptyRecycler.setVisibility(View.VISIBLE);
}
}
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
}
});
}
private void setLikes(final String postKey) {
likesRef.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
if (dataSnapshot.child(postKey).hasChild(myUid)) {
//user has liked this post
//indicate it with new design
likeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_liked, 0, 0, 0);
} else {
//user has not liked
likeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_upvote_black, 0, 0, 0);
}
}
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
}
});
}
private void setDislike(final String postKey) {
dislikeRef.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
if (dataSnapshot.child(postKey).hasChild(myUid)) {
//user has liked this post
//indicate it with new design
dislikeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_disliked, 0, 0, 0);
} else {
//user has not liked
dislikeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_downvote_black, 0, 0, 0);
}
}
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
}
});
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return super.onSupportNavigateUp();
}
@Override
public void onBackPressed() {
// PostFragment.recyclerView.getAdapter().notifyItemChanged(position);
if (isTaskRoot() && getSupportFragmentManager().getBackStackEntryCount() == 0) {
finishAfterTransition();
} else {
super.onBackPressed();
}
}
}
When I press the back button I get this:
┬───
│ GC Root: System class
│
├─ android.app.ActivityThread class
│ Leaking: NO (a class is never leaking)
│ ↓ static ActivityThread.sCurrentActivityThread
│ ~~~~~~~~~~~~~~~~~~~~~~
├─ android.app.ActivityThread instance
│ Leaking: UNKNOWN
│ ↓ ActivityThread.mNewActivities
│ ~~~~~~~~~~~~~~
├─ android.app.ActivityThread$ActivityClientRecord instance
│ Leaking: UNKNOWN
│ ↓ ActivityThread$ActivityClientRecord.nextIdle
│ ~~~~~~~~
├─ android.app.ActivityThread$ActivityClientRecord instance
│ Leaking: UNKNOWN
│ ↓ ActivityThread$ActivityClientRecord.activity
│ ~~~~~~~~
╰→ com.RLD.newmemechat.PostDetailActivity instance
Leaking: YES (ObjectWatcher was watching this because com.RLD.newmemechat.PostDetailActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
key = 533b7987-0f38-41f4-9f1a-e468dcf83264
watchDurationMillis = 8537
retainedDurationMillis = 3530~~~
This is clearly a leak caused by the Android framework. There's nothing in the leaktrace that has to do with your code, from what I can tell ActivityThread maintains a linked list of ActivityThread$ActivityClientRecord in ActivityThread.mNewActivities.
You can see it here:
// List of new activities (via ActivityRecord.nextIdle) that should
// be reported when next we idle.
ActivityClientRecord mNewActivities = null;
It looks like this is done by ActivityThread.Idler which runs when the main thread is idle:
private class Idler implements MessageQueue.IdleHandler {
@Override
public final boolean queueIdle() {
ActivityClientRecord a = mNewActivities;
boolean stopProfiling = false;
if (mBoundApplication != null && mProfiler.profileFd != null
&& mProfiler.autoStopProfiler) {
stopProfiling = true;
}
if (a != null) {
mNewActivities = null;
IActivityTaskManager am = ActivityTaskManager.getService();
ActivityClientRecord prev;
do {
if (localLOGV) Slog.v(
TAG, "Reporting idle of " + a +
" finished=" +
(a.activity != null && a.activity.mFinished));
if (a.activity != null && !a.activity.mFinished) {
try {
am.activityIdle(a.token, a.createdConfig, stopProfiling);
a.createdConfig = null;
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
prev = a;
a = a.nextIdle;
prev.nextIdle = null;
} while (a != null);
}
if (stopProfiling) {
mProfiler.stopProfiling();
}
applyPendingProcessState();
return false;
}
}
This is used to tell the activity manager when the main thread becomes idle after an activity is created. Unfortunately, if the main thread doesn't get idle in between Activity.onCreate() & Activity.onDestroy(), this seem to introduce a leak.