Tuesday 28 September 2010

The art of scalable Android Layouts

One of the harder things to figure-out in Android, is how to do nice layouts that work well on different screen sizes. And, as always, I found it difficult to track-down that information!

The key for me in many circumstances is to position elements in percentage terms, generally using fill_parent; this is the opposite of most of the examples I have seen, which tend to use wrap_content! My approach is to ensure that size is defined in relative terms from the top-down using layout_weight; rather than allowing children to dictate their own size (and thereby mess-up the overall layout) via wrap_content...!

Here is a simple example of my approach.

Imagine you have a screen which you want to split into two panels, aligned horizontally, where you want the right-most panel to take-up around 20% of the screen size.

Create a new layout which is a frame layout. Set layout_width and layout_height to fill_parent. We'll use this for most other elements.

Create a LinearLayout within the frame parent. Set orientation to horizontal. Set layout_width and layout_height to fill_parent.

Create two elements (whatever type you need for your panels, maybe ImageView with a drawable background picture if you're just experimenting) as children of your new LinearLayout. Set both to have layout_width of 0dp and layout_weight of 50. If you want one layout to be wider than the other, simple adjust the weights; the element with the larger weight will be bigger proportionately. Make sure you sent layout_height to be fill_parent; otherwise the panels won't properly fill the screen in different configurations!

Want to split-up your right-hand panel into two vertical elements? Replace your right-hand element with a new LinearLayout. Set layout_width to 0dp, and layout_weight to whatever your previous right-hand element was (e.g. 50). Set orientation to vertical. Set layout_height to fill_parent. Put two new elements (again, maybe ImageViews for example...) in the new LinearLayout; set both to have layout_height of 0dp and layout_weight of 50. If you want one layout to be taller than the other, simple adjust the weights. Make sure you sent layout_width to be fill_parent; otherwise (as before!) the panels won't properly fill the screen in different configurations.

And so on! If you need to use an invisible spacer, you can simply follow the above approach and use an empty LinearLayout with the appropriate layout_weight.

This makes sense once you've tried it out; remember to test the layout you create with a variety of AVD sizes!

Thursday 16 September 2010

Android - building the PDK using the SDK, and adding a system service

Just how difficult can it be to:
- download the Android source code
- figure-out how to add a system service
- call that service from an activity installed to the system after the event
- test this all out with the Android emulator.

The answer is: a lot harder than you might think!

Because this was a complete pain, involving a lot of fiddling-around and tracking-down and experimentation, I thought it would be a good to set-down the steps I had to follow. I really hope it helps somebody else out at some point!

I'm assuming that you're using Windows XP, Vista or 7 as your base machine.

Downloading the Android Source Code (Platform Development Kit - PDK)

The Android Platform Development Kit (PDK) download instructions are here.

http://source.android.com/source/download.html
First things first: install VirtualBox and install Ubuntu 32-bit as a Virtual Machine under Virtual Box.

Then install Android under this Virtual Machine.

You’ll require the java5 jdk.
The 64-bit build does not always work properly... it depends on which version of Android you’re using...:
   
http://groups.google.com/group/android-building/browse_thread/thread/193332fd6850a2a
All the open-source codelines that mirror Google's internal froyo
branches (i.e. froyo and the various android-2.2) use 1.5 like
Google's matching internal codelines do, since that is the version
that Google developed and tested with through the entire froyo
development cycle.
We backported the changes to switch to version 1.6 (64-bit) from our
internal master branch to the open-source master just as we switched
our internal master branch to 1.6, since from that point there was no
requirement for contributions to the open-source master branch to
build under 1.5 any more.
If you need to use the 64-bit version, then in order to complete the sudo apt-get install git-core ... command, you'll need to do this to get the correct JDK configured:
# Tell the system where to get the JDK...
sudo add-apt-repository "deb http://archive.canonical.com/ lucid partner"
# Update the source list:
sudo apt-get update
# Install sun-java6-jdk;
sudo apt-get install sun-java6-jdk
The build problems otherwise are very simiilar to those for the 32-bit system.
However, various blocker issues arose. I gave up, and reverted to 32-bit building...

IMPORTANT NOTE: use the following as your repo init command... otherwise, you’ll get “work-in-progress” which might not build (if you want a different Android source code tag other than froyo, then feel free to use one):

repo init -u git://android.git.kernel.org/platform/manifest.git -b  froyo

If you have problems with Java, try doing this:

sudo update-java-alternatives -s java-1.5.0-sun

Building the Android Source Code

The Android PDK build instructions are here:

http://pdk.android.com/online-pdk/guide/build_system.html

Here is what I did.
cd ~/mydroid
lunch
# Choose 2. simulator
make
============================================
PLATFORM_VERSION_CODENAME=REL
PLATFORM_VERSION=2.2
TARGET_PRODUCT=sim
TARGET_BUILD_VARIANT=eng
TARGET_SIMULATOR=true
TARGET_BUILD_TYPE=debug
TARGET_BUILD_APPS=
TARGET_ARCH=x86
HOST_ARCH=x86
HOST_OS=linux
HOST_BUILD_TYPE=release
BUILD_ID=MASTER
============================================

Note: the alternative of trying choosecombo (selecting: simulator, debug, eng) isn’t recommended by Google heads; they recommend the use of lunch.

To fix this error...:
usr/include/bits/fcntl2.h:51: error: call to ‘__open_missing_mode’ declared with attribute error: open with O_CREAT in second argument needs 3 arguments

... I had to edit frameworks/base/core/jni/android_server_Watchdog.cpp
... and add this new argument at the end of the function call where the reference to O_CREAT:
    , 0644);

To fix this error...:
frameworks/base/opengl/libagl/egl.cpp: In member function ‘virtual EGLBoolean android::egl_window_surface_v2_t::swapBuffers()’: frameworks/base/opengl/libagl/egl.cpp:554: internal compiler error: in add_phi_arg, at tree-phinodes.c:391

... I had to remove the const on line 554 in frameworks/base/opengl/libagl/egl.cpp

If you get problems reported when building, related to a missing file called asoundlib.h, you’ll need to install these packages and then re-attempt the make:
libasound2-dev
lib32asound2-dev

If you get problems reported when building, related to a missing files called something similar to wx/wxprec.h, you’ll need to install this package (WxWidgets) and then re-attempt the make:
sudo  apt-get install libwxgtk2.6-0 libwxbase2.6-0 libwxbase2.6-dev

If you get problems reported when building like this:
|~/mydroid/development/simulator/app/DeviceWindow.cpp:175: undefined reference to `wxImage::HasAlpha() const'

You’ll have to edit:

development/simulator/app/DeviceWindow.cpp - comment-out the lines to HasAlpha()
development/simulator/app/MainFrame.cpp - comment-out the lines to AppendSeparator()

Modify external/stlport/stl/_num_put.c in line 351. to use the first form of the #ifdef block (the second one, which would otherwise be chosen, causes compilation to fail).

Remove external/gtest/Android.mk, external/gtest/test/src/Android.mk and external/gtest/test/Android.mk (which had STL-related problems in my system!)


Installing the Android SDK

Download the Android 2.2 SDK from here:
Expand the download file to e.g. ~/Downloads/android-sdk-linux_x86

Add this to your PATH, e.g. (but: make sure this comes before any other Android-related paths...):

export PATH=$PATH:~/Downloads/android-sdk-linux_x86
Restart your shell!

cd ~/Downloads/android-sdk-linux_x86/tools
export ANDROID_SWT=/home/mycompany/Downloads/android-sdk-linux_x86/tools/lib/x86
android
... Select and install the SDK Platform Android 2.2, API 8, revision 2
Further notes on installing the Android SDK 2.2
Now, copy the Platform SDK so that the PDK can find it!

    # Create a new terminal, and then...
cd ~/mydroid
export PATH=/home/mycompany/mydroid/out/host/linux-x86/bin:$PATH
# Note that when you first do this, ~/mydroid/out/host/linux-x86/platforms and ~/mydroid/out/host/linux-x86/add-ons do NOT exist!
cp -pR \
~/Downloads/android-sdk-linux_x86/platforms \
~/Downloads/android-sdk-linux_x86/platforms \
out/host/linux-x86

Running the emulator with the newly built image

# Create a new terminal, and then...
cd ~/mydroid
export PATH=~/mydroid/out/host/linux-x86/bin:$PATH
export ANDROID_SWT="~/mydroid/out/host/linux-x86/framework"
# Now-up the emulator with the correct image!
./out/host/linux-x86/bin/emulator -system out/target/product/generic/system.img -sysdir out/target/product/generic -kernel prebuilt/android-arm/kernel/kernel-qemu -data out/target/product/generic/userdata.img

Adding a new service to the System

With reference to:
http://groups.google.com/group/android-porting/browse_thread/thread/f9a383ce949d1557?pli=1

Create a new service as frameworks/base/packages/IMyTest, as a very rough clone of frameworks/base/packages/SettingsProvider

Edit build/target/product/core.mk, to add the following line after the line showing DefaultContainerService :

        MyTestService \

Create various files:

mydroid/frameworks/base/packages/MyTestService/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mycompany">
       <application android:label="@string/service_name">
          <service android:name=".MyTestService"
                   android:enabled="true"
                   android:exported="true"/>
       </application>
</manifest>
   
mydroid/frameworks/base/packages/MyTestService/Android.mk


LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-subdir-java-files)
LOCAL_PACKAGE_NAME := MyTestService
LOCAL_CERTIFICATE := platform
include $(BUILD_PACKAGE)

mydroid/frameworks/base/packages/MyTestService/src/com/mycompany/MyTestService.java

package com.mycompany;
import com.mycompany.IMyTestService;
import com.android.internal.content.PackageHelper;
import android.content.Intent;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInfoLite;
import android.content.pm.PackageManager;
import android.content.pm.PackageParser;
import android.net.Uri;
import android.os.Environment;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.StatFs;
import android.app.IntentService;
import android.util.DisplayMetrics;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import android.os.FileUtils;
import android.provider.Settings;
//
// Based on src/com/android/defcontainer/DefaultContainerService.java
// Also see:
//   frameworks/base/packages/MyTestService/AndroidManifest.mk
//   frameworks/base/packages/MyTestService/Android.mk
//   frameworks/base/core/java/com/mycompany/IMyTestService.aidl
//   frameworks/base/Android.mk
//
// Note that the .aidl file can be used by external activity projects.
//
public class MyTestService extends IntentService {
    private static final String TAG = "MyTestService";
    private IMyTestService.Stub mBinder = new IMyTestService.Stub()
    {
          public void setValue(int val)
          {
                  Log.i(TAG, "In setValue! ");
          }
    };
    public MyTestService () {
          super("MyTestService");
          setIntentRedelivery(true);
    }
    @Override
    protected void onHandleIntent(Intent intent)
    {
          Log.i(TAG, "onHandleIntent!");
    }
    public IBinder onBind(Intent intent) {
          Log.i(TAG, "onBind!");
          return mBinder;
    }
}

mydroid/frameworks/base/packages/MyTestService/res/values/strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
       <!-- service name  -->
       <string name="service_name">MyTestService</string>
</resources>

mydroid/frameworks/base/core/java/com/mycompany/IMyTestService.aidl

package com.mycompany;
interface IMyTestService {
/**
* {@hide}
*/
void setValue(int val);
}
The interface file will need to be added to the build system, so edit frameworks/base/Android.mk
... and add the following at the end of the list of LOCAL_SRC_FILES:

core/java/com/mycompany/IMyTestService.aidl \

Type this to make... if you don’t do the make clobber, your new MyTestService.apk file won’t be built!

make clobber
make

You will now see this error reported:

(unknown): error 3: Added class IMyTestService to package android.os
(unknown): error 3: Added class IMyTestService.Stub to package android.os
******************************
You have tried to change the API from what has been previously approved.
To make these errors go away, you have two choices:
 1) You can add "@hide" javadoc comments to the methods, etc. listed in the
    errors above.
 2) You can update current.xml by executing the following command:
       make update-api
    To submit the revised current.xml to the main Android repository,
    you will need approval.
******************************

Type this to work-around this:

    make update-api

Run this to verify that your new service is now in the build images...:
ls -ltr ~/mydroid/out/target/product/generic/system/app/MyTestService.apk
find . -name "*.img" | xargs -- grep "MyTestService.apk"

... This should show that the code you’ve created has appeared in ./userdata.img ...
Edit this to look for IMyTestService,  to verify that it has been added!

~/mydroid/frameworks/base/api/current.xml

5. Test the service APk is picked-up by the emulator...

Start up the emulator (using the previous long command line!)
Using adb logcat, you will find the message saying that the package has changed.
I/PackageManager(   68): /system/app/MyTestService.apk changed; collecting cert

6. Test Program

To test the service, create a "Hello World" activity using the project wizard under Eclipse on Windows.

MyServiceTest.java:
package com.mycompany.MyServiceTest;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import com.mycompany.IMyTestService;
public class MyServiceTest extends Activity {
  /** Called when the activity is first created. */
  IMyTestService imts;
  private ServiceConnection mConnection = new ServiceConnection()
  {
      @Override
      public void onServiceConnected(ComponentName className, IBinder service)
      {
          Log.d("MyServiceTest", "onServiceConnected");
          imts= IMyTestService.Stub.asInterface(service);
          try
          {
              // Try a dummy call to the service!
              imts.setValue(7); // Any value would do, this is just a test!
          }
          catch (Exception e)
          {
              e.printStackTrace();
          }
      }
      @Override
      public void onServiceDisconnected(ComponentName arg0) {
          Log.d("MyServiceTest", "onServiceDisconnected");
          imts = null;
      }
  };
  @Override
  public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      Log.d("MyServiceTest", "onCreate");
      setContentView(R.layout.main);
      // Bind-in to our service!
      Log.d("MyServiceTest", "try to bind service");
      Intent intent = new Intent();
      intent.setClassName("com.mycompany", "com.mycompany.MyTestService");
      bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
      Log.d("MyServiceTest", "done try to bind service");
  }
}

Copy the IMyTestService.aidl file from your Linux box, in your Windows/Eclipse project’s src/com/mycompany sub-folder (you’ll first have to create this folder in your activity’s project folders).

Refresh your project files from the Eclipse menu and rebuild/debug.

This should run benignly under Windows, warning that it was unable to find the service.

Copy the apk file to your Linux VM, and install using:

adb install MyServiceTest.apk

Run the app using the emulator’s launcher; you should see the service used!
I/MyTestService(  360): onBind!
D/MyServiceTest(  354): onServiceConnected
I/MyTestService(  360): In setValue!

Easy, wasn't it? :-D