Search This Blog

Showing posts with label provider. Show all posts
Showing posts with label provider. Show all posts

01 July 2010

Content Provider and Live Folder

We're going to create a basic ContentProvider example based on this tutorial.

Let's start with a multi-module project:

F:\work> mkdir ProviderTest
F:\work> cd ProviderTest

F:\work\ProviderTest> mvn archetype:generate -DarchetypeCatalog=http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml
2: http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml -> galatea-archetype (null)
Confirm properties configuration:
groupId: org.eoti.android.providertest.provider
artifactId: TestProvider
version: 1.0-SNAPSHOT
package: org.eoti.android.providertest.provider

F:\work\ProviderTest> mvn archetype:generate -DarchetypeCatalog=http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml
2: http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml -> galatea-archetype (null)
Confirm properties configuration:
groupId: org.eoti.android.providertest.consumer
artifactId: TestConsumer
version: 1.0-SNAPSHOT
package: org.eoti.android.providertest.consumer

F:\work\ProviderTest> mvn archetype:generate -DarchetypeCatalog=http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml
2: http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml -> galatea-archetype (null)
Confirm properties configuration:
groupId: org.eoti.android.providertest.generator
artifactId: TestGenerator
version: 1.0-SNAPSHOT
package: org.eoti.android.providertest.generator

Create a top-level pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>org.eoti.android.providertest</groupId>
    <version>1.0-SNAPSHOT</version>
    <artifactId>ProviderTest</artifactId>
    <packaging>pom</packaging>
    <name>ProviderTest</name>
    <description>Testing a simple Content Provider and consumer</description>

    <modules>
        <module>TestProvider</module>
        <module>TestConsumer</module>
        <module>TestGenerator</module>
    </modules>
</project>

Open the TestProvider\pom.xml and TestConsumer\pom.xml and add a parent to the pom.xml:
    <parent>
        <groupId>org.eoti.android.providertest</groupId>
        <version>1.0-SNAPSHOT</version>
        <artifactId>ProviderTest</artifactId>
    </parent>


F:\work\ProviderTest> mvn clean install

Replace TestProviderActivity.java with TestProvider.java:
package org.eoti.android.providertest.provider;

import android.app.Activity;
import android.content.*;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Bundle;
import android.provider.BaseColumns;
import android.provider.LiveFolders;
import android.text.TextUtils;
import android.util.Log;

import java.util.HashMap;

public class TestProvider
extends ContentProvider
{
    // Derived from http://developer.android.com/resources/samples/NotePad/src/com/example/android/notepad/NotePadProvider.html
   
    public static final String AUTHORITY = "org.eoti.android.providertest";
    private static class Logs implements BaseColumns
    {
        public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/logs");
        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.eoti.logs";
        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.eoti.log";
        public static final String DEFAULT_SORT_ORDER = "modified DESC";
        public static final String LOG = "log";
        public static final String TIMESTAMP = "timestamp";
    }

    private static String TAG = "TestProvider";
    private static final String DATABASE_NAME = "providertest.db";
    private static final int DATABASE_VERSION = 1;
    private static final String TABLE_NAME = "logs";
    private static final int LOGS = 1;
    private static final int LOG_ID = 2;
    private static final int LIVE_FOLDER_LOGS = 3;
    private static HashMap<String, String> logsProjectionMap;
    private static HashMap<String, String> folderProjectionMap;
    private static final UriMatcher uriMatcher;


    private static class DatabaseHelper extends SQLiteOpenHelper
    {
        public DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
           
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL("CREATE TABLE " + TABLE_NAME + " ("
                + Logs._ID + " INTEGER PRIMARY KEY,"
                + Logs.LOG + " TEXT,"
                + Logs.TIMESTAMP + " INTEGER"
                + ");");
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + ", which will destroy all old data");
            db.execSQL("DROP TABLE IF EXISTS notes");
            onCreate(db);
        }
    }
    private DatabaseHelper dbHelper;
   

    @Override
    public boolean onCreate() {
        dbHelper = new DatabaseHelper(getContext());
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        qb.setTables(TABLE_NAME);

        switch (uriMatcher.match(uri)) {
            case LOGS:
                qb.setProjectionMap(logsProjectionMap);
                break;
            case LOG_ID:
                qb.setProjectionMap(logsProjectionMap);
                qb.appendWhere(Logs._ID + "=" + uri.getPathSegments().get(1));
                break;
            case LIVE_FOLDER_LOGS:
                qb.setProjectionMap(folderProjectionMap);
                break;
            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }

        String orderBy = (TextUtils.isEmpty(sortOrder) ? Logs.DEFAULT_SORT_ORDER : sortOrder);

        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
        c.setNotificationUri(getContext().getContentResolver(), uri);
        return c;
    }

    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)) {
            case LOGS:
            case LIVE_FOLDER_LOGS:
                return Logs.CONTENT_TYPE;
            case LOG_ID:
                return Logs.CONTENT_ITEM_TYPE;
            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues initialValues) {
        if (uriMatcher.match(uri) != LOGS)
            throw new IllegalArgumentException("Unknown URI " + uri);

        ContentValues values = (initialValues == null) ? new ContentValues() : new ContentValues(initialValues);

        if(!values.containsKey(Logs.TIMESTAMP))
            values.put(Logs.TIMESTAMP, System.currentTimeMillis());

        if(!values.containsKey(Logs.LOG))
            values.put(Logs.LOG, "");

        SQLiteDatabase db = dbHelper.getWritableDatabase();
        long rowId = db.insert(TABLE_NAME, Logs.LOG, values);
        if (rowId > 0) {
            Uri noteUri = ContentUris.withAppendedId(Logs.CONTENT_URI, rowId);
            getContext().getContentResolver().notifyChange(noteUri, null);
            return noteUri;
        }

        throw new SQLException("Failed to insert row into " + uri);
    }

    @Override
    public int delete(Uri uri, String where, String[] whereArgs) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int count;
        switch (uriMatcher.match(uri)) {
            case LOGS:
                count = db.delete(TABLE_NAME, where, whereArgs);
                break;
            case LOG_ID:
                String noteId = uri.getPathSegments().get(1);
                count = db.delete(TABLE_NAME, Logs._ID + "=" + noteId + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
                break;
            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }

        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }

    @Override
    public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int count;
        switch (uriMatcher.match(uri)) {
            case LOGS:
                count = db.update(TABLE_NAME, values, where, whereArgs);
                break;
            case LOG_ID:
                String noteId = uri.getPathSegments().get(1);
                count = db.update(TABLE_NAME, values, Logs._ID + "=" + noteId + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
                break;   
            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }

        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }

    static{
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(AUTHORITY, "logs", LOGS);
        uriMatcher.addURI(AUTHORITY, "logs/#", LOG_ID);
        uriMatcher.addURI(AUTHORITY, "livefolders/logs", LIVE_FOLDER_LOGS);

        logsProjectionMap = new HashMap<String,String>();
        logsProjectionMap.put(Logs._ID, Logs._ID);
        logsProjectionMap.put(Logs.LOG, Logs.LOG);
        logsProjectionMap.put(Logs.TIMESTAMP, Logs.TIMESTAMP);

        folderProjectionMap = new HashMap<String, String>();
        folderProjectionMap.put(LiveFolders._ID, Logs._ID + " AS " + LiveFolders._ID);
        folderProjectionMap.put(LiveFolders.NAME, Logs.LOG + " AS " + LiveFolders.NAME);
    }
}


Change your TestProvider\AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.eoti.android.providertest.provider">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <provider android:name="TestProvider" android:authorities="org.eoti.android.providertest"/>
    </application>
</manifest>


Replace TestConsumerActivity.java with TestConsumer.java:
package org.eoti.android.providertest.consumer;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.LiveFolders;

public class TestConsumer extends Activity
{
    public static final String AUTHORITY = "org.eoti.android.providertest";

    private static String TAG = "TestConsumer";
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/livefolders/logs");
    public static final Uri LOG_URI = Uri.parse("content://" + AUTHORITY + "/logs/#");

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        final Intent intent = getIntent();
        final String action = intent.getAction();

        if (LiveFolders.ACTION_CREATE_LIVE_FOLDER.equals(action))
        {
            final Intent liveFolderIntent = new Intent();

            liveFolderIntent.setData(CONTENT_URI);
            liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_NAME, getString(R.string.app_name));
            liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_ICON, Intent.ShortcutIconResource.fromContext(this, R.drawable.icon));
            liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_DISPLAY_MODE, LiveFolders.DISPLAY_MODE_LIST);
            liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_BASE_INTENT, new Intent(Intent.ACTION_EDIT, LOG_URI));
            setResult(RESULT_OK, liveFolderIntent);
        } else {
            setResult(RESULT_CANCELED);
        }

        finish();
    }
}



Change your TestConsumer\AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.eoti.android.providertest.consumer">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name="TestConsumer" android:label="@string/app_name" android:icon="@drawable/icon">
            <intent-filter>
                <action android:name="android.intent.action.CREATE_LIVE_FOLDER" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
    </application>
</manifest>


Update your TestGeneratorActivity:
package org.eoti.android.providertest.generator;

import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;

import java.text.SimpleDateFormat;
import java.util.Date;

public class TestGeneratorActivity extends Activity {
    public static final String AUTHORITY = "org.eoti.android.providertest";
    private static String TAG = "TestGenerator";
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/logs");
    private static final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        ContentValues values = new ContentValues();
        long time = System.currentTimeMillis();
        values.put("timestamp", time);
        values.put("log", sdf.format(new Date(time)));

        Uri uri = getContentResolver().insert(CONTENT_URI, values);
        Toast.makeText(getApplicationContext(), (uri == null) ? " null " : uri.toString(), Toast.LENGTH_SHORT).show();
        finish();
    }
}


Redeploy (mvn clean install).  Launch the generator app.  Add the live folder to your desktop.  Launch it.  Run the generator a few more times. Launch the folder again.

11 June 2010

Accessing call logs

Start with a basic project:

F:\work> mvn archetype:generate -DarchetypeCatalog=http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml
2: http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml -> galatea-archetype (null)
groupId: org.eoti.android.calls
artifactId: CallLogs
version: 1.0-SNAPSHOT
package: org.eoti.android.calls


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

Add the READ_CONTACTS permission to your AndroidManifest.xml:
<uses-permission android:name="android.permission.READ_CONTACTS" />

Add this to the end of your onCreate() in CallLogsActivity:
        Uri allCalls = Uri.parse("content://call_log/calls");
        Cursor c = managedQuery(allCalls, null, null, null, null);
        for(String colName : c.getColumnNames())
            Log.v(TAG, "Column Name: " + colName);

        if (c.moveToFirst())
        {
           do{
               String id = c.getString(c.getColumnIndex(CallLog.Calls._ID));
               String num = c.getString(c.getColumnIndex(CallLog.Calls.NUMBER));
               int type = Integer.parseInt(c.getString(c.getColumnIndex(CallLog.Calls.TYPE)));

                switch (type)
                {
                    case 1: Log.v(TAG, id + ", " +num + ": INCOMING") ; break;
                    case 2: Log.v(TAG, id + ", " +num + ": OUTGOING") ;break;
                    case 3: Log.v(TAG, id + ", " +num + ": MISSED") ; break;
                }
           } while (c.moveToNext());
        }

Redeploy (mvn clean install).  If you watch the logs (adb logcat) while running the app on the emulator, you will see the column names listed -- but not any call logs... How do we fix that?
V/CallLogs(  870): Column Name: numbertype
V/CallLogs(  870): Column Name: new
V/CallLogs(  870): Column Name: duration
V/CallLogs(  870): Column Name: _id
V/CallLogs(  870): Column Name: numberlabel
V/CallLogs(  870): Column Name: name
V/CallLogs(  870): Column Name: number
V/CallLogs(  870): Column Name: type
V/CallLogs(  870): Column Name: date



The easy answer is that you can dial a number on the emulator (even though it won't connect) and then re-run your CallLogs app to see the number you just dialed listed as an OUTGOING call.

What about incoming and missed?  Create another AVD and launch a secondary emulator.  In the titlebar and taskbar icon name, you can see some number associated with each emulator instance (like: 5554).  From one emulator, you can "dial" the number of the other. So, for instance, in emulator 5556 open the dialer and dial 5554.  Dial from one to the other; answer some not others; Dial the other direction, etc.

Run your app again.  In my case, the emulator seemed to not log MISSED calls. 


At some point, you are bound to wonder what those nulls are in the managed query....  from the API:
public final Cursor managedQuery(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

A few brief examples of what you can do (per this tutorial):

uri: Who to get data from?
    Uri allCalls = Uri.parse("content://call_log/calls");

projection: What columns to return?
    String[] projection = new String[]{Calls._ID, Calls.NUMBER, Calls.TYPE};
   
selection: SQL WHERE clause
    String selection = "Calls.NUMBER LIKE '65%'"; //---retrieve numbers beginning with 65---
    String[] selectionArgs = null;
   
selectionArgs: (to populate the selection clause)

sortOrder: SQL ORDER BY clause
    String sortOrder = "Calls.TYPE DESC"; //---sort result by call TYPE descending---
   
So, for example,
Cursor c = managedQuery(
    Uri.parse("content://call_log/calls"),
    new String[]{Calls._ID, Calls.NUMBER, Calls.TYPE},
    "Calls.NUMBER LIKE '65%'",
    null,
    "Calls.TYPE DESC"
);
would return the _id, number, type columns from numbers that started with 65 in descending order....