201008#5

手机屏幕和电脑屏幕不同,不应该向下滚动得过长,否则用户不知道下面其实还有按钮。这点我忽略了,在中间插入了一个变长的列表checkbox。这个要大改一下。不过倒是给我带来了列表下方的按钮无法显示的问题,在梁工指点下,知道原来应该用weight去解决。有时间要回过头研究一下weight。

同事买了台ipad,我又近距离和我的Sony Reader对比了一下大小。是太大了点,但对看视频来说就比6寸好很多。理想的尺寸究竟是多少,7.7寸吗?7.7大改就和我Sony Reader的外壳差不多了。关键看重量。Sony Reader我已经打算把它Retire,送给老婆玩了。

上周在手机Kindle看完《龙纹身的女孩》,有点阅读过饱,整个星期不想看书。补习了《教父》一和二,几乎比我还老的电影。除了长度上有时代的烙印(btw,现在的片子也开始玩超长版),其它几乎看不出来有太大区别,可以比照一下《No Country for Old Men》。难怪成为经典。罗拔迪尼路后生的时候和老了一样有型。

反观现在的内地影视台,仍然在放八路军大智大勇,国民党和日军愚蠢这样的剧情。即使受过恐怖袭击,好莱坞拍一个穆斯林恐怖分子试图策动袭击,美国特工英勇杀敌。如果光是这样肤浅,恐怕也无人问津。愚蠢的东西能够存活多年,背后一定有个愚蠢的系统。

看到一个架构大会的花絮,有厂商利用来做自己的广告,被观众呛了一把。我想厂商是有权卖广告的,但要看什么样的场合。我觉得如果是卖广告的发布会,起码你就不该收门票,甚至还应该发赠品。作为技术大会策划,也应该平衡一下:没错你是需要拉厂商赞助,但最好局限于现场广告banner,不要成为一个Session。每个PPT最后预先速览一下以防有人“偷鸡”。

Content Provider, Uri 和 ContactsContract

我的需求是从电话地址簿里面选择联系人,导入他的姓名和地址。正好同事黄师傅show了一个他写的sample程序,里面有选择联系人的功能,于是借过来用:

Intent intent = new Intent(Intent.ACTION_PICK,ContactsContract.Contacts.CONTENT_URI);
startActivityForResult(intent, PICK_CONTACT);

在onActivityResult方法里,我用下面代码拿到被选中的那个联系人的数据:

Uri uri = data.getData();
ContentResolver resolver = getContentResolver();
Cursor cursor = resolver.query(uri, null, null, null, null);
if (cursor.moveToFirst()) {
int idIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID);
long id = cursor.getLong(idIdx);
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME));
System.out.println(“user name=”+name);
}

到此为止,名字我能正确拿到的。接下来我想拿email,在ContactsContract.Data类里面找EMAIL常量,结果没找到。有点困惑。然后我把cursor的所有column名字和值都遍历一遍打了出来,心想email无论躲在哪个字段里,我都能找到。结果仍然没有,所有字段多半不是null就是数值,没有我填写的email那个值。只有一个叫lookup的字段,里面的值是hashcode,但这个我觉得也不像。

在网上搜了一下,没有答案。后来再把书中有关Content Provider和Uri的概念原理读了一下,再结合官方文档里面的一些描述,我开始有了点眉目。再做了一下实验,终于把email拿到了。

首先要用我不太严谨的语言讲一下Content Provider的机制。其实书里面已经讲得清楚,只是未经实践始终是视而不见。所有Content Provider都会在系统注册一个Uri,而这个Uri就是以“content://”开头的一个字符串,你只要注册了这个Content Provider类,里面指定处理特定模式的字符串,那么发到这个Uri模式下的请求就会交给这个Provider来响应。系统本身内置了一些Content Provider,例如地址簿就是一个。地址簿的Uri是约定的,定义在API的常量里(ContactsContract.Contacts.CONTENT_URI),它实际的值是:content://com.android.contacts/contacts

这是Content Provider和Uri的机制,它只定义了什么拦截处理的机制,对于Content的内部结构,其实是没任何规定的,可以自由组织。简单来讲就是不一定非要是关系数据库不可!这也是我开始搞错的重要原因。

读一下ContactsContract的API文档就知道(btw这些个名字起得让我有点崩溃):联系人信息是一个三层的数据模型。包括Data、RawContacts和Contacts这三个表(此外还有一些其它的扩展表)。简单讲就是每个联系人,都会在这三个表里面有记录。这个设计有点复杂,但初衷也是可以理解的,如果它的数据结构是定义在一个表里那样写得死死的,那怎么能支持复杂的自定义字段?

这三个表里面,联系人ID是共享的(当然了,这是它们联系起来的依据),因此关键是拿到联系人ID。在选择了联系人返回的时候,我们已经可以用下面代码拿到联系人ID:

int idIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID);
long id = cursor.getLong(idIdx);

这个id可以拿来访问DATA表,获取用户的Email信息,代码如下:

Cursor mailCursor=resolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, null, ContactsContract.CommonDataKinds.Email.CONTACT_ID+”=?”, new String[]{Long.toString(id)}, null);
if (mailCursor.moveToFirst()) {
do {
String mail=mailCursor.getString(mailCursor.getColumnIndexOrThrow(Email.DATA));
System.out.println(“email=”+mail);
} while (mailCursor.moveToNext());
}

上面关键之处是用ContactsContract.CommonDataKinds.Email.CONTENT_URI这个Uri(实际值为content://com.android.contacts/data/emails)以及既定的联系人ID约束条件来打开了一个新Cursor。从这个Uri可以看到,它访问的是data表,而不是上面的cursor访问的contacts表。在data表里面,它访问的还是emails这个维度里面的数据,里面可能有多条(例如家庭email地址、工作email地址)。

除了邮件,ContactsContact下面还定义了很多CommonDataKinds,如下图:

这样设计是基于扩展的考虑,但无疑也更加复杂了。具体设计的理由我暂时还未很好理解,不过只要把握住拿邮件地址这个方法,拿其它信息也可以采取同样方式。

201008#4

免费英超回来,我的有线机顶盒也跟着回来了。在第一年搞收费的时候,被迫装了卫星,从此收看电视新闻主要以香港有线新闻和CNN为主,取代了经常被封锁的在广州落地的翡翠台新闻。机顶盒没用了,干脆给了岳父。及后138改加密方式,英超没得看,就逐渐不看。连通过香港有线看国内新闻的渠道都没了,那就算了,只看CNN的国际新闻。说回来还要多谢英超的收费,把我推向境外媒体。

几年前看过一个片子叫《Hard Candy》,讲一个变态男子在聊天室勾搭女网友,强奸杀害。后来另一少女处心积虑地假装被勾搭上,进入对方家中,用药麻到对方。找到他的罪证,虐待一番,并进行了阉割等等。典型的以强凌弱,最后弱者反击,反过来凌虐对方的情节。看完很想有姊妹篇再过一下瘾。最近看的书《The Girls with the Dragon Tattoo》也有类似的情节,看得很是入迷,即使今天和公司同事去漂流,有时间的时候都忍不住拿手机出来继续追。

以为Windows 7就不会再需要因为慢而重装,那就错了。我想这是Windows系统的死穴,可能太多DLL注册了没删除干净。因此当软件越装越多,过了一段时间就会很慢。即使把软件反安装到剩下很少,依然不能解决问题。只能重装。用Ubuntu的那段时间,其实也有一点点这样的感觉,反正,软件都是越装越多的。

同事蔡师傅拿了他新买的诺基亚手机给我看,3XX元。很轻,当然不是智能机,但对纯电话短信用户来讲,又很够用。这些用户可能还有无数,说诺基亚气数已尽,也许真的言之尚早。

把我0.9版的应用推上github服务器时总是报错,研究了一番,才发现原来少做了一个pull的过程。对Git的流程不熟就是这样,当把文档电子书看过一遍,进入似懂非懂的阶段。如果不进入实际应用,就永远停留在纸上谈兵的阶段,永远不能再进一步。“懂了–不懂–又懂了”,总是这样一个过程。

Msysgit的中文问题

Msysgit装好后处理中文是有问题的,包括:ls中文目录名乱码;提交中文的log,push到服务器上会乱码;git log查看服务器pull过来的log乱码。

解决方法:

1、 在Git\etc\gitconfig 文件加入:

[gui]
encoding=utf-8
[i18n]
commitencoding=GBK

2、 在Git\etc\profile,加入:
export LESSCHARSET=utf-8

3、 在Git\etc\git-completion.bash,加入:

alias ls=’ls –show-control-chars –color=auto’
1)是将提交的log注释用utf-8处理,并将拿下了的log注释用中文编码转换显示;2)是让git log中的less处理支持utf-8;3)是让ls命令列出的中文文件名正确显示。

无关紧要的XXX

这周请了前端的老罗来讲点心得,他讲了两个钟(早知给他一个钟),一开篇就是:用户体验是无关紧要的。

这个说法早些时间在一些feed上看过,倒不是他的原创。最大的一个证据,就是“支付宝”。几乎没人敢站出来为支付宝的用户体验辩护,但糟糕的用户体验并不影响它占领市场。既然对业务目标没有多少影响,说它“无关紧要”,似乎也顺理成章。

但仔细想想,其实“无关紧要”可以用在任何地方。例如高性能是无关紧要的,水平扩展是无关紧要的,代码规范、团队合作、技术人员了解业务、了解缓存机制、写技术文档、熟悉Linux操作系统、懂优化数据库等等。。。 随便可以举出一堆,都能找到某些案例,即使这些地方做得不好,项目仍然可以成功。所以。。。它们都是无关紧要的。出粮也是无关紧要的,只要上个月的还有剩。

我明白提出这个说法的人所要表达的沮丧甚至愤怒–也许他并不真的认为是无关紧要。

当中的道理是,一样东西的成功,往往不是一个因素决定的。某一方面做得特别出色导致成功的也有,但某些方面做得不足,不见得就会完蛋。这对项目经理是好消息,一个项目组不可能没个方面的人才都完美无缺,总有些短板。不用怕。

Pad Reader

最近把Sony Reader借给了同事,自己尝试在手机上看小说。看的是《The Girl with the Dragon Tatoo》,btw内容真的很精彩,硬盘上的电影版还是忍住等书看完再说。

我买的是Kindle版电子书,在Kindle for Android上看。Nexus One的3.7寸屏幕,和Sony的6寸比当然有很大差距。这点差距,足以让手机的横屏功能起到关键作用。以前在Sony Reader上,我是懒得去横屏的,因为宽带已经和普通书差不多。但在手机上不一样,如果竖着看,几个单词就要换行,非常影响阅读体验。

以前不懂得把180°翻转给关掉,在躺卧的时候,很容易过度倾侧之下,横屏又变成了竖屏。现在这个一禁用就好办了,只要方向握对,过度倾侧也不会变竖屏的。

电子阅读的最佳尺寸,对于小说来说,我觉得在7寸左右。ipad一代的9寸屏,实在有点太大,它过度照顾游戏和上网的需要,这导致重量也太重,成年男子单手拿10分钟会累。听闻第二代是7.7寸,对于阅读为主的我非常有诱惑力。

如果单纯阅读,其实Droidpad也未尝不可,便宜好多。

Android上SQLite的DB Schema版本维护策略

过去10多年的开发生涯中,基本上都是与数据库打交道。从来没遇过这次搞Android上面的SQLite数据库遇到的问题那么奇怪。后来我明白了:因为以前起搞的都是服务器端编程,DB其实都在一个地方,改数据库结构,都是前后两个版本之间的事,更多关注点都是在如何保证改表对正常服务的冲击减到最小。

而在移动开发上,我们需要考虑的是外面可能有无数个旧版本并存,它们都同时需要升级到最新版本。分别要执行的操作都可能不一样,尤其在要保留用户旧数据的情况下。

在我写的这个Android程序里,用到了SQLite数据库来保存数据。第一次写,所以数据访问的代码是抄书上的,用了SQLiteOpenHelper这个辅助类。基本方法是:

  1. 为每个表都创建一个Adapter类;
  2. 每个这样的Adapter类里面再定义一个继承自SQLiteOpenHelper的内部类。这个类主要帮助包含它的Adapter类执行打开数据库,运行sql的工作。它覆盖了两个关键的方法:onCreate和onUpgrade。分别在DB未创建和DB升级时被回调。

详细的解释可以参考网上资料,可以搜到一大堆,例如这个

在应用只有一个表的时候,用起来是没太大问题的。后来多建了几个表,用着发现数据经常神秘地丢失,有时表又不会自动创建。后来经过多番查找资料,才明白了其中的机制。

首先,DB Schema的版本是针对整个数据库的(即针对整个应用,对于不共享数据库的情况来讲),而并非针对一个表的。它其实保存在SQLite数据库的pragma中,变量名为user_version,用命令行进去就可以看到当前的值。

其次,SQLiteOpenHelper对象被构建时,总是要接受一个version参数,这个参数代表最新版本号。SQLiteOpenHelper会将它和数据库中当前的值对比,如果有差异,就会调用onUpgrade方法,并且把这个数据库的user_version设置为最新的值。

问题就出现在这里,当我有多个数据表、多个Adapter的时候,第一个表被访问时,SQLiteOpenHelper已经将数据库的版本号更新为最新值。当第二、第三个表去访问的时候,就会发现数据库版本已经是最新,就不会调用onUpgrade方法。因此如果这个版本同时改了两个表,就会出错。

究其原因,觉得DB Schema的版本控制代码是全局的,根本不应该散落在各个地方,应该有一个地方中央控制。于是我作了一些改动:

  • 把所有Adapter的内部类的两个方法onCreate和onUpgrade都设置为什么都不做:

@Override
public void onCreate(SQLiteDatabase _db) {
System.out.println(“do nothing”);
}

@Override
public void onUpgrade(SQLiteDatabase _db, int _oldVersion,
int _newVersion) {
System.out.println(“Per-table upgrade is disabled”);
}

  • 创建一个新的类DbUtil来专门负责维护DB Schema的版本。

DbUtil负责记录当前最新的版本号,它有一个静态方法checkDbSchemaVersion(Context context),每次主程序的Activity启动的时候,都会在onCreate里面首先调用一下它来检查数据库版本。这个方法里面包含了一个继承自SQLiteOpenHelper的内部类,和以前的那些Adapter的内部类相似,它也是覆盖了onCreate和onUpgrade方法。onCreate方法的实现比较简单,它就是简单地执行了所有表的create语句。而onUpgrade方法中,我让它调用updateDbSchemaVersion(oldVersion, newVersion),这里的oldVersion是程序在当前运行的数据库里面拿到的版本值,可能是几个版本之前的值;newVersion是本程序设置的最新版本值。

upgradeDbSchemaVersion方法的实现也比较简单,它的作用是根据新旧版本的差异,将两版本之间的SQL语句全部按顺序执行一次,源码如下:

private static void updateDbSchemaVersion(int oldVersion, int latestVersion) {
if (oldVersion>=latestVersion) return;
System.out.println(“Upgrading DB schema from “+oldVersion +” to “+(oldVersion+1));
switch (oldVersion) {
case 2:
break;
case 3:
break;
}

oldVersion++;
updateDbSchemaVersion(oldVersion,latestVersion);
}

可以看到上面的程序是一个递归,它是将程序从2开始,将每个版本与前一版本之间的升级操作写在对应的case语句块中。无论当前当前的数据库版本是多少,总是可以在里面找到合适的位置,然后开始逐个版本升级,直到更新到最新版本为止。

需要注意的是,当程序是新安装(不是升级),也就是数据库不存在的情况下,DbUtil中的SQLiteOpenHelper的onCreate会被回调,但onUpgrade不会被回调,数据版本会即时更新为最新的。因此需要保证onCreate里面执行出来的DB Schema是最新的。所以每个版本除了要写updateDbSchemaVersion中的case,还要确保CREATE语句都是对的,建表没有建漏。

当然把程序再优化一下,把onCreate和onUpgrade整合,也是可以的,能做到严格按照版本升级的时间线来走一遍这些DDL。现在表不多,暂且偷一下懒吧。

201008#3

又想了一下php开发的速度问题,觉得这可能和jsp嵌入数据访问代码的时代相似。当java程序员有过一定的专业训练,就很难再容忍 这样做——即使有个好理由。还不如干脆换语言。
四年前我搭建svn时采用的开发模式,适合人数很少的团队。审视对比了一番,还是觉得应该改掉,与业界的规范“接轨”。
google搜索android的资料与java相比真的痛苦许多,不是没有,就是大部分重复。

选择

选择Java还是PHP?Oracle还是MySQL?这种问题90%的情况下讨论是没必要的,讲个笑话让它过去即可。

10%的情况是那种大家都懂行,有很多假设前提条件大家都已经有默契,即使不明确讲出来也互相心照不宣的情况。

90%是那些双方关心的根本不是同一个方面,某一方付出的代价另一方根本不在乎,而这边得到的好处,那边也根本享受不到。

记得有次开一个会,开发的人程序性能写得很差,明明很多改善空间,但提出的方案是使用Oracle RAC。业主也很赞成,对呀,用来试试吧。当然,因为出钱的不是你们,为啥不试试?为啥不用IBM的深蓝试试?

201008#1

今天要和十几个同事去深圳西冲海滩悠闲一天,此刻正在M记吃早餐等出发。yeah

昨晚纵横上见到黄师傅,赫然出现在深圳世界大观附近。今天没准能和他擦身而过…

这周最大的新闻是google宣布放弃wave。这其实是我一直期待的消息。作为google的粉丝,痛苦的不是偶像犯错,而是偶像被犯的错误一直困扰。承认显然是个解脱。eweek十大问题那个文章已经把根源讲得比较透彻。我最大体会是这东西不够简单。

Tasker是我购买的第二个收费手机软件。这礼拜都沉迷它(尽管同事对它普遍比较冷淡)。除了功能,我觉得它的教程非常精巧。类编程的使用方式,通过简短的教程,把学习曲线压得很平缓。不需要看完长篇大论,就可以投入使用。

Page 1 of 712345...Last »