乐者为王

Do one thing, and do it well.

实现Android底部工具栏

在网上的大部分教程中,底部工具栏通常由TabHost和RadioGroup结合完成,每个工具栏项对应一个Activity。不过,我们要实现的是多个工具栏项在单独的一个Activity上起作用。

简单实现Android顶部工具栏和底部工具栏就是这样的。不过该实现对每个工具栏项设置了固定宽度80dip,导致工具栏项或屏幕大小不定时代码布局会有问题。

这里对它做了些改进,使之能做到适应不定大小的工具栏项或屏幕。改进后的布局代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="0dip"
        android:layout_weight="1.0">
        <ScrollView
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:scrollbars="vertical"
            android:fadingEdge="vertical">
            <TextView
                android:id="@+id/content"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:textSize="17dip" />
        </ScrollView>
    </LinearLayout>

    <include layout="@layout/toolbar" />
</LinearLayout>

以下是工具栏toolbar.xml的布局代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/toolbar_bg">
    <ImageButton
        android:id="@+id/btn_index"
        android:src="@drawable/index"
        android:text="@string/index"
        style="@style/toolbar" />
    <ImageButton
        android:id="@+id/btn_prev"
        android:src="@drawable/btn_prev_bg"
        android:text="@string/prev"
        style="@style/toolbar" />
    <ImageButton
        android:id="@+id/btn_next"
        android:src="@drawable/btn_next_bg"
        android:text="@string/next"
        style="@style/toolbar" />
    <ImageButton
        android:id="@+id/btn_zoomin"
        android:src="@drawable/zoomin"
        android:text="@string/zoomin"
        style="@style/toolbar" />
    <ImageButton
        android:id="@+id/btn_zoomout"
        android:src="@drawable/zoomout"
        android:text="@string/zoomout"
        style="@style/toolbar" />
</LinearLayout>

从已有项目创建Maven archetype

手里有这样的一个项目,其它项目都基于该项目创建,只是对包名做些修改,还有就是替换部分图片和文本内容。每次手工重复类似的修改工作很是繁琐,简直让人发指,需要寻找自动化的解决方案。

Maven界有这么一句话:遇到重复的Maven项目初始配置,就创建自己的archetype。其实不光是Maven项目,其它也是如此。《测试驱动开发》中有个Triangulation法则,《重构》中也有Rule of three,都是用来指导如何解决类似重复问题的。第一次是特殊解决,第二次还是特殊解决,第三次就要抽象解决了。

Maven Archetype Plugin允许从当前存在的项目创建archetype,这样以后用户就可以基于该archetype创建项目了。

archetype-overview

那么如何通过现有的项目创建archetype呢?首先清理项目中那些不必要的文件和目录,然后在根目录下执行:

1
2
3
mvn archetype:create-from-project
cd target/generated-sources/archetype
mvn clean install  # 本地安装

现在就可以使用上面创建的archetype来建立新项目了。在新的目录中执行以下命令即可:

1
2
3
4
5
6
7
8
mvn archetype:generate                          \
  -DarchetypeCatalog=local                      \
  -DarchetypeGroupId=<archetype-groupId>        \
  -DarchetypeArtifactId=<archetype-artifactId>  \
  -DgroupId=<your-groupId>                      \
  -DartifactId=<your-artifactId>                \
  -Dpackage=<your-package>                      \
  -Dversion=1.0

这里要注意的是,不要在target/generated-sources/archetype目录下运行上述命令,否则会生成失败,报如下错误:

1
2
3
[ERROR] Failed to execute goal archetype:generate:
  org.apache.maven.archetype.exception.InvalidPackaging:
  Unable to add module to the current project as it is not of packaging type 'pom'

在Rails中使用Open Flash Chart II

今天我们讲如何用Rails结合Open Flash Chart II(以下简称OFC2)实现如下的图表:

ofc2-glassbar

在项目中安装OFC2插件,生成相关控制器:

1
2
script/plugin install git://github.com/pullmonkey/open_flash_chart.git
script/generate controller stats index

在生成的app/controllers/stats_controller.rb中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class StatsController < ApplicationController
  def index
    @graph = open_flash_chart_object(500, 300, "/stats/graph_code")
  end

  def graph_code
    title = Title.new("Glass Bar")

    bar = BarGlass.new
    bar.set_values([-1,2,3,7,8,-7.3])

    y = YAxis.new
    y.set_range(-10, 10, 5)

    chart = OpenFlashChart.new
    chart.y_axis = y
    chart.set_title(title)
    chart.add_element(bar)
    render :text => chart.to_s
  end
end

在config/routes.rb中添加路由:

1
match 'stats/graph_code' => 'stats#graph_code'

将插件中的assets/javascripts/swfobject.js拷贝到public/javascripts目录下(可以看到该目录下已经有两个open-flash-chart开头的swf文件,如果没有的话,需要从插件中拷过来)。在相应的app/views/stats/index.html中添加如下代码:

1
2
3
<script src="/javascripts/swfobject.js" type="text/javascript"></script>

<%= @graph %>

用Spinner + SQLite实现省市县三级联动

建立省市县行政区划代码数据表。

1
2
3
4
5
6
CREATE TABLE xzqhdm (
    _id integer PRIMARY KEY,
    code numeric,
    region text,
    parent_code numeric
);

parent_code指上一级的行政区划代码,省属于最上级的行政单位,设置它的区划代码为999999。

1
2
3
4
5
6
7
8
9
10
11
12
13
INSERT INTO xzqhdm VALUES(NULL, 110000, "北京市", 999999);
INSERT INTO xzqhdm VALUES(NULL, 110100, "市辖区", 110000);
INSERT INTO xzqhdm VALUES(NULL, 110101, "东城区", 110100);
INSERT INTO xzqhdm VALUES(NULL, 110102, "西城区", 110100);
INSERT INTO xzqhdm VALUES(NULL, 110103, "崇文区", 110100);
INSERT INTO xzqhdm VALUES(NULL, 110104, "宣武区", 110100);
INSERT INTO xzqhdm VALUES(NULL, 110105, "朝阳区", 110100);
INSERT INTO xzqhdm VALUES(NULL, 110106, "丰台区", 110100);
...
INSERT INTO xzqhdm VALUES(NULL, 659001, "石河子市", 659000);
INSERT INTO xzqhdm VALUES(NULL, 659002, "阿拉尔市", 659000);
INSERT INTO xzqhdm VALUES(NULL, 659003, "图木舒克市", 659000);
INSERT INTO xzqhdm VALUES(NULL, 659004, "五家渠市", 659000);

SQLite数据库的操作

如果应用使用到了SQLite数据库,在用户初次使用应用时,需要创建应用使用到的数据表结构及添加一些初始化记录,另外在软件升级的时候,也需要对数据表结构进行更新。Android系统为我们提供了一个名为SQLiteOpenHelper的类,这是一个抽象类,该类用于对数据库版本进行管理,有两个重要的方法,分别是onCreate()和onUpgrade()。

当调用SQLiteOpenHelper的getWritableDatabase()或getReadableDatabase()方法获取数据库实例时,如果数据库不存在,Android系统会自动生成一个数据库文件,接着调用onCreate()方法,onCreate()方法在初次生成数据库时才会被调用,在onCreate()方法里可以生成数据表结构及添加一些应用使用到的初始化数据。onUpgrade()方法在数据库的版本发生变化时会被调用,数据库的版本是由程序员控制的,假设数据库现在的版本是1,由于业务的需要,修改了数据表的结构,这时候就需要升级软件,升级软件时希望更新用户手机里的数据表结构,为了实现这一目的,可以把原来的数据库版本设置为2(或其它数值),并且在onUpgrade()方法里面实现表结构的更新。当软件的版本升级次数比较多,这时在onUpgrade()方法里面可以根据原版号和目标版本号进行判断,然后作出相应的表结构及数据更新。

SQLiteDatabase类则封装了一些操作数据库的常用API,使用该类可以完成对数据进行CRUD操作。主要是execSQL()和rawQuery()方法。execSQL()方法可以执行insert、delete、update和CREATE TABLE之类有更改行为的SQL语句; rawQuery()方法可以执行select语句。SQLiteDatabase还专门提供了对应于CRUD的操作方法: insert()、delete()、update()和query()。

如何将SQLite数据库与APK文件一起发布?可以将数据库文件复制到res/raw目录中,所有在该目录中的文件不会被压缩,这样可以直接提取该目录中的文件。

如何打开res/raw目录中的数据库文件?不能直接打开,需要在程序第一次启动时将该文件复制到手机内存或SD卡中后再打开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class DBHelper extends SQLiteOpenHelper {
    private static String DB_PATH = "/data/data/com.codemany.linkage/databases/";
    private static String DB_NAME = "xzqh.db";
    private static DBHelper databaseHelper;
    private static SQLiteDatabase db;

    private Context context;

    private DBHelper(Context context) {
        super(context, DB_NAME, null, 1);
        this.context = context;
    }

    public static DBHelper getInstance(Context context) {
        if (databaseHelper == null) {
            databaseHelper = new DBHelper(context);
            databaseHelper.openDataBase();

            if (db == null) {
                try {
                    db = databaseHelper.getWritableDatabase();
                    databaseHelper.copyDatabase();
                }
                catch (Exception e) {
                    Log.d("DBHelper", "Error in database creation");
                }

                databaseHelper.openDataBase();
            }
        }
        return databaseHelper;
    }

    private void copyDatabase() throws IOException {
        InputStream is = context.getResources().openRawResource(R.raw.xzqh);
        OutputStream os = new FileOutputStream(DB_PATH + DB_NAME);
        byte[] buffer = new byte[1024];
        int length;
        while ((length = is.read(buffer)) > 0) {
            os.write(buffer, 0, length);
        }

        os.flush();
        os.close();
        is.close();
    }

    private void openDataBase() {
        try {
            db = SQLiteDatabase.openDatabase(
                    DB_PATH + DB_NAME,
                    null,
                    SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
        } catch (SQLiteException e) {
            // database does't exist yet
        }
    }

    public SimpleCursorAdapter getListByParentCode(Context context, String parentCode) {
        SimpleCursorAdapter list = null;
        DBHelper dHelper = new DBHelper(context);
        SQLiteDatabase db = dHelper.getReadableDatabase();
        Cursor cursor = db.rawQuery(
                "SELECT code as _id, region FROM xzqhdm WHERE parent_code = ?",
                new String[] {parentCode});
        if (cursor.getCount() != 0) {
            list = new SimpleCursorAdapter(context,
                    android.R.layout.simple_spinner_item,
                    cursor,
                    new String[] {"region"},
                    new int[] {android.R.id.text1});
            list.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        }
        return list;
    }

    @Override
    public synchronized void close() {
        if (db != null) {
            db.close();
        }
        super.close();
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

在代码实现时遇到的难题是如何在选中region的同时得到对应的code。网上有教程说定制自己的Adapter,覆写bindView()方法,不过有多个Spinner就需要声明多个全局变量;还有教程指出可以直接往Adapter中传递对象(实现一个类,将code和region作为字段),然后覆写对象的toString()方法。后来受到这个帖子的启发,修改了rawQuery中的select语句得以实现Spinner控件中的键值绑定。

原来的查询语句是:

1
SELECT _id, code, region FROM xzqhdm WHERE parent_code = ?

因为传入到CursorAdapter中的Cursor结果集必须包含有列名为_id的列,否则CursorAdapter将不会起作用。而code可以被看作是整数,那么只需要将选出的code当作_id就行了,根据这个想法写出的查询语句如下:

1
SELECT code as _id, region FROM xzqhdm WHERE parent_code = ?

这样,当触发Spinner上的ItemSelected事件时就可以通过最后一个参数id得到当前的code了。

写这篇文章的时候同时也在调试着代码,突然发现其实不需要改写查询语句也是可以实现键值绑定的。只要在onItemSelected()方法中使用如下代码就可以取得相应的值了:

1
2
3
4
5
Cursor cursor = (Cursor)parent.getSelectedItem();
if (cursor != null) {
    int code = cursor.getString(cursor.getColumnIndex("code"));
    String country = cursor.getString(cursor.getColumnIndex("region"));
}

代码下载

Android Tab导航总结

TabHost包括TabWidget和FrameLayout两部分:TabWidget用于展示标签页,FrameLayout用于展示隶属于各个标签页的具体内容。一个带有TabHost的Activity看起来就像这样:

android-tabs

如果Activity是继承自TabAcitivty,那么TabHost的id必须设置为@android:id/tabhost,TabWidget必须设置为@android:id/tabs,FrameLayout需要设置为@android:id/tabcontent。

1、创建一个简单的Tab应用

废话不多说,直接上代码。以下就是main.xml的布局代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/tabhost"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
        <TabWidget
            android:id="@android:id/tabs"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />
        <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent">
            <include android:id="@+id/left" layout="@layout/tab_left" />
            <include android:id="@+id/right" layout="@layout/tab_right" />
        </FrameLayout>
    </LinearLayout>
</TabHost>

左边标签页tab_left.xml的内容:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:text="left"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />
</LinearLayout>

右边标签页tab_right.xml的内容:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:text="right"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />
</LinearLayout>

MainActivity需要从TabActivity继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MainActivity extends TabActivity {

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

        TabHost tabHost = getTabHost();
        tabHost.addTab(tabHost.newTabSpec("left")
                .setIndicator("Left")
                .setContent(R.id.left));
        tabHost.addTab(tabHost.newTabSpec("right")
                .setIndicator("Right")
                .setContent(R.id.right));
        tabHost.setCurrentTab(0);
    }
}

到这里,一个简单至极的Tab应用已经完成了。不过在实际情况中,每个Tab页会有多个控件,并且有很多业务逻辑在其中。如果使用上述代码,将所有控件和逻辑放在一个类中,那么这个类将变的极度臃肿,以后维护也会变的非常困难。需要找个简单且优雅的方案解决这个问题。

2、简单优雅且容易维护的Tab应用

上面的实现有个缺陷,标签页相关的代码逻辑都在MainActivity类中,导致单个类臃肿,给后续维护造成不便。可以考虑把相关的代码逻辑移到对应的类中。

删除main.xml中的include语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/tabhost"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
        <TabWidget
            android:id="@android:id/tabs"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />
        <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent" />
    </LinearLayout>
</TabHost>

新建TabLeftActivity和TabRightActivity类:

1
2
3
4
5
6
7
8
9
10
public class TabLeftActivity extends Activity {

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

        // do something
    }
}
1
2
3
4
5
6
7
8
9
10
public class TabRightActivity extends Activity {

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

        // do something
    }
}

修改MainAcvitity为如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MainActivity extends TabActivity {

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

        TabHost tabHost = getTabHost();
        tabHost.addTab(tabHost.newTabSpec("left")
                .setIndicator("Left")
                .setContent(new Intent(this, TabLeftActivity.class)));
        tabHost.addTab(tabHost.newTabSpec("right")
                .setIndicator("Right")
                .setContent(new Intent(this, TabRightActivity.class)));
        tabHost.setCurrentTab(0);
    }
}

最后,不要忘记在AndroidManifest.xml中注册TabLeftActivity和TabRightAcvitity这两个Activity。

3、如何将Tab显示在下方

这个非常简单,只要将main.xml中的LinearLayout改成RelativeLayout,然后给TagWidget添加android:layout_alignParentBottom="true"属性即可。当然也可以在main.xml中把TabWidget放在FrameLayout下面。

创建Android自定义键盘

在应用中,为了能够快捷地输入字符,我们有时需要使用特殊的软键盘,就像下图显示的那样。这是如何实现的呢?下面就来做个实例详解。

android-custom-keyboard

注意,这篇文章不是教你如何创建输入法,如果你想创建自己的输入法,可以研究文章Creating an Input Method和Android Sample中的SoftKeyboard项目。

在Android中创建软键盘是非常容易的,通过android.inputmethodservice.Keyboard类就可以实现。该类从XML文件中读取软键盘信息,有多少行,每行有多少按键,每个按键代表什么内容等。下面是软键盘的XML代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="utf-8"?>
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
    android:keyWidth="25%p"
    android:horizontalGap="0px"
    android:verticalGap="0px"
    android:keyHeight="@dimen/key_height">
    <Row>
        <Key android:codes="49" android:keyLabel="1" />
        <Key android:codes="50" android:keyLabel="2" />
        <Key android:codes="51" android:keyLabel="3" />
        <Key android:codes="57419"
            android:keyEdgeFlags="right"
            android:keyIcon="@drawable/sym_keyboard_left" />
    </Row>
    <Row>
        <Key android:codes="52" android:keyLabel="4" />
        <Key android:codes="53" android:keyLabel="5" />
        <Key android:codes="54" android:keyLabel="6" />
        <Key android:codes="57421"
            android:keyEdgeFlags="right"
            android:keyIcon="@drawable/sym_keyboard_right" />
    </Row>
    <Row>
        <Key android:codes="55" android:keyLabel="7" />
        <Key android:codes="56" android:keyLabel="8" />
        <Key android:codes="57" android:keyLabel="9" />
        <Key android:codes="-5"
            android:keyHeight="@dimen/key_height_large"
            android:keyEdgeFlags="right"
            android:isRepeatable="true"
            android:keyIcon="@drawable/sym_keyboard_delete" />
    </Row>
    <Row>
        <Key android:codes="-3" android:keyIcon="@drawable/sym_keyboard_done" />
        <Key android:codes="48" android:keyLabel="0" />
        <Key android:codes="88" android:keyLabel="X" />
    </Row>
</Keyboard>

在上面的键盘定义中,Row元素说明这是一行按键的定义,Key元素说明这是一个按键的定义。Key元素通过一些属性来定义每个按键,下面是一些常用的属性介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
codes:代表按键对应的输出值,可以为unicode值或则逗号(,)分割的多个值,也可以为一个字符串。
       在字符串中通过“\”来转义特殊字符,例如“\n”或者“\uXXXX”。Codes通常用来定义该键的键码,
       例如上图中的数字按键1对应的为49。如果提供的是逗号分割的多个值则和普通手机输入键盘一
       样在多个值之间切换。
 keyLabel:代表按键显示的文本内容。
 keyIcon:代表按键显示的图标内容,如果指定了该值则在显示的时候显示为图片不显示文本。
 keyWidth:代表按键的宽度,可以为精确值或则相对值,对于精确值支持多种单位,例如:像素,英
           寸等;相对值为相对于基础取值的百分比,为以%或则%p结尾,其中%p表示相对于父容器。
 keyHeight:代表按键的高度,取值同上。
 horizontalGap:代表按键前的间隙(水平方向),取值同上。
 isSticky:指定按键是否为sticky的。例如Shift大小写切换按键,具有两种状态,按下状态和正常
           状态,取值为true或则false。
 isModifier:指定按键是否为功能键(modifier key),例如Alt或则Shift,取值为true或则false。
 keyOutputText:指定按键输出的文本内容,取值为字符串。
 isRepeatable:指定按键是否是可重复的,如果长按该键可以触发重复按键事件则为true,否则为false。
 keyEdgeFlags:指定按键的对齐指令,取值为left或则right。

然后在main.xml文件末尾加入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
<RelativeLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">
    <android.inputmethodservice.KeyboardView
        android:id="@+id/keyboard_view"
        android:visibility="gone"
        android:focusable="true"
        android:focusableInTouchMode="true"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true" />
</RelativeLayout>

下面是主要的处理代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class MainActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        EditText edit = (EditText)findViewById(R.id.edit);
        edit.setInputType(InputType.TYPE_NULL);
        edit.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                showKeyboard();
            }
        });

        KeyboardView keyboardView = (KeyboardView)findViewById(R.id.keyboard_view);
        keyboardView.setKeyboard(new Keyboard(this, R.xml.qwerty));
        keyboardView.setEnabled(true);
        keyboardView.setPreviewEnabled(true);
        keyboardView.setOnKeyboardActionListener(new OnKeyboardActionListener() {
            @Override
            public void onKey(int primaryCode, int[] keyCodes) {
                Editable editable = edit.getText();
                int start = edit.getSelectionStart();
                if (primaryCode == Keyboard.KEYCODE_CANCEL) {
                    hideKeyboard();
                } else if (primaryCode == Keyboard.KEYCODE_DELETE) {
                    if (editable != null && editable.length() > 0) {
                        editable.delete(start - 1, start);
                    }
                } else if (primaryCode == 57419) {    // go left
                    if (start > 0) {
                        edit.setSelection(start - 1);
                    }
                } else if (primaryCode == 57421) {    // go right
                    if (start < edit.length()) {
                        edit.setSelection(start + 1);
                    }
                } else {
                    editable.insert(start, Character.toString((char)primaryCode));
                }
            }
        });
    }

    private void showKeyboard() {
        int visibility = keyboardView.getVisibility();
        if (visibility == View.GONE || visibility == View.INVISIBLE) {
            keyboardView.setVisibility(View.VISIBLE);
        }
    }

    private void hideKeyboard() {
        int visibility = keyboardView.getVisibility();
        if (visibility == View.VISIBLE) {
            keyboardView.setVisibility(View.INVISIBLE);
        }
    }
}

至此,一个自定义的软键盘就大功告成了。

如何创建Android应用启动界面

1、制作一张启动图片splash.jpg,放在res/drawable文件夹中。

2、新建布局文件splash.xml:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <ImageView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:scaleType="fitXY"
        android:src="@drawable/splash" />
</LinearLayout>

3、建立SplashActivity,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.MotionEvent;

public class SplashActivity extends Activity {
    private boolean active = true;
    private int splashTime = 5000;    // time to display the splash screen in ms

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

        new Thread() {
            @Override
            public void run() {
                try {
                    int waited = 0;
                    while (active && (waited < splashTime)) {
                        sleep(100);
                        if (active) {
                            waited += 100;
                        }
                    }
                } catch (InterruptedException e) {
                } finally {
                    finish();

                    // Run next activity
                    Intent intent = new Intent(SplashActivity.this, MainActivity.class);
                    startActivity(intent);
                    //stop();
                }
            }
        }.start();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            active = false;
        }
        return true;
    }
}

4、修改AndroidManifest.xml文件,将启动界面Activity改为默认启动,并且设置标题栏不可见。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<application android:icon="@drawable/icon" android:label="@string/app_name">
    <activity android:name=".SplashActivity"
              android:theme="@android:style/Theme.NoTitleBar">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

    <activity android:name=".MainActivity"
              android:label="@string/app_name">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </activity>
</application>

导入UC Browser书签到Firefox中

UC Browser保存的书签后缀名是aucf,不能直接导入到Firefox中,必须要把aucf格式转成Firefox支持的格式才行。

先把UC Browser和Firefox的书签格式都分析了一下。Firefox的格式非常简单,就是个HTML文件。UC的是二进制格式,文件以字符串android开头,接着是n个byte的信息,没有分析出来是啥。然后4个byte是书签的数目,再接着的n个byte没去分析,再下来就是每个书签的信息了。书签信息分两部分:名称和地址。都是第1个byte说明后面字符串的长度,字符串以00结尾。

1
2
3
4
5
6
7
8
android    <- 文件头
xxx...     <- n个byte
xxxx       <- 保存的书签数目
xxx...     <- n个byte
xx         <- 书签名称长度
xxx...     <- 书签名称
xx         <- 书签地址长度
xxx...     <- 书签地址

用文本文件打开时可以查看英文字符。书签的地址通常由英文字母组成,所以我们不需要再去分析整个aucf文件的格式,只要把所有书签地址提取出来就行了。下面就是完整的Ruby代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# aucf2ff.rb
if ARGV.length != 1 then
  puts "USAGE: ruby aucf2ff.rb ucfavorite"
  exit
end

ucfavorite = ARGV[0]
if !FileTest.exist?(ucfavorite) then
  puts "File #{ucfavorite} was not found"
  exit
end

ff = File.new(ucfavorite.gsub(".aucf", "_ff.html"), "w")
ff.puts '<!DOCTYPE NETSCAPE-Bookmark-file-1>'
ff.puts '<!-- This is an automatically generated file.'
ff.puts '     It will be read and overwritten.'
ff.puts '     DO NOT EDIT! -->'
ff.puts '<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">'
ff.puts '<TITLE>Bookmarks</TITLE>'
ff.puts '<H1>Bookmarks Menu</H1>'
ff.puts
ff.puts '<DL><p>'
ff.puts '    <DT><H3>UC Bookmarks</H3>'
ff.puts '    <DL><p>'

buffer = IO.read(ucfavorite)
index = buffer.index("http://")
favorites = buffer.slice(index, buffer.length - index)
favorites.split("http://").each do |fav|
  uri = ''
  fav.each_byte do |c|
    break if c == 00
    uri += c.chr
  end
  if (!uri.empty?) then
    ff.puts '<DT><A HREF="http://' + uri + '" LAST_CHARSET="utf-8">' + uri + '</A>'
  end
end

ff.puts '    </DL><p>'
ff.puts '</DL>'
ff.close

通过上面的程序就可以将UC Browser的书签转成Firefox格式,然后在Firefox中导入就可以了。

反编译Android APK文件

2017/9/17更新

classes.dex是Java源代码编译后生成的字节码文件。由于Android使用的Dalvik虚拟机与标准的Java虚拟机是不兼容的,因此,dex文件与class文件相比,不论是文件结构还是opcode都不一样。

目前市面上有多种反编译dex的工具,下面我们就来把它们逐个介绍下。

dexdump

dexdump是Android软件开发套件提供的反编译工具。用法是:首先启动Android模拟器,把要反编译的dex文件用adb push命令推送到其中,然后通过adb shell登录,找到该dex文件,执行dexdump demo.dex。总的来说dexdump功能比较弱,且用起来麻烦,反编译的结果的可读性也很差。

Dedexer

另一个反编译工具是Dedexer,反编译的效果比较好。它可以读取dex格式的文件,生成一种类似于汇编语言的输出。与dexdump相比它至少有3个优点:

1. 不需要在Android模拟器中运行; 2. 反编译后的文件目录结构和源代码的目录结构相近,每个class文件对应一个ddx文件,不像dexdump那样把所有的结果都放在一起; 3. 可以作为反编译引擎。

它的使用的方法如下:

java -jar ddx.jar -o -D -r -d src classes.dex

AXMLPrinter2

APK中的资源通常是压缩过的,用文本工具看都是乱码,不过我们可以用AXMLPrinter2将其转换成可读的XML文件。具体命令为:

java -jar AXMLPrinter2.jar demo.xml output.xml

Apktool

目前最好的反编译工具是Apktool。可以帮助我们把APK文件反编译,输出smali格式的代码、图片和资源等文件,还可以在修改后重新打包。将下载下来的apktool和apktool-install-windows解压到同一目录,可以看到有三个文件:aapt.exe,apktool.bat和apktool.jar。以下是它的使用方法:

1
2
java -jar apktool.jar d demo.apk  # Decoding
java -jar apktool.jar b demo  # Building

dex2jar + JD-GUI

dex2jar是一个能操作Android的dex文件和Java的class的工具集合。它包含有将dex文件转换成jar文件或smali代码的工具:

1
2
d2j-dex2jar.bat demo.apk  # 将文件中的classes.dex转换成jar文件
d2j-dex2smali.bat demo.apk  # 将文件中的classes.dex转换成smali代码

生成的jar文件可以用JD-GUI工具直接打开查看。

JD-GUI从2015年9月开始就停止了维护,对此有担忧的同学可以使用jadx作为JD-GUI的替代品。实际上jadx是可以直接打开APK文件的,而且jadx的反编译结果更加优美简练。

smali

smali可以反编译dex文件,也可以把你修改过的代码重新编译成dex:

1
2
java -jar baksmali.jar classes.dex -o classes
java -jar smali.jar classes -o classes.dex

将Nokia 6670的短信导入HTC Hero G3

这事本该N久前就该干了(09年末把手机从Nokia 6670换到HTC Hero G3),却一直拖着没有做,今天终于把这事作了一个了结。

Nokia下导出的短信是如Messages_Nokia 6670_20091215.xml这样的一个XML文件,文件内容的编码是unicode,格式就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="unicode"?>
<SMS PhoneModel="Nokia 6670">
  <Message DateTime="2011-06-21 10:10:10">
    <Sender>路人甲</Sender>
    <PhoneNumber>+8613988888888</PhoneNumber>
    <Location>Inbox</Location>
    <Text>我是路人甲</Text>
  </Message>

  <Message DateTime="2011-06-21 11:11:11">
    <Sender>路人乙</Sender>
    <PhoneNumber>+8613966666666</PhoneNumber>
    <Location>Outbox</Location>
    <Text>我是路人乙</Text>
  </Message>
</SMS>

本来是想写个Android应用来直接导入这些备份短信的,不过在实现的过程中发现了SMS Backup&Restore应用导出的备份文件和Nokia的备份文件类似,产生了做个将Nokia备份文件转换成SMS Backup&Restore备份文件的念头,又在网上发现了nicholashan的将Nokia的短信导入Hero(可能适用所有Android机型),坚定了我的这个想法。

分析了一下SMS Backup&Restore备份文件的格式,文件内容的编码是utf-8的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<smses count="3">
  <sms protocol="0" address="10658658" date="1308629532000"
    type="1" subject="null" body="我是内容" toa="null"
    sc_toa="null" service_center="null" read="1"
    status="-1" locked="0" />
  <sms protocol="0" address="13566666666" date="1308633733000"
    type="2" subject="null" body="我也是内容" toa="null"
    sc_toa="null" service_center="null" read="1"
    status="-1" locked="0" />
  <sms protocol="0" address="+8613588888888" date="1308641377000"
    type="1" subject="null" body="我还是内容" toa="null"
    sc_toa="null" service_center="null" read="1"
    status="-1" locked="0" />
</smses>

与转换相关的就address、date、type和body四个属性。address是发信人的号码;date是短信发送日期的timestamp。这点nicholashan的确说错了,后面不需要加什么0的(实际上nicholashan自己也怀疑自己的说法);type等于1表示别人发给你的短信,如果是2的话就是你发给其他人的短信;body就是短信内容了。

将Nokia中的时间转换到SMS Backup&Restore的时间可以使用下面的代码:

1
2
3
4
5
6
7
8
DateFormat dt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
long value = 1355296332000L;    // 2012-12-12 12:12:12
Element e = (Element)iter.next();
Attribute attr = e.attribute("DateTime");
try {
    value = dt.parse(attr.getValue()).getTime();
} catch (ParseException ex) {
}

完整的代码如下(使用dom4j实现XML解析):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Iterator;
import java.util.List;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;

public class Main {

    public static void main(String[] args) {
        try {
            SAXReader sr = new SAXReader();
            Document doc = sr.read(new File(args[0]));

            generateDocument(doc);
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void generateDocument(Document doc) {
        List list = doc.selectNodes("//Message");

        Document document = DocumentHelper.createDocument();
        Element smses = document.addElement("smses");
        smses.addAttribute("count", String.valueOf(list.size()));

        DateFormat dt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        long value = 1355296332000L;    // 2012-12-12 12:12:12
        Iterator iter = list.iterator();
        while (iter.hasNext()) {
            Element e = (Element)iter.next();
            Attribute attr = e.attribute("DateTime");
            try {
                value = dt.parse(attr.getValue()).getTime();
            } catch (ParseException e) {
                System.out.println(e.getMessage());
            }
            Node phone = e.selectSingleNode("PhoneNumber");
            Node location = e.selectSingleNode("Location");
            Node text = e.selectSingleNode("Text");

            Element sms = smses.addElement("sms");
            sms.addAttribute("protocol", "0");
            sms.addAttribute("address", phone.getText());
            sms.addAttribute("date", String.valueOf(value));
            if ("Inbox".equals(location.getText())) {
                sms.addAttribute("type", "1");
            } else if ("Outbox".equals(location.getText())) {
                sms.addAttribute("type", "2");
            }
            sms.addAttribute("subject", "null");
            sms.addAttribute("body", text.getText());
            sms.addAttribute("toa", "null");
            sms.addAttribute("sc_toa", "null");
            sms.addAttribute("service_center", "null");
            sms.addAttribute("read", "1");
            sms.addAttribute("status", "-1");
            sms.addAttribute("locked", "0");
        }

        File f = new File("output.xml");
        XMLWriter output = new XMLWriter(new FileWriter(f));
        output.write(document);
        output.close();
    }
}