Controlling home devices from a single hand held device is a dream that will soon become real. Home devices are getting smarter and include more functionality than devices we have now. I developed a simple prototype of Android application that can manage virtual devices. The idea can be useful both for Home Automation to control devices as well as for Smart Maps to display detailed information about the objects on the map such as airports or shopping centers.

Code: https://bitbucket.org/dexity/metahome

MetaHome

Application Description

The application manages predefined items (Restroom, Telephone, Elevator, ATM) by adding or removing them from display. The items can represent any object on the map: controllable home device or non-controllable object on the map. The application uses 3 independent data sources: Memory, Local Database and Web Service. I implemented different data sources to explore user experience. User can select any of the data sources from the Menu. For Web Service I wrote a simple PHP script and put it on remote web server which handles incoming requests from the application. Adding item to the map is as simple as clicking on the dashed square and selecting the item.

Layout and Drawing

The layout consists of three main views: MapView, TextView and ListView.

 1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2    android:orientation="vertical"
 3    android:layout_width="match_parent"
 4    android:layout_height="match_parent">
 5 
 6     <com.surfingbits.metahome.MapView
 7        android:id="@+id/map_view"
 8        android:layout_width="wrap_content"
 9        android:layout_height="240dip"
10        android:layout_margin="10dip"/>
11 
12     <TextView android:id="@+id/status"
13        android:layout_width="wrap_content"
14        android:layout_height="23dip"
15        android:textSize="14dip"/>
16 
17     <ListView android:id="@android:id/list"
18        android:layout_width="match_parent"
19        android:layout_height="wrap_content"
20        android:drawSelectorOnTop="false"/>
21 
22 </LinearLayout>

Both MapView and ListView are different views of the same data model described in the next section. TextView displays the status of selected data source. When the screen is rotated the application will use a different layout: layout-land/main.xml

MapView performs custom drawing of items depending if an item is located in the square or not.

 1 @Override
 2 protected void onDraw(Canvas canvas)
 3 {
 4     super.onDraw(canvas);
 5 
 6     float[] outerR = new float[] { 10, 10, 10, 10, 10, 10, 10, 10 };
 7 
 8     mShapeDrawable = new ShapeDrawable(new RoundRectShape(outerR, null, null));
 9     mShapeDrawable.getPaint().setColor(0xcccccccc);
10     mShapeDrawable.setBounds(0, 0, MAPSIZE, MAPSIZE);
11     mShapeDrawable.draw(canvas);
12 
13     for (int i = 0; i < ldata.size(); i++)
14     {
15         if (ldata.isEmpty(i))
16         {
17             drawPlace(canvas, i);
18         }
19         else {
20             drawIcon(canvas, i);
21         }
22     }
23 }

Data Model and Interface

Data model of the application is very simple. It is described by a list of size 4. The index of the list represents cell index of flattened table: from left to right, top to bottom. The value of the list represents index of item which is placed in the cell. Empty cell has value -1.

Items indices:

0 - Restroom
1 - Telephone
2 - Elevator
3 - ATM

For example: ["0","-1","-1","2"]

means that item Restroom (item index 0) is in cell (0, 0) (cell index 0) and Elevator (item index 2) is in cell (1, 1) (cell index 3).

To make operations on the cells independent on the data source I implemented a general interface. All manipulation with the data storage is performed through this interface. If you want to add a new data storage you need to add also implementation of the interface.

1 public interface IMapData {
2     abstract void set(int index, int value);
3     abstract int get(int index);
4     abstract int[] getAll();
5     abstract void remove(int index);
6     abstract boolean isEmpty(int index);
7     abstract int size();
8 }

Cells list is then translated to ImageText class which is used by views to display image and corresponding label.

 1 class ImageText
 2 {
 3     String text;
 4     Bitmap icon;
 5     public ImageText(Bitmap icon, String text)
 6     {
 7         this.icon   = icon;
 8         this.text   = text;
 9     }
10 }

Data Sources

As mentioned before, I implemented 3 independent data sources each of which can be selected from the application:

  • Memory
  • Local Database
  • Web Service

Local Database is a simple SQLite database which resides on Android file system and has the following database schema:

$ sqlite3 cells.db
sqlite> .schema
CREATE TABLE android_metadata (locale TEXT);
CREATE TABLE cells (_id INTEGER PRIMARY KEY,cid INTEGER,cell INTEGER);
sqlite> select * from cells;
1|0|0
2|1|2
3|2|1
4|3|-1

I will show here the examples of implementation set() method for each of these data sources:

Memory

1 class MemoryMapData implements IMapData
2 {
3     private int[] cells     = new int[MetaHome.NUM_ITEMS];
4 
5     public void set(int index, int value) {
6         cells[index]    = value;
7     }
8     ...
9 }

Local Database

 1 class DatabaseMapData implements IMapData
 2 {
 3     private DatabaseHelper mOpenHelper;
 4 
 5     private static final String DBNAME = "cells.db";
 6     private static final int DBVERSION = 3;
 7 
 8     public void set(int index, int value) {
 9 
10         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
11         ContentValues values    = new ContentValues();
12         values.put(Cells.COLUMN_CELL, value);
13         db.update(Cells.TABLE_NAME, values, Cells.COLUMN_ID+"=?", new String[] {Long.toString(index)});
14     }
15     ...
16 }

Web Service

 1 class WebMapData implements IMapData
 2 {
 3 
 4     private static final String ENDPOINT    = "http://surfingbits.com/metahome/index.php";
 5     private static final String KEY         = "access_key";
 6 
 7     public void set(int index, int value) {
 8         HashMap<String, String> params  = new HashMap<String, String>();
 9         params.put("action", "set");
10         params.put("cell", Long.toString(index));
11         params.put("value", Long.toString(value));
12         JSONObject js   = getContentObject(params);
13 
14         boolean status;
15         try {
16             status  = js.getBoolean("status");
17             Log.i("Web", "Set value cell["+index+"] = "+value+": "+status);
18         } catch (Exception e) {
19             Log.e("WebError", e.toString());
20         }
21     }
22     ...
23 }

Web Service Interface and Implementation

For Web Service I implemented a simple PHP script which handles incoming requests from the application. In the backend the script also uses SQLite database with the same database schema as Android is using for the Local Database source.

  1 <?php
  2 
  3 // Copyright 2011 Alex Dementsov
  4 
  5 $key        = "access_key";
  6 $actions    = array("get", "getall", "remove", "set", "size", "empty");
  7 
  8 class Handler
  9 {
 10     private $db;
 11 
 12     public function __construct(){
 13         $this->open_db();
 14     }
 15 
 16     function run()
 17     {
 18         global $key, $actions;
 19 
 20         if (!(isset($_GET["key"]) && $_GET["key"] == $key) ||
 21             !(isset($_GET["action"]) && in_array($_GET["action"], $actions)))
 22         {
 23             echo "Wrong key or action";
 24             return;
 25         }
 26 
 27         switch($_GET["action"])
 28         {
 29             case "get":
 30                 return $this->get();
 31             case "getall":
 32                 return $this->get_all();
 33             case "remove":
 34                 return $this->remove();
 35             case "set":
 36                 return $this->set();
 37             case "size":
 38                 return $this->size();
 39             case "empty":
 40                 return $this->isempty();
 41         }
 42     }
 43     function get()
 44     {
 45         if (!isset($_GET["cell"]))
 46             return $this->status_fail();
 47 
 48         $row    = $this->getValue();
 49         if (!$row)
 50             return $this->status_fail();
 51 
 52         return json_encode(array("cell" => $row["cid"], "value" => $row["cell"]));
 53     }
 54 
 55 
 56     function get_all()
 57     {
 58         $array  = array(-1, -1, -1, -1);
 59         $rows   = $this->getValues();
 60         if (!$rows)
 61             return $this->status_fail();
 62 
 63         foreach ($rows as $i => $value)
 64         {
 65             $idx = $value["cid"];
 66             if ($idx >= 0 && $idx < 4)
 67                 $array[$idx] = $value["cell"];
 68         }
 69         return json_encode(array("cells" => $array));
 70     }
 71 
 72     function remove()
 73     {
 74         if (!isset($_GET["cell"]))
 75             return $this->status_fail();
 76 
 77         return $this->setValue(-1);
 78     }
 79 
 80     function set()
 81     {
 82         if (!isset($_GET["cell"]) or !isset($_GET["value"]))
 83             return $this->status_fail();
 84 
 85         return $this->setValue($_GET["value"]);
 86     }
 87 
 88     private function update($arr)
 89     {
 90         // Executes query and returns status
 91         $query  = "update cells set cell=? where cid=?";
 92         $q      = $this->db->prepare($query);
 93         $q->execute($arr) or die(print_r($this->db->errorInfo(), true));
 94         return $q;
 95     }
 96 
 97     private function getValue()
 98     {
 99         // Returns value of a cell
100         $result = $this->db->query("select * from cells where cid=".$_GET["cell"]) or die($this->status_fail());
101         return $result->fetch();
102     }
103 
104     private function getValues()
105     {
106         // Returns value of a cell
107         $result = $this->db->query("select * from cells limit 4") or die($this->status_fail());
108         return $result->fetchAll();
109     }
110 
111     private function setValue($value)
112     {
113         $arr    = array($value, $_GET["cell"]);
114         $q      = $this->update($arr);
115         if (!$q)
116             return $this->status_fail();
117 
118         return $this->status_ok();
119     }
120 
121     function size()
122     {
123         return json_encode(array("size" => 4));
124     }
125 
126     function isempty()
127     {
128         $row    = $this->getValue();
129         if (!$row)
130             return $this->status_fail();
131 
132         if ($row["cell"] == -1)
133             return json_encode(array("empty" => True));
134 
135         return json_encode(array("empty" => False));
136     }
137 
138     private function status_fail()
139     {
140         return $this->status("fail");
141     }
142 
143     private function status_ok()
144     {
145         return $this->status("ok");
146     }
147 
148     private function status($status)
149     {
150         return json_encode(array("status" => $status));
151     }
152 
153     private function open_db()
154     {
155         try {
156             $this->db = new PDO('sqlite:db/cells.db');
157         }
158         catch(Exception $e) {
159             die($this->status_fail());
160         }
161     }
162 }
163 
164 $handler = new Handler();
165 echo $handler->run();
166 ?>

Here is the interface for the Web Service:

Get All Cells

Example Request:
http://HOSTNAME/metaservice/index.php?action=getall&key=access_key

Example Response:
{
    "cells": ["2", "3", "-1", "-1"]
}

Get Cell

Example Request:
http://HOSTNAME/metaservice/index.php?action=get&cell=3&key=access_key

Example Response:
{
    "cell": 3,
    "value": 2
}

Remove Cell

Example Request:
http://HOSTNAME/metaservice/index.php?action=remove&cell=3&key=access_key

Example Response:
{
    "status": "ok"
}

Set Cell

Example Request:
http://HOSTNAME/metaservice/index.php?action=set&cell=3&value=2&key=access_key

Example Response:
{
    "status": "ok"
}

Get Cells Size

Example Request:
http://HOSTNAME/metaservice/index.php?action=size&key=access_key

Example Response:
{
    "size": 4
}

Is Empty

Example Request:
http://HOSTNAME/metaservice/index.php?action=empty&cell=3&key=access_key

Example Response:
{
    "empty": true
}

If something goes wrong the response returns error:

{
    "status": "fail"
}

Caching Mechanism and Other Trick

Using Web Service data source without caching makes user experience not very pleasant. I implemented some caching mechanism to make experience better. Remote calls are made to Web Service only when it is absolutely necessary, for example to change the state of the cells. Other requests are made locally from the stored data attribute ldata.

1 public class MapView extends View {
2 
3     private IMapData ldata;         // Local storage for the data (cache)
4     ...
5 }

This attribute is used for all three data sources even when it somewhat duplicates storage (as in Memory data source). This simple caching works surprisingly well for Web Service data source.

Other issue I had with the application is the screen rotation. When you rotate the mobile phone all the local objects are gone because Android restarts the running Activity unless you take some measures to keep the state. Here is the reference to Android documentation which discusses this issue: http://developer.android.com/guide/topics/resources/runtime-changes.html

To do the trick I implemented onSaveInstanceState() and onRestoreInstanceState(Parcelable state).

 1 public class MapView extends View {
 2 
 3     @Override
 4     protected Parcelable onSaveInstanceState() {
 5         Bundle b = new Bundle();
 6 
 7         Parcelable s = super.onSaveInstanceState();
 8         b.putParcelable("map_super_state", s);
 9         b.putIntArray("map_cells", dumpCells(ldata));
10         b.putIntArray("memory_cells", dumpCells(memory));
11         return b;
12     }
13 
14     @Override
15     protected void onRestoreInstanceState(Parcelable state)
16     {
17         Bundle b = (Bundle) state;
18         Parcelable superState = b.getParcelable("map_super_state");
19         loadCells(ldata, b, "map_cells");
20         loadCells(memory, b, "memory_cells");
21 
22         super.onRestoreInstanceState(superState);
23     }
24     ...
25 }

MetaHome Layout

MetaHome Select

Real Example

To show the possible application of the idea I present here the map of the Terminal 4 at Los Angeles International Airport (LAX). It looks a bit more complicated than what I showed you before :).

LAX Terminal 4