- 标签,表示菜单中只有一个菜单条目。
7.2.3. 更新StatusActivity,装载菜单
前面提到,菜单是在用户按下设备的菜单按钮时打开。在用户第一次按下菜单按钮时,系统会触发Activity的onCreateOptionsMenu()
方法,在这里装载menu.xml
文件中表示的菜单。这与第六章StatusActivity类
中为Activity装载布局文件的做法有些相似。都是读取XML文件,为其中的每个XML元素创建一个Java对象,并按照XML元素的属性初始化对象。
可以看出,只要Activity不被销毁,这个菜单就会一直储存在内存中,而onCreateOptionsMenu()
最多只会被触发一次。当用户选择某菜单项时,会触发onOptionsItemSelected
,这在下一节讨论。
我们需要为Activity加入装载菜单的相关代码,为此加入onCreateOptionsMenu()
方法。它将只在用户第一次按下菜单键时触发。
// Called first time user clicks on the menu button
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater(); //
inflater.inflate(R.menu.menu, menu); //
return true; //
}
获取MenuInflater对象。
使用inflater对象装载XML资源文件。
要让菜单显示出来,必须返回True。
7.2.4. 更新StatusActivity,捕获菜单事件
我们还需要捕获菜单条目的点击事件。为此添加另一个回调方法,onOptionsItemSelected()
。它在用户单击一个菜单条目时触发。
// Called when an options item is clicked
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { //
case R.id.itemPrefs:
startActivity(new Intent(this, PrefsActivity.class)); //
break;
}
return true; //
}
此方法在任意菜单条目被点击时都会触发,因此我们需要根据不同的条目ID做不同的处理。暂时这里只有一个菜单条目。不过随着程序复杂度的增长,需要添加新条目的时候,只要在switch语句中引入新的条目ID即可,也是非常容易扩展的。
startActivity()
方法允许我们打开一个新的Activity。在这里,我们创建一条新的Intent,表示打开PrefsActivity
。
返回true,表示事件处理成功。
Tip:
同原先一样,可以使用Eclipse的快捷功能Source→Override/Implement Methods
生成onCreateOptionsMenu()
、onOptionsItemSelected()
方法的声明。
7.2.5. 字符串资源
更新后的strings.xml
大致如下:
例 7.5. res/values/strings.xml
Yamba 2
Yamba 2
Please enter your 140-character status
Update
Status Update
Prefs
Username
Password
API Root
Please enter your username
Please enter your password
URL of Root API for your service
到这里,你已经可以查看新的选项界面了。打开程序,在消息界面中选择Menu→Prefs即可,如 图 7.4. PrefsActivity 。不妨尝试更改用户名与密码的设置再重启应用程序,查看选项数据是否依然存在。
图 7.4. PrefsActivity
7.3. SharedPreferences
已经有了选项界面,也有了存储用户名、密码、API root等选项数据的办法,剩下的就是读取选项数据了。要在程序中访问选项数据的内容,就使用Android框架的SharedPreference
类。
这个类允许我们在程序的任何部分(比如Activity,Service,BroadcastReceiver,ContentProvider)中访问选项数据,这也正是SharedPreference这个名字的由来。
在StatusActivity中新加入一个成员prefs:
SharedPreferences prefs;
然后在onCreate()中添加一段代码,获取SharedPreferences对象的引用。
@Override
public void onCreate(Bundle savedInstanceState) {
...
// Setup preferences
prefs = PreferenceManager.getDefaultSharedPreferences(this); //
prefs.registerOnSharedPreferenceChangeListener(this); //
}
每个程序都有自己唯一的SharedPreferences对象,可供当前上下文中所有的构件访问。我们可以通过PreferenceManager.getDefaultSharedPreferences()
来获取它的引用。名字中的"Shared"可能会让人疑惑,它是指允许在当前程序的各部分间共享,而不能与其它程序共享。
选项数据可以随时为用户修改,因此我们需要提供一个机制来跟踪选项数据的变化。为此我们在StatusActivity中提供一个OnSharedPreferenceChangeListener
接口的实现,并注册到SharedPreferences对象中。具体内容将在后面讨论。
现在用户名、密码与API root几项都已定义。接下来我们可以重构代码中的Twitter对象部分,消除原先的硬编码。我们可以为StatusActivity添加一个私有方法,用以返回可用的twitter对象。它将惰性地初始化twitter对象,也就是先判断twitter对象是否存在,若不存在,则创建新对象。
private Twitter getTwitter() {
if (twitter == null) { //
String username, password, apiRoot;
username = prefs.getString("username", ""); //
password = prefs.getString("password", "");
apiRoot = prefs.getString("apiRoot", "http://yamba.marakana.com/api");
// Connect to twitter.com
twitter = new Twitter(username, password); //
twitter.setAPIRootUrl(apiRoot); //
}
return twitter;
}
仅在twitter为null时创建新对象。
从SharedPreferences对象中获取username与password。getString()
的第一个参数是选项条目的键,比如"username"和"password",第二个参数是条目不存在时备用的默认值。因此,如果用户在登录前还没有设置个人选项,到这里用户名密码就都是空的,自然也就无法登录。但是由于jtwitter设计的原因,用户只有在尝试发送消息时才会看到错误。
按照用户提供的用户名密码登录。
我们需要提供自己的API root才可以访问twitter服务。
到这里,我们不再直接引用twitter对象,而统一改为通过getTwitter()
获取它的引用。修改后的onClick()
方法如下:
public void onClick(View v) {
// Update twitter status
try {
getTwitter().setStatus(editText.getText().toString());
} catch (TwitterException e) {
Log.d(TAG, "Twitter setStatus failed: " + e);
}
}
留意,我们虽将初始化的代码移到了外面,但它依然是阻塞的,可能会因为网络状况的不同产生一定的延迟。日后我们仍需对此做些考虑,利用AsyncTask来改进它。
前面修改onCreate()
时曾提到,我们需要跟踪用户名与密码的变化。因此,在当前的类中添加onSharedPreferenceChanged()
方法,然后在onCreate()
中通过pres.registerOnSharedPreferenceChangeListener(this)
将它注册到prefs对象,这样就得到一个回调函数,它会在选项数据变化时触发。在这里我们只需简单将twitter设为null,到下次获取引用时,getTwitter()
会重新创建它的实例。
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
// invalidate twitter object
twitter = null;
}
7.4. 简介文件系统
话说回来,前面的这些选项数据又是储存在设备的哪里?我的用户名与密码是否安全?在这些问题之前,我们需要先对Android的文件系统有所了解。
7.4.1. 浏览文件系统
访问Android的文件系统有两种方式。一种是通过Eclipse,另一种是通过命令行。
Eclipse中提供了一个File Explorer工具供我们访问文件系统。要打开它,可以选择Window→Show View→Other…→Android→File Explorer
,也可以单击右上角的DDMS中访问它。要打开DDMS,可以单击右上角的DDMS Perspective ,也可以选择Window→Open Perspective→Other…→DDMS
。如果在工作台上连接着多台设备,选择出正确的设备,然后就可以访问它的文件系统了。
如果更喜欢命令行,adb shell就是你的首选。通过它,你可以像访问Unix系统那样访问设备的文件系统。
7.4.2. 文件系统的分区
Android设备主要有三个分区,参见 图 7.5. "通过Eclipse的File Explorer查看文件系统" ,即:
系统分区:/system/
SDCard分区:/sdcard/
用户数据分区:/data/
图 7.5. 通过Eclipse的File Explorer查看文件系统
7.4.3. 系统分区
系统分区用于存放整个Android操作系统。预装的应用程序、系统库、Android框架、Linux命令行工具等等,都存放在这里。
系统分区是以只读模式挂载的,应用开发者针对它的发挥空间不大,在此我们不多做关注。
在仿真器中,系统分区对应的映像文件是system.img
,它与平台相关,位于android-sdk/platforms/android-8/images目录。
7.4.4. SDCard 分区
SDCard分区是个通用的存储空间。你的程序只要拥有WRITE_TO_EXTERNAL_STORAGE权限,即可随意读写这个文件系统。这里最适合存放音乐、照片、视频等大文件。
留意,Android自FroYo版本开始,Eclipse File中显示的挂载点从/sdcard改为了/mnt/sdcard。这是因为FroYo引入的新特性,允许在SDCard中存储并执行程序。
对应用开发者而言,SDCard分区无疑十分有用。不过它的文件结构也相对松散一些,管理时需要注意。
这个分区的镜像文件一般是sdcard.img,位于对应设备的AVD目录之下,也就是~/.android/avd
之下的某个子目录。对真机而言,它就是一个SD卡。
7.4.5. 用户数据分区
对开发者和用户来讲,用户数据分区才是最重要的。用户数据都储存在这里,下载的应用程序储存在这里,而且所有的应用程序数据也都储存在这里。
用户安装的应用程序都储存在/data/app目录,而开发者关心的数据文件都储存在/data/data目录。在这个目录之下,每个应用程序对应一个单独的子目录,按照Java package的名字作为标识。从这里可以再次看出Java package在Android安全机制中的地位。
Android框架提供了许多相关的辅助函数,允许应用程序访问文件系统,比如getFilesDir()。
这个分区的镜像文件是user-data.img,位于对应设备的AVD目录之下。同前面一样,也是在~/.android/avd/之下的某个子目录。
新建应用程序的时候,你需要为Java代码指定一个package,按约定,它的名字一般都是逆序的域名,比如com.marakana.yamba
。应用安装之后,Android会为应用单独创建一个目录/data/data/com.marakana.yamba/
。其中的内容就是应用程序的私有数据。
/data/data/com.marakana.yamba2/
下面也有子目录,但是结构很清晰,不同的数据分在不同的目录之下,比如首选项数据就都位于/data/data/com.marakana.yamba2/shared_prefs/
。通过Eclipse的File Explorer访问这一目录,可以在里面看到一个com.marakana.yamba2_preferences.xml
文件。你可以把它拷贝出来,也可以在adb shell中直接查看。
adb shell是adb的另一个重要命令,它允许你访问设备(真机或者虚拟机)的shell。就像下面这样:
[user:~]> adb shell
# cd /data/data/com.marakana.yamba2/shared_prefs
# cat com.marakana.yamba2_preferences.xml
password
http://yamba.marakana.com/api
student
#
这个XML文件里表示的就是这个程序中的选项数据。可见,用户名、密码与API root都在这里。
7.4.6. 文件系统的安全机制
对安全问题敏感的同学肯定要问了,这样安全吗?明文存储的用户名密码总是容易让人神经紧张。
其实这个问题就像是在大街上捡到一台笔记本,我们可以拆开它的硬盘,但不一定能读取它的数据。/data/data
之下的每个子目录都有单独的用户帐号,由Linux负责管理。也就是说,只有我们自己的应用程序才拥有访问这一目录的权限。数据既然无法读取,明文也就是安全的。
在仿真器上,我们只要拥有root权限即可访问整个文件系统。这有助于方便开发者进行调试。
(译者注:作者在此说法似乎有矛盾之处。如果手机被偷走,依然不能排除小偷使用root权限窃取其中密码的可能。译者的建议是尽量避免使用明文。谈及安全问题,神经紧张一些总不会错。)
7.5. 总结
到这里,用户可以设置自己的用户名与密码。同时移除原先的硬编码,使得程序更加可用。
图7.6 "Yamba完成图" 展示了目前我们已完成的部分。完整图参见 图5.4 "Yamba设计图" 。
图 7.6. Yamba完成图
8. Service
Service 与 Activity 一样,同为 Android 的基本构件。其不同在于 Service 只是应用程序在后台执行的一段代码,而不需要提供用户界面。
Service 独立于 Activity 执行,无需理会 Activity 的状态如何。比如我们的 Yamba 需要一个 Service 定时访问服务端来检查新消息。它会一直处于运行状态,而不管用户是否开着Activity。
同 Activity 一样,Service 也有着一套精心设计的生存周期,开发者可以定义其状态转换时发生的行为。Activity 的状态由 ActivityManager 控制,而 Service 的状态受 Intent 影响。假如有个 Activity 需要用到你的 Service ,它就会发送一个 Intent 通知打开这个 Service (若该 Service 正在执行中则不受影响)。通过 Intent 也可以人为停止(即销毁)一个 Service 。
Service分为Bound Service和Unbound Service两种。Bound Service 提供了一组API,以允许其他应用通过 AIDL(Android Interface Definition Language,Andorid接口描述语言 ,参见 第十四章 ) 与之进行交互。不过在本章中,我们主要关注的是 Unbound Service,它只有两种状态:执行( Started ) 或停止( Stopped 或 Destoyed )。而且它的生存周期与启动它的Activity无关。
在本章,我们将动手创建一个 Service 。它在后台执行,获取用户在 Twitter 上最新的 Timeline ,并输出到日志。这个 Service 会在一个独立的线程中执行,因此也会顺便讲解一些关于并行程序设计的相关知识。此外,本章还将提到通过 Toast 向用户提示信息的方法,以及为 Service 和 Activity 所共享的应用程序上下文的相关知识。
到本章结束,你将拥有一个可以发消息、定时更新Timeline的可用程序。
8.1. Yamba的Application对象
前面我们已在 StatusActivity
中实现了选项界面。现在还需要一个辅助函数 getTwitter()
来获得 Twitter 对象,籍以同服务端交互。
到这里需要用到应用其它部分的功能了,怎么办?可以复制粘贴,但不提倡这样。正确的做法是把需要重用的代码分离出来,统一放到一个各部分都可以访问的地方。对 Android 而言,这个地方就是 Application 对象。
Application 对象中保存着程序各部分所共享的状态。只要程序中的任何部分在执行,这个对象都会被系统创建并维护。 大多数应用直接使用来自框架提供的基类 android.app.Application
。不过你也可以继承它,来添加自己的函数。
接下来我们实现自己的 Application 对象,即 YambaApplication
。
创建一个 Java 类 YambaApplication
。
在 AndroidManifest.xml
文件中注册新的 Application 对象。
8.1.1. YambaApplication类
首先要做的是创建新类文件,这个类名为 YambaApplication
,它继承了框架中 Application
作为其基类。
接下来需要把一些通用的代码移动到这个类中。通用的代码指那些可以被程序各部分所重用的代码,比如连接到服务端或者读取配置数据。
留意 Application
对象里面除了常见的 onCreate()
,还提供了一个 onTerminate()
接口,用于在程序退出时的进行一些清理工作。虽然在这里我们没有什么需要清理,但是借机记录一些log信息也不错,方便观察程序在何时退出。在后面,我们可能会回到这里,。
例 8.1. YambaApplication.java
package com.marakana.yamba3;
import winterwell.jtwitter.Twitter;
import android.app.Application;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
public class YambaApplication1 extends Application implements
OnSharedPreferenceChangeListener { //
private static final String TAG = YambaApplication1.class.getSimpleName();
public Twitter twitter; //
private SharedPreferences prefs;
@Override
public void onCreate() { //
super.onCreate();
this.prefs = PreferenceManager.getDefaultSharedPreferences(this);
this.prefs.registerOnSharedPreferenceChangeListener(this);
Log.i(TAG, "onCreated");
}
@Override
public void onTerminate() { //
super.onTerminate();
Log.i(TAG, "onTerminated");
}
public synchronized Twitter getTwitter() { //
if (this.twitter == null) {
String username = this.prefs.getString("username", "");
String password = this.prefs.getString("password", "");
String apiRoot = prefs.getString("apiRoot",
"http://yamba.marakana.com/api");
if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)
&& !TextUtils.isEmpty(apiRoot)) {
this.twitter = new Twitter(username, password);
this.twitter.setAPIRootUrl(apiRoot);
}
}
return this.twitter;
}
public synchronized void onSharedPreferenceChanged(
SharedPreferences sharedPreferences, String key) { //
this.twitter = null;
}
}
YambaApplication
只有作为 Application
的子类才可以作为一个合法的 Application 对象。另外你可能发现了我们在将 OnSharedPreferenceChangeListener
的实现从 StatusActivity
移动到了这里。
Twitter
和 SharedPreferences
在这里都成为了共享对象的一部分,而不再为 StatusActivity
所私有。
onCreate()
在 Application 对象第一次创建的时候调用。只要应用的任何一个部分启动(比如 Activity 或者 Service ), Application 对象都会随之创建。
onTerminate()
调用于应用结束之前,在里面可以做些清理工作。在这里我们只用来记录log。
我们也把 StatusActivity
的 getTwitter()
方法移动到这里,因为它会为程序的其它部分所调用,这一来有利于提高代码的重用。留意下这里的 synchronized
关键字, Java 中的 synchronized方法 表示此方法在同一时刻只能由一个线程执行。这很重要,因为我们的应用会在不同的线程里用到这个函数。
onSharedPreferenceChanged()
也从 StatusActivity
移动到了 YambaApplication
。
现在我们已经有了YambaApplication
类,也转移了 StatusActivity
的一部分功能过来。接下来可以进一步简化 StatusActivity
:
例 8.2. StatusActivity using YambaApplication
...
Twitter.Status status = ((YambaApplication) getApplication())
.getTwitter().updateStatus(statuses[0]); //
...
现在是使用来自 YambaApplication
的 getTwitter()
方法,而不再是局部调用。类似的,其它需要访问服务端的代码也都需要使用这个方法。
8.1.2. 更新Manifest文件
最后一步就是通知我们的应用选择 YambaApplication
而非默认的 Application
了。更新 Manifest 的文件,为
节点增加一个属性:
...
...
节点中的属性 android:name=".YambaApplication"
告诉系统使用 YambaApplication 类的实例作为 Application 对象。
好,到这里我们已经成功地将 StatusActivity
里面一些通用的功能转移到了 YambaApplication
之中。这一过程就是代码重构。添加了功能就记着重构,这是个好习惯。
8.1.3. 简化 StatusActivity
现在我们可以通过 YambaApplication 获取 Twitter 对象了,接下来需要对 StatusActivity 进行修改,在其中使用 YambaApplication 提供的功能。下面是新版的 PostToTwitter
:
class PostToTwitter extends AsyncTask {
// Called to initiate the background activity
@Override
protected String doInBackground(String... statuses) {
try {
YambaApplication yamba = ((YambaApplication) getApplication()); //
Twitter.Status status = yamba.getTwitter().updateStatus(statuses[0]); //
return status.text;
} catch (TwitterException e) {
Log.e(TAG, "Failed to connect to twitter service", e);
return "Failed to post";
}
}
...
}
在当前上下文中调用 getApplication()
获取 Application 对象的引用。这里的 Application 对象来自我们自定义的 YambaApplication ,因此需要一个额外的类型转换。
得到 Application 对象的引用之后即可调用其中的函数了,比如 getTwitter()
。
以上,可以看到我们是如何一步步将 StatusActivity
中的功能重构到 Application 对象之中的。接下来就利用这些共享出来的功能,实现我们的 Updater Service。
8.2. UpdaterService
在本章的引言曾经提到,我们需要一个 Service 能一直在后台执行,并且定期获取 Twitter 的最新消息并存入本地的数据库。这一来我们的程序在离线时也有缓存的数据可读。我们称这个 Service 为 UpdaterService
。
创建 Service 的步骤如下:
创建一个表示这个 Service 的 Java 类。
在 Manifest 文件中注册这个 Service 。
启动 Service 。
8.2.1. 创建 UpdaterService 类
创建 Service 的过程与创建 Activity 或者其它基础构件类似,首先创建一个类,继承 Android 框架中提供的基类。
先新建一个 Java 文件。在 src 目录中选择你的 Java package ,右键选择 New→Class ,在 class name 一栏输入 UpdaterService 。这样就在 package 中新建出来一个 UpdaterService.java 文件。
回忆一下,在 "Service" 一节有提到过一般的 Service 的生存周期,如图 8.1 "Service的生命周期" :
图8.1. Service的生命周期
接下来,我们需要覆盖几个相关的方法:
onCreate()
在 Service 初始化时调用。
onStartCommand()
在 Service 启动时调用。
onDestory()
在 Service 结束时调用。
这里可以使用Eclipse的辅助工具,进入 Source→Override/Implement Methods ,选上这三个方法即可。
鉴于“最小可用”原则,我们在学习的时候也该从简入手。到这里,先只在每个覆盖的方法中记录些日志。我们Service的样子大致如下:
例 8.3. UpdaterService.java, version 1
package com.marakana.yamba3;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
public class UpdaterService1 extends Service {
static final String TAG = "UpdaterService"; //
@Override
public IBinder onBind(Intent intent) { //
return null;
}
@Override
public void onCreate() { //
super.onCreate();
Log.d(TAG, "onCreated");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) { //
super.onStartCommand(intent, flags, startId);
Log.d(TAG, "onStarted");
return START_STICKY;
}
@Override
public void onDestroy() { //
super.onDestroy();
Log.d(TAG, "onDestroyed");
}
}
因为频繁地使用Log.d(),我会在所有主要的类中声明一个TAG常量。
onBind()
在Bound Service中使用,它会返回一个Binder的具体实现。在这里还没有用到Bound Service,因此返回null。
onCreate()
调用于Service初次创建时,而不一定是startService()的结果。可以在这里做些一次性的初始化工作。
onStartCommand()
调用于Service收到一个startService()的Intent而启动时。对已经启动的Service而言,依然可以再次收到启动的请求,这时就会调用onStartCommand()
。
onDestory()
调用于Service收到一个stopService()的请求而销毁时。对应onCreate()
中的初始化工作,可以在这里做一些清理工作。
8.2.2. 更新Manifest文件
我们的 Service 已经有了个样子,接下来就跟对待其它构件一样在 Manifest 文件中注册它,不然就是无法使用的。打开 AndroidManifest.xml ,单击最右边的 tab 查看 XML 源码,把如下代码加入
节点:
...
...
...
...
同为 Android 的基本构件 ,Service 与 Activity 是平等的。因此在 Manifest 文件中,它们处于同一级别。
8.2.3. 添加菜单项
现在我们已经定义并且注册了这个 Service,接下来考虑一个控制它启动或者停止的方法。最简单的方法就是在我们的选项菜单中添加一个按钮。便于理解起见,我们先从这里入手。更智能的方法我们稍候讨论。
为添加启动/停止的按钮,我们需要在 menu.xml 添加两个菜单项,就像在 "Menu Resource" 一节中添加 Prefs 菜单项一样。更新后的 menu.xml 是这个样子:
例 8.4. menu.xml
此项在前一章定义。
ServiceStart 一项拥有常见的几个属性:id
, title
, icon
。icon
在这里同样是个 Android
的资源。
ServiceStop
与 ServiceStart
相似。
menu.xml 已经更新,接下来就是让它们捕获用户的点击事件。
8.2.4. 更新选项菜单的事件处理
要捕获新条目的点击事件,我们需要更新 StatusActivity
中的 onOptionsItemSelected()
方法,这跟我们在 "更新StatusActivity,装载菜单" 一节中所做的一样。打开 StatusActivity.java 文件,找到 onOptionsItemSelected
方法。现在里边已经有了为不同条目提供支持的大体框架,要增加两个“启动 Service ”与“关闭 Service ”两个条目,需要分别为 UpdaterService
通过startService()
和 stopService()
发送 Intent 。更新后的代码如下:
// Called when an options item is clicked
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.itemServiceStart:
startService(new Intent(this, UpdaterService.class)); //
break;
case R.id.itemServiceStop:
stopService(new Intent(this, UpdaterService.class)); //
break;
case R.id.itemPrefs:
startActivity(new Intent(this, PrefsActivity.class));
break;
}
return true;
}
创建一个Intent,用以启动UpdaterService。如果这个Service还没有启动,就会调用onCreate()
方法。然后再调用onStartCommand()
方法,不过它不管Service是否启动都会被调用。
同样,这里调用stopService()
为UpdaterService发送一个Intent。如果Service正在运行中,就会调用Service的onDestory()
方法。否则,简单忽略这个Intent。
在这个例子中我们使用了 Explicit Intent ,明确指明接收 Intent 的目标类,即 UpdaterService.class
。
8.2.5. 测试Service
现在可以重启你的程序(仿真器是不需要重启的)。当你的程序启动时,单击菜单选项中新增的按钮,即可随意控制 Service 的启动与停止。
检验 Service 是否正常执行的话,打开 Logcat 查看程序生成的日志信息。在 "Android的日志机制" 一节中我们曾提到,查看 Log 既可以通过 Eclipse ,也可以通过命令行。
检验Service正常执行的另一条途径是,进入Android Settings 查看它是否出现在里面:回到主屏幕,点击Menu
,选择Setting
,然后进入Applications
→Running services
。正常的话,你的Service就会显示在里面。如 图8.2 执行中的Service 。
图 8.2. 执行中的Service
好,你的 Service 已经能够正常运行了,只是还不能做多少事情。
8.3. 在 Service 中循环
根据设计,我们的 Service 需要被频繁地唤醒,检查消息更新,然后再次“睡眠”一段时间。这一过程会持续进行下去,直到 Service 停止。实现时可以将这一过程放在一个循环中,每迭代一次就暂停一段时间。在这里可以利用 Java 提供的 Thread.sleep()
方法,可以让当前进程以毫秒为单位暂停一段时间,并让出 CPU。
在这里还有一点需要考虑,那就是Service连接到服务端获取数据这一行为本身,需要花费相当长的时间。网络操作的执行效率直接受网络接入方式、服务端的响应速度以及一些不可预知的因素影响,延迟是很常见的。
要是把检查更新的操作放在主线程的话,网络操作中的任何延时都会导致用户界面僵死,这会给用户留下一个很不好的印象。甚至很可能会让用户不耐烦,直接调出 "Force Close or Wait" 对话框(参见‘Android的线程机制’ 一节)把我们的应用杀死。
解决这一问题的最好办法,就是利用Java内置的线程支持,把网络相关的操作放到另一个线程里。Service在控制UI的主线程之外执行,这使得它们与用户交互的代码总是分离的。这点对Yamba这样与网络交互的界面而言尤为重要,而且对其它场景也同样适用,哪怕它花费的时间不长。
例 8.5. UpdaterService.java, version 2
package com.marakana.yamba3;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
public class UpdaterService2 extends Service {
private static final String TAG = "UpdaterService";
static final int DELAY = 60000; // a minute
private boolean runFlag = false; //
private Updater updater;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
this.updater = new Updater(); //
Log.d(TAG, "onCreated");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
this.runFlag = true; //
this.updater.start();
Log.d(TAG, "onStarted");
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
this.runFlag = false; //
this.updater.interrupt(); //
this.updater = null;
Log.d(TAG, "onDestroyed");
}
/**
* Thread that performs the actual update from the online service
*/
private class Updater extends Thread { //
public Updater() {
super("UpdaterService-Updater"); //
}
@Override
public void run() { //
UpdaterService2 updaterService = UpdaterService2.this; //
while (updaterService.runFlag) { //
Log.d(TAG, "Updater running");
try {
// Some work goes here...
Log.d(TAG, "Updater ran");
Thread.sleep(DELAY); //
} catch (InterruptedException e) { //
updaterService.runFlag = false;
}
}
}
} // Updater
}
声明一个常量,用以表示网络更新的时间间隔。我们也可以把它做在选项里,使之可以配置。
这个标志变量用以方便检查 Service 的执行状态。
Updater 在另一个线程中进行网络更新。这个线程只需创建一次,因此我们在 onCreate()
中创建它。
在 Service 启动时,它会调用 onStartCommand()
方法,我们就在这里启动 Updater 线程再合适不过。同时设置上标志变量,表示 Service 已经开始执行了。
与之对应,我们可以在 onDestroy()
中停止 Updater 线程。再修改标志变量表示 Service 已经停止。
我们通过调用 interrupt()
来停止一个线程执行,随后设置变量的引用为 null ,以便于垃圾收集器清理。
在这里定义 Updater 类。它是个线程,因此以 Java 的 Thread 类为基类。
给我们的线程取一个名字。这样便于在调试中辨认不同的线程。
Java 的线程必须提供一个 run()
方法。
简单得到对 Service 的引用。 Updater 是 Service 的内部类。
这个循环会一直执行下去,直到 Service 停止为止。记着 runFlag 变量是由 onStartCommand()
与 onDestroy()
修改的。
调用 Thread.sleep()
暂停Updater线程一段时间,前面我们将DELAY设置为1分钟。
对执行中的线程调用 interrupt()
,会导致 run()
中产生一个 InterruptException
异常。 我们捕获这个异常,并设置 runFlag 为 false ,从而避免它不断重试。
8.3.1. 测试正常运行
到这里,已经可以运行程序并启动 Service 了。只要观察 log 文件你就可以发现,我们的 Service 会每隔两分钟记录一次任务的执行情况。而 Service 一旦停止,任务就不再执行了。
如下为 LogCat 的输出结果,从中可以看出我们 Service 的执行情况:
D/UpdaterService( 3494): onCreated
D/UpdaterService( 3494): onStarted
D/UpdaterService( 3494): Updater running
D/UpdaterService( 3494): Updater ran
D/UpdaterService( 3494): Updater running
D/UpdaterService( 3494): Updater ran
...
D/UpdaterService( 3494): onDestroyed
可见,我们的 Service 在最终销毁之前,循环了两次,其创建与启动皆正常。
8.4. 从 Twitter 读取数据
我们已经有了个大体的框架,接下来就连接到 Twitter ,读取数据并且在程序中显示出来。Twitter 或者其他的微博平台提供的 API 都各不相同。这时可以使用三方库 jtwitter.jar
,它提供了一个 Twitter
类作为封装。里边最常用的功能之一就是 getFriendsTimeline()
,它可以返回24小时中自己和朋友的最新20条消息。
要使用 Twitter API 的这一特性,首先应连接到 Twitter 服务。我们需要一个用户名、密码,以及一个API授权。回忆下本章前面 “Yamba 的 Application 对象” 一节中,我们已经把大部分相关的功能重构到了 YambaApplication 中。因此我们得以在这里重用这一功能,因为包括 Service 在内的程序中任何一个构件,都可以访问同一个 Application 对象。
在这里我们需要小小地修改下 YambaAppliaction ,好让别人知道这个 Service 是否正在运行。因此在 YambaApplication 中添加一个标志变量,配合 getter 与 setter 用以访问与更新:
public class YambaApplication extends Application implements OnSharedPreferenceChangeListener {
private boolean serviceRunning; //
...
public boolean isServiceRunning() { //
return serviceRunning;
}
public void setServiceRunning(boolean serviceRunning) { //
this.serviceRunning = serviceRunning;
}
}
这个标志变量表示了 Service 的运行状态。注意它是个私有成员,不可以直接访问。
这个全局方法用以访问标志变量 serviceRunning
的值。
另一个全局方法,用以设置标志变量 serviceRunning
的值。
接下来我们可以为 UpdaterService
写些代码,让它连接到API,读取朋友的最新消息。
例 8.6. UpdaterService.java, final version
package com.marakana.yamba3;
import java.util.List;
import winterwell.jtwitter.Twitter;
import winterwell.jtwitter.TwitterException;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
public class UpdaterService extends Service {
private static final String TAG = "UpdaterService";
static final int DELAY = 60000; // wait a minute
private boolean runFlag = false;
private Updater updater;
private YambaApplication yamba; //
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
this.yamba = (YambaApplication) getApplication(); //
this.updater = new Updater();
Log.d(TAG, "onCreated");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
this.runFlag = true;
this.updater.start();
this.yamba.setServiceRunning(true); //
Log.d(TAG, "onStarted");
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
this.runFlag = false;
this.updater.interrupt();
this.updater = null;
this.yamba.setServiceRunning(false); //
Log.d(TAG, "onDestroyed");
}
/**
* Thread that performs the actual update from the online service
*/
private class Updater extends Thread {
List timeline; //
public Updater() {
super("UpdaterService-Updater");
}
@Override
public void run() {
UpdaterService updaterService = UpdaterService.this;
while (updaterService.runFlag) {
Log.d(TAG, "Updater running");
try {
// Get the timeline from the cloud
try {
timeline = yamba.getTwitter().getFriendsTimeline(); //
} catch (TwitterException e) {
Log.e(TAG, "Failed to connect to twitter service", e); //
}
// Loop over the timeline and print it out
for (Twitter.Status status : timeline) { //
Log.d(TAG, String.format("%s: %s", status.user.name, status.text)); //
}
Log.d(TAG, "Updater ran");
Thread.sleep(DELAY);
} catch (InterruptedException e) {
updaterService.runFlag = false;
}
}
}
} // Updater
}
这个变量用以方便访问 YambaApplication
对象,便于使用其中的共享功能,比如读取用户设置、连接到远程服务等。
通过 getApplication()
方法获得 YambaApplication
对象的引用。
一旦启动了这个 Service ,我们就设置 YambaApplication
中的标志变量 serviceRunning
。
同样,等 Service 停止时也修改 YambaApplication
对象中的 serviceRunning
。
我们使用了 Java 中的泛型来定义一个存放 Twitter.Status
的 List 变量。
调用 YambaApplication
中的 getTwitter()
方法获得 Twitter 对象,然后调用 getFriendTimeline()
来获得24小时内朋友最新的20条消息。注意因为这个函数需要访问云端服务,所以其运行时间受网络延迟影响比较大。我们把它安置在一个独立的线程中,从而防止它拖慢用户界面的响应。
网络相关的操作失败的原因有很多。我们在这里捕获异常,并打印出错时的堆栈信息。其输出在 Logcat 中可见。
现在我们已经初始化了 timeline 这个 List ,可以遍历其中的元素。最简单的方法就是使用 Java 的 "for each" 循环,自动地遍历我们的 List ,分别把每个元素的引用交给变量 status。
暂时我们先把消息也就是“谁说了什么“输出到 Logcat。
8.4.1. 测试 Service
好,我们可以运行我们的程序并启动 Service ,观察 Logcat 中记录的朋友消息。
D/UpdaterService( 310): Marko Gargenta: it is great that you got my message
D/UpdaterService( 310): Marko Gargenta: hello this is a test message from my android phone
D/UpdaterService( 310): Marko Gargenta: Test
D/UpdaterService( 310): Marko Gargenta: right!
...
8.5. 总结
我们已经有了一个可用的 Service,只是启动/停止还需要人工操作,仍略显粗放。这个 Service 能够连接到服务端更新朋友的最新消息。目前我们只是把这些消息输出到 Logcat 中,到下一章我们就把它们存进数据库里。
图8.3 "Yamba完成图" 展示了目前为止我们已完成的部分。完整图参见 图5.4 "Yamba设计图" 。
图 8.3. Yamba完成图
9. 数据库
Android 系统中的许多数据都是持久化地储存在数据库中,比如联系人、系统设置、书签等等。 这样可以避免因为意外情况(如杀死进程或者设备关机)而造成的数据丢失。
可是,在移动应用程序中使用数据库又有什么好处? 把数据留在可靠的云端,不总比存储在一个容易丢失容易损坏的移动设备中更好?
可以这样看:移动设备中的数据库是对网络世界的一个重要补充。虽说将数据存储在云端有诸多好处,但是我们仍需要一个快速而稳定的存储方式,保证应用程序在没有网络时依然能够正常工作。这时,就是将数据库当作缓存使用,而Yamba正是如此。
本章介绍 Android 系统中数据库的使用方法。我们将在Yamba中新建一个数据库,用来存储从服务端收到的消息更新。将数据存储在本地,可以让Yamba节约访问网络的开销,从而加速Timeline的显示。 另由Service负责在后台定期抓取数据到数据库,以保证数据的实时性。这对用户体验的提升是大有好处的。
9.1. 关于 SQLite
SQLite是一个开源的数据库,经过一段时间的发展,它已经非常稳定,成为包括Android在内的许多小型设备平台的首选。 Android选择SQLite的理由有:
零配置。开发者不必对数据库本身做任何配置,这就降低了它的使用门槛。
无需服务器。SQLite 不需要独立的进程,而是以库的形式提供它的功能。省去服务器,可以让你省心不少。
单文件数据库。这一特性允许你直接使用文件系统的权限机制来保护数据。Android将每个应用程序的数据都放在独立的安全沙盒(sandbox)中,这点我们已经有所了解。
开放源码。
Android 框架提供了几套不同的接口,允许开发者简单高效地访问 SQLite 数据库。本章我们将关注最基本的那套接口。SQLite 的默认接口是SQL,不过一个好消息是 Android 提供了更高层的封装来简化开发者的工作。
Note:
Android 内建了 SQLite 支持, 但这并不是说 SQLite 是数据持久化的唯一选择。 你仍可以使用其他数据库系统,比如 JavaDB 或者 MongoDB,但这样就无法利用 Android内建的数据库支持了,而且必需将它们打包到程序中一起发布才行。另外,SQLite的定位不是重量级的数据库服务器,而是作为自定义数据文件的替代品。
9.2. DbHelper 类
针对SQLite数据库的相关操作,Android提供了一套优雅的接口。要访问数据库,你需要一个辅助类来获取数据库的“连接”,或者在必要时创建数据库连接。这个类就是 Android框架中的SQLiteOpenHelper
,它可以返回一个 SQLiteDatabase
对象。
我们将在后面的几节中介绍DbHelper相关的一些注意事项。至于SQL以及数据库的基本常识(比如规范化)则不打算涉及了,毕竟这些知识很容易就可以在别处学到,而且我相信多数读者也都是有一定基础的。尽管如此,即使读者没有数据库的相关基础,依然不妨碍对本章的理解。
9.2.1. 数据库原型及其创建
数据库原型(schema)是对数据库结构的描述。 在Yamba的数据库中,我们希望储存从Twitter获取的数据的以下字段:
created_at
消息的发送时间
txt
文本内容
user
消息的作者
表中的每一行数据对应一条Twitter消息,以上四项就是我们要在原型中定义的数据列了。另外,我们还需要给每条消息定义一个唯一的ID,以方便特定消息的查找。同其它数据库相同,SQLite也允许我们将ID定义为主键并设置自增属性,保持键值唯一。
数据库原型是在程序启动时创建的,因此我们在DbHelper
的onCreate()
方法中完成此项工作。在以后的迭代中,我们可能需要添加新列或者修改旧列,为方便版本控制,我们将为每个原型添加一个版本号,以及一个方法onUpgrade()
,用以修改数据库的原型。
onCreate()
和onUpgrade
两个方法就是我们应用程序中唯一需要用到SQL的地方了。在onCreate()
方法中,需要执行CREATE TABLE
来创建表;在onUpgrade()
方法中,需要执行ALTER TABLE
来修改原型,不过这里为方便起见,直接使用DROP TABLE
删除表然后再重新创建它。当然,DROP TABLE
会毁掉表中所有的数据,但在Yamba中这样做完全没有问题,因为用户一般只关心过去24小时的消息,即使丢失了,仍可以重新抓取回来。
9.2.2. 四种主要操作
DbHelper类提供了自己的封装来简化SQL操作。经观察人们发现,绝大多数的数据库操作不外乎只有四种,也就是添加(Create)、查询(Query)、修改(Update)、删除(Delete),简称为 CRUD 。为满足这些需求,DbHelper
提供了以下方法:
insert()
向数据库中插入一行或者多行
query()
查询符合条件的行
update()
更新符合条件的行
delete()
删除符合条件的行
以上的每个方法都有若干变种(译者注:比如insertOrThrow),分别提供不同的功能。要调用上面的方法,我们需要创建一个ContentValues对象作为容器,将关心的数据暂存到里面。本章将以插入操作为例,讲解数据库操作的基本过程,而其它操作一般都是大同小异的。
那么,为什么不直接使用 SQL 呢?有三个主要的原因:
首先从安全角度考虑,直接使用 SQL 语句很容易导致 SQL 注入攻击。 这是因为 SQL 语句中会包含用户的输入, 而用户的输入都是不可信任的,不加检查地构造 SQL 语句的话,很容易导致安全漏洞。
其次从性能的角度,重复执行SQL语句非常耗时,因为每次执行都需要对其进行解析。
最后,使用 DbHelper
有助于提高程序的健壮性,使得许多编程错误可以在编译时发现。若是使用 SQL,这些错误一般得到运行时才能被发现。
很遗憾,Android框架对SQL的DDL(Data Definition Language,数据定义语言)部分支持不多,缺少相应的封装。因此要创建表,我们只能通过execSQL()
调用来运行CREATE TABLE
之类的SQL语句。但这里不存在用户输入,也就没有安全问题;而且这些代码都很少执行,因此也不会对性能造成影响。
9.2.3. Cursor
查询得到的数据将按照Cursor(游标)的形式返回。通过Cursor,你可以读出得到的第一行数据并移向下一行,直到遍历完毕、返回空为止。也可以在数据集中自由移动,读取所得数据的任意一行。
一般而言,SQL的相关操作都存在触发SQLException
异常的可能,这是因为数据库不在我们代码的直接控制范围内。比如数据库的存储空间用完了,或者执行过程被意外中断等等,对我们程序来说都属于不可预知的错误。因此好的做法是,将数据库的有关操作统统放在try/catch
中间,捕获SQLException
异常。
这里可以利用Eclipse的快捷功能:
选择需要处理异常的代码段,一般就是 SQL 操作相关的地方。
在菜单中选择 “Source→Surround With→Try/catch Block”
,Eclipse 即可自动生成合适的 try/catch
语句。
在 catch
块中添加异常处理的相关代码。这里可以调用 Log.e()
记录一条日志, 它的参数可以是一个标记、消息或者异常对象本身。
9.3. 第一个例子
接下来我们将创建自己的辅助类DbHelper
,用以操作数据库。当数据库不存在时,它负责创建数据库;当数据库原型变化时,它负责数据库的更新。
同前面的很多类一样,它也是继承自Android框架中的某个类,也就是SQLiteOpenHelper
。我们需要实现该类的构造函数,onCreate()
方法和onUpgrade()
方法。
例 9.1. DbHelper.java, version 1
package com.marakana.yamba4;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;
import android.util.Log;
public class DbHelper1 extends SQLiteOpenHelper { //
static final String TAG = "DbHelper";
static final String DB_NAME = "timeline.db"; //
static final int DB_VERSION = 1; //
static final String TABLE = "timeline"; //
static final String C_ID = BaseColumns._ID;
static final String C_CREATED_AT = "created_at";
static final String C_SOURCE = "source";
static final String C_TEXT = "txt";
static final String C_USER = "user";
Context context;
// Constructor
public DbHelper1(Context context) { //
super(context, DB_NAME, null, DB_VERSION);
this.context = context;
}
// Called only once, first time the DB is created
@Override
public void onCreate(SQLiteDatabase db) {
String sql = "create table " + TABLE + " (" + C_ID + " int primary key, "
+ C_CREATED_AT + " int, " + C_USER + " text, " + C_TEXT + " text)"; //
db.execSQL(sql); //
Log.d(TAG, "onCreated sql: " + sql);
}
// Called whenever newVersion != oldVersion
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { //
// Typically do ALTER TABLE statements, but...we're just in development,
// so:
db.execSQL("drop table if exists " + TABLE); // drops the old database
Log.d(TAG, "onUpgraded");
onCreate(db); // run onCreate to get new database
}
}
将SQLiteOpenHelper
作为基类。
数据库文件名。
为数据库保留一个版本号。这在需要修改原型时将会十分有用,使得用户可以平滑地升级数据库。
定义数据库相关的一些常量,方便在其它类中引用它们。
覆盖SQLiteOpenHelper的构造函数。将前面定义的几个常量传递给父类,并保留对context
的引用。
拼接将要传递给数据库的SQL语句。
在onCreate()
中可以得到一个数据库对象,调用它的execSQL()
方法执行SQL语句。
onUpgrade()
在数据库版本与当前版本号不匹配时调用。它负责修改数据库的原型,使得数据库随着程序的升级而更新。
Note:
早些时候曾提到, onUpgrade()
中一般都是执行ALERT TABLE
语句。不过现在我们的程序还没有发布,也就没有升级可言。因此便直接删除了旧的数据表并重建。
接下来我们将重构原先的Service,使之能够打开数据库,并将服务端得到的数据写入进去。
9.4. 重构 UpdaterService
UpdaterService
是负责连接到服务端并抓取数据的Service,因此由它负责将数据写入数据库是合理的。
接下来重构UpdaterService
,为它添加写入数据库的相关代码。
例 9.2. UpdaterService.java, version 1
package com.marakana.yamba4;
import java.util.List;
import winterwell.jtwitter.Twitter;
import winterwell.jtwitter.TwitterException;
import android.app.Service;
import android.content.ContentValues;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import android.os.IBinder;
import android.util.Log;
public class UpdaterService1 extends Service {
private static final String TAG = "UpdaterService";
static final int DELAY = 60000; // wait a minute
private boolean runFlag = false;
private Updater updater;
private YambaApplication yamba;
DbHelper1 dbHelper; //
SQLiteDatabase db;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
this.yamba = (YambaApplication) getApplication();
this.updater = new Updater();
dbHelper = new DbHelper1(this); //
Log.d(TAG, "onCreated");
}
@Override
public int onStartCommand(Intent intent, int flag, int startId) {
if (!runFlag) {
this.runFlag = true;
this.updater.start();
((YambaApplication) super.getApplication()).setServiceRunning(true);
Log.d(TAG, "onStarted");
}
return Service.START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
this.runFlag = false;
this.updater.interrupt();
this.updater = null;
this.yamba.setServiceRunning(false);
Log.d(TAG, "onDestroyed");
}
/**
* Thread that performs the actual update from the online service
*/
private class Updater extends Thread {
List timeline;
public Updater() {
super("UpdaterService-Updater");
}
@Override
public void run() {
UpdaterService1 updaterService = UpdaterService1.this;
while (updaterService.runFlag) {
Log.d(TAG, "Updater running");
try {
// Get the timeline from the cloud
try {
timeline = yamba.getTwitter().getFriendsTimeline(); //
} catch (TwitterException e) {
Log.e(TAG, "Failed to connect to twitter service", e);
}
// Open the database for writing
db = dbHelper.getWritableDatabase(); //
// Loop over the timeline and print it out
ContentValues values = new ContentValues(); //
for (Twitter.Status status : timeline) { //
// Insert into database
values.clear(); //
values.put(DbHelper1.C_ID, status.id);
values.put(DbHelper1.C_CREATED_AT, status.createdAt.getTime());
values.put(DbHelper1.C_SOURCE, status.source);
values.put(DbHelper1.C_TEXT, status.text);
values.put(DbHelper1.C_USER, status.user.name);
db.insertOrThrow(DbHelper1.TABLE, null, values); //
Log.d(TAG, String.format("%s: %s", status.user.name, status.text));
}
// Close the database
db.close(); //
Log.d(TAG, "Updater ran");
Thread.sleep(DELAY);
} catch (InterruptedException e) {
updaterService.runFlag = false;
}
}
}
} // Updater
}
我们需要多次用到db
和dbHelper
两个对象,因此把它们定义为类的成员变量。
Android中的Service
本身就是Context
的子类,因此可以通过this
创建DbHelper
的实例。DbHelper
可在需要时创建或者升级数据库。
连接到服务端并获取最新的Timeline,插入数据库。 先通过YambaApplication
中的 getTwitter()
获取Twitter对象,然后使用 getFriendsTimeline()
调用 Twitter API 获取24小时内最新的20条消息。
获取写入模式的数据库对象。在第一次调用时,将触发 DbHelper
的onCreate()
方法。
ContentValues
是一种简单的键值对结构,用以保存字段到数据的映射。
遍历所得的所有数据。这里使用了 Java 的 for-each 语句来简化迭代过程。
为每条记录分别生成一个ContentValues
。这里我们重用了同一个对象:在每次迭代中,都反复清空它,然后绑定新的值。
调用 insert()
将数据插入数据库。留意在这里我们并没有使用任何SQL语句,而是使用了既有的封装。
最后不要忘记关闭数据库。这很重要,不然会无谓地浪费资源。
一切就绪,准备测试。
9.4.1. 测试
到这里,我们将测试数据库是否创建成功、能否正常写入数据。一步步来。
9.4.1.1. 验证数据库是否创建成功
数据库若创建成功,你就可以在/data/data/com.marakana.yamba/databases/timeline.db
找到它。 要验证它是否存在,你可以使用Eclipse中DDMS的File Explorer界面,也可以在命令行的 adb shell 中执行命令 ls /data/data/com.marakana.yamba/databases/timeline.db
。
要使用 Eclipse 的 File Explorer,点击右上角的 DDMS
或者选择 Windows→Show View→Other…→Android→File Explorer
。 随后就可以查看目标设备的文件系统了。
到这里我们已确认数据库文件存在,但仍不能确定数据库的原型是否正确,这将放在下一节。
9.4.1.2. 使用 sqlite3
Android 附带了一个命令行工具 sqlite3
,供我们访问数据库。
要验证数据库原型是否正确,你需要:
打开终端或者命令提示符。
输入 adb shell
,连接到仿真器或者真机。
切换到数据库所在的目录: cd /data/data/com.marakana.yamba/databases/
。
通过 sqlite3 timeline.db
打开数据库。
打开数据库之后,你可以见到一个提示符 sqlite>
:
[user:~]> adb shell
# cd /data/data/com.marakana.yamba/databases/
# ls
timeline.db
# sqlite3 timeline.db
SQLite version 3.6.22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite>
在这里,你可以使用两种命令来操作SQLite数据库:
标准 SQL 命令。比如 insert ...
, update ...
, delete ...
, select ...
以及 create table ...
, alter table ...
等等。SQL 是一门完整的语言,本书不打算多做涉及。需要注意的是,在sqlite3中你需要使用半角分号 ;
表示语句结束。
sqlite3
命令。它们都是SQLite特有的,输入.help
可以看到这些命令的列表。在这里,我们需要的是通过.schema
命令来查看数据库的原型是否正确。
# sqlite3 timeline.db
SQLite version 3.6.22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .schema
CREATE TABLE android_metadata (locale TEXT);
CREATE TABLE timeline ( _id integer primary key,created_at integer, source text, txt text, user text );
最后一行输出显示,表中的字段有_id
,created_at
,source
,txt
和user
。可知表的原型是正确的。
Warning:
新人很容易犯一个错误,那就是在错误的目录下执行sqlite3 timeline.db
命令,然后就会发现表没有创建。SQLite不会提示你正在使用一个不存在的文件,而是直接创建它。所以务必在正确的目录下(/data/data/com.marakana.yamba/databases)执行命令,或者使用绝对路径:sqlite3 /data/data/com.marakana.yamba/databases/timeline.db
。
现在已经可以创建并打开数据库了。接下来继续重构UpdaterService,为它添加插入数据到数据库的相关代码。
要验证数据是否插入成功,也是同样使用sqlite3
命令,具体过程与上面相似,兹不赘述。
9.4.2. 数据库约束
再次运行这个Service,你会发现它执行失败,而在logcat中得到许多SQLException
。而这都是数据库约束(database constraint)抛出的异常。
这是因为我们插入了重复的ID。前面从服务端抓取消息数据时,获得了消息的ID字段,并作为主键一并插入本地数据库。但是我们每分钟都会通过getFriendsTimeline()
重新抓取最近24小时的20条消息,因此除非你的朋友在一个分钟里发了超过20条消息,那么插入的_id就肯定会发生重复。而_id作为主键,又是不允许重复的。这样在插入数据时,就违反了数据库的约束,于是抛出SQLException
。
因此在插入时,应首先检查是否存在重复数据,但我们不想在代码中添加额外的逻辑,数据库的事情交给数据库解决就足够了。解决方案是:原样插入,然后忽略可能发生的异常就可以了。
为此,我们把db.insert()
改成db.insertOrThrow()
,然后捕获并忽略SQLException
。
...
try {
db.insertOrThrow(DbHelper.TABLE, null, values); //①
Log.d(TAG, String.format("%s: %s", status.user.name, status.text));
} catch (SQLException e) { //②
// Ignore exception
}
...
当插入操作失败时,代码会抛出异常。
捕获这一异常,并简单忽略。这将留在后面一节做进一步改进。
到这里代码已经能够正常工作,但仍不够理想。重构的机会又来了。
9.5. 重构数据库访问
前面我们重构了UpdaterService
,使它能够访问数据库。但这对整个程序来讲仍不理想,因为程序的其它部分可能也需要访问数据库,比如TimelineActivity
。因此好的做法是,将数据库的相关代码独立出来,供UpdaterService与TimelineActivity重用。
为实现代码的重用,我们将创建一个新类StatusData
,用以处理数据库的相关操作。它将SQLite操作封装起来,并提供接口,供Yamba中的其它类调用。这一来Yamba中的构件若需要数据,那就访问StatusData即可,而不必纠结于数据库操作的细节。在第十二章中,我们还将基于这一设计,提供ContentProvider的实现。
例 9.3. StatusData.java
package com.marakana.yamba4;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class StatusData { //
private static final String TAG = StatusData.class.getSimpleName();
static final int VERSION = 1;
static final String DATABASE = "timeline.db";
static final String TABLE = "timeline";
public static final String C_ID = "_id";
public static final String C_CREATED_AT = "created_at";
public static final String C_TEXT = "txt";
public static final String C_USER = "user";
private static final String GET_ALL_ORDER_BY = C_CREATED_AT + " DESC";
private static final String[] MAX_CREATED_AT_COLUMNS = { "max("
+ StatusData.C_CREATED_AT + ")" };
private static final String[] DB_TEXT_COLUMNS = { C_TEXT };
// DbHelper implementations
class DbHelper extends SQLiteOpenHelper {
public DbHelper(Context context) {
super(context, DATABASE, null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.i(TAG, "Creating database: " + DATABASE);
db.execSQL("create table " + TABLE + " (" + C_ID + " int primary key, "
+ C_CREATED_AT + " int, " + C_USER + " text, " + C_TEXT + " text)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("drop table " + TABLE);
this.onCreate(db);
}
}
private final DbHelper dbHelper; //
public StatusData(Context context) { //
this.dbHelper = new DbHelper(context);
Log.i(TAG, "Initialized data");
}
public void close() { //
this.dbHelper.close();
}
public void insertOrIgnore(ContentValues values) { //
Log.d(TAG, "insertOrIgnore on " + values);
SQLiteDatabase db = this.dbHelper.getWritableDatabase(); //
try {
db.insertWithOnConflict(TABLE, null, values,
SQLiteDatabase.CONFLICT_IGNORE); //
} finally {
db.close(); //
}
}
/**
*
* @return Cursor where the columns are _id, created_at, user, txt
*/
public Cursor getStatusUpdates() { //
SQLiteDatabase db = this.dbHelper.getReadableDatabase();
return db.query(TABLE, null, null, null, null, null, GET_ALL_ORDER_BY);
}
/**
*
* @return Timestamp of the latest status we ahve it the database
*/
public long getLatestStatusCreatedAtTime() { //
SQLiteDatabase db = this.dbHelper.getReadableDatabase();
try {
Cursor cursor = db.query(TABLE, MAX_CREATED_AT_COLUMNS, null, null, null,
null, null);
try {
return cursor.moveToNext() ? cursor.getLong(0) : Long.MIN_VALUE;
} finally {
cursor.close();
}
} finally {
db.close();
}
}
/**
*
* @param id of the status we are looking for
* @return Text of the status
*/
public String getStatusTextById(long id) { //
SQLiteDatabase db = this.dbHelper.getReadableDatabase();
try {
Cursor cursor = db.query(TABLE, DB_TEXT_COLUMNS, C_ID + "=" + id, null,
null, null, null);
try {
return cursor.moveToNext() ? cursor.getString(0) : null;
} finally {
cursor.close();
}
} finally {
db.close();
}
}
}
StatusData 中的多数代码都是来自原先的 DbHelper.java 。现在 DbHelper 已经成为了 StatusData 的内部类。 仅供 StatusData 内部使用。 这意味着,在 StatusData 之外的调用者不必知道 StatusData 中的数据是以什么方式存储在什么地方。这样有助于使得系统更加灵活,而这也正是后面引入的ContentProvider的基本思想。
声明一个不可变的类成员dbHelper。final关键字用于保证某类成员只会被赋值一次,
在构造函数中初始化DbHelper的实例。
将dbHelper的close()
方法暴露出去,允许他人关闭数据库。
我们对DbHelper中db.insert...()
方法的改进。
仅在必要时打开数据库,也就是在写入之前。
在这里,我们调用insertWithOnConflict()
,并将SQLiteDatabase.CONFLICT_IGNORE
作为最后一个参数,表示如果有数据重复,则忽略异常。原因在前文的“数据库约束”一节已有说明。
不要忘记关闭数据库。我们把它放在finally子句中,这就可以保证无论出现何种错误,数据库都可以正确地关闭。同样的风格还可以在getLatestStatusCreatedAtTime()
和getStatusTextById()
中见到。
返回数据库中的所有消息数据,按时间排序。
getLatestStatusCreatedAtTime()
返回表中最新一条消息数据的时间戳(timestamp)。通过它,我们可以得知当前已存储的最新消息的日期,在插入新消息时可作为过滤的条件。
getStatusTextById()
返回某一条消息数据的文本内容。
到这里,数据库的相关操作已都独立到了StatusData中。接下来把它放到Application
对象里面,这一来即可令其它构件(比如UpdaterService
与TimelineActivity
)与StatusData建立起has-a关系,方便访问。
例 9.4. YambaApplication.java
...
private StatusData statusData; //
...
public StatusData getStatusData() { //
return statusData;
}
// Connects to the online service and puts the latest statuses into DB.
// Returns the count of new statuses
public synchronized int fetchStatusUpdates() { //
Log.d(TAG, "Fetching status updates");
Twitter twitter = this.getTwitter();
if (twitter == null) {
Log.d(TAG, "Twitter connection info not initialized");
return 0;
}
try {
List statusUpdates = twitter.getFriendsTimeline();
long latestStatusCreatedAtTime = this.getStatusData()
.getLatestStatusCreatedAtTime();
int count = 0;
ContentValues values = new ContentValues();
for (Status status : statusUpdates) {
values.put(StatusData.C_ID, status.getId());
long createdAt = status.getCreatedAt().getTime();
values.put(StatusData.C_CREATED_AT, createdAt);
values.put(StatusData.C_TEXT, status.getText());
values.put(StatusData.C_USER, status.getUser().getName());
Log.d(TAG, "Got update with id " + status.getId() + ". Saving");
this.getStatusData().insertOrIgnore(values);
if (latestStatusCreatedAtTime < createdAt) {
count++;
}
}
Log.d(TAG, count > 0 ? "Got " + count + " status updates"
: "No new status updates");
return count;
} catch (RuntimeException e) {
Log.e(TAG, "Failed to fetch status updates", e);
return 0;
}
}
...
在YambaApplication
中添加私有成员StatusData
。
其它部分若要访问它,只有通过这个方法。
这里的代码几乎都是来自原先的UpdaterService
。它将运行在独立的线程中,连接到服务端抓取数据,并保存到数据库里面。
接下来可以利用新的YambaApplication
,重构原先的UpdaterService
。现在run()
中的代码已都移到了fetchStatusUpdates()
中,而且UpdaterService
也不再需要内部的StatusData
对象了。
例 9.5. UpdaterService.java
...
private class Updater extends Thread {
public Updater() {
super("UpdaterService-Updater");
}
@Override
public void run() {
UpdaterService updaterService = UpdaterService.this;
while (updaterService.runFlag) {
Log.d(TAG, "Running background thread");
try {
YambaApplication yamba = (YambaApplication) updaterService
.getApplication(); //
int newUpdates = yamba.fetchStatusUpdates(); //
if (newUpdates > 0) { //
Log.d(TAG, "We have a new status");
}
Thread.sleep(DELAY);
} catch (InterruptedException e) {
updaterService.runFlag = false;
}
}
}
} // Updater
...
从Service对象中可以获取YambaApplication
的实例。
调用刚才新加入的 fetchStatusUpdates()
方法,功能与原先的 run()
基本一致。
fetchStatusUpdates()
以获取的消息数量作为返回值。暂时我们只利用这个值来调试,不过后面将有其它作用。
9.6. 总结
到这里,Yamba已经可以从服务端抓取数据,并储存到数据库中了。虽然仍不能将它们显示出来,但已经可以验证,这些数据是可用的。
下图展示了目前为止我们已经完成的部分。完整图参见 图5.4 "Yamba设计图" 。
图 9.1. Yamba完成图
10. List与Adapter
在本章,你将学到选择性控件(比如ListView
)的创建方法。但是这里讨论的重点绝对不在用户界面,而在于进一步巩固我们在上一章中对“数据”的理解——前面是简单地读取数据再输出到屏幕,到这里改为使用Adapter直接将数据库与List绑定在一起。你可以创建一个自己的Adapter,从而添加额外的功能。在此,我们新建一个 Activity ,并将它作为用户发送/阅读消息的主界面。
前面我们已经实现了发送消息、从 Twitter 读取消息、缓存入本地数据库等功能,在这里,我们再实现一个美观高效的UI允许用户阅读消息。到本章结束,Yamba将拥有三个 Activity 和一个 Service 。
10.1. TimelineActivity
接下来我们新建一个Activity,即TimelineActivity
,用以显示朋友的消息。它从数据库中读取消息,并显示在屏幕上。在一开始我们的数据库并不大,但是随着应用使用时间的增长,其中的数据量就不可遏制了。我们必须针对这个问题做些考虑。
我们将创建这一Activity分成两步。保证经过每一轮迭代,应用都是完整可用的。
第一次迭代:使用一个TextView显示数据库中的所有数据,由于数据量可能会比较大,我们将它置于ScrollView中,加一个滚动条。
第二次迭代:改用ListView与Adapter,这样伸缩性更好,效率也更高。你将在这里了解到Adapter与List的工作方式。
最后创建一个自定义的Adapter,在里面添加些额外的业务逻辑。这需要深入Adapter的内部,你可以体会它的设计动机与应用方式。
10.2. TimelineActivity的基本布局
在第一次迭代中,我们为TimelineActivity创建一个新的布局(Layout),它使用一个TextView来展示数据库中的所有消息。在刚开始的时候数据量不大,这样还是没有问题的。
10.2.1. 简介ScrollView
不过数据一多,我们就不能保证所有的消息正好排满一页了,这时应使用'ScrollView'
使之可以滚动。ScrollView与Window相似,不过它可以在必要时提供一个滚动条,从而允许在里面存放超过一屏的内容。对付可能会变大的View,用ScrollView把它包起来就行。比如这里我们靠TextView输出所有朋友的消息,消息一多,它也跟着变大。在较小的屏幕中显示不开,就把它放在ScrollView里使之可以滚动。
ScrollView中只能存放单独一个子元素。要让多个View一起滚动,就需要像前面StatusActivity Layout
一节所做的那样,先把它们放在另一个Layout里,随后把整个Layout添加到ScrollView里。
通常你会希望ScrollView能够填满屏幕的所有可用空间。把它的高度与宽度都设置为fill_parent
即可。
一般很少通过Java代码控制ScrollView,因此它不需要id。
在这个例子中,我们使用ScrollView将TextView包了起来。以后TextView中的内容变多体积变大,ScrollView就会自动为它加上滚动条。
例 10.1. res/layout/timeline_basic.xml
在Activity屏幕顶部显示的标题。留意字符串titleTimeline
的定义在/res/values/strings.xml
文件中,具体参见第六章的 字符串资源 一节。
ScrollView包含TextView,在需要时添加滚动条。
TextView用以显示文本,在这里就是从数据库中读取的用户消息。
10.2.2. 创建TimelineActivity类
我们已经有了一个布局文件,接下来创建 TimelineActivity
类。同其它文件一样,进入 Eclipse Package Explorer,右击 com.marakana.yamba 包,选择New→Class
,在名字一栏输入 TimelineActivity 。
同前面一样,我们创建的类只要是基本构件,无论是 Activity、Service、Broadcast Reciever 还是 Content Provider,都是基于 Android 框架中的基类继承出来的子类。对Activity而言,其基类即 Activity
。
我们通常会为每个 Activity 实现一个 onCreate()
方法 ,这是初始化数据库的好地方。与之对应,我们在 onDestroy()
方法 中清理 onCreate()
中创建的资源,也就是关闭数据库。我们希望显示的消息尽可能最新,因此把查询数据库的代码放在 onResume()
方法中,这样在界面每次显示时都会执行。代码如下:
package com.marakana.yamba5;
import android.app.Activity;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.widget.TextView;
public class TimelineActivity1 extends Activity { //
DbHelper dbHelper;
SQLiteDatabase db;
Cursor cursor;
TextView textTimeline;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.timeline);
// Find your views
textTimeline = (TextView) findViewById(R.id.textTimeline);
// Connect to database
dbHelper = new DbHelper(this); //
db = dbHelper.getReadableDatabase(); //
}
@Override
public void onDestroy() {
super.onDestroy();
// Close the database
db.close(); //
}
@Override
protected void onResume() {
super.onResume();
// Get the data from the database
cursor = db.query(DbHelper.TABLE, null, null, null, null, null,
DbHelper.C_CREATED_AT + " DESC"); //
startManagingCursor(cursor); //
// Iterate over all the data and print it out
String user, text, output;
while (cursor.moveToNext()) { //
user = cursor.getString(cursor.getColumnIndex(DbHelper.C_USER)); //
text = cursor.getString(cursor.getColumnIndex(DbHelper.C_TEXT));
output = String.format("%s: %s\n", user, text); //
textTimeline.append(output); //
}
}
}
这是个Activity,因此继承自Android框架提供的Activity
类。
我们需要访问数据库以得到Timeline的相关数据,而onCreate()
正是连接数据库的好地方。
通过dbHelper
打开数据库文件之后,我们需要它的getReadableDatabase()
或getWritableDatabase()
才可以得到实际的数据库对象。在这里我们只需读取Timeline的相关数据,因此按只读方式打开数据库。
我们得记着关闭数据库并释放资源。数据库是在onCreate()
中打开,因此可以在onDestroy()
中关闭。注意onDestroy()
只有在系统清理资源时才被调用。
调用query()
方法从数据库中查询数据,返回一个Cursor对象作为迭代器。参数似乎多的过了头,不过几乎都是对应着SQL的SELECT语句的各个部分。在这里也就相当与SELECT * FROM timeline ORDER BY created_at DESC
。这些null
表示我们并没有使用SQL语句中相应的部分,比如WHERE
, GROUPING
, 与 HAVING
等。
startManagingCursor()
用于提示Activity自动管理Cursor的生命周期,使之同自己保持一致。“保持一致”的意思是,它能保证在Activity销毁时,同时释放掉Cursor关联的数据,这样有助于优化垃圾收集的性能。如果没有自动管理,那就只能在各个方法中添加代码,手工地管理Cursor了。
cursor——回忆下Cursors
一节的内容——即通过query()
方法查询数据库所得的结果。按照表的形式,暂存多行多列的数据。每一行都是一条独立的记录——比如Timeline中的一条消息——而其中的列则都是预先定义的,比如 _id
, created_at
, user
以及txt
。前面提到Cursor是个迭代器,我们可以通过它在每次迭代中读取一条记录,而在没有剩余数据时退出迭代。
对于 cursor 的当前记录,我们可以通过类型与列号来获取其中的值。由此,cursor.getString(3)
返回一个字符串,表示消息的内容; cursor.getLong(1)
返回一个数值,表示消息创建时的时间戳。但是,把列号硬编码在代码中不是个好习惯,因为数据库的原型一旦有变化,我们就不得不手工调整相关的代码,而且可读性也不好。更好的方法是,使用列名——回想下,我们曾在 第九章 中定义过 C_USER
与 C_TEXT
等字符串——调用 cursor.getColumnIndex()
得到列号,然后再取其中的值。
使用 String.format()
对每行输出进行格式化。在这里我们选择使用TextView控件显示数据,因此只能显示文本,或者说有格式的文本。我们在以后的迭代中更新这里。
最后把新的一行追加到textTimeline的文本中,用户就可以看到了。
上面的方法对较小的数据集还没问题,但绝对不是值得推荐的好方法。更好的方法是使用 ListView ,正如下文所示——它可以绑定数据库,伸缩性更好的同时也更加高效。
10.3. 关于Adapter
一个ScrollView足以应付几十条记录,但是数据库里要有上百上千条记录时怎么办?全部读取并输出当然是很低效的。更何况,用户不一定关心所有的数据。
针对这一问题,Android提供了Adapter,从而允许开发者为一个View绑定一个数据源(如图10.1, "Adapter" )。典型的情景是,View即ListView,数据即Cursor或者数组(Array),Adapter也就与之对应:CursorAdapter或者ArrayAdapter。
图 10.1. Adapter
10.3.1. 为TimelineActivity添加ListView
同前面一样,第一步仍是修改资源文件。修改timeline.xml
,为Timeline的布局添加一个ListView。
例 10.3. res/layout/timeline.xml
添加ListView与添加其它控件(widget)是一样的。主要的属性: id
,layout_height
以及layout_width
。
10.3.2. ListView vs. ListActivity
ListActivity
就是含有一个ListView
的Activty,我们完全可以拿它作为TimelineActivity
的基类。在此我们独立实现自己的Activity,再给它加上ListView
,是出于便于学习的考虑。
如果Activity里只有一个ListView `` ,使用
ListActivity可以稍稍简化下代码。有XML绑定,为
ListActivity添加元素也轻而易举。不过,在这里使用的数据源是Cursor而不是数组(因为我们的数据来自数据库),先前也曾添加过一个
TextView作为
ScrollView的标题——已经有了一定程度的自定义,再换用
ListActivity``就前功尽弃了。
10.3.3. 为单行数据创建一个Layout
还有一个XML文件需要考虑。timeline.xml
描述整个Activity的布局,我们也需要描述单行数据的显示方式——也就是屏幕上显示的单条消息,谁在什么时间说了什么。
最简单的方法是给这些行单独创建一个XML文件。同前面新建的XML文件一样,选择File→New→Android New XML File打开Android New XML File对话框,命名为row.xml,type一项选择Layout。
我们在这里选择LinearLayout
,让它垂直布局,分两行。第一行包含用户名与时间戳,第二行包含消息的内容。留意第一行中用户名与时间戳的位置是水平分布的。
ListView中单行数据的Layout定义在文件row.xml
中。
10.3.4. 在TimelineActivity.java中创建一个Adapter
已经有了相应的XML文件,接下来修改Java代码,把Adapter创建出来。Adapter通常有两种形式:一种基于数组(Array),一种基于Cursor。这里我们的数据来自数据库,因此选择基于Cursor的Adapter。而其中最简单的又数SimpleCursorAdapter
。
SimpleCursorAdapter
需要我们给出单行数据的显示方式(这在row.xml中已经做好了)、数据(在这里是一个Cursor)、以及record到List中单行的映射方法。最后的这个参数负责将Cursor的每列映射为List中的一个View。
例 10.5. TimelineActivity.java, version 2
package com.marakana.yamba5;
import android.app.Activity;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
public class TimelineActivity2 extends Activity {
DbHelper dbHelper;
SQLiteDatabase db;
Cursor cursor; //
ListView listTimeline; //
SimpleCursorAdapter adapter; //
static final String[] FROM = { DbHelper.C_CREATED_AT, DbHelper.C_USER,
DbHelper.C_TEXT }; //
static final int[] TO = { R.id.textCreatedAt, R.id.textUser, R.id.textText }; //
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.timeline);
// Find your views
listTimeline = (ListView) findViewById(R.id.listTimeline); //
// Connect to database
dbHelper = new DbHelper(this);
db = dbHelper.getReadableDatabase();
}
@Override
public void onDestroy() {
super.onDestroy();
// Close the database
db.close();
}
@Override
protected void onResume() {
super.onResume();
// Get the data from the database
cursor = db.query(DbHelper.TABLE, null, null, null, null, null,
DbHelper.C_CREATED_AT + " DESC");
startManagingCursor(cursor);
// Setup the adapter
adapter = new SimpleCursorAdapter(this, R.layout.row, cursor, FROM, TO); //
listTimeline.setAdapter(adapter); //
}
}
这个Cursor用以读取数据库中的朋友消息。
listTimeLine
即我们用以显示数据的ListView。
adapter
是我们自定义的Adapter,这在后文中讲解。
FROM
是个字符串数组,用以指明我们需要的数据列。其内容与我们前面引用数据列时使用的字符串相同。
TO
是个整型数组,对应布局row.xml中指明的View的ID,用以指明数据的绑定对象。FROM与TO的各个元素必须一一对应,比如FROM[0]
对应TO[0]
,FROM[1]
对应TO[1]
,以此类推。
获取XML布局中声明的ListView。
获得了Cursor形式的数据,row.xml
定义了单行消息的布局,常量FROM与TO表示了映射关系,现在可以创建一个SimpleCursorAdapter
。
最后通知ListView使用这个Adapter。
10.4. TimelineAdapter
TimelineAdapter就是我们自定义的Adapter。虽说SimpleCursorAdapter映射起数据来倒也直白,但是我们在这里还有个Unix时间戳(timestamp)需要特殊处理。这也属于TimelineAdapter的工作:加入业务逻辑,将Unix时间戳转换为相对时间。可知SimpleCursorAdapter
是在bindView()
里面处理View的显示,因此覆盖这个方法,在数据显示之前处理一下即可。
一般来讲,若不清楚类里需要覆盖的方法是哪个,查阅相关的官方文档即可。在这里,可以参阅http://developer.android.com/reference/android/widget/SimpleCursorAdapter.html。
例 10.6. TimelineAdapter.java
package com.marakana.yamba5;
import android.content.Context;
import android.database.Cursor;
import android.text.format.DateUtils;
import android.view.View;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;
public class TimelineAdapter extends SimpleCursorAdapter { //
static final String[] FROM = { DbHelper.C_CREATED_AT, DbHelper.C_USER,
DbHelper.C_TEXT }; //
static final int[] TO = { R.id.textCreatedAt, R.id.textUser, R.id.textText }; //
// Constructor
public TimelineAdapter(Context context, Cursor c) { //
super(context, R.layout.row, c, FROM, TO);
}
// This is where the actual binding of a cursor to view happens
@Override
public void bindView(View row, Context context, Cursor cursor) { //
super.bindView(row, context, cursor);
// Manually bind created at timestamp to its view
long timestamp = cursor.getLong(cursor
.getColumnIndex(DbHelper.C_CREATED_AT)); //
TextView textCreatedAt = (TextView) row.findViewById(R.id.textCreatedAt); //
textCreatedAt.setText(DateUtils.getRelativeTimeSpanString(timestamp)); //
}
}
创建我们自定义的 Adapter,它继承了 Android 提供的 Adapter 类,这里将它命名为 SimpleCursorAdapter 。
跟上个例子一样,这个常量用以指明数据库中我们感兴趣的列。
这个常量用以指明数据列对应View的ID。
因为是新定义的类,因此需要一个构造函数。在这里仅仅通过super调用父类的构造函数即可。
这里只覆盖了一个方法,即bindView()
。这个方法在映射每行数据到View时调用,Adapter的主要工作在这里进行了。要重用SimpleCursorAdapter原有的映射操作(数据到View),记得先调用super.bindView()。
覆盖默认针对timestamp的映射操作,需要先得到数据库中timestamp的值。
然后找到对应的TextView,它的定义在row.xml
。
最后,依据timestamp的值设置textCreatedAt
的值为相对时间。通过DateUtils.getRelativeTimeSpanString()
。
前面将Adapter相关的一些细节移入了TimelineAdapter
,因此可以进一步简化TimelineActivity
类。
例 10.7. TimelineActivity.java, version 3
package com.marakana.yamba5;
import android.app.Activity;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.widget.ListView;
public class TimelineActivity3 extends Activity {
DbHelper dbHelper;
SQLiteDatabase db;
Cursor cursor;
ListView listTimeline;
TimelineAdapter adapter; //
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.timeline);
// Find your views
listTimeline = (ListView) findViewById(R.id.listTimeline);
// Connect to database
dbHelper = new DbHelper(this);
db = dbHelper.getReadableDatabase();
}
@Override
public void onDestroy() {
super.onDestroy();
// Close the database
db.close();
}
@Override
protected void onResume() {
super.onResume();
// Get the data from the database
cursor = db.query(DbHelper.TABLE, null, null, null, null, null,
DbHelper.C_CREATED_AT + " DESC");
startManagingCursor(cursor);
// Create the adapter
adapter = new TimelineAdapter(this, cursor); //
listTimeline.setAdapter(adapter); //
}
}
修改SimpleCursorAdapter
为TimelineAdapter
。
新建一个TimelineAdapter
的实例,交给它上下文及数据的引用。
将ListView
与这个Adapter关联,从而与数据库绑定。
10.5. ViewBinder: TimelineAdapter之外的更好选择
除去继承一个 TimelineAdapter 并覆盖 bindView()
方法外,我们也可以直接为 SimpleCursorAdapter 添加业务逻辑。这样既能保留原先的 bindView()
所做的工作,又省去一个类,更加简便。
SimpleCursorAdapter
提供了一个 setViewBinder()
方法为它的业务逻辑提供扩展,它取一个 ViewBinder
的实现作为参数。 ViewBinder
是个接口,里面声明了一个 setViewValue()
方法,也就是在这里,它将具体的日期元素与View真正地绑定起来。
同样,我们可以在官方文档中发现这一特性。
如下即针对 TimelineAdapter
的最后一轮迭代,在此实现一个自定义的 ViewBinder
作为常量,并把它交给 SimpleCursorAdapter
。
例 10.8. TimelineActivity.java with ViewBinder
...
@Override
protected void onResume() {
...
adapter.setViewBinder(VIEW_BINDER); //
...
}
// View binder constant to inject business logic that converts a timestamp to
// relative time
static final ViewBinder VIEW_BINDER = new ViewBinder() { //
public boolean setViewValue(View view, Cursor cursor, int columnIndex) { //
if (view.getId() != R.id.textCreatedAt)
return false; //
// Update the created at text to relative time
long timestamp = cursor.getLong(columnIndex); //
CharSequence relTime = DateUtils.getRelativeTimeSpanString(view
.getContext(), timestamp); //
((TextView) view).setText(relTime); //
return true; //
}
};
...
将一个自定义的ViewBinder实例交给对应的Adapter。VIEW_BINDER的定义在后面给出。
ViewBinder的实现部分。留意它是一个内部类,外面的类不可以使用它,因此不必把它暴露在外。同时留意下static final
表明它是一个常量。
我们需要实现的唯一方法即setViewValue()
。它在每条数据与其对应的View绑定时调用。
首先检查这个View是不是我们关心的,也就是表示消息创建时间的那个TextView。若不是,返回false,Adapter也就按其默认方式处理绑定;若是,则按我们的方式继续处理。
从cursor
中取出原始的timestamp数据。
使用上个例子相同的辅助函数DateUtils.getRelativeTimeSpanString()
将时间戳(timestamp)转换为人类可读的格式。这也就是我们扩展的业务逻辑。
更新对应View中的文本。
返回true,因此SimpleCursorAdapter
不再按照默认方式处理绑定。
10.6. 更新Manifest文件
现在有了TimelineActivity
,大可让它作为Yamba程序的“主界面”。毕竟比起自言自语,用户更喜欢关注朋友的动态。
这就需要更新manifest文件了。同原先一样,我们将TimelineActivity列在AndroidManifest.xml文件的元素中。可参考"Update Manifest File"一节中添加选项界面时的情景。
要把它设为程序的“主界面”,我们需要为它注册到特定的Intent。通常情况是,用户点击启动你的程序,系统就会发送一个Intent。你必须有个Activity能“侦听”到这个Intent才行,因此Android提供了IntentFilter,使之可以过滤出各自感兴趣的Intent。在XML中,它通过
元素表示,其下至少应含有一个
元素,以表示我们感兴趣的Intent。
你可以注意到,StatusActivity
比PrefsActivity
多出了一段XML代码,这便是IntentFilter的部分。
里面有个特殊的action
,android.intent.action.MAIN
,即指明了在用户打算启动我们的程序时首先启动的组件。除此之外还有个
元素,用以通知系统这个程序会被加入到main Launcher
之中,这一来用户就可以见到我们程序的图标,点击即可启动。这个目录(category)的定义就是android.intent.category.LAUNCHER
。
好,要把TimelineActivity
置为主入口,我们只需加上相应的声明,再把StatusActivity
中的代码挪过去即可。
例 10.9. AndroidManifest.xml
将这个Activity所关心的Intent列出,并在系统中注册。
通知系统,这就是用户启动时显示的主界面。
目录LAUNCHER
通知Home程序,将本程序的图标显示在Launcher中。
StatusActivity
就不需要IntentFilter了。
10.6.1. 程序初始化
现在用户启动程序就会首先看到Timeline界面。但是用户必须先设置个人选项并启动Service,否则就没有消息显示。这很容易让人摸不着头脑。
一个解决方案是,在启动时检查用户的个人选项是否存在。若不存在,就跳到选项界面,并给用户一个提示,告诉她下一步该怎么做。
...
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Check whether preferences have been set
if (yamba.getPrefs().getString("username", null) == null) { //
startActivity(new Intent(this, PrefsActivity.class)); //
Toast.makeText(this, R.string.msgSetupPrefs, Toast.LENGTH_LONG).show(); //
}
...
}
...
检查用户的个人选项是否设置。在这里先只检查username
即可,因为有了username
,往往就意味着所有个人选项都已设置。在第一次启动程序时个人选项还不存在,因此username
(或者其它任意一项)肯定为null。
启动PrefsActivity
。留意这里的startActivity()
调用给系统发送一个Intent,但并不会在这里退出onCreate()
的执行。这一来就允许用户在设置完毕之后,可以回到Timeline界面。
显示一条弹出消息(即Toast),提示用户该怎么做。同前面一样,这里假定你在strings.xml
中提供了msgSetupPrefs
的定义。
10.6.2. BaseActivity
现在我们有了Timeline界面,接下来需要一个选项菜单,就像在Options Menu
一节中对StatusActivity
所做的那样。这对Timeline界面来说很重要,因为作为主界面,要没有菜单,用户就没法访问其它界面或者控制Service的开关了。
要实现上述功能,我们可以把StatusActivity
中相关的代码都复制粘贴过来,但这不是好办法。相反,我们应该优先考虑的是重构。在这里,我们可以将StatusActivity
中相应的功能放在另一个Activity
中,并使其作为基类。参见 图 10.2. "重构BaseActivity" 。
图 10.2. 重构BaseActivity
我们新建一个类BaseActivity
,然后把需要重用的代码挪到里面。这里重用的代码就是:获得YambaApplication
对象引用、选项菜单的相关代码(onCreateOptionsMenu()
和onOptionsItemSelected()
)。
10.6.3. Service开关
在这个地方分别提供开/关两个按钮,不如只留一个按钮做开关。因此需要修改菜单,并为BaseActivity
添加一个onMenuOpened()
方法来动态控制按钮文本以及图标的变化。
首先修改munu.xml 文件,添加新的菜单项也就是开关按钮。这时原先启动/关闭Service的两个按钮已经没必要了,删除即可。
例 10.10. res/menu/menu.xml
将原先的itemServiceStart
与itemServiceStop
替换为itemToggleService
。
例 10.11. BaseActivity.java
package com.marakana.yamba5;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
/**
* The base activity with common features shared by TimelineActivity and
* StatusActivity
*/
public class BaseActivity extends Activity { //
YambaApplication yamba; //
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
yamba = (YambaApplication) getApplication(); //
}
// Called only once first time menu is clicked on
@Override
public boolean onCreateOptionsMenu(Menu menu) { //
getMenuInflater().inflate(R.menu.menu, menu);
return true;
}
// Called every time user clicks on a menu item
@Override
public boolean onOptionsItemSelected(MenuItem item) { //
switch (item.getItemId()) {
case R.id.itemPrefs:
startActivity(new Intent(this, PrefsActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
break;
case R.id.itemToggleService:
if (yamba.isServiceRunning()) {
stopService(new Intent(this, UpdaterService.class));
} else {
startService(new Intent(this, UpdaterService.class));
}
break;
case R.id.itemPurge:
((YambaApplication) getApplication()).getStatusData().delete();
Toast.makeText(this, R.string.msgAllDataPurged, Toast.LENGTH_LONG).show();
break;
case R.id.itemTimeline:
startActivity(new Intent(this, TimelineActivity.class).addFlags(
Intent.FLAG_ACTIVITY_SINGLE_TOP).addFlags(
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
break;
case R.id.itemStatus:
startActivity(new Intent(this, StatusActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
break;
}
return true;
}
// Called every time menu is opened
@Override
public boolean onMenuOpened(int featureId, Menu menu) { //
MenuItem toggleItem = menu.findItem(R.id.itemToggleService); //
if (yamba.isServiceRunning()) { //
toggleItem.setTitle(R.string.titleServiceStop);
toggleItem.setIcon(android.R.drawable.ic_media_pause);
} else { //
toggleItem.setTitle(R.string.titleServiceStart);
toggleItem.setIcon(android.R.drawable.ic_media_play);
}
return true;
}
}
BaseActivity
是个Activity。
声明一个共享的YambaApplication对象,使之可为所有子类访问。
在onCreate()
中获得yamba
的引用。
将StatusActivity
的onCreateOptionsMenu
挪到了这里。
onOptionsItemSelected
也是同样来自StatusActivity
。不过留意下这里的变动:它不再检查启动/停止Service的两个条目,改为检查itemToggleService
一个条目。通过yamba中的标志变量我们可以得到Service的运行状态,基于此,决定启动还是关闭。
onMenuOpened()
是个新加入的方法,它在菜单打开时为系统所调用,参数menu
即我们的选项菜单。我们可以在这里控制开关的显示。
在menu
对象中找到我们新建的开关条目。
检查Service是否正在运行,如果是,则设置对应的标题与图标。留意我们在这里是通过Java的API手工地修改GUI的内容,而无关xml。
如果Service没有运行,则设置对应的标题与图标,用户点击它即可启动Service。这样我们就实现了开关按钮随Service状态的不同而变化。
好,已经有了BaseActivity
类,接下来修改TimelineActivity
也使用它。如下为TimelineActivity
的完整实现:
例 10.12. TimelineActivity.java, final version
package com.marakana.yamba5;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.text.format.DateUtils;
import android.view.View;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.SimpleCursorAdapter.ViewBinder;
public class TimelineActivity extends BaseActivity { //
Cursor cursor;
ListView listTimeline;
SimpleCursorAdapter adapter;
static final String[] FROM = { DbHelper.C_CREATED_AT, DbHelper.C_USER,
DbHelper.C_TEXT };
static final int[] TO = { R.id.textCreatedAt, R.id.textUser, R.id.textText };
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.timeline);
// Check if preferences have been set
if (yamba.getPrefs().getString("username", null) == null) { //
startActivity(new Intent(this, PrefsActivity.class));
Toast.makeText(this, R.string.msgSetupPrefs, Toast.LENGTH_LONG).show();
}
// Find your views
listTimeline = (ListView) findViewById(R.id.listTimeline);
}
@Override
protected void onResume() {
super.onResume();
// Setup List
this.setupList(); //
}
@Override
public void onDestroy() {
super.onDestroy();
// Close the database
yamba.getStatusData().close(); //
}
// Responsible for fetching data and setting up the list and the adapter
private void setupList() { //
// Get the data
cursor = yamba.getStatusData().getStatusUpdates();
startManagingCursor(cursor);
// Setup Adapter
adapter = new SimpleCursorAdapter(this, R.layout.row, cursor, FROM, TO);
adapter.setViewBinder(VIEW_BINDER); //
listTimeline.setAdapter(adapter);
}
// View binder constant to inject business logic for timestamp to relative
// time conversion
static final ViewBinder VIEW_BINDER = new ViewBinder() { //
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
if (view.getId() != R.id.textCreatedAt)
return false;
// Update the created at text to relative time
long timestamp = cursor.getLong(columnIndex);
CharSequence relTime = DateUtils.getRelativeTimeSpanString(view
.getContext(), timestamp);
((TextView) view).setText(relTime);
return true;
}
};
}
首先将基类由系统提供的Activity
改为我们的BaseActivity
。由此也就继承来了yamba对象,以及选项菜单的相关支持。
在这里检查用户的个人选项是否存在,若不存在,则切换到选项界面。
在界面显示时,初始化消息列表。这是个私有方法,定义在后面。
我们希望在界面关闭时,关闭数据库并释放资源。数据库是在yamba
对象中的getStatusUpdates()
方法中打开的。
辅助方法setupList()
用以获取数据、设置Adapter并将其绑定于ListView。
在这里将ViewBinder绑定于List。参见"ViewBinder:TimelineAdapter之外的更好选择"一节。
ViewBinder的定义。
到这里,我们已经将TimelineActivity
重构的差不多了。按照如上的步骤,我们可以进一步简化StatusActivity
,将选项菜单相关的代码清掉,从而使得BaseActivity
、StatusDate
、TimelineActivity
的职责更加分明。
图 10.3 "TimelineActivity" 展示了Timeline界面的成品图。
图 10.3. TimelineActivity
10.7. 总结
到这里,Yamba在能够发消息之外,也能够阅读朋友的消息了。我们的程序仍是完整可用的。
图 10.4 "Yamba完成图" 展示了目前为止我们已完成的部分。完整图参见 图 5.4 "Yamba设计图" 。
图 10.4. Yamba完成图
11. Broadcast Receiver
本章介绍Broadcast Receiver(广播接收者 )的相关知识,以及它在不同场景中的不同应用。在这里,我们将创建几个Broadcast Receiver作为实例,分别用以实现在开机时启动UpdaterService、即时更新Timeline、响应网络连接状态的变化。此外,我们还需要将它们注册到应用程序,也需要对广播的Intent有所理解。最后,我们将明确地定义应用程序的相关权限,以保证安全。
到本章结束,我们即可完成应用的大部分功能,比如发送消息、获取Timeline、即时更新Timeline以及自动启动等等。而且,在网络断开的时候,它依然可以运行(但不能收发数据)。
11.1. 关于Broadcast Receiver
Broadcast Receiver是Android中发布/订阅机制(又称为Observer模式 )的一种实现。应用程序作为发布者,可以广播一系列的事件,而不管接收者是谁。Receiver作为订阅者,从发布者那里订阅事件,并过滤出自己感兴趣的事件。如果某事件符合过滤规则,那么订阅者就被激活,并得到通知。
(译者注:Receiver是Broadcast Receiver的简写,下同)
我们曾在第四章提到过,Broadcast Receiver
是应用程序针对事件做出响应的相关机制,而事件就是广播发送的Intent。Receiver会在接到Intent时唤醒,并触发相关的代码,也就是onReceive()
。
11.2. BootReceiver
在Yamba中,UpdaterService负责在后台定期抓取数据。但现在只能在打开应用之后,让用户在菜单中选择Start Service
手工启动它才会开始执行。这样显然是不友好的。
如果能让UpdaterService随着系统启动而自动启动,那就可以省去用户的手工操作了。为此,我们将新建一个Broadcast Receiver用以响应系统启动的事件,即BootReceiver
,并由它负责启动UpdaterService。BootReciever的代码只有如下几行:
例 11.1. BootReceiver.java
package com.marakana.yamba6;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class BootReceiver extends BroadcastReceiver { //
@Override
public void onReceive(Context context, Intent intent) { //
context.startService(new Intent(context, UpdaterService.class)); //
Log.d("BootReceiver", "onReceived");
}
}
BootReceiver
继承自BroadcastReceiver
,它是所有Receiver的基类。
我们只需实现一个方法,那就是onReceive()
。它将在收到符合过滤规则的Intent时触发。
通过Intent启动UpdaterService
。onReceive()
在触发时可以得到一个Context
对象,我们将它原样传给UpdaterService
。对现在的UpdaterService
而言,这个Context
对象并不是必需的,但稍后我们就能够发现它的重要性。
到这里,我们已经实现了BootReceiver,但还没有将它注册到系统,因此仍无法响应来自系统的事件。
11.2.1. 将BootReceiver注册到Manifest文件
要将BootReceiver注册到系统,我们需要在Manifest文件中添加它的声明,还需要定义它的intent-filter,用以过滤出自己关心的事件。
例 11.2. AndroidManifest.xml: 在标签之下
...
...
针对某特定事件,在声明了它的intent-filter之后,还需要赋予应用程序以相应的权限。在这里那就是android.permission.RECEIVE_BOOT_COMPLETED
。
例 11.3. AndroidManifest.xml: 在标签之下
...
...
Note:
如果不声明权限,那么在事件发生时应用程序就会得不到通知,也就无从执行响应的代码。不幸的是,程序根本无法察觉事件丢失的情况,也没有任何错误提示,出现bug的话将很难调试。
11.2.2. 测试 BootReceiver
接下来重启设备。重启之后若一切正常,UpdaterService就应该处于运行状态了。你可以通过LogCat的输出来检查它是否启动成功,也可以通过System Settings检查它是否存在于当前正在运行的Service列表。
要使用System Settings,可以回到主屏幕,点击菜单按钮,选择Settings→Applications→Running Services
。如果正常,即可在列表中见到UpdaterService,表示BootReceiver已经收到了系统广播的事件,并成功地启动了UpdaterService。
11.3. TimelineReceiver
目前,UpdaterService可以定期抓取最新的消息数据,但它无法通知TimelineActivity
。因此Timeline界面仍不能做到即时更新。
为解决这一问题,我们将创建另一个BroadcastReceiver,使它作为TimelineActivity
的内部类。
例 11.4. TimelineActivity.java,内部类TimelineReceiver
...
class TimelineReceiver extends BroadcastReceiver { //
@Override
public void onReceive(Context context, Intent intent) { //
cursor.requery(); //
adapter.notifyDataSetChanged(); //
Log.d("TimelineReceiver", "onReceived");
}
}
...
同前面一样,Broadcast Receiver都是以BroadcastReceiver
作为基类。
唯一需要实现的方法是onReceive()
。其中的代码会在Receiver被触发时执行。
通知cursor
对象刷新自己。对此,只需调用requery()
方法,重新执行它的上一次数据查询就可以了。
通知adapter对象,它的内部数据已经被修改。
要让Receiver正常工作,我们还需要将它注册。不过这里不像刚才的BootReceiver
那样在Manifest文件中添加声明就好,我们需要动态地注册TimelineReceiver。因为 TimelineReceiver
仅在用户查看 Timeline 界面时才有意义,将它静态地注册到Manifest文件的话,会无谓地浪费资源。
例 11.5. TimelineActivity.java中的内部类TimelineReceiver
...
@Override
protected void onResume() {
super.onResume();
// Get the data from the database
cursor = db.query(DbHelper.TABLE, null, null, null, null, null,
DbHelper.C_CREATED_AT + " DESC");
startManagingCursor(cursor);
// Create the adapter
adapter = new TimelineAdapter(this, cursor);
listTimeline.setAdapter(adapter);
// Register the receiver
registerReceiver(receiver, filter); //
}
@Override
protected void onPause() {
super.onPause();
// UNregister the receiver
unregisterReceiver(receiver); //
}
...
在TimelineActivity
进入 Running 状态时注册 Receiver 。前面在"Running状态" 一节中曾提到,在Activity进入Running状态之前,肯定要经过onResume()
。因此,这里是注册Receiver的好地方。
与之相对,Activity在进入Stopped状态之前,肯定会经过onPause()
。可以在这里注销Receiver。
前面还剩一个filter
对象没解释:它是一个IntentFilter
的实例,用以过滤出Receiver感兴趣的事件。在这里,我们通过一条表示Action事件的字符串来初始化它。
例 11.6. TimelineActivity.java,修改后的 onCreate()
...
filter = new IntentFilter("com.marakana.yamba.NEW_STATUS"); //
...
创建IntentFilter的实例,以"com.marakana.yamba.NEW_STATUS"
作为构造函数的参数,表示一条Action事件。在这里使用了一个字符串,以后可以将它定义成常量,方便在其它地方使用。
11.4. 发送广播
最后,为触发这个事件,我们需要广播一条能够匹配filter的Intent。前面的BootReceiver
只管接收来自系统的广播,也就没必要负责发送Intent。但对TimelineReceiver
来说,它接收的广播是来自应用程序本身,发送Intent也就需要我们自己负责了。
在第八章 Service 中,我们为UpdaterService
创建了一个内部类Updater
,负责在独立的线程中连接到服务端并抓取数据。第一手数据在它手里,因此由它负责发送通知是合理的。
例 11.7. UpdaterService.java, 内部类Updater
...
private class Updater extends Thread {
Intent intent;
public Updater() {
super("UpdaterService-Updater");
}
@Override
public void run() {
UpdaterService updaterService = UpdaterService.this;
while (updaterService.runFlag) {
Log.d(TAG, "Running background thread");
try {
YambaApplication yamba =
(YambaApplication) updaterService.getApplication(); //
int newUpdates = yamba.fetchStatusUpdates(); //
if (newUpdates > 0) { //
Log.d(TAG, "We have a new status");
intent = new Intent(NEW_STATUS_INTENT); //
intent.putExtra(NEW_STATUS_EXTRA_COUNT, newUpdates); //
updaterService.sendBroadcast(intent); //
}
Thread.sleep(60000); //
} catch (InterruptedException e) {
updaterService.runFlag = false; //
}
}
}
}
...
获取Application对象的引用。
前面我们曾在Application对象中实现了fetchStatusUpdates()
方法,用以获取数据并写入数据库,并将获取数据的数量作为返回值返回。
检查是否得到新数据。
初始化Intent。NEW_STATUS_INTENT
是一个常量,表示一项Action。在这里,它就是"com.marakana.yamba.NEW_STATUS"。Android并没有限制Action的名字,不过按照约定,一般都在它名字的前面加上package的名字。
可以为Intent添加附加数据。在这里,通知他人新得到数据的数目是有意义的。因此通过putExtra()
方法将一个数值加入Intent,并与一个键值(NEW_STATUS_EXTRA_COUNT)关联起来。
我们已经确定得到了新数据,接下来就是将Intent广播出去。sendBroadcast()
是个Context对象中的方法,而Service是Context的子类,因此也可以通过updaterService对象来调用sendBroadcast()
。传入的参数就是刚刚创建的Intent。
让线程休眠一分钟,避免CPU过载。
如果线程被任何原因中断,则设置runFlag为false,表示Service已停止运行。
Note:
UpdaterService
会持续广播着Intent,而不受TimelineReceiver
影响。如果TimelineReceiver未被注册,则UpdaterService广播的Intent会被简单忽略。
到这里,UpdaterService
会在每次收到新数据时广播一条Intent。TimelineReceiver
则可以接收到这些Intent,并更新TimelineActivity所显示的内容。
11.5. NetworkReceiver
现在,我们的Service已经可以跟随系统启动,并每分钟定时尝试连接服务端以获取数据。但这样的设计存在一个问题,那就是即使设备没有联网,程序仍定时试图连接服务端抓取数据,这样只会无谓地浪费电能。假如你正在坐一趟国际航班,上面没有网络连接,几个小时下来不难想像这将多么浪费。电能与网络连接都不是永久的,我们应尽量减少无谓的操作。
一个解决方案是监听网络的连接情况,并由它来决定Service的启动关闭。系统会在网络连接情况发生变化时发送一条Intent,另外也有系统服务可用于查看当前的网络连接情况。
接下来,我们创建一个新的Receiver:NetworkReceiver
。步骤同前面一样,新建一个Java类,并继承BroadcastReceiver
,然后在Manifest文件中注册它。
例 11.8. NetworkReceiver.java
package com.marakana.yamba6;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.util.Log;
public class NetworkReceiver extends BroadcastReceiver { //
public static final String TAG = "NetworkReceiver";
@Override
public void onReceive(Context context, Intent intent) {
boolean isNetworkDown = intent.getBooleanExtra(
ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); //
if (isNetworkDown) {
Log.d(TAG, "onReceive: NOT connected, stopping UpdaterService");
context.stopService(new Intent(context, UpdaterService.class)); //
} else {
Log.d(TAG, "onReceive: connected, starting UpdaterService");
context.startService(new Intent(context, UpdaterService.class)); //
}
}
}
同前面一样,它也是BroadcastReceiver
的子类。
系统会在发送广播时,在这条Intent中加入一组附加数据,表示网络的连接情况,也就是ConnectivityManager.EXTRA_NO_CONNECTIVITY
所对应的布尔值。前面我们曾为自己的Intent加入附加数据,在这里相反,只管取出数据即可。若这个值为true,则表示网络断开。
如果网络断开,则向UpdaterService发送一条Intent,令它停止。通过系统传入的Context对象。
如果网络连接,则启动UpdaterService,与前面相反。
Note:
在Activity或者Service中,我们可以直接调用startActivity()
、startService()
、stopService()
等方法。这是因为Activity与Service都是Context的子类,它们自身就是一个Context对象。但BroadcastReceiver并不是Context的子类,因此发送Intent,仍需通过系统传递的Context对象。
新的Receiver已经编写完毕,接下来将它注册到Manifest文件。
例 11.9. AndroidManifest.xml: 在标签之下
...
...
我们还需要更新应用程序所需的权限。在系统中,网络连接状况是受保护的信息,要获取它,我们需要用户赋予应用程序以专门的权限。
例 11.10. AndroidManifest.xml: 在标签之下
...
...
访问网络所需的权限。这是在第六章 Android用户界面 中定义的。如果没有这项权限,应用程序就无法连接到网络。
监视系统启动所需的权限。如果没有这项权限,应用程序就无法捕获系统启动 这一事件,而且没有任何错误提示。
监视网络连接情况所需的权限。与上面相似,如果没有这项权限,应用程序就无法获知网络连接情况的变化,而且没有任何错误提示。
11.6. 添加自定义权限
我们曾在第六章的 更新Manifest文件,获取Internet权限 一节讨论过,应用程序若要访问系统的某项功能(比如连接网络、发短信、打电话、读取通讯录、拍照等等),那就必须获取相应的权限。比如现在的Yamba,就需要连接网络、监视系统启动、监视网络连接情况这三项权限。它们都在Manifest文件中部分给出了声明,至于能否得到这些权限,则由用户在安装时决定,要么全部赋予,要么全部拒绝。
现在UpdaterService可以广播Intent,但我们不一定希望所有程序都可以收发这条广播。不然,其它程序只要知道Action事件的名字,即可伪造我们的广播,更可以随意监听我们程序的动态,从而导致一些不可预料的问题。
为此,我们需要为 Yamba 定义自己的权限,对发布者与订阅者双方都做些限制,以修补这项安全漏洞。
11.6.1. 在 Manifest 文件中定义权限
首先是给出权限的定义。解释它们是什么、如何使用、处于何种保护级别。
例 11.11. 在Manifest文件中定义权限
...
-->
android:label="@string/send_timeline_notifications_permission_label"
android:description="@string/send_timeline_notifications_permission_description"
android:permissionGroup="android.permission-group.PERSONAL_INFO"
android:protectionLevel="normal" />
权限的名字,作为引用权限的标识符。在这里,我们通过它来保护Timeline更新事件的广播。
权限的标签(Label)。它会在安装应用程序的授权步骤显示给用户,作为一个人类可读的权限名称,其内容应尽量做到言简意赅。留意,我们将它的值定义在了string.xml
文件中。
关于这项权限的描述,用以简介权限的含义与作用。
权限组。此项可选,但很有用。通过它,可以将我们的权限归类到系统定义的权限组。也可以定义自己的权限组,但不常见。
权限的安全等级,表示这项权限的危险程度,等级越高越危险。其中'normal'
表示最低的安全等级。
定义另一项权限,用于保护Timeline更新事件的接收。步骤与上相同。
给出了这些定义之后,我们仍需为应用程序申请这些权限。这里就同申请系统权限一样了,通过标签。
到这里,我们添加了两项自定义的权限,也为应用程序申请了对这两项权限。接下来,我们需要保证广播的发布者与订阅者分别能够符合自己的权限。
11.6.2. 为UpdaterService应用权限机制
UpdaterService会在每次收到新数据时广播一条Intent,但我们不希望任何人都可以收到这条Intent,因此限制只有拥有授权的Receiver才可以收到这条Intent。
例 11.12. UpdaterService中的内部类Updater
...
private class Updater extends Thread {
static final String RECEIVE_TIMELINE_NOTIFICATIONS =
"com.marakana.yamba.RECEIVE_TIMELINE_NOTIFICATIONS"; //
Intent intent;
public Updater() {
super("UpdaterService-Updater");
}
@Override
public void run() {
UpdaterService updaterService = UpdaterService.this;
while (updaterService.runFlag) {
Log.d(TAG, "Running background thread");
try {
YambaApplication yamba = (YambaApplication) updaterService
.getApplication();
int newUpdates = yamba.fetchStatusUpdates();
if (newUpdates > 0) {
Log.d(TAG, "We have a new status");
intent = new Intent(NEW_STATUS_INTENT);
intent.putExtra(NEW_STATUS_EXTRA_COUNT, newUpdates);
updaterService.sendBroadcast(intent, RECEIVE_TIMELINE_NOTIFICATIONS); //
}
Thread.sleep(DELAY);
} catch (InterruptedException e) {
updaterService.runFlag = false;
}
}
}
} // Updater
...
要求Receiver拥有的权限的名字。它必须要与Manifest文件中的定义保持一致。
将权限的名字作为sendBroadcast()
调用的第二个参数。如果Receiver没有相应的权限,就不会收到这条广播。
要保证发送端的安全,我们不需要对 TimelineReceiver 做任何改动。因为用户已经赋予相应的权限,所以能够正确的获取通知。 但是对于接收端,同样应该确保发送者是我们认可的。
11.6.3. 为Receiver应用权限机制
我们还需要检查Receiver得到的广播是否合法。为此,在注册Receiver时为它添加上相关的权限信息。
例 11.13. TimelineReceiver in TimelineActivity.java
...
public class TimelineActivity extends BaseActivity {
static final String SEND_TIMELINE_NOTIFICATIONS =
"com.marakana.yamba.SEND_TIMELINE_NOTIFICATIONS"; //
...
@Override
protected void onResume() {
super.onResume();
...
// Register the receiver
super.registerReceiver(receiver, filter,
SEND_TIMELINE_NOTIFICATIONS, null); //
}
...
}
将权限名字定义为一个常量。它的值必须与Manifest文件中的定义保持一致。
在onResume()
中注册TimelineReceiver
时,添加它所需的权限信息。限制只对拥有这项权限的发送者进行响应。
现在,我们已为收发双方应用了权限机制。读者可以在这个过程中体会Android权限机制的精妙之处。
11.7. 小结
到这里,Yamba已经接近完工,它已经可以发送消息、阅读Timeline、开机自动启动,以及在收到新消息时更新Timeline的显示。
图 11.1 "Yamba完成图" 展示了我们目前已完成的部分。完整图参见 图 5.4 "Yamba设计图" 。
图 11.1. Yamba完成图
12. Content Provider
比方说,你的设备里可能已经有了一个庞大的联系人数据库。联系人应用可以查看它们,拨号应用也可以,有些设备(比如HTC)还带着不同版本的联系人应用与拨号应用。既然都需要用到联系人数据,如果它们各自使用独立数据库的话,事情就会比较麻烦。
Content Provider 提供了一个统一的存储接口,允许多个程序访问同一个数据源。在联系人的这个例子里,还有个应用程序在背后扮演着数据源的角色,提供一个 Content Provider 作为接口,供其它程序读写数据。接口本身很简单:insert()、update()、delete()与query(),与第九章提到数据库的接口类似。
在 Android 内部就大量使用了 Content Provider。除了前面提到的联系人外,系统设置、书签也都是 Content Provider 。另外系统中所有的多媒体文件也都注册到了一个 Content Provider 里,它被称作 MediaStore,通过它可以检索系统中所有的图片、音乐与视频。
12.1. 创建Content Provider
创建一个Content Provider的步骤如下:
创建一个新类,并继承系统中的 ContentProvider。
声明你的CONTENT_URI。
实现所有相关的方法,包括insert(),update(),delete(),query(),getID()和getType()。
将你的Content Provider注册到AndroidManifest.xml文件。
在某个包中新建一个 Java 类StatusProvider
。它与其它构件一样,也必须继承 Android 框架中的基类,也就是 ContentProvider
。
进入Eclipse,选择package,然后File→New→Java Class
,输入StatusProvider
。然后修改这个类使之继承ContentProvider
,同时调整import语句(Ctrl-Shift-O),导入必要的Java package。结果如下:
package com.marakana.yamba7;
import android.content.ContentProvider;
public class StatusProvider extends ContentProvider {
}
当然,这段代码仍不完整,我们还需要提供几个方法的实现。这里有个快捷功能,那就是单击这个类名,在quick fix列表中选择"Add unimplemented methods",Eclipse即可自动生成所缺方法的模板。
12.1.1. 定义URI
程序内部的不同对象可以通过变量名相互引用,因为它们共享着同一个地址空间。但是不同程序的地址空间是相互隔离的,要引用对方的对象,就需要某种额外的机制。Android对此的解决方案就是全局资源标识符 (Uniform Resource Identifier , URI),使用一个字符串来表示Content Provider以及其中资源的位置。每个URI由三或四个部分组成,如下:
URI的各个部分
content://com.marakana.yamba.statusprovider/status/47
A B C D
A部分,总是content://,固定不变。
B部分,con.marakana.yamba.provider
,称作"典据"(authority)。通常是类名,全部小写。这个典据必须与我们在Manifest文件中的声明相匹配。
C部分,status,表示对应数据的类型。它可以由任意/
分隔的单词组成。
D部分,47,表示对应条目的ID,此项可选。如果忽略,则表示数据的整个集合。
有时需要引用整个Content Provider,那就忽略D部分;有时只需要其中一项,那就保留D部分,指明对应的条目。另外我们这里只有一个表,因此C部分是可以省略的。
如下是定义常量:
public static final Uri CONTENT_URI = Uri
.parse("content://com.marakana.yamba7.statusprovider");
public static final String SINGLE_RECORD_MIME_TYPE =
"vnd.android.cursor.item/vnd.marakana.yamba.status";
public static final String MULTIPLE_RECORDS_MIME_TYPE =
"vnd.android.cursor.dir/vnd.marakana.yamba.mstatus";
关于这两个MIME类型,我们将在后面“获取数据类型”一节中详细讨论。还要在类里定义一个statusData对象,方便以后引用它。
StatusData statusData;
加入这个statusData对象,是因为数据库的访问都统一到了StatusProvider这个类中。
12.1.2. 插入数据
要通过Content Provider插入记录,我们需要覆盖insert()
方法。调用者需要提供Content Provider的URI(省略ID)和待插入的值,插入成功可以得到新记录的ID。为此,我们将新记录对应的URI作为返回值。
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = statusData.dbHelper.getWritableDatabase(); //#1
try {
long id = db.insertOrThrow(StatusData.TABLE, null, values); //#2
if (id == -1) {
throw new RuntimeException(String.format(
"%s: Failed to insert [%s] to [%s] for unknown reasons.", TAG,
values, uri)); //#3
} else {
return ContentUris.withAppendedId(uri, id); //#4
}
} finally {
db.close(); //#5
}
}
打开数据库,写入模式。
尝试将数据插入,成功的话将返回新记录的ID。
插入过程中若出现失败,则返回-1。我们可以抛出一个运行时异常,因为这是个不该发生的情况。
永远不要忘记关闭数据库。finally
子句是个合适地方。
12.1.3. 更新数据
通过Content Provider的接口更新数据,我们需要:
Content Provider的URI
其中的ID可选。如果提供,则更新ID对应的记录。如果省略,则表示更新多条记录,并需要提供一个选择条件。
待更新的值
这个参数的格式是一组键值对,表示待更新的列名与对应的值。
选择条件
这些条件组成了SQL语句的WHERE部分,选择出待更新的记录。如果URI中提供了ID,这些条件就会被省略。
考虑到URI中是否存在ID的两种情况,代码如下:
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
long id = this.getId(uri); //
SQLiteDatabase db = statusData.dbHelper.getWritableDatabase(); //
try {
if (id < 0) {
return db.update(StatusData.TABLE, values, selection, selectionArgs); //
} else {
return db.update(StatusData.TABLE, values, StatusData.C_ID + "=" + id, null); //
}
} finally {
db.close(); //
}
}
使用辅助方法getId()
获取URI中的ID。如果URI中不存在ID,此方法返回-1
。getId()
的定义在本章后面给出。
打开数据库,写入模式。
如果不存在ID,则按照选择条件筛选待更新的所有条目。
如果存在ID,则使用ID作为WHERE部分的唯一参数,限制只更新一条记录。
永远不要忘记关闭数据库。
12.1.4. 删除数据
删除数据与更新数据很相似。URI中的ID也是可选的。
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
long id = this.getId(uri); //
SQLiteDatabase db = statusData.dbHelper.getWritableDatabase(); //
try {
if (id < 0) {
return db.delete(StatusData.TABLE, selection, selectionArgs); //
} else {
return db.delete(StatusData.TABLE, StatusData.C_ID + "=" + id, null); //
}
} finally {
db.close(); //
}
}
使用辅助方法getId()
获取URI中的ID。如果不存在ID,则返回-1。
打开数据库,写入模式。
如果不存在ID,则按照选择条件筛选待删除的所有条目。
如果存在ID,则使用ID作为WHERE部分的唯一参数,限制只删除一条记录。
永远不要忘记关闭数据库。
12.1.5. 查询数据
通过Content Provider查询数据,我们需要覆盖query()
方法。这个方法的参数有很多,不过大多只是原样交给数据库即可,我们不需要多做修改。
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
long id = this.getId(uri); //
SQLiteDatabase db = statusData.dbHelper.getReadableDatabase(); //
if (id < 0) {
return db.query(StatusData.TABLE, projection, selection, selectionArgs, null,
null, sortOrder); //
} else {
return db.query(StatusData.TABLE, projection, StatusData.C_ID + "=" + id, null, null, null,
null); //
}
}
通过辅助方法getId()
获取URI中的ID。
打开数据库,只读模式。
如果不存在ID,我们简单将本方法中的参数原样交给数据库。留意,数据库的insert()
方法有两个额外的参数,分别对应SQL语句的GROUPING和HAVING部分。Content Provider中没有它们的对应物,因此设为null。
如果存在ID,则使用ID作为WHERE语句的唯一参数,返回对应的记录。
Note:
关闭数据库的同时会销毁Cursor
对象,因此我们在读取数据完毕之前不能关闭数据库,而Cursor对象的生存周期则需要使用者负责管理。针对这一情景,可以利用Activity
的startManagingCursor()
方法。
12.1.6. 获取数据类型
ContentProvider
必须能够给出数据的MIME类型。MIME类型是针对URI而言,表示单个条目和表示多个条目的URI的MIME类型就是不同的。在本章前面,我们曾定义了单个条目的MIME类型vnd.android.cursor.item/vnd.marakana.yamba.status
,而多个条目的MIME类型是vnd.android.cursor.dir/vnd.marakana.yamba.status
。便于他人获取相应的MIME类型,我们需要在ContentProvider
类中实现一个getType()
方法。
MIME类型的第一部分可以是vnd.android.cursor.item
或者vnd.android.cursor.dir
,前者表示单个条目,后者表示多个条目。MIME类型的第二部分与应用程序相关,在这里可以是vnd.marakana.yamba.status
或者vnd.marakana.yamba.mstatus
,由公司名或者应用程序名,以及内容类型组成。
前面提过URI的结尾可能会是数字。如果是,那这个数字就是某个记录的ID。如果不是,则表示URI指向多条记录。
下面的代码展示了getType()
的实现,以及前面用过多次的辅助方法getId()
:
@Override
public String getType(Uri uri) {
return this.getId(uri) < 0 ? MULTIPLE_RECORDS_MIME_TYPE
: SINGLE_RECORD_MIME_TYPE; //
}
private long getId(Uri uri) {
String lastPathSegment = uri.getLastPathSegment(); //
if (lastPathSegment != null) {
try {
return Long.parseLong(lastPathSegment); //
} catch (NumberFormatException e) { //
// at least we tried
}
}
return -1; //
}
getType()
使用辅助方法getId()
判断URI中是否包含ID部分。如果得到负值,表示URI中不存在ID,因此返回vnd.android.cursor.dir/vnd.marakana.yamba.mstatus
作为MIME类型。否则返回vnd.android.cursor.item/vnd.marakana.yamba.status
。我们先前已经定义了这些常量。
为得到ID,获取URI的最后一个部分。
如果最后一个部分非空,则尝试转换为长整型并返回它。
最后一个部分可能不是数字,因此类型转换可能会失败。
返回-1表示URI中不含有合法的ID。
12.1.7. 更新Android Manifest文件
同其它基本构件一致,我们需要将这个Content Provider注册到Manifest文件中。注意,这里的android:authorities
定义了URI的典据 (authority),也就是访问这个Content Provider的凭据。一般来说,这个典据就是Content Provider的类名,或者类所在的package名。在此我们选择前者。
...
...
到这里,我们的Content Provider就已全部完成,接下来就可以在其它构件里使用它了。不过目前为止,各构件访问数据库的操作仍统一在YambaApplication中的StatusData对象上,然而这个Content Provider还没有派上什么用场。但要为其它应用程序提供数据的话,就离不开Content Provider了。
12.2. 在小部件中使用Content Provider
如前面所说,Content Provider的作用是为其它应用程序提供数据。在自给自足之外,将自己融入到Android的生态系统会是更好的态度。其它应用程序若需要我们的数据,就可以通过Content Provider索取。
接下来我们将新建一个小部件(widget),用来演示Content Provider的作用。这里的"小部件"跟View类的俗称"控件"不是一回事,它可以挂在主屏幕上,提供一些快捷功能。
(译者注:"小部件"与"控件"的英文原文都是widget,很容易让人混淆,实际上并无关系)
Android通常自带着一些小部件,比如时钟、相框、电源控制、音乐播放器与搜索框等等。它们一般显示在主屏幕上,你也可以通过Add to Home Screen
对话框将它们添加到主屏幕。本节的目标就是创建一个Yamba小部件,使之可以挂在主屏幕上。
Yamba小部件不会太复杂,仅仅展示最近的消息更新即可。为创建它,我们将以AppWidgetProviderInfo
为基类,新建一个YambaWidget类,最后把它注册到Manifest文件里。
12.2.1. 实现YambaWidget类
YambaWidget
就是这个小部件所对应的类,它以AppWidgetProvider为基类。后者是Android框架为创建小部件所专门提供的一个类,它本身是BroadcastReceiver
的子类,因此YambaWidget
自然也算是一个Broadcast Receiver。在小部件更新、删除、启动、关闭时,我们都会相应地收到一条广播的Indent。同时,这个类也继承着onUpdate()
、onDeleted()
、onEnabled()
、onDisabled()
和onReceive()
几个回调方法,我们可以随意覆盖它们。在这里,只覆盖onUpdate()
和onReceive()
两个方法即可。
现在对小部件的设计已有大致了解,下面是具体实现:
例 12.1. YambaWidget.java
package com.marakana.yamba7;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.text.format.DateUtils;
import android.util.Log;
import android.widget.RemoteViews;
public class YambaWidget extends AppWidgetProvider { //
private static final String TAG = YambaWidget.class.getSimpleName();
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) { //
Cursor c = context.getContentResolver().query(StatusProvider.CONTENT_URI,
null, null, null, null); //
try {
if (c.moveToFirst()) { {//#4}
CharSequence user = c.getString(c.getColumnIndex(StatusData.C_USER)); //
CharSequence createdAt = DateUtils.getRelativeTimeSpanString(context, c
.getLong(c.getColumnIndex(StatusData.C_CREATED_AT)));
CharSequence message = c.getString(c.getColumnIndex(StatusData.C_TEXT));
// Loop through all instances of this widget
for (int appWidgetId : appWidgetIds) { //
Log.d(TAG, "Updating widget " + appWidgetId);
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.yamba_widget); //
views.setTextViewText(R.id.textUser, user); //
views.setTextViewText(R.id.textCreatedAt, createdAt);
views.setTextViewText(R.id.textText, message);
views.setOnClickPendingIntent(R.id.yamba_icon, PendingIntent
.getActivity(context, 0, new Intent(context,
TimelineActivity.class), 0));
appWidgetManager.updateAppWidget(appWidgetId, views); //
}
} else {
Log.d(TAG, "No data to update");
}
} finally {
c.close(); // {#10}
}
Log.d(TAG, "onUpdated");
}
@Override
public void onReceive(Context context, Intent intent) { // {#10}
super.onReceive(context, intent);
if (intent.getAction().equals(UpdaterService.NEW_STATUS_INTENT)) { // {#11}
Log.d(TAG, "onReceived detected new status update");
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); // {#12}
this.onUpdate(context, appWidgetManager, appWidgetManager
.getAppWidgetIds(new ComponentName(context, YambaWidget.class))); // {#13}
}
}
}
如前所说,我们的小部件是AppWidgetProvider
的子类,而AppWidgetProvider
又是BroadcastReceiver
的子类。
此方法在小部件状态更新时触发,因此将主要功能的实现放在这里。在稍后将它注册到Manifest文件时,我们将为它的状态更新设置一个时间间隔,也就是30分钟。
终于可以用上我们的Content Provider了。在前面我们编写StatusProvider
时可以体会到,它的API与SQLite数据库的API十分相似,甚至返回值也是同样的Cursor
对象。不过主要区别在于,在这里不是给出表名,而是给出URI。
在这里,我们只关心服务端最新的消息更新。因此Cursor
指向第一个的元素若存在,那它就是最新的消息。
如下的几行代码读出Cursor
对象中的数据,并储存到局部变量中。
用户可以挂载多个Yamba小部件,因此需要遍历并更新所有小部件。appWidgetId
是某个小部件的标识符,在这里我们是更新所有小部件,因此不必关心某个appWidgetId
具体的值。
小部件所在的View位于另一个进程中,因此使用RemoteViews
。RemoteViews
是专门为小部件设计的某种共享内存机制。
得到了另一个进程地址空间中View的引用,即可更新它们。
更新过RemoteViews
对象,然后调用AppWidgetManager的updateAppWidget()
方法,将发送一条消息,通知系统更新所有的小部件。这是个异步操作,不过实际执行会在onUpdate()
之后。
不管得到新消息与否,都要记得释放从Content Provider中得到的对象。这是个好习惯。
对一般的小部件来说,onReceive()
并无必要。不过小部件既然是Broadcast Receiver,而UpdaterService
在每获得一条消息时都会发一条广播。那我们可以利用这个性质,在onReceive()
中触发onUpdate()
,实现消息的更新。
如果得到新消息,获取当前上下文的AppWidgetManager
对象。
触发onUpdate()
。
到这里,Yamba小部件已经编写完毕。作为一个Broadcast Receiver,它可以定时更新,也可以在获取新消息时得到通知,然后遍历主屏幕上所有的Yamba小部件并更新它们。
接下来设计小部件的外观布局。
12.2.2. 创建XML布局
小部件的外观布局很简单。留意我们在这里重用了TimelineActivity中用到的row.xml
文件,用以表示消息的显示。另外再给它加一个小标题,在主屏幕上更醒目些。
例 12.2. res/layout/yamba_widget.xml
应用LinearLayout
使之水平排列,图标显示在左边,消息显示在右边。
小部件的图标,与Yamba的图标一致。
留意我们使用了
标签。这样可以引入现有的row.xml
文件的内容,而不必复制粘贴代码。
这个布局很简单,不过麻雀虽小,五脏俱全。接下来定义小部件的基本信息。
12.2.3. 创建``AppWidgetProviderInfo``文件
这个XML文件用于描述小部件的基本信息。它一般用于定义小部件的布局、更新频率以及尺寸等信息。
例 12.3. res/xml/yamba_widget_info.xml
在这里,我们将小部件的更新频率设为30分钟(1800000毫秒)。另外也定义了它的布局,以及标题和大小。
12.2.4. 更新Manifest文件
最后更新Manifest文件,将这个小部件注册。
...
...
...
...
前面提到,小部件也是个Broadcast Receiver,因此也需要一个
的声明放在
标签里,通过
筛选出感兴趣的Intent,比如ACTION_APPWIDGET_UPDATE
。
标签表示小部件的元信息是定义在yamba_widget_info
这个XML文件里。
好了。我们已经实现了一个完整的小部件,可以测试一下。
12.2.5. 测试
把它安装到设备上。长按Home键进入主屏幕,然后点击选择小部件。在这里就可以看到Yamba小部件了。添加到主屏幕上之后,它应当能够显示最新的消息更新。
只要你的UpdaterService还在运行,就应该看到消息会随时更新。这就证明小部件运行正常了。
12.3. 总结
恭喜,Yamba已经大工告成!你可以稍作调优,加入一些个性元素,然后就可以发布到市场去了。
图12.1 "Yamba完成图" 展示了我们目前已完成的部分。届此,图5.4 "Yamba设计图" 中的设计我们已全部实现。
图 12.1. Yamba完成图
13. 系统服务
同其它现代操作系统一样,Android也内置了一系列的系统服务。它们都是随着系统启动,并一直处于运行状态,随时可供开发者访问。比如位置服务、传感器服务、WiFi服务、Alarm服务、Telephony服务、Bluetooth服务等等。
本章介绍几个常见的系统服务,并思考如何将它们应用到Yamba。我们先在一个小例子里引入传感器服务,借以观察系统服务的一般特性,然后通过位置服务为Yamba添加地理坐标的功能。
另外,我们会重构Yamba的代码来获得IntentService
的支持,继而引出Alarm服务,并凭借它们优化UpdaterService的实现。
13.1. 实例:指南针
理解系统服务,我们先从一个简单的样例——指南针(Compass)——开始。它可以通过传感器服务获得传感器的输出,旋转屏幕上的表盘(Rose)显示方位。传感器服务是个有代表性的系统服务,也不难懂。
在例子中,我们先创建一个Activity,由它来访问传感器服务,订阅来自传感器的输出。然后自定义一个表盘控件(Rose),使之可以依据传感器端得到的数据旋转一定的角度。
13.1.1. 使用系统服务的一般步骤
使用系统服务,就调用getSystemService()
。它返回一个表示系统服务的Manager对象,随后凭它就可以访问系统服务了。系统服务大多都是发布/订阅的接口,使用起来大约就是准备一个回调方法,将你的程序注册到相应的系统服务,然后等它的通知。而在Java中的通行做法是,实现一个内含回调方法的侦听器(Listener),并把它传递给系统服务。
有一点需要注意,那就是访问系统服务可能会比较费电。比如访问GPS数据或者传感器操作,都会额外消耗设备的电能。为了节约电能,我们可以仅在界面激活时进行传感器操作,使不必要的操作减到最少。用Activity生命周期(参见"Activity生命周期"一节)的说法就是,我们仅在Running状态中响应这些操作。
进入Running状态之前必经onResume()
,离开Running状态之后必经onPause()
。因此要保证只在Running状态中使用系统服务,就在onResume()
中注册到系统服务,并在onPause()
中注销即可。在某些情景中,我们可能希望将Activity注册在onStart()
与onStop()
之间,甚至onCreate()
与onDestroy()
之间,好让它在整个生命周期里都在注册中。但在这里,我们并不希望在onCreate()
中就开始使用系统服务,因为onCreate()
时,Activity还未显示,在此注册到系统服务只会空耗电能。由此可以看出,对Activity的生命周期有所理解,对省电是肯定有帮助的。
13.1.2. 获取指南针的更新
表示传感器服务(Sensor Service)的类是SensorManager
,接下来就是跟它打交道了。在Activity中实现一个SensorEventListener
,将它注册到SensorManager
中即可订阅特定传感器的更新。为了省电,我们将传感器操作安排在onResume()
与onPause()
之间。然后实现侦听器(Listener),在Activity中提供onAccuracyChanged()
和onSensorChanged()
两个方法。前者必须,后者可选。不过我们对传感器的精度(Accuracy)并无兴趣——因为方向传感器(Orientation Sensor)的精度在中间并不会发生变化,我们真正感兴趣的是它的数据。所以在这里我们将前者留空,而提供后者的实现。
在方向传感器状态变化时,传感器服务会回调onSensorChanged()
,通知侦听器(Listener)得到了新数据。这些数据都是0~359之间的浮点数组成的数组。方位角(azimuth)与磁偏角(pitch与roll),构成了下边的坐标,如 //图13.1 "坐标轴"/:
(译者注: 磁偏角:磁针指示的方向并不是正南正北,而是微偏西北和东南,这在科学上被称作磁偏角。)
下标[0],方位角(azimuth):垂直于Z轴,沿Y轴正方向顺时针旋转的角度。
下标[1],横摇(pitch):垂直于Y轴,沿Z轴正方向顺时针旋转的角度。
下标[2],纵摇(roll):垂直于Y轴,沿X轴正方向顺时针旋转的角度。
我们的指南针只关心第一个元素,也就是方位角。不同传感器的返回值的含义各有不同,对此需要查询相应的文档 http://d.android.com/reference/android/hardware/SensorManager.html。
图 13.1. 坐标轴
13.1.3. 指南针的主界面
指南针的主界面里只有一个控件,那就是表盘(Rose)。它也将自己注册给SensorManager
,监听来自传感器的事件,调整表盘的角度。
例 13.1. Compass.java
package com.marakana;
import android.app.Activity;
import android.content.res.Configuration;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.util.Log;
import android.view.Window;
import android.view.WindowManager;
// implement SensorListener
public class Compass extends Activity implements SensorEventListener { //
SensorManager sensorManager; //
Sensor sensor;
Rose rose;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) { //
super.onCreate(savedInstanceState);
// Set full screen view
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
requestWindowFeature(Window.FEATURE_NO_TITLE);
// Create new instance of custom Rose and set it on the screen
rose = new Rose(this); //
setContentView(rose); //
// Get sensor and sensor manager
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); //
sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); //
Log.d("Compass", "onCreated");
}
// Register to listen to sensors
@Override
public void onResume() {
super.onResume();
sensorManager.registerListener(this, sensor,
SensorManager.SENSOR_DELAY_NORMAL); //
}
// Unregister the sensor listener
@Override
public void onPause() {
super.onPause();
sensorManager.unregisterListener(this); //
}
// Ignore accuracy changes
public void onAccuracyChanged(Sensor sensor, int accuracy) { //
}
// Listen to sensor and provide output
public void onSensorChanged(SensorEvent event) { //
int orientation = (int) event.values[0]; //
Log.d("Compass", "Got sensor event: " + event.values[0]);
rose.setDirection(orientation); //
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
}
Compass
会监听来自传感器的事件,因此需要提供SensorEventListener
接口的实现。
定义几个私有变量,分别表示传感器对象(sensor),SensorManager
与表盘(rose)。
初始化sensor
是个一次性的操作,因此我们把它放在onCreate()
中执行。
设置此Activity的状态为全屏。
创建一个Rose
控件的实例,这是我们自定义的控件。
这个Activity中唯一的控件就是Rose
。这算是个特殊情况,一般而言,在这个地方多是引用XML资源表示的Layout。
获得SensorManager
对象。
通过SensorManager
对象,选择我们关心的传感器。
同前面所说,在onResume()
中将自己注册到系统服务,侦听传感器的输出。
对应onResume()
,在onPause()``中注销系统服务。
依然提供onAccuracyChanged()
的实现,因为这对SensorEventListener接口来说是必需的。但是留空,前面已有解释。
传感器在状态改变时会回调onSensorChanged()
,表示设备的方向发生变化。具体的角度信息储存在SensorEvent
对象中。
我们只关心返回值中的第一个元素。
得到新的方向信息,更新表盘的角度。
Note:
传感器输出的数据流可能是不稳定的,接到数据的时间间隔不可预知。我们可以为传感器提供一个建议的时间间隔,但只是建议,不是强制。另外仿真器没有提供传感器的支持,要测试这个程序就需要一台拥有方向传感器的真机。好在多数Android手机都有这一功能。
13.1.4. 自定义的表盘控件
Rose
是我们自定义的UI控件,它可以旋转,表示指南针的表盘。Android中的任何UI控件都是View
的子类。Rose
的主要部分是一个图片,这一来我们可以让它继承自ImageView
类,在较高的层面上实现它,从而可以使用原有的方法实现装载并显示图片。
自定义一个UI控件,onDraw()
算是最重要的方法之一,它负责控件的绘制与显示。在Rose
这里,图片的绘制可由父类完成,我们需要做的就是添加自己的逻辑,使图片可以按圆心旋转一定的度数,以表示指南针的方位。
例 13.2. Rose.java
package com.marakana;
import android.content.Context;
import android.graphics.Canvas;
import android.widget.ImageView;
public class Rose extends ImageView { //
int direction = 0;
public Rose(Context context) {
super(context);
this.setImageResource(R.drawable.compassrose); //
}
// Called when component is to be drawn
@Override
public void onDraw(Canvas canvas) { //
int height = this.getHeight(); //
int width = this.getWidth();
canvas.rotate(direction, width / 2, height / 2); //
super.onDraw(canvas); //
}
// Called by Compass to update the orientation
public void setDirection(int direction) { //
this.direction = direction;
this.invalidate(); // request to be redrawn
}
}
我们的控件必须是View
的子类。它大体就是一张图片,因此可以让它继承自ImageView
,得以使用现有的功能。
ImageView
本身已有设置图片内容的方法,我们需要做的就是为它指明相应的图片。留意,compassrose.jpg
文件在/res/drawable
之下。
onDraw()
由Layout Manager负责调用,指示控件来绘制自己。它会传递过来一个Canvas
对象,你可以在这里执行自定义的绘制操作。
已获得Canvas
对象,可以计算自身的宽高。
简单地以中点为圆心,按度数旋转整个Canvas
。
通知super
将图片绘制到这个旋转了的Canvas
上。到这里表盘就显示出来了。
setDirection()
由Compass
负责调用,它会根据传感器中获得的数据调整Rose的方向。
对View调用invalidate()
,就可以要求它重绘。也就是在稍后调用onDraw()
。
到这里,指南针程序已经可用了。在设备横放时,表盘应大致指向北方。仿真器没有传感器的支持,因此这个程序只能在真机上运行。
13.2. 位置服务
前面已对传感器服务的工作方式有所了解,接下来看下位置服务的 API。同传感器服务类似,位置服务是通过 LocationManager
进行管理,而且也是通过 getSystemService() 获取它的引用。
使用位置服务,我们需要传递给它一个侦听器(Listener),这一来在位置改变的时候可以作出响应。同前面相似,我们在这里实现一个 LocationListener
接口。
"使用系统服务的一般步骤" 一节曾提到,使用 GPS 服务或者其它位置操作都是非常费电的。为尽量地节约电能,我们只在 Running 状态中使用 位置服务。因此利用 Activity 生命周期的设定,将它限制在 onResume()
与 onPause()
之间。
13.2.1. 实例: Where Am I?
使用这个例子展示 Android 中位置服务的用法。首先通过 LocationManager 利用可用的信息源(GPS或者无线网)获取当前的位置信息,然后使用 Geocoder 将它转换为人类可读的地理地址。
13.2.1.1. 布局
本例的布局不是我们关注的重点。里面只有一个表示标题的TextView控件,和一个表示输出的TextView控件。其中输出可能会比较长,因此把它包在一个ScrollView里。
例 13.3. res/layout/main.xml
程序的标题。
输出可能会比较长,甚至超过屏幕的尺寸。因此,通过ScrollView为它加一个滚动条。
表示输出的TextView,WhereAmI
界面中唯一的动态部分。
13.2.1.2. 作为LocationListener的Activity
这就是我们唯一的Activity。我们将在这里初始化界面,连接到LocationManager,然后使用Geocoder检索我们的地理地址。LocationManager可以通过不同的位置信息源(比如GPS或者网络)来获得我们的当前位置,以经纬度的格式返回。Geocoder以此检索在线的位置数据库,即可获得当前位置的地理地址。检索的结果可能有多个,精度不一。
例 13.4. WhereAmI.java
package com.marakana;
import java.io.IOException;
import java.util.List;
import android.app.Activity;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
public class WhereAmI extends Activity implements LocationListener { //
LocationManager locationManager; //
Geocoder geocoder; //
TextView textOut; //
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
textOut = (TextView) findViewById(R.id.textOut);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); //
geocoder = new Geocoder(this); //
// Initialize with the last known location
Location lastLocation = locationManager
.getLastKnownLocation(LocationManager.GPS_PROVIDER); //
if (lastLocation != null)
onLocationChanged(lastLocation);
}
@Override
protected void onResume() { //
super.onRestart();
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000,
10, this);
}
@Override
protected void onPause() { //
super.onPause();
locationManager.removeUpdates(this);
}
// Called when location has changed
public void onLocationChanged(Location location) { //
String text = String.format(
"Lat:\t %f\nLong:\t %f\nAlt:\t %f\nBearing:\t %f", location
.getLatitude(), location.getLongitude(), location.getAltitude(),
location.getBearing()); //
textOut.setText(text);
// Perform geocoding for this location
try {
List addresses = geocoder.getFromLocation(
location.getLatitude(), location.getLongitude(), 10); //
for (Address address : addresses) {
textOut.append("\n" + address.getAddressLine(0)); //
}
} catch (IOException e) {
Log.e("WhereAmI", "Couldn't get Geocoder data", e);
}
}
// Methods required by LocationListener
public void onProviderDisabled(String provider) {
}
public void onProviderEnabled(String provider) {
}
public void onStatusChanged(String provider, int status, Bundle extras) {
}
}
留意WhereAmI实现了LocationListener
。它是LocationManager
用来通知地址变更的接口。
指向LocationManager
的私有变量。
指向Geocoder的私有变量。
textOut是表示输出结果的TextView。
在当前上下文通过getSystemService()
获取LocationManager
的引用。有关当前上下文 的有关内容,可见Application Context
一节。
创建一个Geocoder的实例,将当前上下文传递给它。
还需要一段时间才可以获得新的位置信息。因此LocationManager
会缓存着上次得到的地址,可以减少用户的等待。
同前面一样,在进入Running状态之前也就是onResume`()
时注册到系统服务。在这里,通过requestLocationUpdates()
订阅位置服务的更新。
在离开Running状态之后也就是onPause()
时注销。
onLocationChanged()
是LocationManager
负责调用的一个回调方法,在位置信息发生变化时触发。
得到了Location对象。它里面有不少有用的信息,我们格式化一下,使之容易阅读。
只要得到Location对象,就可以检索"geocode"了,它可以将经纬度转换为实际的地址。
如果得到正确的地址,就输出到textOut。
LocationListener
接口中还有许多其它的回调方法,这里没有用到这些功能,因此留空。
13.2.1.3. Manifest文件
这个app的Manifest文件也很普通。需要留意的一个地方是,要注册LocationListener,必先获取相应的权限。GPS和网络是最常用的位置信息源,不过Android也预留了其它信息源的扩展接口——未来也可能有更好更精确的信息源出现。为此,Android将位置信息的权限分成了两级:精确位置(Fine Location)和近似位置(Coarse Location)。
例 13.5. AndroidManifest.xml
声明这个app使用了位置信息源。针对GPS,它的权限是android.permission.ACCESS_FINE_LOCATION
;针对无线网,它的权限是android.permission.ACCESS_COARSE_LOCATION
。
到这里,WhereAmI程序就已经完工了。它演示了通过LocationManager获取位置信息,并通过Geocoder将它转换为地理地址的方法。成品如 图 13.2. WhereAmI :
图 13.2. WhereAmI
13.3. 用上位置服务,重构Yamba
WhereAmI程序只是用来演示位置服务使用方法的小样例。接下来,我们将位置服务应用到Yamba里面。
13.3.1. 更新首选项
在将用户的位置信息广播出去之前,我们需要事先征得用户本人的同意。这不只是个人偏好,更牵涉到个人隐私。首选项就是询问用户本人意愿的一个好地方。在这里,我们可以使用ListPreference
。它可以提供一列选项,同时为每个选项对应一个值。
为此我们在strings.xml
中添加两列string资源:一列给用户看,表示选项的正文;一列表示每一选项对应的值。修改后的string.xml
文件如下:
...
- None, please
- GPS via satellites!
- Mobile Network will do
- NONE
- gps
- network
留意这两列string资源是一一对应的,因此其下的条目数相等。
已经有了选项的名与值,接下来修改prefs.xml
。
例 13.6. 修改过的 res/xml/prefs.xml
新加的ListPreference提供了三个选项:通过GPS、通过网络、不跟踪位置。
13.3.2. 重构YambaApplication
已经有了具体的选项条目可供用户选择,我们还需要将这一选项的值暴露在YambaApplication里面,好让应用的其它部分也可以访问它,尤其是StatusActivity。
在YambaApplication.java
里面简单添加一个getter方法即可:
例 13.7. YambaApplication.java
public class YambaApplication extends Application implements
OnSharedPreferenceChangeListener {
...
public static final String LOCATION_PROVIDER_NONE = "NONE";
...
public String getProvider() {
return prefs.getString("provider", LOCATION_PROVIDER_NONE);
}
}
getter函数准备就绪,接下来重构StatusActivity即可。
13.3.3. 重构StatusActivity
StatusActivity是位置信息。同WhereAmI一样,我们仍调用LocationManager的getSystemService()
,并注册到位置服务,订阅其更新。也同样实现一个LocationListener接口,也就意味着需要在Activity里面添加几个回调方法,好在位置变化时得到新的location对象。到下次更新状态的时候,即可在这里看到我们的位置信息。
例 13.8. StatusActivity.java
package com.marakana.yamba8;
import winterwell.jtwitter.Twitter;
import android.graphics.Color;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
public class StatusActivity extends BaseActivity implements OnClickListener,
TextWatcher, LocationListener { //
private static final String TAG = "StatusActivity";
private static final long LOCATION_MIN_TIME = 3600000; // One hour
private static final float LOCATION_MIN_DISTANCE = 1000; // One kilometer
EditText editText;
Button updateButton;
TextView textCount;
LocationManager locationManager; //
Location location;
String provider;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.status);
// Find views
editText = (EditText) findViewById(R.id.editText);
updateButton = (Button) findViewById(R.id.buttonUpdate);
updateButton.setOnClickListener(this);
textCount = (TextView) findViewById(R.id.textCount);
textCount.setText(Integer.toString(140));
textCount.setTextColor(Color.GREEN);
editText.addTextChangedListener(this);
}
@Override
protected void onResume() {
super.onResume();
// Setup location information
provider = yamba.getProvider(); //
if (!YambaApplication.LOCATION_PROVIDER_NONE.equals(provider)) { //
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); //
}
if (locationManager != null) {
location = locationManager.getLastKnownLocation(provider); //
locationManager.requestLocationUpdates(provider, LOCATION_MIN_TIME,
LOCATION_MIN_DISTANCE, this); //
}
}
@Override
protected void onPause() {
super.onPause();
if (locationManager != null) {
locationManager.removeUpdates(this); //
}
}
// Called when button is clicked
public void onClick(View v) {
String status = editText.getText().toString();
new PostToTwitter().execute(status);
Log.d(TAG, "onClicked");
}
// Asynchronously posts to twitter
class PostToTwitter extends AsyncTask {
// Called to initiate the background activity
@Override
protected String doInBackground(String... statuses) {
try {
// Check if we have the location
if (location != null) { //
double latlong[] = {location.getLatitude(), location.getLongitude()};
yamba.getTwitter().setMyLocation(latlong);
}
Twitter.Status status = yamba.getTwitter().updateStatus(statuses[0]);
return status.text;
} catch (RuntimeException e) {
Log.e(TAG, "Failed to connect to twitter service", e);
return "Failed to post";
}
}
// Called once the background activity has completed
@Override
protected void onPostExecute(String result) {
Toast.makeText(StatusActivity.this, result, Toast.LENGTH_LONG).show();
}
}
// TextWatcher methods
public void afterTextChanged(Editable statusText) {
int count = 140 - statusText.length();
textCount.setText(Integer.toString(count));
textCount.setTextColor(Color.GREEN);
if (count < 10)
textCount.setTextColor(Color.YELLOW);
if (count < 0)
textCount.setTextColor(Color.RED);
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
// LocationListener methods
public void onLocationChanged(Location location) { //
this.location = location;
}
public void onProviderDisabled(String provider) { //
if (this.provider.equals(provider))
locationManager.removeUpdates(this);
}
public void onProviderEnabled(String provider) { //
if (this.provider.equals(provider))
locationManager.requestLocationUpdates(this.provider, LOCATION_MIN_TIME,
LOCATION_MIN_DISTANCE, this);
}
public void onStatusChanged(String provider, int status, Bundle extras) { //
}
}
令StatusActivity
实现LocationListener
,也就是供LocationManager回调的接口。
定义几个私有变量,分别表示LocationManager、Location对象与位置信息源(Provider)。
获取YambaApplication对象提供的位置信息源(Provider)。使用哪个信息源由用户在首选项中决定。
检查用户是否愿意发布自己的位置信息。
如果检查通过,就使用getSystemService()获取位置信息。这个调用只是获得现有系统服务的引用,因此它的代价并不是很高昂。
如果可以,获取缓存中的位置信息。
注册到LocationManager
,订阅位置的更新。在这里,我们可以为位置变化指明一个时间间隔以及位置间距。我们只关心城市级别的位置变化,因此将间距的值定为一千米,时间间隔定为一小时(3,600,000微秒)。留意这只是给系统的一个提示,而非强制。
在用户发布消息时,检查当前是否有位置信息存在。如果有,就把它放在一个double类型的数组里,传给Twitter对象的setMyLocation()
方法。
实现LocationManager触发的回调方法,onLocationChanged()
。它在位置变化时触发,以一个新的Location对象作为参数。
这个方法在位置信息源不可用时触发。我们可以在这里注销订阅,以节约电能。
在位置信息源可用时,重新订阅位置信息的更新。
这个方法在位置信息源变化时触发,在此留空。
到这里,Yamba已经实现了位置更新的功能。用户可以设置首选项,按自己的意愿选择位置信息源,也可以关闭这个功能。
接下来看下另一个系统服务:Alarm服务,我们将通过它来触发IntentService。
13.4. IntentService
我们已对系统服务的工作方式有所了解,接下来可以利用系统服务的有关特性,简化UpdaterService的实现。回忆下前面的内容:UpdaterService会一直运行,定期从服务端抓取Timeline的更新。由于Service默认与UI在同一个线程(或者说,都在UI线程中执行)中执行,为避免网络操作造成界面的响应不灵,我们需要新建一个Updater线程,并将UpdaterService放在里面独立执行。然后在Service的onCreate()
与onStartCommand()
中让线程开始,并一直执行下去,直到onDestroy()
为止。另外,UpdaterService会在两次更新之间休眠一段时间。这是 第八章 Service 中的内容,但在这里,我们将介绍更简便的实现方法。
IntentService
是Service
的一个子类,也是通过startService()
发出的intent激活。与一般Service的不同在于,它默认在一个独立的 工人线程 (Worker Thread )中执行,因此不会阻塞UI线程。另一点,它一旦执行完毕,生命周期也就结束了。不过只要接到startService()
发出的Intent,它就会新建另一个生命周期,从而实现重新执行。在此,我们可以利用Alarm服务实现定期执行。
同一般的Service不同,我们不需要覆盖onCreate()
、onStartCommand()
、onDestroy()
及onBind()
,而是覆盖一个onHandleIntent()
方法。网络操作的相关代码就放在这里。另外IntentService要求我们实现一个构造函数,这也是与一般Service不同的地方。
简言之,除了创建一个独立线程使用普通的Service之外,我们可以使用IntentService在它自己的工人线程中实现同样的功能,而且更简单。到现在剩下的只是定期唤醒它,这就引出了AlarmManager
——另一个系统服务。
以上的原理就在于,IntentService
的onHandleIntent()
会在独立的线程中执行。
例 13.9. UpdaterService.java,基于IntentService
package com.marakana.yamba8;
import android.app.IntentService;
import android.content.Intent;
import android.util.Log;
public class UpdaterService1 extends IntentService { //
private static final String TAG = "UpdaterService";
public static final String NEW_STATUS_INTENT = "com.marakana.yamba.NEW_STATUS";
public static final String NEW_STATUS_EXTRA_COUNT = "NEW_STATUS_EXTRA_COUNT";
public static final String RECEIVE_TIMELINE_NOTIFICATIONS = "com.marakana.yamba.RECEIVE_TIMELINE_NOTIFICATIONS";
public UpdaterService1() { //
super(TAG);
Log.d(TAG, "UpdaterService constructed");
}
@Override
protected void onHandleIntent(Intent inIntent) { //
Intent intent;
Log.d(TAG, "onHandleIntent'ing");
YambaApplication yamba = (YambaApplication) getApplication();
int newUpdates = yamba.fetchStatusUpdates();
if (newUpdates > 0) { //
Log.d(TAG, "We have a new status");
intent = new Intent(NEW_STATUS_INTENT);
intent.putExtra(NEW_STATUS_EXTRA_COUNT, newUpdates);
sendBroadcast(intent, RECEIVE_TIMELINE_NOTIFICATIONS);
}
}
}
继承自IntentService,而非Service。
这里需要一个构造函数。在这里,可以给Service一个命名,以便于在观测工具(比如TraceView)中,辨认相应的线程。
这是关键的方法。其中的代码会在独立的工人线程中执行,因此不会影响到主UI线程的响应。
下面将所做的变化广播出去,具体可见 第十一章 中的 "广播 Intent" 一节。
这样,我们重构了原先的Service。要测试它,不妨先把Start/Stop Service的按钮改为Refresh按钮。为此需要修改menu.xml
添加这个新条目,然后在BaseActivity类中修改它的处理方法。
例 13.10. res/xml/menu.xml
...
把itemToggle
改为itemRefresh
,这样更合适一些。同时必须更新string.xml
中对应的条目。
接下来修改BaseActivity.java
文件,添加这个按钮的处理方法,也就是修改onOptionsItemSelected()
中对应的case语句。同时onMenuOpened()
已经没有用处了,删掉即可。
例 13.11. BaseActivity.java,添加Refresh按钮之后
public class BaseActivity extends Activity {
...
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
...
case R.id.itemRefresh:
startService(new Intent(this, UpdaterService.class)); //
break;
...
}
return true;
}
...
}
简单地发送一个Intent,启动UpdaterService。
这样选项菜单就多了一个Refresh按钮,可以启动一个Service让它在后台更新消息。测试的话,点击这个按钮就好。
实现同样的功能还有一个选择,那就是AsyncTask。在设计角度讲,使用AsyncTask甚至可能比IntentService更加合适——它使得这些功能在UI层面即可全部实现,这在 第六章 中的 Android的线程机制 已有提及。不过在这里,我们更希望了解IntentService的工作方式,通过演示不难看出,它的功能并不弱于普通的Service。
接下来需要做的,就是定时触发UpdaterService。因此引入AlarmManager。
13.4.1. AlarmManager
在重构之前,UpdaterService是一个普通的Service,一直处于运行状态,每执行一次网络操作就休眠一段时间然后循环;重构之后改用了IntentService,每收到startService()发出的Intent只会执行一次,随即结束。因此,我们需要一个办法定期发送Intent来启动它。
这就引出了Android的另一个系统服务——Alarm服务。它由AlarmManager
控制,允许你在一定时间之后触发某操作。它可以是一次结束,也可以是周期重复,因此可以利用它来定时启动我们的Service。Alarm服务通知的事件都是以Intent形式发出,或者更准确一点,以PendingIntent的形式。
13.4.1.1. PendingIntent
PendingIntent是Intent与某操作的组合。一般是把它交给他人,由他人负责发送,并在未来执行那项操作。发送Intent的方法并不多,创建PendingIntent的方法也是如此——只有PendingIntent类中的几个静态方法,在创建的同时也将相关的操作绑定了。回忆一下,同为发送Intent,启动Activity靠startActivity()
,启动Service靠startService()
,发送广播靠sendBroadcast()
,而创建对应于startService()
操作的PendingIntent,则靠它的静态方法getService()
。
已经知道了如何交代他人发送Intent,也知道了如何通过Alarm服务实现定期执行。接下来就是找个合适的地方,将两者结合起来。而这个地方就是BootReceiver
。
在编写相关的代码之前,我们先增加一条首选项:
13.4.2. 添加Interval选项
例 13.12. strings.xml,添加Interval选项相关的string-array
...
- Never
- Fifteen minutes
- Half hour
- An hour
- Half day
- Day
- 0
- 900000
- 1800000
- 3600000
- 43200000
- 86400000
不同的选项名。
选项名对应的值。
有了这两列数组,接下来修改prefs.xml添加Interval(时间间隔)选项。
例 13.13. prefs.xml,添加Interval选项
...
这个选项条目是ListPreference
。它显示一列单选选项,其中的文本由android:entries
表示,对应的值由android:entryValues
表示。
接下来修改BootReceiver,添加Alarm服务相关的代码。
13.4.3. 修改BootReceiver
回忆下前面的"BootReceiver"一节。BootReceiver在设备开机时唤醒,先前我们利用这一特性,在这里启动UpdaterService,而UpdaterService将一直处于运行状态。但是到现在,UpdaterService会一次性退出,这样已经不再适用了。
适用的是,使用Alarm服务定时发送Intent启动UpdaterService。为此,我们需要得到AlarmManager的引用,创建一个PendingIntent负责启动UpdaterService,并设置一个时间间隔。要通过PendingIntent启动Service,就调用PendingIntent.getService()
。
例 13.14. BootReceiver.java,通过Alarm服务定时启动UpdaterService
package com.marakana.yamba8;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent callingIntent) {
// Check if we should do anything at boot at all
long interval = ((YambaApplication) context.getApplicationContext())
.getInterval(); //
if (interval == YambaApplication.INTERVAL_NEVER) //
return;
// Create the pending intent
Intent intent = new Intent(context, UpdaterService.class); //
PendingIntent pendingIntent = PendingIntent.getService(context, -1, intent,
PendingIntent.FLAG_UPDATE_CURRENT); //
// Setup alarm service to wake up and start service periodically
AlarmManager alarmManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE); //
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, System
.currentTimeMillis(), interval, pendingIntent); //
Log.d("BootReceiver", "onReceived");
}
}
YambaApplication
对象中有个简单的getter方法,可以返回interval选项的值。
检查用户设置,如果interval的值为INTERVAL_NEVER
(零),那就不检查更新。
这个Intent负责启动UpdaterService。
将Intent与启动Service的操作绑定起来,创建新的PendingIntent。-1
表示没有使用requestCode。最后一个参数表示这个Intent是否已存在,我们不需要重新创建Intent,因此仅仅更新(update)当前已有的Intent。
通过getSystemService()
获取AlarmManager的引用。
setInexactRepeating()
表示这个PendingIntent将被重复发送,但并不关心时间精确与否。ELAPSED_REALTIME表示Alarm仅在设备活动时有效,而在待机时暂停。后面的参数分别指Alarm的开始时间、interval、以及将要执行的PendingIntent。
接下来可以将我们的程序安装在设备上(这就安装了更新的BootReceiver),并重启设备。在设备开机时,logcat即可观察到BootReceiver启动,并通过Alarm服务启动了UpdaterService。
13.5. 发送通知
到这里再体验一个系统服务——那就是Notification服务。前面我们花了大功夫让UpdaterService在后台运行,并定期抓取消息的最新更新。但是如果用户压根注意不到它,那么这些工作还有什么意义?Android的标准解决方案就是,在屏幕顶部的通知栏弹出一个通知。而实现这一功能,需要用到Notification服务。
收到新消息,由UpdaterService首先得知,因此我们把通知的功能加入到UpdaterService
类里:先获取Notification服务的引用,创建一个Notification对象,然后将收到消息的相关信息作为通知交给它。而Notification对象本身带有一个PendingIntent,当用户点击通知的时候,可以将界面转到Timeline界面。
例 13.15. UpdaterService.java,利用Notification服务
package com.marakana.yamba8;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.util.Log;
public class UpdaterService extends IntentService {
private static final String TAG = "UpdaterService";
public static final String NEW_STATUS_INTENT = "com.marakana.yamba.NEW_STATUS";
public static final String NEW_STATUS_EXTRA_COUNT = "NEW_STATUS_EXTRA_COUNT";
public static final String RECEIVE_TIMELINE_NOTIFICATIONS = "com.marakana.yamba.RECEIVE_TIMELINE_NOTIFICATIONS";
private NotificationManager notificationManager; //
private Notification notification; //
public UpdaterService() {
super(TAG);
Log.d(TAG, "UpdaterService constructed");
}
@Override
protected void onHandleIntent(Intent inIntent) {
Intent intent;
this.notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); //
this.notification = new Notification(android.R.drawable.stat_notify_chat,
"", 0); //
Log.d(TAG, "onHandleIntent'ing");
YambaApplication yamba = (YambaApplication) getApplication();
int newUpdates = yamba.fetchStatusUpdates();
if (newUpdates > 0) {
Log.d(TAG, "We have a new status");
intent = new Intent(NEW_STATUS_INTENT);
intent.putExtra(NEW_STATUS_EXTRA_COUNT, newUpdates);
sendBroadcast(intent, RECEIVE_TIMELINE_NOTIFICATIONS);
sendTimelineNotification(newUpdates); //
}
}
/**
* Creates a notification in the notification bar telling user there are new
* messages
*
* @param timelineUpdateCount
* Number of new statuses
*/
private void sendTimelineNotification(int timelineUpdateCount) {
Log.d(TAG, "sendTimelineNotification'ing");
PendingIntent pendingIntent = PendingIntent.getActivity(this, -1,
new Intent(this, TimelineActivity.class),
PendingIntent.FLAG_UPDATE_CURRENT); //
this.notification.when = System.currentTimeMillis(); //
this.notification.flags |= Notification.FLAG_AUTO_CANCEL; //
CharSequence notificationTitle = this
.getText(R.string.msgNotificationTitle); //
CharSequence notificationSummary = this.getString(
R.string.msgNotificationMessage, timelineUpdateCount);
this.notification.setLatestEventInfo(this, notificationTitle,
notificationSummary, pendingIntent); //
this.notificationManager.notify(0, this.notification);
Log.d(TAG, "sendTimelineNotificationed");
}
}
指向NotificationManager
的私有类成员,也就通过它来访问Notification系统服务。
创建一个Notification对象供类成员访问,每得到新消息需要发送通知时就更新它。
调用getSystemService()
获取Notification服务。
创建Notification对象,在稍后使用。在这里先给它指明一个图标,至于通知的文本和时间戳(timestamp)则暂时留空。
私有方法sendTimelineNotification()
,在得到新消息时调用它发送通知。
当用户收到通知并点击通知条目时,切换到Timeline界面,这个PendingIntent也随之销毁。
更新Notification对象的相关信息。这里是最新消息的timestamp。
要求NotificationManager
在用户点击时关闭这项通知,届时通知栏将不再显示这项通知。
为Notification对象提供标题(title)与简介(summary),都是取自string.xml
中的字符串。留意R.string.msgNotificationMessage
里面留了一个参数,表示新消息的数目,因此我们可以使用String.format()
。
最后,要求NotificationManager
发送这条通知。在这里,我们不需要通知的ID,因此留0。ID是引用Notification对象的标识符,通常用来关闭一个通知。
13.6. 总结
到这里,我们已经观察了几个系统服务:传感器服务、位置服务、Alarm服务以及Notification服务。除此之外,Android还提供了很多其它的系统服务,篇幅所限未能提及。但你可以留意到它们之间的共性,有一些自己的总结。另外,我们在重构UpdaterService时,也用到了IntentService
与PendingIntent
。
14. Android接口描述语言
Android中的每个应用程序都运行于独立的进程中。出于安全考虑,程序不可以直接访问另一个程序中的内容。但不同程序之间交换数据是允许的,为此Android提供了一系列的通信机制。其中之一是前面我们提到的Intent,它是一种异步的机制,在发送时不必等待对方响应。
不过有时我们需要更直接一些,同步地访问其它进程中的数据。这类通信机制就叫做 进程间通信(Interprocess Communication) ,简称IPC。
为实现进程间通信,Android设计了自己的IPC协议。由于许多细节需要考虑,因此IPC机制在设计上总是趋于复杂,其中的难点就在于数据的传递。比如在远程方法调用的参数传递,需要将内存中的数据转化成易于传递的格式,这被称作 序列化(Marshaling) ,反过来,接收参数的一方也需要将这种格式转换回内存中的数据,这被称作 反序列化(Unmarshaling) 。
为了简化开发者的工作,Android提供了 Android接口描述语言(Android Interface Definition Language) ,简称AIDL。这是一个轻量的IPC接口,使用Java开发者熟悉的语法形式,自动生成进程间通信中间繁复的代码。
作为展示AIDL实现进程间通信的例子,我们将新建两个应用程序:一个是LogService,作为服务端;一个是LogClient,作为客户端与远程的LogService绑定。
14.1. 实现远程Service
LogService的功能很简单,就是接收并记录客户端发来的日志信息。
首先申明远程Service的接口。接口就是API,表示Service对外提供的功能。我们使用AIDL语言编写接口,并保存到Java代码的相同目录之下,以.aidl
为扩展名。
AIDL的语法与Java的接口(interface)十分相似,都是在里面给出方法的声明。不同在于,AIDL中允许的数据类型与一般的Java接口不完全一样。AIDL默认支持的类型有:Java中的基本类型,以及String、List、Map以及CharSequence等内置类。
要在AIDL中使用自定义类型(比如一个类),你就必须让它实现Parcelable
接口,允许Android运行时对它执行序列化/反序列化才行。在这个例子中,我们将创建一个自定义类型Message。
14.1.1. 编写AIDL
先定义Service的接口。如下可见,它与一般的Java接口十分相似。有CORBA经验的读者肯定可以认出AIDL与CORBA的IDL之间的渊源。
例 14.1. ILogService.aidl
package com.marakana.logservice; //
import com.marakana.logservice.Message; //
interface ILogService { //
void log_d(String tag, String message); //
void log(in Message msg); //
}
同Java一样,AIDL代码也需要指明它所在的package;
不同在于,即使在同一个package中,我们仍需显式地导入其它的AIDL定义。
指定接口的名字。按照约定,名字以I字母开头。
这个方法很简单,只有表示输入的参数而没有返回值。留意String不是Java的基本类型,不过AIDL把它当作基本类型对待。
这个方法取我们自定义的Message对象作为输入参数。它的定义在后面给出。
接下来查看对应Message
的AIDL实现。
例 14.2. Message.aidl
package com.marakana.logservice; //
/* */
parcelable Message;
指明所在的package。
声明Message是一个可序列化(parcelable)的对象。这个对象将在Java中定义。
到这里我们已经完成了AIDL部分。在你保存文件的同时,Eclipse会自动生成服务端相关代码的 占位符(stub) 。它像是完整的一个服务端,可以接受客户端的请求,但实际上里面并没有任何业务逻辑,具体的业务逻辑还需要我们自己添加,这就是为什么称它为“占位符”。新生成的Java文件位于Gen
目录之下,地址是/gen/com/marakana/logservice/LogService.java
。因为这些代码是根据AIDL生成的,因此不应再做修改。正确的做法是,修改AIDL文件,然后使用Android SDK的aidl工具重新生成。
有了AIDL和生成的Java stub文件,接下来实现这个Service。
14.1.2. 实现Service
同Android中的其它Service一样,LogService
也必须以系统中的Service
类为基类。但不同在于,在这里我们忽略了onCreate()
、onStartCommand()
、onDestroy()
几个方法,却提供了onBind()
方法的实现。远程Service中的方法开始于客户端发出的请求,在客户端看来,这被称作“绑定(bind)到Service”。而对服务端而言,就是在这时触发Service类的onBind()
方法。
在远程Service的实现中,onBind()
方法需要返回一个IBinder对象表示远程Service的一个实现。为实现IBinder
,我们可以继承自动生成的ILogervice.Stub
类。同时在里面提供我们自定义的AIDL方法,也就是几个log()
方法。
例 14.3. LogService.java
package com.marakana.logservice;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
public class LogService extends Service { //
@Override
public IBinder onBind(Intent intent) { //
final String version = intent.getExtras().getString("version");
return new ILogService.Stub() { //
public void log_d(String tag, String message) throws RemoteException { //
Log.d(tag, message + " version: " + version);
}
public void log(Message msg) throws RemoteException { //
Log.d(msg.getTag(), msg.getText());
}
};
}
}
LogService
以Android框架的Service
为基类。前面已经见过不少Service,但这里与众不同。
它是个Bound Service,因此必须提供onBind()
的实现,使之返回一个IBinder的实例。参数是客户端传来的Intent,里面包含一些字符串信息。在客户端的实现部分,我们将介绍在Intent中附带少量数据并传递给远程Service的方法。
IBinder
的实例由ILogService.Stub()
方法生成,它是自动生成的stub文件中的一个辅助方法,位置在/gen/com/marakana/logservice/LogService.java
。
log_d()是个简单的方法,它取两个字符串作参数,并记录日志。简单地调用系统的Log.d()
即可。
还有一个log()
方法,它取可序列化的Message
对象做参数。在此提取出Tag与消息内容,调用Android框架的Log.d()
方法。
现在我们已经实现了Java的Service部分,接下来实现可序列化的Message对象。
14.1.3. 实现一个Parcel
进程间传递的Message也是个Java对象,在传递与接收之间我们需要额外进行编码/解码——也就是序列化/反序列化。在Android中,可以序列化/反序列化的对象就被称作 Parcel ,作为Parcelable 接口的实例。
作为Parcel
,对象必须知道如何处理自身的编码/解码。
例 14.4. Message.java
package com.marakana.logservice;
import android.os.Parcel;
import android.os.Parcelable;
public class Message implements Parcelable { //
private String tag;
private String text;
public Message(Parcel in) { //
tag = in.readString();
text = in.readString();
}
public void writeToParcel(Parcel out, int flags) { //
out.writeString(tag);
out.writeString(text);
}
public int describeContents() { //
return 0;
}
public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { //
public Message createFromParcel(Parcel source) {
return new Message(source);
}
public Message[] newArray(int size) {
return new Message[size];
}
};
// Setters and Getters
public String getTag() {
return tag;
}
public void setTag(String tag) {
this.tag = tag;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
如前所说,Message
类需实现Parcelable
接口。
作为Parcelable
接口的实现,类中必须相应地提供一个构造函数,使之可以根据Parcel重新构造原先的对象。在这里,我们将Parcel中的数据存入局部变量,注意其中的顺序很重要,必须与写入时的顺序保持一致。
writeToParcel()
方法对应于构造函数。它读取对象当前的状态,并写入Parcel。同上,变量的写入顺序必须与前面读取的顺序保持一致。
此方法留空。因为这个Parcel中没有别的特殊对象。
每个Parcelable对象必须提供一个Creator。这个Creator用于依据Parcel重新构造原先的对象,在此仅仅调用其它方法。
一些私有数据的getter/setter方法。
至此服务端的Java部分已告一段落,接下来将Service注册到Manifest文件。
14.1.4. 注册到Manifest文件
只要新加入了一个构件,就需要将它注册到系统。而注册到系统的最简单方法,就是注册到Manifest文件。
同前面注册UpdaterService一样,这里也是通过标签表示Service。不同在于这里的Service将被远程调用,因此它负责响应的Action也有不同,这在标签下的标签中指明。
在这里给出Service的定义。
这个Service与UpdaterService的不同在于,它为远程客户端所调用,但客户端那边并没有这个类,所以不能通过类名直接调用。客户端能做的是发送Intent,使之作出响应。为此,通过与设置相应的过滤规则。
到这里,Service已经完成。下面是客户端的实现。
14.2. 实现远程客户端
我们已经有了远程Service,接下来实现它的客户端,然后测试两者是否工作正常。我们这里有意将服务端与客户端分在两个不同的package中,因为它们是两个独立的程序。
好,在Eclipse中新建一个项目,步骤同以前一样,兹不赘述。不过这里有一点不同,那就是它依赖于前一个项目,也就是LogService。这一点很重要,因为LogClient需要知道LogService的接口,这就在AIDL文件。在Eclipse 中添加依赖的步骤如下:
创建LogClient项目之后,在Package Explorer中右击该工程,选择Properties。
在Properties for LogClient
对话框,选择Java Build Path
,然后选择Properties标签。
在这一标签中,单击Add…
指向LogService工程。
以上,即将LogService加入LogService的工程依赖。
14.2.1. 绑定到远程Service
客户端可以是一个Activity,这样我们可以在图形界面中看出它的工作状况。在这个Activity中,我们将绑定到远程Service,随后就可以像一个本地的Service那样使用它了。Android的Binder将自动处理其间的序列化/反序列化操作。
绑定操作是异步的,我们先发出请求,至于具体的操作可能会在稍后进行。为此,我们需要一个回调机制来响应远程服务的连接和断开。
连接上Service之后,我们就可以像本地对象那样使用它了。不过,要传递复杂的数据类型(比如自定义的Java对象),就必须把它做成Parcel。对我们的Message类型而言,它已经实现了Parcelable接口,拿来直接使用即可。
例 14.5. LogActivity.java
package com.marakana.logclient;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import com.marakana.logservice.ILogService;
import com.marakana.logservice.Message;
public class LogActivity extends Activity implements OnClickListener {
private static final String TAG = "LogActivity";
ILogService logService;
LogConnection conn;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Request bind to the service
conn = new LogConnection(); //
Intent intent = new Intent("com.marakana.logservice.ILogService"); //
intent.putExtra("version", "1.0"); //
bindService(intent, conn, Context.BIND_AUTO_CREATE); //
// Attach listener to button
((Button) findViewById(R.id.buttonClick)).setOnClickListener(this);
}
class LogConnection implements ServiceConnection { //
public void onServiceConnected(ComponentName name, IBinder service) { //
logService = ILogService.Stub.asInterface(service); //
Log.i(TAG, "connected");
}
public void onServiceDisconnected(ComponentName name) { //
logService = null;
Log.i(TAG, "disconnected");
}
}
public void onClick(View button) {
try {
logService.log_d("LogClient", "Hello from onClick()"); //
Message msg = new Message(Parcel.obtain()); //
msg.setTag("LogClient");
msg.setText("Hello from inClick() version 1.1");
logService.log(msg); //
} catch (RemoteException e) { //
Log.e(TAG, "onClick failed", e);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroyed");
unbindService(conn); //
logService = null;
}
}
LogConnection
用于处理远程Service的连接/断开。具体将在后面介绍。
它是一个Action Intent,用于连接到远程Service。它必须与Manifest文件中LogService
相应
部分的Action相匹配。
把数据加入Intent,它可以在另一端解析出来。
bindService()请求Android运行时将这个Action绑定到相应的远程Service。在Intent之余,我们也传递一个远程Service的ServiceConnection
对象,用以处理实际的连接/断开操作。
LogConnection
用来处理远程Service的连接/断开。它需要以ServiceConnection
为基类,提供onServiceConnected()
与onServiceDisconnected()
的实现。
我们需要将这个Bound Service转换为LogService
类型。为此,调用辅助函数ILogService.Stub.asInterface()
。
onServiceDisconnected()
在远程Service失效时触发。可以在里面执行一些必要的清理工作。在这里,我们仅仅将logService
设为null,方便垃圾收集器的回收。
如果成功绑定了远程Service,那就可以像调用本地方法那样调用它了。在前面LogService的实现部分曾提到,logService.log_d()
所做的工作就是将两个字符串传递给log_d()
。
如前所说,如果需要在远程调用中传递自定义对象,那就必须先把它做成Parcel。既然Message
已经是Parcelable对象,将它作为参数传递即可。
得到Parcel之后,简单调用logService.log()
方法,将它传递给LogService
。
远程调用可能会很不稳定,因此最好加入一个针对RemoteException
的错误处理。
在Activity将要销毁之前,解除Service的绑定以释放资源。
到这里,客户端已经完工。它的界面很简单,只有一个触发onClick()
调用的按钮。只要用户点击按钮,我们的客户端程序就会调用一次远程Service。
14.2.2. 测试运行
尝试在Eclipse中运行客户端。Eclipse知道LogClient与LogService之间的依赖关系,因此会在设备中同时安装这两个package。客户端程序启动之后,应该会绑定到Service。尝试点击按钮,检查LogServic的日志操作。adb中的logcat输出应如下:
...
I/LogActivity( 613): connected
...
D/LogClient( 554): Hello from onClick() version: 1.0
D/LogClient( 554): Hello from inClick() version 1.1
...
第一行来自客户端的LogConnection
,表示成功绑定到远程Service。后两行来自于远程Service,一个来自LogService.log_d()
,取两个字符串做参数;另一个来自LogService.log()
,取一个Message
对象的Parcel做参数。
在adb shell中运行ps命令查看设备中的进程,你将可以看到两个条目,分别表示客户端与服务端。
app_43 554 33 130684 12748 ffffffff afd0eb08 S com.marakana.logservice
app_42 613 33 132576 16552 ffffffff afd0eb08 S com.marakana.logclient
由此可以证明,客户端与服务端是两个独立的应用程序。
14.3. 总结
Android提供了一套高性能的基于共享内存的进程间通信机制,这就是Bound Service。通过它可以创建出远程的Service:先在Android接口描述语言(AIDL)中给出远程接口的定义,随后实现出这个接口,最后通过IBinder
对象将它们连接起来。这样就允许了无关的进程之间的相互通信。
15. Native Development Kit (NDK)
Native Development Kit(本地代码开发包 ,简称NDK) ,是Android与本地代码交互的工具集。通过NDK,你可以调用平台相关的本地代码或者一些本地库。而这些库一般都是由C或者C++编写的。
在Android的Gingerbread版本中,NDK引入了NativeActivity,允许你使用纯C/C++来编写Activity。不过,NativeActivity
并不是本章的主题。在本章,我们将主要介绍如何在应用程序中集成原生的C代码。
(译者注:Gingerbread即Android 2.3)
15.1. NDK是什么
调用本地代码的主要目的就是提升性能。如你所见,在一些底层的系统库之外,NDK更提供有许多数学与图像相关库的支持。在图像处理以及运算密集型的程序上,使用NDK可以显著提高性能。值得一提的是,最近手机游戏行业的快速发展,也大大地推进了NDK的应用。
通过JNI调用的本地代码也同样在Dalvik VM中执行,与Java代码共处于同一个沙盒,遵循同样的安全策略。因此,试图通过C/C++来完成一些"Java无法做到的"破坏性工作是不被推荐的。Android已经提供了一套完整且优雅的API,底层的硬件也得到了很好的封装。身为应用开发者,一般并没有干涉底层的必要。
15.2. NDK 的功能
在本地程序的开发中,开发者会遇到许多常见问题,而NDK正是针对这些问题设计的。
15.2.1. 工具链
Java通过JNI(Java Native Interface) 来访问本地代码。你需要将代码编译为目标架构的机器指令,这就需要在开发机上搭建一个合适的构建环境。不过可惜的是,搭建一套合适的交叉编译环境绝非易事。
NDK提供了交叉编译所需的完整工具链。通过它的构建系统,你可以很轻松地将本地代码集成到应用中。
15.2.2. 打包库文件
如果应用程序需要加载某个本地库,那就必须保证系统能够找到它。在Linux下,它们通常都是通过LD_LIBRARY_PATH
中定义的路径找到。在Android中,这个环境变量中只包含一个路径,即system/lib
。这里存在一个问题,那就是/system
分区一般都是只读的,应用程序无法将自己的本地库安装到/system/lib
。
为解决这一问题,NDK使用了这样的机制,那就是将本地库打包进APK文件,当用户安装含有本地库的APK文件时,系统将在/data/data/your_package/ lib创建一个目录,用来存放应用程序的本地库,而不允许其它程序访问。这样的打包机制简化了Android的安装过程,也避免了本地库的版本冲突。此外,这也大大地扩展了Android的功能。
15.2.3. 文档与标准头文件
NDK拥有完善的文档和充分的样例,也附带有标准的C/C++头文件,比如:
libc(C 运行时库 )的头文件
libm(数学库 )的头文件
JNI 接口的头文件
libz(zlib压缩库 )的头文件
liblog(Android 日志系统 )的头文件
OpenGL ES 1.1和 OpenGL ES 2.0(3D图像库 )的头文件
libjnigraphics(图像库 )的头文件(仅在Android 2.2及以上版本中可用)
最小化的 C++ 头文件
OpenSL ES本地音频库的头文件
Android本地程序API
通过这些头文件,我们能够对NDK的适用范围有个大致了解。具体内容我们将在后面详细讨论。
15.3. NDK实例: 计算菲波那契数列
前面提到,NDK适用于计算密集型的应用程序。我们不妨取一个简单的算法,分别在Java和C中实现,然后比较它们的运行速度。
于是,我选择了计算 菲波那契数列 的算法作为实例。它足够简单,通过C和Java实现都不困难。另外在实现方式上,也有递归和迭代两种方式可供选择。
在编写代码之前,先了解下菲波那契数列 的定义:
fib(0)=0
fib(1)=1
fib(n)=fib(n-1)+fib(n-2)
它看起来会像是这样: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...
在这个例子中,我们将要:
新建一个 Java 类来表示菲波那契库。
生成本地代码的头文件。
使用 C 来实现算法。
编译并创建共享库。
在 Android 应用中调用这个库。
15.3.1. FibLib
FibLib 中定义了菲波那契数列 的4种算法:
Java递归版本
Java迭代版本
本地递归版本
本地迭代版本
我们从简入手,先提供Java版本的实现,稍后再实现C的本地版本。
例 15.1. FibLib.java
package com.marakana;
public class FibLib {
// Java implementation - recursive
public static long fibJ(long n) { //
if (n <= 0)
return 0;
if (n == 1)
return 1;
return fibJ(n - 1) + fibJ(n - 2);
}
// Java implementation - iterative
public static long fibJI(long n) { //
long previous = -1;
long result = 1;
for (long i = 0; i <= n; i++) {
long sum = result + previous;
previous = result;
result = sum;
}
return result;
}
// Native implementation
static {
System.loadLibrary("fib"); //
}
// Native implementation - recursive
public static native long fibN(int n); //
// Native implementation - iterative
public static native long fibNI(int n); //
}
Java 递归版本的菲波那契算法
Java 迭代版本的菲波那契算法,与迭代相比省去了递归的开销。
本地版本将在共享库中实现,这里只是告诉 JVM 在需要时加载这个库。
定义了对应的函数,但是并不实现他们。注意这里使用了 native
关键字。它告诉 JVM 该函数的代码在共享库中实现。在调用这个函数前应该加载对应的库。
这是迭代的版本。
到这里,FibLib.java已经编写完毕,但上面的native
方法还没有实现。接下来就是用C实现本地部分了。
15.3.2. JNI 头文件
接下来需要做的首先是,创建相应的JNI头文件。这需要用到一个Java的标准工具,也就是javah。它附带在JDK中,你可以在JDK/bin
中找到它。
跳转到项目的bin
目录,执行:
[Fibonacci/bin]> javah -jni com.marakana.FibLib
javah -jni
取一个类名作为参数。需要注意,不是所有的类都默认处于Java的Classpath中,因此切换到项目的bin目录再执行这条命令是最简单的方法。在这里,我们假设当前目录已经在Classpath中,保证javah -jni com.marakana.FibLib
能够正确地加载对应的类。
上述命令会生成一个文件com_marakana_FibLib.h
,它就是供我们引用的头文件。
在编写本地代码之前,我们需要先整理一下项目目录。Eclipse可以帮助我们完成许多工作,但它还没有提供NDK的相关支持,因此这些步骤需要手工完成。
首先,在项目目录下新建一个目录jni
,用来存放本地代码和相关的文件。你可以在Eclipse的Package Explorer中右击FibLib项目,选择 New→Folder
。
然后把新生成的头文件移动到这个目录:
[Fibonacci/bin]> mv com_marakana_FibLib.h ../jni/
看一下刚刚生成的文件:
include::code/Fibonacci/jni/com_marakana_FibLib.h
这个文件是自动生成的,我们不需要也不应手工修改它。你可以在里面见到待实现函数的签名:
...
JNIEXPORT jlong JNICALL Java_com_marakana_FibLib_fibN
(JNIEnv *, jclass, jlong);
...
JNIEXPORT jlong JNICALL Java_com_marakana_FibLib_fibNI
(JNIEnv *, jclass, jlong);
...
这便是JNI的标准签名。它有着某种命名规范,从而将这些函数和com.marakana.FibLib
中的native
方法关联起来。可以留意其中的返回值是jlong
,这是JNI中标准的整数类型。
这些参数也同样有意思:JNIEnv
、jclass
、jlong
。所有的JNI函数都至少有着前两个参数:第一个参数JNIEnv
指向Java VM的运行环境,第二个参数则表示着函数属于的类或者实例。如果属于类,则类型为jclass
,表示它是一个类方法;如果属于实例,则类型为jobject
,表示它是一个实例方法。第三个参数jlong
,则表示菲波那契算法的输入,也就是参数n
。
好,头文件已准备完毕,接下来就可以实现C函数了。
15.3.3. C 函数实现
我们需要新建一个C文件来存放本地代码。简单起见,我们将这个文件命名为fib.c
,和刚才生成的头文件保持一致,同样放置在jni
目录中。右击jni
目录,选择New→File
,并保存为fib.c
。
Note:
在你打开C文件时,Eclipse可能会调用外部编辑器而不是在自己的编辑窗口中打开。这是因为用于Java开发的Eclipse还没有安装C开发工具的支持。要解决这个问题,你可以通过Help→Install New Software
为Eclipse安装C的开发工具支持,也可以通过右键菜单Open With→Text Editor
强制在Eclipse中打开。
C版本的菲波那契算法与Java版本差异不大。下面就是fib.c
中的代码:
例 15.2. jni/fib.c
#include "com_marakana_FibLib.h" /* */
/* Recursive Fibonacci Algorithm */
long fibN(long n) {
if(n<=0) return 0;
if(n==1) return 1;
return fibN(n-1) + fibN(n-2);
}
/* Iterative Fibonacci Algorithm */
long fibNI(long n) {
long previous = -1;
long result = 1;
long i=0;
int sum=0;
for (i = 0; i <= n; i++) {
sum = result + previous;
previous = result;
result = sum;
}
return result;
}
/* Signature of the JNI method as generated in header file */
JNIEXPORT jlong JNICALL Java_com_marakana_FibLib_fibN
(JNIEnv *env, jclass obj, jlong n) {
return fibN(n);
}
/* Signature of the JNI method as generated in header file */
JNIEXPORT jlong JNICALL Java_com_marakana_FibLib_fibNI
(JNIEnv *env, jclass obj, jlong n) {
return fibNI(n);
}
导入头文件com_marakana_FibLib.h
。这个文件是前面通过 javah -jni com.marakana.FibLib
命令生成的。
菲波那契算法的递归实现,与 Java 中的版本很相似。
菲波那契算法的迭代实现。
JNI中定义的接口函数,与com_marakana_FibLib.h
的函数签名相对应。在这里我们为它加上变量名,然后调用前面定义的C函数。
同上。
菲波那契算法的C部分已经实现,接下来就是构建共享库了。为此,我们需要一个合适的Makefile。
15.3.4. Makefile
要编译本地库,那就需要在Android.mk
中给出编译项目的描述。如下:
例 15.3. jni/Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := fib
LOCAL_SRC_FILES := fib.c
include $(BUILD_SHARED_LIBRARY)
它是Android构建系统的一部分。我们在这里给出了输入文件(fib.c
)和输出(fib
模块)的定义。其中模块名会根据系统平台的不同而采用不同的命名规范,比如在ARM平台上,输出的文件名就是libfib.so
。
这就写好了Makefile,随后构建即可。
15.3.5. 构建共享库
假定你已经安装好了NDK,解下来就可以构建共享库了:切换到项目目录,执行ndk /ndk-build
即可。其中ndk 表示你的NDK安装目录。
构建完成之后,你可以见到一个新的子目录lib
,里面放有刚刚生成的共享库文件。
Note:
共享库默认是面向ARM平台构建,这样可以方便在仿真器上运行。
在下一节,我们将共享库打包进APK文件,供应用程序调用。
15.3.6. FibActivity
FibActivity允许用户输入一个数字,分别运行四种算法计算这个值对应的菲波那契数,并将不同算法花费的时间输出,供我们比较。这个Activity需要调用到FibLib
和linfib.so
。
例 15.4. FibActivity.java
package com.marakana;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
public class Fibonacci extends Activity implements OnClickListener {
TextView textResult;
Button buttonGo;
EditText editInput;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Find UI views
editInput = (EditText) findViewById(R.id.editInput);
textResult = (TextView) findViewById(R.id.textResult);
buttonGo = (Button) findViewById(R.id.buttonGo);
buttonGo.setOnClickListener(this);
}
public void onClick(View view) {
int input = Integer.parseInt(editInput.getText().toString()); //
long start, stop;
long result;
String out = "";
// Dalvik - Recursive
start = System.currentTimeMillis(); //
result = FibLib.fibJ(input); //
stop = System.currentTimeMillis(); //
out += String.format("Dalvik recur sive: %d (%d msec)", result, stop
- start);
// Dalvik - Iterative
start = System.currentTimeMillis();
result = FibLib.fibJI(input); //
stop = System.currentTimeMillis();
out += String.format("\nDalvik iterative: %d (%d msec)", result, stop
- start);
// Native - Recursive
start = System.currentTimeMillis();
result = FibLib.fibN(input); //
stop = System.currentTimeMillis();
out += String.format("\nNative recursive: %d (%d msec)", result, stop
- start);
// Native - Iterative
start = System.currentTimeMillis();
result = FibLib.fibNI(input); //
stop = System.currentTimeMillis();
out += String.format("\nNative iterative: %d (%d msec)", result, stop
- start);
textResult.setText(out); //
}
}
将字符串转换为数字。
在计算前,记录当前时间。
通过 FibLib
的方法计算菲波那契数,这里调用的是 Java 递归版本。
将当前时间减去先前记录的时间,即可得到算法花费的时间,以毫秒为单位。
使用 Java 的迭代算法执行计算。
使用本地的递归算法。
最后是本地的迭代算法。
格式化输出并且打印在屏幕上,
15.3.7. 测试
到这里就可以做些测试了。需要留意的是,如果n
的值比较大,那可能会消耗很长的计算时间,尤其是递归算法。另外计算是在主线程中进行,期间会影响到界面的响应,如果没有响应的时间过长,就有可能出现 图6.9 那样的Application Not Responding
,影响测试结果。因此我建议将n
的值设在25~30之间。此外,为了避免主线程的阻塞,你完全可以将计算的过程交给一个异步任务(AsyncTask,参见 第六章 的 AsynTask 一节)来执行,这将作为一个小练习留给读者。
经过一些试验以后,你会发现本地版本的算法几乎比 Java 版本快了一个数量级(图 15.1 "33的菲波那契数" )。
图 15.1. 33的菲波纳契数
从测试结果可以看出,本地代码对计算密集型应用的性能提升是很诱人的。通过NDK,可以使得本地代码的编写变得更加简单。
15.4. 总结
在Android 2.3 Gingerbread版本中,NDK引入了对本地Activity的支持。这样即可通过C语言直接编写Activity,从而简化Android游戏的开发过程。
SOURCE:(index.t2t)
你可能感兴趣的:(android,service,eclipse,数据库,layout,string)
android系统selinux中添加新属性property
辉色投像
1.定位/android/system/sepolicy/private/property_contexts声明属性开头:persist.charge声明属性类型:u:object_r:system_prop:s0图12.定位到android/system/sepolicy/public/domain.te删除neverallow{domain-init}default_prop:property
LocalDateTime 转 String
igotyback
java 开发语言
importjava.time.LocalDateTime;importjava.time.format.DateTimeFormatter;publicclassMain{publicstaticvoidmain(String[]args){//获取当前时间LocalDateTimenow=LocalDateTime.now();//定义日期格式化器DateTimeFormatterformat
每日一题——第九十题
互联网打工人no1
C语言程序设计每日一练 c语言
题目:判断子串是否与主串匹配#include#include#include//////判断子串是否在主串中匹配//////主串///子串///boolisSubstring(constchar*str,constchar*substr){intlenstr=strlen(str);//计算主串的长度intlenSub=strlen(substr);//计算子串的长度//遍历主字符串,对每个可能得
C#中使用split分割字符串
互联网打工人no1
c#
1、用字符串分隔:usingSystem.Text.RegularExpressions;stringstr="aaajsbbbjsccc";string[]sArray=Regex.Split(str,"js",RegexOptions.IgnoreCase);foreach(stringiinsArray)Response.Write(i.ToString()+"");输出结果:aaabbbc
Google earth studio 简介
陟彼高冈yu
旅游
GoogleEarthStudio是一个基于Web的动画工具,专为创作使用GoogleEarth数据的动画和视频而设计。它利用了GoogleEarth强大的三维地图和卫星影像数据库,使用户能够轻松地创建逼真的地球动画、航拍视频和动态地图可视化。网址为https://www.google.com/earth/studio/。GoogleEarthStudio是一个基于Web的动画工具,专为创作使用G
关于提高复杂业务逻辑代码可读性的思考
编程经验分享
开发经验 java 数据库 开发语言
目录前言需求场景常规写法拆分方法领域对象总结前言实际工作中大部分时间都是在写业务逻辑,一般都是三层架构,表示层(Controller)接收客户端请求,并对入参做检验,业务逻辑层(Service)负责处理业务逻辑,一般开发都是在这一层中写具体的业务逻辑。数据访问层(Dao)是直接和数据库交互的,用于查数据给业务逻辑层,或者是将业务逻辑层处理后的数据写入数据库。简单的增删改查接口不用多说,基本上写好一
SQL Server_查询某一数据库中的所有表的内容
qq_42772833
SQL Server 数据库 sqlserver
1.查看所有表的表名要列出CrabFarmDB数据库中的所有表(名),可以使用以下SQL语句:USECrabFarmDB;--切换到目标数据库GOSELECTTABLE_NAMEFROMINFORMATION_SCHEMA.TABLESWHERETABLE_TYPE='BASETABLE';对这段SQL脚本的解释:SELECTTABLE_NAME:这个语句的作用是从查询结果中选择TABLE_NAM
python是什么意思中文-在python中%是什么意思
编程大乐趣
Python中%有两种:1、数值运算:%代表取模,返回除法的余数。如:>>>7%212、%操作符(字符串格式化,stringformatting),说明如下:%[(name)][flags][width].[precision]typecode(name)为命名flags可以有+,-,''或0。+表示右对齐。-表示左对齐。''为一个空格,表示在正数的左侧填充一个空格,从而与负数对齐。0表示使用0填
深入理解 MultiQueryRetriever:提升向量数据库检索效果的强大工具
nseejrukjhad
数据库 python
深入理解MultiQueryRetriever:提升向量数据库检索效果的强大工具引言在人工智能和自然语言处理领域,高效准确的信息检索一直是一个关键挑战。传统的基于距离的向量数据库检索方法虽然广泛应用,但仍存在一些局限性。本文将介绍一种创新的解决方案:MultiQueryRetriever,它通过自动生成多个查询视角来增强检索效果,提高结果的相关性和多样性。MultiQueryRetriever的工
MongoDB Oplog 窗口
喝醉酒的小白
MongoDB 运维
在MongoDB中,oplog(操作日志)是一个特殊的日志系统,用于记录对数据库的所有写操作。oplog允许副本集成员(通常是从节点)应用主节点上已经执行的操作,从而保持数据的一致性。它是MongoDB副本集实现数据复制的基础。MongoDBOplog窗口oplog窗口是指在MongoDB副本集中,从节点可以用来同步数据的时间范围。这个窗口通常由以下因素决定:Oplog大小:oplog的大小是有限
python os 环境变量
CV矿工
python 开发语言 numpy
环境变量:环境变量是程序和操作系统之间的通信方式。有些字符不宜明文写进代码里,比如数据库密码,个人账户密码,如果写进自己本机的环境变量里,程序用的时候通过os.environ.get()取出来就行了。os.environ是一个环境变量的字典。环境变量的相关操作importos"""设置/修改环境变量:os.environ[‘环境变量名称’]=‘环境变量值’#其中key和value均为string类
Redis系列:Geo 类型赋能亿级地图位置计算
Ly768768
redis bootstrap 数据库
1前言我们在篇深刻理解高性能Redis的本质的时候就介绍过Redis的几种基本数据结构,它是基于不同业务场景而设计的:动态字符串(REDIS_STRING):整数(REDIS_ENCODING_INT)、字符串(REDIS_ENCODING_RAW)双端列表(REDIS_ENCODING_LINKEDLIST)压缩列表(REDIS_ENCODING_ZIPLIST)跳跃表(REDIS_ENCODI
ARM驱动学习之基础小知识
JT灬新一
ARM 嵌入式 arm开发 学习
ARM驱动学习之基础小知识•sch原理图工程师工作内容–方案–元器件选型–采购(能不能买到,价格)–原理图(涉及到稳定性)•layout画板工程师–layout(封装、布局,布线,log)(涉及到稳定性)–焊接的一部分工作(调试阶段板子的焊接)•驱动工程师–驱动,原理图,layout三部分的交集容易发生矛盾•PCB研发流程介绍–方案,原理图(网表)–layout工程师(gerber文件)–PCB板
【PG】常见数据库、表属性设置
江无羡
数据库
PG的常见属性配置方法数据库复制、备份相关表的复制标识单表操作批量表操作链接数据库复制、备份相关表的复制标识单表操作通过ALTER语句单独更改一张表的复制标识。ALTERTABLE[tablename]REPLICAIDENTITYFULL;批量表操作通过代码块的方式,对某个schema中的所有表一起更新其复制标识。SELECTtablename,CASErelreplidentWHEN'd'TH
C++ | Leetcode C++题解之第409题最长回文串
Ddddddd_158
经验分享 C++ Leetcode 题解
题目:题解:classSolution{public:intlongestPalindrome(strings){unordered_mapcount;intans=0;for(charc:s)++count[c];for(autop:count){intv=p.second;ans+=v/2*2;if(v%2==1andans%2==0)++ans;}returnans;}};
nosql数据库技术与应用知识点
皆过客,揽星河
NoSQL nosql 数据库 大数据 数据分析 数据结构 非关系型数据库
Nosql知识回顾大数据处理流程数据采集(flume、爬虫、传感器)数据存储(本门课程NoSQL所处的阶段)Hdfs、MongoDB、HBase等数据清洗(入仓)Hive等数据处理、分析(Spark、Flink等)数据可视化数据挖掘、机器学习应用(Python、SparkMLlib等)大数据时代存储的挑战(三高)高并发(同一时间很多人访问)高扩展(要求随时根据需求扩展存储)高效率(要求读写速度快)
insert into select 主键自增_mybatis拦截器实现主键自动生成
weixin_39521651
insert into select 主键自增 mybatis delete返回值 mybatis insert返回主键 mybatis insert返回对象 mybatis plus insert返回主键 mybatis plus 插入生成id
前言前阵子和朋友聊天,他说他们项目有个需求,要实现主键自动生成,不想每次新增的时候,都手动设置主键。于是我就问他,那你们数据库表设置主键自动递增不就得了。他的回答是他们项目目前的id都是采用雪花算法来生成,因此为了项目稳定性,不会切换id的生成方式。朋友问我有没有什么实现思路,他们公司的orm框架是mybatis,我就建议他说,不然让你老大把mybatis切换成mybatis-plus。mybat
openssl+keepalived安装部署
_小亦_
项目部署 keepalived openssl
文章目录OpenSSL安装下载地址编译安装修改系统配置版本Keepalived安装下载地址安装遇到问题安装完成配置文件keepalived运行检查运行状态查看系统日志修改服务service重新加载systemd检查配置文件语法错误OpenSSL安装下载地址考虑到后面设备可能没法连接到外网,所以采用安装包的方式进行部署,下载地址:https://www.openssl.org/source/old/
关于Mysql 中 Row size too large (> 8126) 错误的解决和理解
秋刀prince
mysql mysql 数据库
提示:啰嗦一嘴,数据库的任何操作和验证前,一定要记得先备份!!!不会有错;文章目录问题发现一、问题导致的可能原因1、页大小2、行格式2.1compact格式2.2Redundant格式2.3Dynamic格式2.4Compressed格式3、BLOB和TEXT列二、解决办法1、修改页大小(不推荐)2、修改行格式3、修改数据类型为BLOB和TEXT列4、其他优化方式(可以参考使用)4.1合理设置数据
2.2.6 通知类控件 Toast、Menu
常思行
本文例程下载:WillFlow_Toast、WillFlowMenu一、什么是Toast?Toast也被叫做吐司,是Android系统提供的一种非常好的提醒方式,在程序中可以使用它将一些短小的信息通知给用户,它有如下两个特点:Toast是没有焦点的Toast显示的时间有限过一定的时间就会自动消失所以一般来讲Toast的使用并不会影响我们的正常操作,并且它通常不会占用太大的屏幕空间,有着良好的用户体
多线程之——ExecutorCompletionService
阿福德
在我们开发中,经常会遇到这种情况,我们起多个线程来执行,等所有的线程都执行完成后,我们需要得到个线程的执行结果来进行聚合处理。我在内部代码评审时,发现了不少这种情况。看很多同学都使用正确,但比较啰嗦,效率也不高。本文介绍一个简单处理这种情况的方法:直接上代码:publicclassExecutorCompletionServiceTest{@TestpublicvoidtestExecutorCo
2024.9.6 Python,华为笔试题总结,字符串格式化,字符串操作,广度优先搜索解决公司组织绩效互评问题,无向图
RaidenQ
python 华为 leetcode 算法 力扣 广度优先 无向图
1.字符串格式化name="Alice"age=30formatted_string="Name:{},Age:{}".format(name,age)print(formatted_string)或者name="Alice"age=30formatted_string=f"Name:{name},Age:{age}"print(formatted_string)2.网络健康检查第一行有两个整数m
Java爬虫框架(一)--架构设计
狼图腾-狼之传说
java 框架 java 任务 html解析器 存储 电子商务
一、架构图那里搜网络爬虫框架主要针对电子商务网站进行数据爬取,分析,存储,索引。爬虫:爬虫负责爬取,解析,处理电子商务网站的网页的内容数据库:存储商品信息索引:商品的全文搜索索引Task队列:需要爬取的网页列表Visited表:已经爬取过的网页列表爬虫监控平台:web平台可以启动,停止爬虫,管理爬虫,task队列,visited表。二、爬虫1.流程1)Scheduler启动爬虫器,TaskMast
MongoDB知识概括
GeorgeLin98
持久层 mongodb
MongoDB知识概括MongoDB相关概念单机部署基本常用命令索引-IndexSpirngDataMongoDB集成副本集分片集群安全认证MongoDB相关概念业务应用场景:传统的关系型数据库(如MySQL),在数据操作的“三高”需求以及应对Web2.0的网站需求面前,显得力不从心。解释:“三高”需求:①Highperformance-对数据库高并发读写的需求。②HugeStorage-对海量数
mac 备份android 手机通讯录导入iphone,iphone如何导出通讯录(轻松教你iPhone备份通讯录的方法)...
weixin_39762838
mac 备份android 手机通讯录导入iphone
在日新月异的手机更替中,换手机已经成为一个非常稀松平常的事情,但将旧手机上面的通讯录导入到新手机还是让不少小伙伴为难,本篇将给大家详细讲解这方面的知识:“苹果手机通讯录怎么导入到新手机”及“安卓手机通讯录导入到新手机”的方法。一、苹果手机通讯录导入到新手机常用方法(SIM卡导入)在苹果手机主频幕上找到“设置”,单击进入设置菜单,下拉菜单列表,点击“邮件、通讯录、日历”,然后找到“导入SIM卡通讯录
android 更改窗口的层次,浮窗开发之窗口层级
Ms.Bu
android 更改窗口的层次
最近在项目中遇到了这样的需求:需要在特定的其他应用之上悬浮自己的UI交互(拖动、输入等复杂的UI交互),和九游的浮窗类似,不过我们的比九游的体验更好,我们越过了很多授权的限制。浮窗效果很多人都知道如何去实现一个简单的浮窗,但是却很少有人去深入的研究背后的流程机制,由于项目中浮窗交互比较复杂,遇到了些坑查看了很多资料,故总结浮窗涉及到的知识点:窗口层级关系(浮窗是如何“浮”的)?浮窗有哪些限制,如何
Android应用性能优化
轻口味
Android
Android手机由于其本身的后台机制和硬件特点,性能上一直被诟病,所以软件开发者对软件本身的性能优化就显得尤为重要;本文将对Android开发过程中性能优化的各个方面做一个回顾与总结。Cache优化ListView缓存:ListView中有一个回收器,Item滑出界面的时候View会回收到这里,需要显示新的Item的时候,就尽量重用回收器里面的View;每次在getView函数中inflate新
Mongodb Error: queryTxt ETIMEOUT xxxx.wwwdz.mongodb.net
佛一脚
error react mongodb 数据库
背景每天都能遇到奇怪的问题,做个记录,以便有缘人能得到帮助!换了一台电脑开发nextjs程序。需要连接mongodb数据,对数据进行增删改查。上一台电脑好好的程序,新电脑死活连不上mongodb数据库。同一套代码,没任何修改,搞得我怀疑人生了,打开浏览器进入mongodb官网毫无问题,也能进入线上系统查看数据,网络应该是没问题。于是我尝试了一下手机热点,这次代码能正常跑起来,连接数据库了!!!是不
入门MySQL——查询语法练习
K_un
前言:前面几篇文章为大家介绍了DML以及DDL语句的使用方法,本篇文章将主要讲述常用的查询语法。其实MySQL官网给出了多个示例数据库供大家实用查询,下面我们以最常用的员工示例数据库为准,详细介绍各自常用的查询语法。1.员工示例数据库导入官方文档员工示例数据库介绍及下载链接:https://dev.mysql.com/doc/employee/en/employees-installation.h
博客网站制作教程
2401_85194651
java maven
首先就是技术框架:后端:Java+SpringBoot数据库:MySQL前端:Vue.js数据库连接:JPA(JavaPersistenceAPI)1.项目结构blog-app/├──backend/│├──src/main/java/com/example/blogapp/││├──BlogApplication.java││├──config/│││└──DatabaseConfig.java
Java 并发包之线程池和原子计数
lijingyao8206
Java计数 ThreadPool 并发包 java线程池
对于大数据量关联的业务处理逻辑,比较直接的想法就是用JDK提供的并发包去解决多线程情况下的业务数据处理。线程池可以提供很好的管理线程的方式,并且可以提高线程利用率,并发包中的原子计数在多线程的情况下可以让我们避免去写一些同步代码。
这里就先把jdk并发包中的线程池处理器ThreadPoolExecutor 以原子计数类AomicInteger 和倒数计时锁C
java编程思想 抽象类和接口
百合不是茶
java 抽象类 接口
接口c++对接口和内部类只有简介的支持,但在java中有队这些类的直接支持
1 ,抽象类 : 如果一个类包含一个或多个抽象方法,该类必须限定为抽象类(否者编译器报错)
抽象方法 : 在方法中仅有声明而没有方法体
package com.wj.Interface;
[房地产与大数据]房地产数据挖掘系统
comsci
数据挖掘
随着一个关键核心技术的突破,我们已经是独立自主的开发某些先进模块,但是要完全实现,还需要一定的时间...
所以,除了代码工作以外,我们还需要关心一下非技术领域的事件..比如说房地产
&nb
数组队列总结
沐刃青蛟
数组队列
数组队列是一种大小可以改变,类型没有定死的类似数组的工具。不过与数组相比,它更具有灵活性。因为它不但不用担心越界问题,而且因为泛型(类似c++中模板的东西)的存在而支持各种类型。
以下是数组队列的功能实现代码:
import List.Student;
public class
Oracle存储过程无法编译的解决方法
IT独行者
oracle 存储过程
今天同事修改Oracle存储过程又导致2个过程无法被编译,流程规范上的东西,Dave 这里不多说,看看怎么解决问题。
1. 查看无效对象
XEZF@xezf(qs-xezf-db1)> select object_name,object_type,status from all_objects where status='IN
重装系统之后oracle恢复
文强chu
oracle
前几天正在使用电脑,没有暂停oracle的各种服务。
突然win8.1系统奔溃,无法修复,开机时系统 提示正在搜集错误信息,然后再开机,再提示的无限循环中。
无耐我拿出系统u盘 准备重装系统,没想到竟然无法从u盘引导成功。
晚上到外面早了一家修电脑店,让人家给装了个系统,并且那哥们在我没反应过来的时候,
直接把我的c盘给格式化了 并且清理了注册表,再装系统。
然后的结果就是我的oracl
python学习二( 一些基础语法)
小桔子
pthon 基础语法
紧接着把!昨天没看继续看django 官方教程,学了下python的基本语法 与c类语言还是有些小差别:
1.ptyhon的源文件以UTF-8编码格式
2.
/ 除 结果浮点型
// 除 结果整形
% 除 取余数
* 乘
** 乘方 eg 5**2 结果是5的2次方25
_&
svn 常用命令
aichenglong
SVN 版本回退
1 svn回退版本
1)在window中选择log,根据想要回退的内容,选择revert this version或revert chanages from this version
两者的区别:
revert this version:表示回退到当前版本(该版本后的版本全部作废)
revert chanages from this versio
某小公司面试归来
alafqq
面试
先填单子,还要写笔试题,我以时间为急,拒绝了它。。时间宝贵。
老拿这些对付毕业生的东东来吓唬我。。
面试官很刁难,问了几个问题,记录下;
1,包的范围。。。public,private,protect. --悲剧了
2,hashcode方法和equals方法的区别。谁覆盖谁.结果,他说我说反了。
3,最恶心的一道题,抽象类继承抽象类吗?(察,一般它都是被继承的啊)
4,stru
动态数组的存储速度比较 集合框架
百合不是茶
集合框架
集合框架:
自定义数据结构(增删改查等)
package 数组;
/**
* 创建动态数组
* @author 百合
*
*/
public class ArrayDemo{
//定义一个数组来存放数据
String[] src = new String[0];
/**
* 增加元素加入容器
* @param s要加入容器
用JS实现一个JS对象,对象里有两个属性一个方法
bijian1013
js对象
<html>
<head>
</head>
<body>
用js代码实现一个js对象,对象里有两个属性,一个方法
</body>
<script>
var obj={a:'1234567',b:'bbbbbbbbbb',c:function(x){
探索JUnit4扩展:使用Rule
bijian1013
java 单元测试 JUnit Rule
在上一篇文章中,讨论了使用Runner扩展JUnit4的方式,即直接修改Test Runner的实现(BlockJUnit4ClassRunner)。但这种方法显然不便于灵活地添加或删除扩展功能。下面将使用JUnit4.7才开始引入的扩展方式——Rule来实现相同的扩展功能。
1. Rule
&n
[Gson一]非泛型POJO对象的反序列化
bit1129
POJO
当要将JSON数据串反序列化自身为非泛型的POJO时,使用Gson.fromJson(String, Class)方法。自身为非泛型的POJO的包括两种:
1. POJO对象不包含任何泛型的字段
2. POJO对象包含泛型字段,例如泛型集合或者泛型类
Data类 a.不是泛型类, b.Data中的集合List和Map都是泛型的 c.Data中不包含其它的POJO
 
【Kakfa五】Kafka Producer和Consumer基本使用
bit1129
kafka
0.Kafka服务器的配置
一个Broker,
一个Topic
Topic中只有一个Partition() 1. Producer:
package kafka.examples.producers;
import kafka.producer.KeyedMessage;
import kafka.javaapi.producer.Producer;
impor
lsyncd实时同步搭建指南——取代rsync+inotify
ronin47
1. 几大实时同步工具比较 1.1 inotify + rsync
最近一直在寻求生产服务服务器上的同步替代方案,原先使用的是 inotify + rsync,但随着文件数量的增大到100W+,目录下的文件列表就达20M,在网络状况不佳或者限速的情况下,变更的文件可能10来个才几M,却因此要发送的文件列表就达20M,严重减低的带宽的使用效率以及同步效率;更为要紧的是,加入inotify
java-9. 判断整数序列是不是二元查找树的后序遍历结果
bylijinnan
java
public class IsBinTreePostTraverse{
static boolean isBSTPostOrder(int[] a){
if(a==null){
return false;
}
/*1.只有一个结点时,肯定是查找树
*2.只有两个结点时,肯定是查找树。例如{5,6}对应的BST是 6 {6,5}对应的BST是
MySQL的sum函数返回的类型
bylijinnan
java spring sql mysql jdbc
今天项目切换数据库时,出错
访问数据库的代码大概是这样:
String sql = "select sum(number) as sumNumberOfOneDay from tableName";
List<Map> rows = getJdbcTemplate().queryForList(sql);
for (Map row : rows
java设计模式之单例模式
chicony
java设计模式
在阎宏博士的《JAVA与模式》一书中开头是这样描述单例模式的:
作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。 单例模式的结构
单例模式的特点:
单例类只能有一个实例。
单例类必须自己创建自己的唯一实例。
单例类必须给所有其他对象提供这一实例。
饿汉式单例类
publ
javascript取当月最后一天
ctrain
JavaScript
<!--javascript取当月最后一天-->
<script language=javascript>
var current = new Date();
var year = current.getYear();
var month = current.getMonth();
showMonthLastDay(year, mont
linux tune2fs命令详解
daizj
linux tune2fs 查看系统文件块信息
一.简介:
tune2fs是调整和查看ext2/ext3文件系统的文件系统参数,Windows下面如果出现意外断电死机情况,下次开机一般都会出现系统自检。Linux系统下面也有文件系统自检,而且是可以通过tune2fs命令,自行定义自检周期及方式。
二.用法:
Usage: tune2fs [-c max_mounts_count] [-e errors_behavior] [-g grou
做有中国特色的程序员
dcj3sjt126com
程序员
从出版业说起 网络作品排到靠前的,都不会太难看,一般人不爱看某部作品也是因为不喜欢这个类型,而此人也不会全不喜欢这些网络作品。究其原因,是因为网络作品都是让人先白看的,看的好了才出了头。而纸质作品就不一定了,排行榜靠前的,有好作品,也有垃圾。 许多大牛都是写了博客,后来出了书。这些书也都不次,可能有人让为不好,是因为技术书不像小说,小说在读故事,技术书是在学知识或温习知识,有
Android:TextView属性大全
dcj3sjt126com
textview
android:autoLink 设置是否当文本为URL链接/email/电话号码/map时,文本显示为可点击的链接。可选值(none/web/email/phone/map/all) android:autoText 如果设置,将自动执行输入值的拼写纠正。此处无效果,在显示输入法并输
tomcat虚拟目录安装及其配置
eksliang
tomcat配置说明 tomca部署web应用 tomcat虚拟目录安装
转载请出自出处:http://eksliang.iteye.com/blog/2097184
1.-------------------------------------------tomcat 目录结构
config:存放tomcat的配置文件
temp :存放tomcat跑起来后存放临时文件用的
work : 当第一次访问应用中的jsp
浅谈:APP有哪些常被黑客利用的安全漏洞
gg163
APP
首先,说到APP的安全漏洞,身为程序猿的大家应该不陌生;如果抛开安卓自身开源的问题的话,其主要产生的原因就是开发过程中疏忽或者代码不严谨引起的。但这些责任也不能怪在程序猿头上,有时会因为BOSS时间催得紧等很多可观原因。由国内移动应用安全检测团队爱内测(ineice.com)的CTO给我们浅谈关于Android 系统的开源设计以及生态环境。
1. 应用反编译漏洞:APK 包非常容易被反编译成可读
C#根据网址生成静态页面
hvt
Web .net C# asp.net hovertree
HoverTree开源项目中HoverTreeWeb.HVTPanel的Index.aspx文件是后台管理的首页。包含生成留言板首页,以及显示用户名,退出等功能。根据网址生成页面的方法:
bool CreateHtmlFile(string url, string path)
{
//http://keleyi.com/a/bjae/3d10wfax.htm
stri
SVG 教程 (一)
天梯梦
svg
SVG 简介
SVG 是使用 XML 来描述二维图形和绘图程序的语言。 学习之前应具备的基础知识:
继续学习之前,你应该对以下内容有基本的了解:
HTML
XML 基础
如果希望首先学习这些内容,请在本站的首页选择相应的教程。 什么是SVG?
SVG 指可伸缩矢量图形 (Scalable Vector Graphics)
SVG 用来定义用于网络的基于矢量
一个简单的java栈
luyulong
java 数据结构 栈
public class MyStack {
private long[] arr;
private int top;
public MyStack() {
arr = new long[10];
top = -1;
}
public MyStack(int maxsize) {
arr = new long[maxsize];
top
基础数据结构和算法八:Binary search
sunwinner
Algorithm Binary search
Binary search needs an ordered array so that it can use array indexing to dramatically reduce the number of compares required for each search, using the classic and venerable binary search algori
12个C语言面试题,涉及指针、进程、运算、结构体、函数、内存,看看你能做出几个!
刘星宇
c 面试
12个C语言面试题,涉及指针、进程、运算、结构体、函数、内存,看看你能做出几个!
1.gets()函数
问:请找出下面代码里的问题:
#include<stdio.h>
int main(void)
{
char buff[10];
memset(buff,0,sizeof(buff));
ITeye 7月技术图书有奖试读获奖名单公布
ITeye管理员
活动 ITeye 试读
ITeye携手人民邮电出版社图灵教育共同举办的7月技术图书有奖试读活动已圆满结束,非常感谢广大用户对本次活动的关注与参与。
7月试读活动回顾:
http://webmaster.iteye.com/blog/2092746
本次技术图书试读活动的优秀奖获奖名单及相应作品如下(优秀文章有很多,但名额有限,没获奖并不代表不优秀):
《Java性能优化权威指南》