Search This Blog

31 May 2010

Disclaimer


Use the Alert Dialogs example as a starting point.....

Add this icon to your src\main\android\res\drawable directory

I grabbed that icon from the Tango project, but had to change the filename to be compatable with Android.

Then from the Google Analytics example, create an analytics profile, update your pom.xml and AndroidManifest.xml


Instead of putting the tracker code directly into the onCreate method, let's move that into the "I Accept!" button.  Note the change to onDestroy as well!

public class DisclaimerTestActivity extends Activity {
    private static String TAG = "DisclaimerTest";
    private GoogleAnalyticsTracker tracker = null;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        AlertDialog dialog = new AlertDialog.Builder(this).create();
        dialog.setTitle("Disclaimer");
        dialog.setMessage("In order to provide support for this application, we reserve the right to anonymously track and report usage information in this application.  We will track which parts of the application are popular but will not track who you are or what you type / look at.");
        dialog.setIcon(R.drawable.network);
        dialog.setButton("I Accept!", new DialogInterface.OnClickListener(){
            public void onClick(DialogInterface dialogInterface, int i) {
                tracker = GoogleAnalyticsTracker.getInstance();
                tracker.start("UA-YOUR-ACCOUNT-HERE", DisclaimerTestActivity.this);
                setContentView(R.layout.main);
                track("/");
                dispatch();
                return;
            }
        });
        dialog.setButton2("No Way!", new DialogInterface.OnClickListener(){
            public void onClick(DialogInterface dialogInterface, int i) {
                finish();
                return;
            }
        });
        dialog.show();
    }

    @Override
    protected void onDestroy()
    {
        super.onDestroy();
        if(tracker != null) tracker.stop();
    }

    // track("Clicks", "Button", "clicked", 77
    private void track(String category, String action, String label, int value)
    {
        tracker.trackEvent(category, action, label, value);
    }

    // track("/testApplicationHomeScreen");
    // track("/download");
    private void track(String page)
    {
        tracker.trackPageView(page);
    }

    // Allows sending in bulk by queuing up multiple tracks before calling dispatch
    // timestamps based on when dispatch is called
    private void dispatch()
    {
        tracker.dispatch();
    }
}


Redeploy (mvn clean install).  Now, if you accept the disclaimer it will do analytic notifications (which can take up to 24 hours to show up).  If you cancel, it will just close the app.

30 May 2010

Google Analytics

Let's integrate Google Analytics into your app...

Go into your analytics account and click on "Add Website Profile" at the bottom of the table.  According to Google:
using a fake but descriptive website URL (e.g. http://mymobileapp.mywebsite.com)
I assume this is because it has to be a second-level domain and not a full Market URL.

Write down the "Web Property ID" in the yellow box (ie: something like UA-12345-67)

Reminder, Google says:
You must indicate to your users, either in the application itself or in your terms of service, that you reserve the right to anonymously track and report a user's activity inside of your app.
Make sure to hit "Save and Finish"

Download the Google Analytics SDK for Android.  Extract it somewhere convenient.

Deploy it to your local repo
F:\java\GoogleAnalyticsAndroid_0.7> mvn install:install-file -Dfile=libGoogleAnalytics.jar -DgroupId=com.google.android.apps.analytics -DartifactId=GoogleAnalyticsTracker -Dversion=0.7 -Dpackaging=jar

Let's create a basic app.

F:\work> mvn archetype:generate -DarchetypeCatalog=http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml

Choose the galatea-archetype plugin
groupId: org.eoti.android
artifactId: AnalyticsTest
version: 1.0-SNAPSHOT
package: org.eoti.android

Assuming your emulator is running...


F:\work> cd AnalyticsTest
F:\work\AnalyticsTest> mvn clean install

Add this dependency to your pom.xml:
<dependency>
  <groupId>com.google.android.apps.analytics</groupId>
  <artifactId>GoogleAnalyticsTracker</artifactId>
  <version>0.7</version>
</dependency>

Add these to your AndroidManfiest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />



Edit your activity:
public class AnalyticsTestActivity extends Activity
{
    private static String TAG = "AnalyticsTest";
    private GoogleAnalyticsTracker tracker;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        tracker = GoogleAnalyticsTracker.getInstance();
        tracker.start("UA-YOUR-ACCOUNT-HERE", this);

        setContentView(R.layout.main);
        track("/");
        dispatch();
    }

    @Override
    protected void onDestroy()
    {
        super.onDestroy();
        tracker.stop();
    }

    // track("Clicks", "Button", "clicked", 77
    private void track(String category, String action, String label, int value)
    {
        tracker.trackEvent(category, action, label, value);
    }

    // track("/testApplicationHomeScreen");
    // track("/download");
    private void track(String page)
    {
        tracker.trackPageView(page);
    }

    // Allows sending in bulk by queuing up multiple tracks before calling dispatch
    // timestamps based on when dispatch is called
    private void dispatch()
    {
        tracker.dispatch();
    }
}


Make sure to change your UA-YOUR-ACCOUNT-HERE to match your Web Property ID from above.


Redeploy (mvn clean install) and run your app... and then... voila???

No, not quite.  From Google:
Google Analytics generally updates your reports every 24 hours. This means that it could take 24 hours for data to appear in your account after you have first installed the tracking code.

Looks like I will be checking back tomorrow to make sure it works ;)


Update: About 18 hours later:

Live Wallpapers

Let's try doing some tabs.... We'll start with our basic app...

F:\work> mvn archetype:generate -DarchetypeCatalog=http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml
   
Choose the galatea-archetype plugin
groupId: org.eoti.android
artifactId: LiveWallpaperTest
version: 1.0-SNAPSHOT
package: org.eoti.android

Make sure the emulator is running...

F:\work> cd LiveWallpaperTest
F:\work\LiveWallpaperTest> mvn clean install

Rename your LiveWallpaperTestActivity to LiveWallpaperTestService and replace the contents:
public class LiveWallpaperTestService extends WallpaperService {
    private static String TAG = "LiveWallpaperTest";

    @Override
    public Engine onCreateEngine() {
        return new LiveWallpaperTestEngine();
    }

    class LiveWallpaperTestEngine
    extends WallpaperService.Engine
    {
        @Override
        public void onCreate(SurfaceHolder surfaceHolder) {
            super.onCreate(surfaceHolder);
        }
    }
}




Add yourself to src\main\android\res\values\strings.xml:
 <string name="author">Malachi de AElfweald</string>


Create a src\main\android\res\xml\wallpaper.xml:
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
   android:author="@string/author"
   android:description="@string/app_name"
   android:thumbnail="@drawable/icon"
/>


In the AndroidManifest.xml, replace this bit:
<activity android:name=".LiveWallpaperTestActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>
with this bit:
<service android:name=".LiveWallpaperTestService" android:permission="android.permission.BIND_WALLPAPER">
    <intent-filter>
    <action android:name="android.service.wallpaper.WallpaperService" />
    </intent-filter>
    <meta-data android:name="android.service.wallpaper" android:resource="@xml/wallpaper"/>
</service>

Redeploy (mvn clean install), click Menu and choose LiveWallpaper. Select your wallpaper and click "Set Wallpaper".

Ok, just a black background doesn't feel very live... let's liven it up just a little...

Replace the contents of your LiveWallpaperTestService:

public class LiveWallpaperTestService extends WallpaperService {
    private static String TAG = "LiveWallpaperTest";

    @Override
    public Engine onCreateEngine() {
        return new LiveWallpaperTestEngine();
    }

    class LiveWallpaperTestPainter
    extends Thread
    {
        private SurfaceHolder holder;
        private Rect frame;
        private boolean die = false;
        private MotionEvent event = null;
        private Paint cornsilk;
        private boolean visible = true;

        public LiveWallpaperTestPainter(SurfaceHolder holder)
        {
            this.holder = holder;
            frame = holder.getSurfaceFrame();
            cornsilk = new Paint(Paint.ANTI_ALIAS_FLAG);
            cornsilk.setColor(Color.rgb(255,248,220));
        }

        public void die()
        {
            this.die = true;
        }

        public void setVisible(boolean visible)
        {
            this.visible = visible;
        }

        public void setTouch(MotionEvent event)
        {
            this.event = event;
        }

        public void run()
        {
            Canvas canvas = null;
            while(!die)
            {
                if(!visible)
                {
                    try{Thread.sleep(500);}catch(Exception e){}
                    continue;
                }

                canvas = holder.lockCanvas();
                if(canvas != null)
                {
                    render(canvas);
                    holder.unlockCanvasAndPost(canvas);
                }
            }
        }

        protected void render(Canvas canvas)
        {
            if(event == null) return;
            canvas.drawColor(0xFF000000);
           
            canvas.drawLine(frame.left, frame.top, event.getX(), event.getY(), cornsilk);
            canvas.drawLine(frame.right, frame.top, event.getX(), event.getY(), cornsilk);
            canvas.drawLine(frame.left, frame.bottom, event.getX(), event.getY(), cornsilk);
            canvas.drawLine(frame.right, frame.bottom, event.getX(), event.getY(), cornsilk);
        }
    }

    class LiveWallpaperTestEngine
    extends WallpaperService.Engine
    {
        private LiveWallpaperTestPainter painter = null;

        @Override
        public void onCreate(SurfaceHolder surfaceHolder) {
            super.onCreate(surfaceHolder);
            this.setTouchEventsEnabled(true);
        }

        @Override
        public void onTouchEvent(MotionEvent event) {
            super.onTouchEvent(event);
            painter.setTouch(event);
        }

        @Override
        public void onSurfaceCreated(SurfaceHolder holder) {
            super.onSurfaceCreated(holder);
            painter = new LiveWallpaperTestPainter(holder);
            painter.start();
        }

        @Override
        public void onSurfaceDestroyed(SurfaceHolder holder) {
            if(painter != null) painter.die();
            super.onSurfaceDestroyed(holder);
        }

        @Override
        public void onVisibilityChanged(boolean visible) {
            super.onVisibilityChanged(visible);
            painter.setVisible(visible);
        }
    }
}

Redeploy (mvn clean install) and set your wallpaper.  Now, a "X" will mark where you last touched/clicked.

29 May 2010

Tabs

Let's try doing some tabs.... We'll start with our basic app...

F:\work> mvn archetype:generate -DarchetypeCatalog=http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml

Choose the galatea-archetype plugin
groupId: org.eoti.android
artifactId: TabTest
version: 1.0-SNAPSHOT
package: org.eoti.android

Make sure the emulator is running...
F:\work> cd TabTest
F:\work\TabTest> mvn clean install

Replace the contents of your src\main\android\res\layout\main.xml:

<?xml version="1.0" encoding="utf-8"?>
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
         android:id="@+id/my_tabhost"
         android:layout_width="fill_parent"
         android:layout_height="fill_parent">
    <TabWidget
            android:id="@android:id/tabs"
            android:layout_width="fill_parent"
            android:layout_height="65px"/>
    <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_width="fill_parent"
            android:layout_height="200px"
            android:paddingTop="65px">
        <LinearLayout
                android:id="@+id/content1"
                android:orientation="vertical"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent">
            <TextView
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:text="@string/weather"
                    />
        </LinearLayout>
        <LinearLayout
                android:id="@+id/content2"
                android:orientation="vertical"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent">
            <TextView
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:text="@string/faces"
                    />
        </LinearLayout>
    </FrameLayout>
</TabHost>


Replace the contents of your src\main\android\res\values\strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">TabTest</string>
    <string name="weather">This is the weather tab</string>
    <string name="faces">This is the faces tab</string>
</resources>


Insert this into the end of your onCreate method in your activity (src\main\java\org\eoti\android\TabTest.java in my case):
        TabHost tabs = (TabHost) this.findViewById(R.id.my_tabhost);
        tabs.setup();

        TabHost.TabSpec spec1 = tabs.newTabSpec("First tab");
        spec1.setIndicator("Weather");
        spec1.setContent(R.id.content1);
        tabs.addTab(spec1);

        TabHost.TabSpec spec2 = tabs.newTabSpec("Second tab");
        spec2.setIndicator("Face");
        spec2.setContent(R.id.content2);
        tabs.addTab(spec2);

        tabs.setCurrentTabByTag("First tab");

Redeploy (mvn clean install) and you will have some basic text-based tabs... 

But, maybe we want some graphics too... This tutorial says:
You need an icon for each of your tabs. For each icon, you should create two versions: one for when the tab is selected and one for when it is unselected. The general design recommendation is for the selected icon to be a dark color (grey), and the unselected icon to be a light color (white).
But I wanted to be a little more creative, so I grabbed a couple icons from the Tango project [note: had to replace '-' in filenames with '_']:
Download these images into src\main\android\res\drawable\





Create face.xml into src\main\android\res\drawable:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/face-devilish" android:state_selected="true" />
    <item android:drawable="@drawable/face-angel" />
</selector>

Create weather.xml into src\main\android\res\drawable:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/weather-showers-scattered" android:state_selected="true" />
    <item android:drawable="@drawable/weather-snow" />
</selector>

In your activity, replace this line:
        spec1.setIndicator("Weather");
with:
        spec1.setIndicator("Weather", this.getResources().getDrawable(R.drawable.weather));

And replace this:
        spec2.setIndicator("Face");
with:
        spec2.setIndicator("Face", this.getResources().getDrawable(R.drawable.face));


While we are at it, open your AndroidManifest.xml and replace this line:
        <activity android:name=".TabTestActivity">
with:
        <activity android:name=".TabTestActivity" android:theme="@android:style/Theme.NoTitleBar">

Redeploy (mvn clean install) and you will have nice icons on your tabs...

End result should look like these:




It is also possible to have the tabs load different Views or Activities by having the activity extend TabActivity. Maybe I'll do another tutorial for that.

Orientation changes

I spent a couple hours today trying to write a tutorial on orientation changes.  It should have been pretty straight-forward, but it never worked as advertised.  Then I came across this quote from here:

The emulator does NOT handle screen orientation changes correctly, AFAIK. The same code that successfully calls onConfigurationChanged() on the Sprint HTC Hero and causes the Activity object to not be regenerated, has a different behavior in the emulator. In the emulator, onConfigurationChanged() is never called, the Activity object is destroyed and recreated on every orientation change, and the display is repainted to the correct orientation only after the first orientation change and is not repainted thereafter. 

 I guess I will try to rewrite this tutorial once I find a way to make that work on the emulator.

28 May 2010

Writing files

Let's start by creating a basic app:

F:\work> mvn archetype:generate -DarchetypeCatalog=http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml

Choose the galatea-archetype plugin:
groupId: org.eoti.android
artifactId: FileTest
version: 1.0-SNAPSHOT
package: org.eoti.android


F:\work> cd FileTest
F:\work\FileTest> mvn clean install

Let's rewrite our hello world activity:
public class FileTestActivity extends Activity {
    private static String TAG = "FileTest";
    private static String FILENAME_PRIVATE = "filetest1.txt";
    private static String FILENAME_SDCARD = "/sdcard/filetest2.txt";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        try {
            PrintWriter out = new PrintWriter(openFileOutput(FILENAME_PRIVATE, MODE_PRIVATE));
            out.format("This is a test!\n");
            out.close();
            Log.i(TAG, "try: adb pull /data/data/org.eoti.android/files/filetest1.txt");
        } catch (FileNotFoundException e) {
            Log.e(TAG, "Error writing file #1", e);
        }

        try{
            PrintWriter out = new PrintWriter(new File(FILENAME_SDCARD));
            out.format("This is another test!\n");
            out.close();
            Log.i(TAG, "try: adb pull /sdcard/filetest2.txt");
        } catch (FileNotFoundException e) {
            Log.e(TAG, "Error writing file #2", e);
        }
    }
}

Redeploy (mvn clean install) and run the app.  You should now be able to grab the generated files from another command prompt:
adb pull /data/data/org.eoti.android/files/filetest1.txt
adb pull /sdcard/filetest2.txt

Saving State

Building on our Menus example, let's take a look at saving and restoring state.

Let's replace the content of your activity (src\main\java\org\eoti\android\MenuTestActivity.java  in my case):

public class MenuTestActivity extends Activity {
    private static String TAG = "MenuTest";
    private static String CURRENT_MENU_KEY = "CURRENT_MENU_KEY";
    private Menu menu;
    private SharedPreferences prefs;   
    private int current_menu;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        prefs = getPreferences(MODE_PRIVATE);
        changeMenu(prefs.getInt(CURRENT_MENU_KEY, R.id.first));
    }

    @Override
    protected void onPause() {
        super.onPause();
        SharedPreferences.Editor ed = prefs.edit();
        ed.putInt(CURRENT_MENU_KEY, current_menu);
        ed.commit();
    }

    @Override
    protected void onResume() {
        super.onResume();
        changeMenu(prefs.getInt(CURRENT_MENU_KEY, R.id.first));
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.mainmenu, menu);
        this.menu = menu;
        changeMenu(current_menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        return changeMenu(item.getItemId());
    }

    private boolean changeMenu(int id)
    {
        switch (id)
        {
            case R.id.first:
                if(menu != null)
                {
                    menu.setGroupVisible(R.id.first_menu, true);
                    menu.setGroupVisible(R.id.second_menu, false);
                }
                current_menu = id;
                return true;
            case R.id.second:
                if(menu != null)
                {
                    menu.setGroupVisible(R.id.first_menu, false);
                    menu.setGroupVisible(R.id.second_menu, true);
                }
                current_menu = id;
                return true;
        }
        return false;
    }
}

Redeploy (mvn clean install) and launch your app.  Now, when you exit the app (back, home, whatever) and relaunch, it will default to the same menu choice it was on before closing.