Translate

Sonntag, 21. Dezember 2014

View.setClickable() does not work

What the ...?


Some time ago I stumbled upon something weird, while working on an Android  application again.
I created a button that should not be clickable in the beginning until some
conditions applied. Because it should still be visible, I made it half opaque and set the button to not clickable with button.setClickable(false). But what was happening is,
that even though I used the right method, the button was still useable and I spent
a lot of time figuring out why.

If you want to know, why it seems that view.setClickable(false) doesn't work sometimes,
read my post below!



Example


Let's assume you want to create a button and set it programmatically to not clickable.
Like me in the introduction to this post.
Then you will likely end up with something like this:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    private void initAcceptButton() {
        acceptButton = (Button) rootLayout.findViewById(R.id.fragment_home_accept_button);
        acceptButton.setClickable(false);
        acceptButton.setAlpha(HALF_TRANSPARENT);
        acceptButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onAcceptClicked();
            }
        });   
    }


And now you run you project on an emulator or a testdevice and wonder why the button is still
clickable.


Solution


We have to make a closer look to the method view.setOnClickListener() here,
which is the common way to react on userinteractions with a listener set
programmatically. I assume that you've already used this method hundred of times
and that you think that you almost exactly know what it does.
Well, sure, setOnClickListener sets the listener, which you provide as an
argument for the method, and which will be invoked once the view gets clicked.
So far so good. But let's take a look at the documention (I recommend to do
this with all your used methods).

Google says:

Register a callback to be invoked when this view is clicked. If this view is not clickable, it becomes clickable.

Like what? If this view is not clickable, it becomes clickable. Seriously?
So this method does not only set the OnClickListener, but also resets the flag for 

beeing clickable back to true. In my opinion this is a design flaw in the method
setOnClickListener, because it violates the single responsibility principle,
which is also applicable to methods. One method, one task. If the methods
name indicates that it attaches the provided listener to the view, it should do
exactly that thing!

Ok, now we know why our button is still clickable, we can easily change our code to
fix the weird bug and work like intended:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    private void initAcceptButton() {
        acceptButton = (Button) rootLayout.findViewById(R.id.fragment_home_accept_button);
        acceptButton.setAlpha(HALF_TRANSPARENT);
        acceptButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onAcceptClicked();
            }
        });
        acceptButton.setClickable(false);
    }

We just call the setClickable method after the setOnClickListener and prevent it to reset the flag.

I hope you like this post and I was able to help someone out there!

Cheers!





Recommandations

I can highly recommend to read the book Design Patters.
Because I don't want to make any advertisement, I provide a link to wikipedia:

Design Patterns: Elements of Reusable Object-Oriented Software

This is like THE book about design patterns, although there are a lot of more good books
out there, which are maybe easier to read, understand and structured. But they are all based
on this one, thats why I like to share it with you and recommend to read it.


Mittwoch, 17. Dezember 2014

Missing extra content in PendingIntents


What the...?


In this post, I am going to describe a common misconception about
PendingIntents in Android and how they can fool you, if you don't
know exactly what you are doing. An annoying experience almost
every developer makes in the beginning, is to use PendingIntents for various actions
in the future and getting puzzled if they recognize that it doesn't matter what they are
putting into the Intent, they always get the same PendingIntent with the same
extra content. You want to know how to put different information's into your
PendingIntent and why it seems that the extra content does not change?
Read 
my post below!

tl;dr? You can also directly look at the solution at the end!

PendingIntents


Usually you will use PendingIntents as a kind of token, that you can provide to third party
applications to execute parts of your app. Some examples would be to start an Activity
after a Notifcation was clicked or canceled by the user. Or you want the AlarmManager
to start your application in some point in the future.

However, almost every android developer stumbled already upon Intents and used them to
start new Activities, send small messages to a Service or provide information's within the
Bundle inside of the Intent, also called extra content.

The problem


Like in a normal Intent you can add some extra content to the Intent that is
given to the PendingIntent to launch the Activity. Usually you will do this to
distinguish between different startup modes or just to determine the parent
of the PendingIntent.

Let's assume you develope a small chat and you want to display a Notification
to the user, every time your application is in background and your service
recognizes that a new member entered the chatroom.
When the notification get clicked, it should start your ChatActivity and hand over
the name of the recently logged in member.

You will end up to do something simmilar to this:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * Created by Steve on 18.12.2014.
 */
public final class ChatNotificationCreator {

    public static final String MEMBER_NAME = "org.awesomechat.ChatNotificationCreator.name";
    public static final int NOTIFICATION_ID = 1338;
    
    private ChatNotificationCreator() {}

    public static void showNewMemberNotification(String memberName, Context context) {
        Intent chatIntent = new Intent();
        chatIntent.setClass(context, ChatActivity.class);
        chatIntent.putExtra(MEMBER_NAME, memberName);
        PendingIntent resultPendingIntent = PendingIntent.getActivity(context, 0, chatIntent, 0);

        NotificationCompat.Builder mBuilder =
                new NotificationCompat.Builder(context)
                        .setSmallIcon(R.drawable.notification_icon)
                        .setContentTitle(context.getString(R.string.notification_title))
                        .setContentText(context.getString(R.string.notification_message))
                        .setAutoCancel(true)
                        .setLights(Color.GREEN, 100, 100) //green works for most phones
                        .setPriority(NotificationCompat.PRIORITY_MAX)
                        .setContentIntent(resultPendingIntent);

        NotificationManager mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
    }
}

The important part is the line 15, where the PendingIntent gets created.

Once your done, you will be happy that this code will actually opens your ChatActivity.
If you tap on the Notification and if you created an applicable ChatActivity.class as well,
you can determine the name of the new member by simply extract it out of the Intent via:


1
intent.getExtras().getString(ChatNotificationCreator.MEMBER_NAME, "");
(be aware of possible NPE here if  there are no extras given to the Intent, e.g. your Activity is exported)

But now the magic happens!

A second member enters the room while your chat is in background.
The notifcation will be created and displayed. You tap on it, the ChatActivity will open,
but boom - the name seems to be still the same as for the first member.

And the third member will also trigger a Notification and it will open the Activity again,
but nevertheless, the name displayed is still the one from the first logged in member.

So why this is happening?


A PendingIntent as said, is used to start an Activity or Service in the future, from 
an application or process that is not yours. It's intented that there is only one 
PendingIntent at the time for the same kind of action. Therefore, even if you change
the information's given by the Intent that will be launched, it is still the same PendingIntent.
The system will ignore your new created PendingIntent.

Source: Quote from Google Reference:
[...]Because of this behavior, it is important to know when two Intents are considered to be the same for purposes of retrieving a PendingIntent. A common mistake people make is to create multiple PendingIntent objects with Intents that only vary in their "extra" contents, expecting to get a different PendingIntent each time.[...]
In my humble opinion, this behaviour is kind of logical, but not clear enough for developers.
Although the api says for the return value of the method PendingIntent.getActivity()

Returns an existing or new PendingIntent matching the given parameters.

I doubt that the majority of developers will directly guess that their received Intent will be
the same, even if they change the information's containing in the extra content.

The Solution


The solution for this problem is quite simple. If we want to update the current PendingIntent,
because we just want to change the containing information's, but not the Activity/Service that
will be launched, then we need to add PendingIntent.FLAG_UPDATE_CURRENT as a parameter for getActivity, likes this:

1
PendingIntent resultPendingIntent = PendingIntent.getActivity(context, 0, chatIntent, PendingIntent.FLAG_UPDATE_CURRENT);

If we want to cancel the recently created PendingIntent, because we (for example) want
to change the Activity to launch, than we add  PendingIntent.FLAG_CANCEL_CURRENT 


1
PendingIntent resultPendingIntent = PendingIntent.getActivity(context, 0, chatIntent, PendingIntent.FLAG_CANCEL_CURRENT);

This also applies for services, with getService(...).

I hope you like this post and I was able to help someone.
Leave your comments below!

Cheers!



SourcesGoogle Reference PendingIntent