20 December 2011

MongoDB and Authentication

By default, MongoDB allows access to the database without authentication. Adding a user with a username/password is easy, but authenticating might be a bit tricky since the official documentation does not say the command directly.
First, we add an admin account. Navigate to the MongoDB directory on your machine then start the database.
$ ./mongo
> use admin
> db.addUser(adminuser, adminpassword)
Switch to the database of your choice and add users to it.
> use foo
> db.addUser(myuser, userpassword)
This adds a user myuser that has read and write access to the database. If we want a user with read-only access, set the third parameter for addUser().
> db.addUser(guest, guestpassword, true)
You can check for users with access to a particular database like thus:
> db.system.users.find().pretty()
{
        "_id" : ObjectId("4ee9863d954eb7168e07089d"),
        "user" : "zarah",
        "readOnly" : false,
        "pwd" : "70581bfb1e32e2286df11fe119addc7a"
}
{
        "_id" : ObjectId("4ee98658954eb7168e07089e"),
        "user" : "guest",
        "readOnly" : true,
        "pwd" : "88558f1ece63fa0b528012b9840bd9de"
}

Now stop the MongoDB server and restart it with authentication enabled.
$ ./mongod --auth
> mongo foo -u myuser -p userpassword
where foo is the database that myuser has access to.
You can now read and write into database foo. Notice however that querying for databases would result to an error:
> show dbs
Mon Dec 19 17:21:20 uncaught exception: listDatabases failed:{ "errmsg" : "need to login", "ok" : 0 }

Exit MongoDB and login again, this time using the read-only account. If we try inserting a document, an error should appear:
> db.foo.insert({"title","MongoDB Authentication Test"})
unauthorized
The read-only account can query for collections and use find() and its variations. It can't, however, query for databases.

06 December 2011

Hello, it's me again.


To my two readers out there, hello! It's been a while since I posted here. I was transferred to another (non-Android) project and lost all my Internet privileges, hence the silence. I still can't believe almost every other site is blocked by the office firewall! Makes software development ten times harder. Ugh.

Anyway, as of the last three months, I have been working on backend development (J2EE). I don't know a lot about it, and the past months have been a great journey of learning. It is quite a shock being exposed all in one go to so many technologies and tools, I'm lucky my head didn't explode.

Happy as I am to learn new things, I am quite sad to leave Android development behind. I'm still hoping though that I would be given the chance to go back to it, and pick up where I left off. Hopefully the Android train won't be too far off by then.

In the meantime, I will be posting stuff that I've learned while working on J2EE, and hopefully somebody can learn from my mistakes. :)

07 September 2011

Where's my R.java?

This afternoon, I tried importing an existing project into Eclipse. Doing a Project > Clean usually clears up the R.java not found errors, but this time it didn't work. I tried re-importing the project, copy-and-pasting it into a new workspace, restarting Eclipse, but the error is still there.

Just a quick note to self: If there is an error in the layout files, resolve that first then Clean.

I find it weird that no more specific error message is presented. Oh well.

11 July 2011

ADT 12: Not so shiny after all


For all the shininess that ADT 12 promised, it seems that it also broke one major feature of DDMS: Launching emulators.

After updating to ADT 12, I kept on seeing that error when launching an emulator instance. Restarting Eclipse doesn't help any.

Anyway, the apparent cause of this error is that ADT 12 has some problems with the SDK location having spaces. If you have already forgotten, it's in Window > Preferences > Android.

To work around this bug, it is either you move your SDK to a folder in a path without spaces; or, modify the existing path to either of the following:
  • If your SDK is in Program Files: C:/PROGRA~1/<path_to_sdk>
  • If your SDK is in Program Files(x86): C:/PROGRA~2/<path_to_sdk>
Edit (20110711 1943): A bug has already been filed for this issue. Star to be notified of updates.

28 June 2011

A break: When I grow up

Tearing my hair out on my latest project. So here's some Garbage to cheer me up.

04 June 2011

Passing complex objects to another Activity

Several months ago, I was faced with a problem of passing a complex object to another Activity. There are several ways of doing this:
  • "Deconstructing" the complex object to simple data types and passing them as extras through putExtra()
  • Making the object Parcelable
  • Making the object Serializable
I don't really understand the concepts behind an object being Parcelable or Serializable, so I was not comfortable using that approach. Deconstructing the object, on the other hand, is easier but it is quite hard to manage as the number of fields in the object increases.

And then I came across another solution: using Bundles. In this post, I will try to explain how to do that.

In this sample project, we want to pass an instance of an AttendeeObject from our main activity to another. To better illustrate how we can use this method for different data types, an AttendeeObject will hold three pieces of information: a String name, an integer age, and a boolean indicating the person's presence.

First, we construct the AttendeeObject like any old POJO. Create all the fields you want the object to contain and generate getters and setters for each. We then create the methods we would need to be able to pass, and subsequently receive, this object between activities.

To pass an AttendeeObject, we would need to put it in a Bundle and pass it using Intent#putExtra(String key, Bundle bundle).
public Bundle bundleAttendee(AttendeeObject attendee){
     Bundle bundle = new Bundle();
     bundle.putString(NAME, attendee.getName());
     bundle.putInt(AGE, attendee.getAge());
     bundle.putBoolean(PRESENCE, attendee.isPresent());
  
     return bundle;
}
Here, we simply put into a Bundle the values in the object that we want to pass.

Next, we should be able to convert this bundle back to an AttendeeObject in the receiving Activity. This task is done by the following method:
public AttendeeObject unBundleAttendee(Bundle attendeeBundle){
     AttendeeObject attendee = new AttendeeObject();
     attendee.setName(attendeeBundle.getString(NAME));
     attendee.setAge(attendeeBundle.getInt(AGE));
     attendee.setIsPresent(attendeeBundle.getBoolean(PRESENCE));
  
     return attendee;
}
Now, we are ready to use this in our application. I created a simple app that asks the user for the three values we would need to populate AttendeeObject.
I won't discuss the details of creating the layout file, but it might be worthy to note that the possible values for the Spinner are just "Yes" and "No".

When the user clicks on the "Create Attendee" button, the application creates a new AttendeeObject based on the values in the form and adds this to an ArrayList of AttendeeObjects.
private void onCreateAttendee() {
     // Create a new AttendeeObject
     AttendeeObject attendee = new AttendeeObject();
     attendee.setName(mNameText.getText().toString());
     attendee.setAge(Integer.valueOf(mAgeText.getText().toString()));
     attendee.setIsPresent(mCurrentPresence);
  
     // You can then do whatever you want with this object
     // like adding it to an ArrayList or saving it to a database
     mAttendees.add(attendee);
  
     Toast.makeText(ComplexObjectDemo.this, "Attendee added!", Toast.LENGTH_SHORT).show();
}

When the user clicks on the "Submit Attendee" button, the app "packages" the chosen value into a Bundle and sets it as an extra to the Intent. For illustration purposes, the application always gets the second AttendeeObject in the ArrayList.
private void onSubmitObject() {
     Intent intent = new Intent(ComplexObjectDemo.this, ComplexObjectReceiverDemo.class);

     // For simplicity, we will always get the second value
     Bundle value = createAttendeeBundle(1);

     // In a "real" application, the Key should be defined in a constants file
     intent.putExtra("test.complex.attendee", value);
  
     startActivity(intent);
}

private Bundle createAttendeeBundle(int index) {
     // Here, mAttendee is an instance of AttendeeObject
     // and mAttendees is an ArrayList holding all the created attendees
     return mAttendee.bundleAttendee(mAttendees.get(index));
}


Upon receiving the Intent, the new activity simply displays the values in the AttendeeObject passed into it. Here's how we can "unbundle" the Bundle:
@Override
public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     setContentView(R.layout.activity_complex_demo_receiver);

     // Get the extras passed to this Activity        
     setUpViews(getIntent().getExtras());        
}

private void setUpViews(Bundle extras) {
     // Again, the key should be in a constants file!
     Bundle extraAttendee = extras.getBundle("test.complex.attendee");
 
     AttendeeObject attendee = new AttendeeObject();

     // Deconstruct the Bundle into the AttendeeObject
     attendee = attendee.unBundleAttendee(extraAttendee);

     // The AttendeeObject fields are now populated, so we set the texts
     ((TextView) findViewById(R.id.attendee_age_value))
            .setText(String.valueOf(attendee.getAge()));
     ((TextView) findViewById(R.id.attendee_name_value))
            .setText(attendee.getName());
     ((TextView) findViewById(R.id.attendee_presence_value))
            .setText(String.valueOf(attendee.isPresent()));
}
Here's how the receiving activity looks like:Here's the complete AttendeeObject class.
package droidista.example;

import android.os.Bundle;

public class AttendeeObject {

 public static final String NAME = "attendee.name";
 public static final String AGE = "attendee.age";
 public static final String PRESENCE = "attendee.presence";
 
 
 private String mName;
 private int mAge;
 private boolean mIsPresent;
 
 public AttendeeObject(){
  
 }
 
 public Bundle bundleAttendee(AttendeeObject attendee){
  Bundle bundle = new Bundle();
  bundle.putString(NAME, attendee.getName());
  bundle.putInt(AGE, attendee.getAge());
  bundle.putBoolean(PRESENCE, attendee.isPresent());
  
  return bundle;
 }
 
 public AttendeeObject unBundleAttendee(Bundle attendeeBundle){
  AttendeeObject attendee = new AttendeeObject();
  attendee.setName(attendeeBundle.getString(NAME));
  attendee.setAge(attendeeBundle.getInt(AGE));
  attendee.setIsPresent(attendeeBundle.getBoolean(PRESENCE));
  
  return attendee;
 }
 
 public String getName() {
  return mName;
 }

 public void setName(String name) {
  this.mName = name;
 }

 public int getAge() {
  return mAge;
 }

 public void setAge(int age) {
  this.mAge = age;
 }

 public boolean isPresent() {
  return mIsPresent;
 }

 public void setIsPresent(boolean isPresent) {
  this.mIsPresent = isPresent;
 }
}
And our main activity:
package droidista.example;

import java.util.ArrayList;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;
import android.widget.AdapterView.OnItemSelectedListener;

public class ComplexObjectDemo extends Activity implements OnClickListener {
 
 private Button mGoToNextIntent, mCreateAttendee, mClearFields;
 private EditText mNameText, mAgeText;
 private Spinner mPresence;
 
 AttendeeObject mAttendee = new AttendeeObject();
 
 private boolean mCurrentPresence;
 
 private ArrayList<AttendeeObject> mAttendees = new ArrayList<AttendeeObject>();
 
 @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_complex_demo);
        
        setUpViews();
 }

 private void setUpViews() {
  setUpSpinner();  
     
  mNameText = (EditText)findViewById(R.id.name_text_input);
  mAgeText = (EditText)findViewById(R.id.age_text_input);  
  
  mGoToNextIntent = (Button)findViewById(R.id.btn_submit_object);
  mGoToNextIntent.setOnClickListener(this);
  
  mCreateAttendee = (Button)findViewById(R.id.btn_create_attendee);
  mCreateAttendee.setOnClickListener(this);
  
  mClearFields = (Button) findViewById(R.id.btn_clear_fields);
  mClearFields.setOnClickListener(this);
 } 
 
 private void setUpSpinner() {
  mPresence = (Spinner) findViewById(R.id.presence_spinner);
     ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
             this, R.array.is_present_values, android.R.layout.simple_spinner_item);
     adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
     mPresence.setAdapter(adapter);
 }
 
 
 private Bundle createAttendeeBundle(int index) {
  // Here, mAttendee is an instance of AttendeeObject
  // and mAttendees is an ArrayList holding all the created attendees
  return mAttendee.bundleAttendee(mAttendees.get(index));
 }

 public void onClick(View v) {
  switch(v.getId()){
  case R.id.btn_create_attendee:
   onCreateAttendee();
   break;
  case R.id.btn_submit_object:
   onSubmitObject();  
   break;
  case R.id.btn_clear_fields:
   onClearFields();
   break;
  }
 }

 private void onClearFields() {
  mAgeText.setText("");
  mNameText.setText("");
  mPresence.setSelection(0);
 }

 private void onSubmitObject() { 

  // For simplicity, we will always get the second value
  Bundle value = createAttendeeBundle(1);

  // In a "real" application, the Key should be defined in a constants file
  Intent intent = new Intent(ComplexObjectDemo.this, ComplexObjectReceiverDemo.class);
  intent.putExtra("test.complex.attendee", value);
   
  // Start the new activity
  startActivity(intent);
 }

 private void onCreateAttendee() {
  // Create a new AttendeeObject
  AttendeeObject attendee = new AttendeeObject();
  attendee.setName(mNameText.getText().toString());
  attendee.setAge(Integer.valueOf(mAgeText.getText().toString()));
  attendee.setIsPresent(mCurrentPresence);
  
  // You can then do whatever you want with this object
  // like adding it to an ArrayList or saving it to a database
  mAttendees.add(attendee);
  
  // Notify the user
  Toast.makeText(ComplexObjectDemo.this, "Attendee added!", Toast.LENGTH_SHORT).show();
 }

 public class MyOnItemSelectedListener implements OnItemSelectedListener {

     public void onItemSelected(AdapterView parent, View view, int pos, long id) {
      mCurrentPresence = (pos==0) ? true : false;      
     }

     public void onNothingSelected(AdapterView parent) { /*Do nothing.*/  }
 }
}
And here's the second activity:
package droidista.example;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class ComplexObjectReceiverDemo extends Activity {
 
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_complex_demo_receiver);

  // Get the extras passed to this Activity 
  setUpViews(getIntent().getExtras());        
 }

 private void setUpViews(Bundle extras) {
  // Again, the key should be in a constants file!
  Bundle extraAttendee = extras.getBundle("test.complex.attendee");
  AttendeeObject attendee = new AttendeeObject();

  // Deconstruct the Bundle into the AttendeeObject
  attendee = attendee.unBundleAttendee(extraAttendee);

  // The AttendeeObject fields are now populated, so we set the texts
  ((TextView) findViewById(R.id.attendee_age_value)).setText(String.valueOf(attendee.getAge()));
  ((TextView) findViewById(R.id.attendee_name_value)).setText(attendee.getName());
  ((TextView) findViewById(R.id.attendee_presence_value)).setText(String.valueOf(attendee.isPresent()));
 }

}

28 May 2011

Dreaming of Google I/O: ADT preview

It is my dream to one day attend Google I/O. But seeing as I'm from a Third World country where everyday is a practice in cost-cutting, it is very unlikely that I would fulfill that dream anytime soon. I haven't sat down and computed the actual cost, but thinking about it makes my head spin. Off the top of my head:
  • US Visa application = P6500 (~USD150)
  • Round trip plane fare ticket = P90,000 (~USD2000, if I'm lucky)
  • Google I/O ticket = P21,000 (~USD450, if my research is correct)
Then of course, there's food, transportation, accommodations, and pocket money. *sigh*

For now, I would have to content myself with watching videos from the sessions. Yesterday, I watched Android SDK tech lead's Xavier Ducrohet's session on the Android Development Tools. I had a little interaction with Xavier on the Android developers Google group before, and it is refreshing to finally map a face to the name. I kinda feel bad too, because the few times that I talked to him in the groups, it is to complain about the problems I'm having whenever I update my ADT. Eclipse, the SDK and I have a love-hate relationship with each other. But that's another story.

Anyway, I was watching the session while having lunch which turned out to be a mistake. I was so awestruck by the new tools that I totally forgot to eat and my chicken got cold! I was gushing about the tools, but of course no one can relate as I am the only Android developer in our company. I was applauding and cheering by myself. Oh well.

Needless to say, I am so impressed with the new version of ADT! (More exclamation marks should follow, but I'm trying to act mature) I used to hate making layout files. I am lightyears away from being artistic, I don't have a strong background in XML, and I simply find all the available features overwhelming. It's developer option paralysis, I would think. But the new ADT makes making layouts look like F-U-N! I love how it's sleeker, seemingly more user-friendly (I'd retain the qualifier until I get to try it out), and more feature-packed.

Some of the reasons I'm already loving the new ADT:
1. Smarter extract to include
The current ADT already has an extract to include feature, but the new version looks smarter. When you choose to extract a layout stub as an include, the ADT looks for the exact same layout in all your other layout files and replaces them with the include tag! Brilliant, I say!

2. XML editors: attribute auto-complete
Oh. My. God. I have been ranting about this for so long! There are simply too many attributes available for each type of widget I find it impossible to remember everything! And then of course I have to make sure that I am putting the correct value formats into those attributes! I admit that it terrified me when I was starting out in Android; having this feature saves the newbies from a lot of heartache.

3. Layout actions toolbar
Easier to set attributes without having to scroll down a lot then finding out you put in the wrong value and then having to scroll down again. Nifty!

4. Widget Previews in the palette
One time I was creating a layout file, I wanted to put an indeterminate progress bar but I want it to use the small progress bar style. But then I lost my list of native Android styles so I have to Google for the correct style that I want (?android:attr/android:progressBarStyleSmall is a little hard to remember).

Soon I can choose precisely what I want before dragging widgets onto the canvas! The previews plus the attribute autocomplete feature would work wonders, I would guess. And it is wonderful that the previews change with the selected theme!

5. Support for more widgets
I used to think that I'm doing an include wrong, because the graphical editor doesn't show the layout I'm including. It turns out that I have to put it inside a LinearLayout just to have the editor render it properly. But it looks like those days are behind us now!

Also gone are the days when the ADT cannot render TabHosts! I'm doing my happy dance now!

6. (Hopefully) easier drag-and-drop
Dragging-and-dropping in RelativeLayouts is a pain in the ass. The editor simply does not listen to me! But in the new ADT it looks like the new anchors are easier to understand than the current one, with squares all around.

I haven't tried dragging a view directly into the outline in the current ADT, but knowing that I would have it soon makes me happy!

7. Custom views available in the GUI editor
And then Xavier's team showed us how much they love us by including custom views in the widget palette. I haven't really used that many custom views, but I guess a lot of developers who customize like crazy would love this feature.



That being said, I would like to thank Xavier and his team for making sure that us developers continue to love doing what we do by making our jobs a little easier. I would like to think that they take time to listen to our opinions, and they for sure make it easy for us to talk to them. I love how accessible they are, and how very supportive of us. Sending good vibes to you and your team, Xavier!

I just hope that having this new ADT would not make developers lazy. I wish that we still make sure we understand how the XML file works, that we still read the documentation for the widgets, that we simply do not rely on automation to do the work for us. For starters, make sure you know how to change an EditText to accept passwords, or phone numbers, or only letters without relying on the palette.

I admit that I screech like a true fan girl when Romain Guy or Xavier answer my tweets. *blush*

Here's the session's video. Gush with me!

10 May 2011

Changing a button's text color

There are times that when changing a button's background color, we also want to change the text's color. There is a method setTextColor(int color) specifically for this purpose. Seems pretty straightforward enough, but it took me a few tries to get it right the first time I tried using it. Documenting it here so that I wouldn't forget.

Here's a screenshot of my button with its default settings.

What I'm trying to do is have the color of the text change to a shade of blue once the button is pressed. But look what I got.
That is no shade of blue! Far from it! I tried changing the alpha value of the #AARRGGBB value, but I still end up with the same gray shade. It turns out that you cannot pass a direct R.* id to the setTextColor() method.

Here's how I finally managed to make it work:
int newColor = getResources().getColor(R.color.button_new_color);
((Button)view).setTextColor(newColor);
And tada!

26 April 2011

Using CWAC's EndlessAdapter with a custom adapter

In one of my projects, the app has the potential to display a very, and I mean very, long list. To minimize the loading time of the app, I limit the number of items initially included in the list and then add to it as the user scrolls down.

For this purpose, Mark Murphy's EndlessAdapter works wonders. I was trying to make it work with a CursorAdapter though, but due to time constraints, I was not able to continue with my experiment.

And then I found out that some of the items in my list are HTML-formatted. Huh. So I have to have a custom adapter to override getView(). I patterned my code on the demo included in the EndlessAdapter project and I was at a loss. Maybe it's because I just came from a vacation and my mind refuses to work. Hmmm.

To cut a long and arduous journey short, I was able to figure it out. Here's a sample activity that displays a list of countries from an array. To illustrate using a custom adapter, I add the list item number when setting the item text.

I will discuss the pertinent parts of the code in detail.
private void init() {
 LIST_SIZE = COUNTRIES.length;  
 for (int i=0; i<=BATCH_SIZE; i++){
  countriesSub.add(COUNTRIES[i]);   
 }
 
 setLastOffset(BATCH_SIZE);
 displayList(countriesSub);
}
In this part of the code, I get the first 10 items in the list as the initial contents of the list. Of course, our current list is small and is peanuts for ListView. This is just to illustrate my point. In my app, I initially load 2,000 items. I also make sure to remember where I am in my source list. In my case, it is the offset in the original array. This might also be a row in the DB, or the ID in an RSS feed.

The ArrayList countriesSub holds the items that are currently in my list. As the user goes through the list, this array will grow.

To display my list, I set an instance of DemoAdapter as my list adapter. DemoAdapter inherits from EndlessAdapter and in its constructor, I give it an instance of my custom ArrayAdapter.
@Override
  public View getView(int position, View convertView, ViewGroup parent) {

   ViewHolder holder;   

   if(convertView==null){
    LayoutInflater inflater = (LayoutInflater)
    EndlessCustom.this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    convertView = inflater.inflate(android.R.layout.simple_list_item_1, null);
    holder = new ViewHolder();
    holder.word = (TextView)convertView.findViewById(android.R.id.text1);

    convertView.setTag(holder);
   } else{
    holder=(ViewHolder)convertView.getTag();
   }

   holder.word.setText(String.valueOf(position+1) + ". " + countriesSub.get(position));
   return convertView;
  }

The important part in my custom ArrayAdapter is the getView() method. In this method, I tell the adapter to not simply display the .toString() value of the item, but to add a number before it.

Notice that I use the ViewHolder pattern as illustrated in the Android Developers site.
DemoAdapter() {
 super(new CustomArrayAdapter(EndlessCustom.this, 
  android.R.layout.simple_list_item_1, android.R.id.text1, countriesSub));

 rotate=new RotateAnimation(0f, 360f, Animation.RELATIVE_TO_SELF,
  0.5f, Animation.RELATIVE_TO_SELF,
  0.5f);
 rotate.setDuration(600);
 rotate.setRepeatMode(Animation.RESTART);
 rotate.setRepeatCount(Animation.INFINITE);
}

So now we come to the EndlessAdapter part. In the constructor, I pass into it an instance of my custom ArrayAdapter, indicating the source of the list, the layout for each row, and the TextView ID from the layout. I also instantiated the animation that will be used while the list is loading the additional items.
@Override
protected boolean cacheInBackground() {
 tempList.clear();
 int lastOffset = getLastOffset();
 if(lastOffset < LIST_SIZE){
  int limit = lastOffset + BATCH_SIZE;
  for(int i=(lastOffset+1); (i<=limit && i<LIST_SIZE); i++){
   tempList.add(COUNTRIES[i]);
  } 
   
  setLastOffset(limit);

  if(limit<LIST_SIZE){
   return true;
  } else {
   return false;
  }
 } else  {
  return false;
 }
}
We have to override cacheInBackground() for EndlessAdapter to work. Here we do the heavy lifting like querying the server, reading from the DB, etc. Here, I load the next 10 items from the original list and put them in a temporary ArrayList. I also check whether I have loaded all the data, and if so, tell the EndlessAdapter to not show the extra row at the bottom. I do this by returning false from the method.
@Override
protected void appendCachedData() {
 @SuppressWarnings("unchecked")
 ArrayAdapter<String> arrAdapterNew = (ArrayAdapter<String>)getWrappedAdapter();

 int listLen = tempList.size();
 for(int i=0; i<listLen; i++){
  arrAdapterNew.add(tempList.get(i));
 }
}
Finally, I add the newly retrieved rows to my current list. And that's it.

The complete Java file for this activity follows:
package com.test.example;

import java.util.ArrayList;
import java.util.List;

import android.app.ListActivity;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import com.commonsware.cwac.endless.EndlessAdapter;

public class EndlessCustom extends ListActivity {

 static int LIST_SIZE;
 private int mLastOffset = 0;
 
 static final int BATCH_SIZE = 10;
 
 ArrayList<String> countriesSub = new ArrayList<String>();
 
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.lib_activity_dictionary);
  init();
 }

 private void init() {
  LIST_SIZE = COUNTRIES.length;  
  for (int i=0; i<=BATCH_SIZE; i++){
   countriesSub.add(COUNTRIES[i]);   
  }
  setLastOffset(BATCH_SIZE);
  displayList(countriesSub);
 }

 private void setLastOffset(int i) {
  mLastOffset = i;  
 }
 
 private int getLastOffset(){
  return mLastOffset;
 }

 private void displayList(ArrayList<String> countriesSub) {  
  setListAdapter(new DemoAdapter());
 }

 private class CustomArrayAdapter extends ArrayAdapter<String>{

  public CustomArrayAdapter(Context context, int resource,
    int textViewResourceId, List<String> objects) {
   super(context, resource, textViewResourceId, objects);
  }  

  @Override
  public View getView(int position, View convertView, ViewGroup parent) {

   ViewHolder holder;   

   if(convertView==null){
    LayoutInflater inflater = (LayoutInflater)
    EndlessCustom.this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    convertView = inflater.inflate(android.R.layout.simple_list_item_1, null);
    holder = new ViewHolder();
    holder.word = (TextView)convertView.findViewById(android.R.id.text1);

    convertView.setTag(holder);
   } else{
    holder=(ViewHolder)convertView.getTag();
   }

   holder.word.setText(String.valueOf(position+1) + ". " + countriesSub.get(position));
   return convertView;
  }

  public class ViewHolder{
   public TextView word;        
  }  
 }

 class DemoAdapter extends EndlessAdapter {
  private RotateAnimation rotate=null;
  ArrayList<String> tempList = new ArrayList<String>();
  
  DemoAdapter() {
   super(new CustomArrayAdapter(EndlessCustom.this, 
     android.R.layout.simple_list_item_1, android.R.id.text1, countriesSub));

   rotate=new RotateAnimation(0f, 360f, Animation.RELATIVE_TO_SELF,
     0.5f, Animation.RELATIVE_TO_SELF,
     0.5f);
   rotate.setDuration(600);
   rotate.setRepeatMode(Animation.RESTART);
   rotate.setRepeatCount(Animation.INFINITE);
  }

  @Override
  protected View getPendingView(ViewGroup parent) {
   View row=getLayoutInflater().inflate(R.layout.row, null);

   View child=row.findViewById(android.R.id.text1);
   child.setVisibility(View.GONE);
   child=row.findViewById(R.id.throbber);
   child.setVisibility(View.VISIBLE);
   child.startAnimation(rotate);

   return(row);
  }

  @Override
  protected boolean cacheInBackground() {
   tempList.clear();
   int lastOffset = getLastOffset();
   if(lastOffset < LIST_SIZE){
    int limit = lastOffset + BATCH_SIZE;
    for(int i=(lastOffset+1); (i<=limit && i<LIST_SIZE); i++){
     tempList.add(COUNTRIES[i]);
    }    
    setLastOffset(limit);
    
    if(limit<LIST_SIZE){
     return true;
    } else {
     return false;
    }
   } else  {
    return false;
   }
  }


  @Override
  protected void appendCachedData() {

   @SuppressWarnings("unchecked")
   ArrayAdapter<String> arrAdapterNew = (ArrayAdapter<String>)getWrappedAdapter();

   int listLen = tempList.size();
   for(int i=0; i<listLen; i++){
    arrAdapterNew.add(tempList.get(i));
   }
  }
 }
 
 
 static final String[] COUNTRIES = new String[] {
        "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", 
        "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda", "Argentina",
        "Armenia", "Aruba", "Australia", "Austria", "Azerbaijan",
        "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium",
        "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia",
        "Bosnia and Herzegovina", "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory",
        "British Virgin Islands", "Brunei", "Bulgaria", "Burkina Faso", "Burundi",
        "Cote d'Ivoire", "Cambodia", "Cameroon", "Canada", "Cape Verde",
        "Cayman Islands", "Central African Republic", "Chad", "Chile", "China",
        "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo",
        "Cook Islands", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czech Republic",
        "Democratic Republic of the Congo", "Denmark", "Djibouti", "Dominica", "Dominican Republic",
        "East Timor", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea",
        "Estonia", "Ethiopia", "Faeroe Islands", "Falkland Islands", "Fiji", "Finland",
        "Former Yugoslav Republic of Macedonia", "France", "French Guiana", "French Polynesia",
        "French Southern Territories", "Gabon", "Georgia", "Germany", "Ghana", "Gibraltar",
        "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guinea", "Guinea-Bissau",
        "Guyana", "Haiti", "Heard Island and McDonald Islands", "Honduras", "Hong Kong", "Hungary",
        "Iceland", "India", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", "Italy", "Jamaica",
        "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Kuwait", "Kyrgyzstan", "Laos",
        "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg",
        "Macau", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands",
        "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia", "Moldova",
        "Monaco", "Mongolia", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia",
        "Nauru", "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand",
        "Nicaragua", "Niger", "Nigeria", "Niue", "Norfolk Island", "North Korea", "Northern Marianas",
        "Norway", "Oman", "Pakistan", "Palau", "Panama", "Papua New Guinea", "Paraguay", "Peru",
        "Philippines", "Pitcairn Islands", "Poland", "Portugal", "Puerto Rico", "Qatar",
        "Reunion", "Romania", "Russia", "Rwanda", "Sqo Tome and Principe", "Saint Helena",
        "Saint Kitts and Nevis", "Saint Lucia", "Saint Pierre and Miquelon",
        "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Saudi Arabia", "Senegal",
        "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands",
        "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "South Korea",
        "Spain", "Sri Lanka", "Sudan", "Suriname", "Svalbard and Jan Mayen", "Swaziland", "Sweden",
        "Switzerland", "Syria", "Taiwan", "Tajikistan", "Tanzania", "Thailand", "The Bahamas",
        "The Gambia", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey",
        "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Virgin Islands", "Uganda",
        "Ukraine", "United Arab Emirates", "United Kingdom",
        "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan",
        "Vanuatu", "Vatican City", "Venezuela", "Vietnam", "Wallis and Futuna", "Western Sahara",
        "Yemen", "Yugoslavia", "Zambia", "Zimbabwe"
    };
}

If you know of a more efficient way to do this, please do not hesitate to let me know! :)

31 March 2011

My EditText is cut off by the on-screen keyboard!

With clients demanding left and right that my app should look like an iPhone app, I tend to be unappreciative of the way Android natively handles UI interactions and such. Notice how the screen automagically scrolls up when you click on an EditText? It turns out that in iPhone development, the developer does this manually (indicate how much the view should scroll when the on-screen keyboard appears, then scroll it back down afterwards). HA!

But even magic fails sometimes. Has this ever happened to you?

The bottommost EditText is cut off. And we don't want that!

So what do we do? Do we programmatically scroll the view up? I don't want to do that! It turns out that we can just wrap the whole view in a ScrollView and it will scroll up properly!


The only downside to this is that you might want to hide the scrollbar when the view moves up to accommodate the on-screen keyboard. And to do that, I was trying to set the android:scrollbars="none" attribute but for one reason or another the scrollbar is still being drawn. To make the scrollbar disappear, we can do it from code as such:
((ScrollView)findViewById(R.id.my_scrollview))
.setVerticalScrollBarEnabled(false);
And we're done!

18 February 2011

Using a custom font in WebView

In one of my projects, I needed to display some special characters that the Android OS by itself cannot seem to render. I figured that I would need to provide a custom font that includes the characters that I needed.

If I was using a TextView, I could use TextView#setTypeFace. But I was using a WebView and I feared that things would be more complicated than that. So how do I do this?

Here's how we can make it work.

Step 1: We would need to have our font face included in our project's /assets folder. So look for a TTF file that you can use freely, make sure the author/creator allows you to re-distribute it!

Step 2: Edit your HTML file to include some CSS stuff, just so the WebView would know what font you want to use. Here's a sample file:
<html><head><link href="YourCssFile.css" rel="stylesheet" type="text/css" /></head><body><span class="phon">This string contains special characters: əˈpåstrəfi </span></body></html>
Make sure that the href references are correct. In this case, my CSS file, HTML file and font file are in the same folder.

Step 3: Define your CSS file. In this case, our YourCssFile.css would be:
@font-face {
font-family: "My font";
src: url('MyFontFile.TTF');
}

.phon, .unicode {
display: inline;
font-family: 'My font', Verdana, sans-serif;
font-size: 14pt;
font-weight: 500;
font-style:normal;
color: black;
}
Step 4: Load the file in your WebView! You can use
WebView webView = (WebView) findViewById(R.id.myWebview);
webView.loadUrl("path/to/html/file");
or
webView.loadDataWithBaseURL("file:///android_asset/",
article, "text/html", Encoding.UTF_8.toString(), null);
If you will use the second option, your article variable would contain the HTML string. Just make sure that you escape your quotation marks with a backslash (\).
IMPORTANT NOTE: This feature seems to be broken in Android 2.0 and 2.1, as reported here.

17 February 2011

It never ends!

Been ultra super busy the past few weeks. Also learning a lot of new things. And renewing my battle with orientation change, AsyncTasks and dialog boxes.

So, I have been thinking of what to document here next. Here are my choices:
- Loading HTML pages stored in the SD card in a WebView
- Managing your SharedPreferences
- Using a ViewStub

I had a long list in my mind yesterday, but I managed to not write it down. *facepalm*

07 February 2011

*stealth ninja mode on*

Over the past couple of weeks, this blog has been getting unusually high traffic. Which means I get more than one hit per week.

So if it isn't too much to ask, can I please know how you got here, and what you were looking for. And please please let me know if I said anything wrong, or if you know of a better/easier/more optimized way to do the stuff I'm trying to talk about. Thanks! :)

02 February 2011

That damn seekbar thumb

If you have ever needed to use a SeekBar, you definitely would have noticed how hard it is to move the slider (aka thumb) when it is set to the minimum or maximum value. The slider tends to be cut in half, and fitting your finger into it to press it becomes a test of patience.

See how small the slider becomes when it reaches the far ends of the SeekBar? Crazy!

Luckily, I found a way (just today!) to move the slider just a little tiny bit to make it easier to press. Apparently, there is a method called setThumbOffset() that allows us to nudge the slider by a number of pixels.

It's pretty easy to use, aside from the fact that it accepts pixels and not dip measurements. Anyway, here's how to do it:
int pixels = convertDipToPixels(8f);
SeekBar mySeekBar = (SeekBar) findViewById(R.id.quiz_settings_seekbar);
mySeekBar.setOnSeekBarChangeListener(mySeekBarListener);
mySeekBar.setThumbOffset(pixels);
I convert dip measurements to pixels to better manage the growing number of resolutions of screen sizes present. Here's the code to do that:
private int convertDipToPixels(float dip) {
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
float density = metrics.density;
return (int)(dip * density);
}
Aaaaaaaand this is now how our slider looks:Applause! Confetti! Applause!

30 January 2011

Enabling/disabling menu items on the fly

In one of my applications, I want to disable some menu entries if the database is not valid or is not present at all. To do that, I make use of the onPrepareOptionsMenu() API.

Let's say the user is on the Quiz activity. When the user presses the MENU button, the Quiz item should not be there anymore; and until the application validates the presence of a database, I want the Books item to be unclickable.

So I inflate my menu as usual from the XML file:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.my_menu, menu);

removeSomeItems(menu);
return true;
}

private void removeSomeItems(Menu menu){
MenuItem books = menu.findItem(R.id.menu_books);
books.setEnabled(false); // I want this item to be available eventually

menu.removeItem(R.id.menu_quiz); // I want this item to be unavailable for this activity
}

You have to remember though, that this method is called only ONCE, the very first time the user opens the menu.

The activity does its business as usual. When I am done checking if the database is there already, I want the user to be able to use the Books item. I have a boolean field that I set when the database is validated, and so now I can enable the menu.
@Override
public boolean onPrepareOptionsMenu (Menu menu){
super.onPrepareOptionsMenu(menu);

if(mIsDbValid){
MenuItem books = menu.findItem(R.id.menu_books);
books.setEnabled(true);
}

return true;
}


You can change the contents of the menu as often as you want using onPrepareOptionsMenu() because that method is called each and every time the user presses the MENU button.

And that's it!

18 January 2011

Just wondering

You know those postscripts that Google engineers have on their posts in forums? I wonder if they have a sort of "standards body" that came up with it. They all sound like this:
Note: please don't send private questions to me, as I don't have time to provide private support. All such questions should be posted on public
forums, where I and others can see and answer them


I wonder why I wonder about these things.