- Android开发权威指南(第二版)
- 李宁编著
- 814字
- 2024-12-21 14:45:37
6.6 Android源代码与过滤器
源代码目录:src/ch06/AndroidSrcFilter
只从Android官方文档中获取所有的过滤器、Action等信息是不可能的,因为这些文档的编写者未必参与了Android的设计,所以并不一定对Android相关方面的细节描述得特别清楚,而且还有不少错误。因此这就需要我们从其他地方来获取这些知识,例如Android源代码、反编译APK程序等。本节只讨论如何利用Android源代码获取与过滤器相关的信息,在下一节会介绍如何通过反编译的方式获取同样的信息。在本书的其他章节还会继续与读者一起通过分析Android源代码、反编译各种资源的方式获取更多、更权威的信息。
6.6.1 系统内置程序有哪些窗口可以利用
到现在为止已经可以使用多种方式调用另一个应用程序的窗口了,但可能很多读者一直有一个疑问,如果是调用其他应用程序(不是自己编写的程序)中的窗口,又是如何得知这些程序中哪些窗口提供了Action,哪些窗口可以通过显式方式调用呢?这些问题的答案将在本节和下一节为读者揭晓。
在Intent类中定义了一些Action常量,这些常量用来调用系统的一些窗口或接收系统广播。通常选中某一个常量后会在弹出的提示信息框中显示与该窗口相关的解释,如图6-8所示。
▲图6-8 Action常量与相应的解释
尽管Intent类中定义了用于调用系统窗口的常量,但并没有为全部的Action定义常量,而且可能有一些解释不太清楚,所以需要采用下面的方法获取更详细的信息。
由于任何Android应用程序的所有窗口都必须在AndroidManifest.xml文件中声明,所以只要获取相应内置程序的AndroidManifest.xml文件的源代码即可。虽然可以通过获取APK文件的方式得到AndroidManifest.xml文件的内容。但由于系统内置程序的APK文件都在系统的目录中(/system/app),如果没有root权限根本访问不了该目录,更别提获取APK文件了。尽管熟悉Linux的读者知道可以用su或sudo命令暂时将用户权限提升到root。但很多手机安装的Android系统根本就没有su或sudo命令,所以为了方法更通用,本节采用了直接查看Android源代码的方法(为了方便读者,在随书光盘中已经包含了最新的Android源代码)获取我们需要的信息。
Android系统内置应用程序的源代码都放在了如下的目录:
<Android源代码根目录>/packages/apps
每一个应用以一个单独的目录存放,例如,下面是一些常用内置应用的目录。
Browser:浏览器。
Calculator:计算器。
Calendar:日历。
Camera:照相机。
Contacts:联系人。
PackageInstaller:APK安装器。有很多文件管理器在单击APK文件时可以进行安装,就是调用了PackageInstaller中的某个负责安装程序的窗口。
Phone:电话管理,包括拨号、来去电记录等窗口。
Settings:系统设置。
Launcher2:Android系统的启动程序。在系统启动后第一个运行的就是该程序,主要包括桌面、图标、程序列表等。经常提及的定制ROM,UI部分主要就是修改Launcher2。
下面几节会选一些典型的过滤器进行分析,并通过Java代码调用这些过滤器所在的窗口。
6.6.2 显示计算器(Calculator)
计算器是笔者最喜欢的一个程序,因为Calculator可能是唯一没有使用Android SDK内部API的程序,也就意味着Calculator可以单独提出来作为独立的第三方程序,并通过常规的方法安装到系统中。
对于很多初学者来说,由于并没有在Intent类中找到带Calculator的Activity Action,可能第一反应是系统不允许调用Calculator。不过在这里可以肯定地告诉大家,系统中所有带UI的内置程序都允许调用相应的窗口,只是有的Action或Category不太好找罢了。
现在先不考虑Intent类中定义的常量,首先来看看Calculator中AndroidManifest.xml文件的源代码。Calculator程序只有一个窗口类(Calculator),因此AndroidManifest.xml文件的内容也很简单,下面是声明Calculator类的代码。
源代码文件:<Android源代码根目录>/packages/apps/Calculator/AndroidManifest.xml
<activity android:name="Calculator"
android:theme="@android:style/Theme.Holo.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.APP_CALCULATOR" />
</intent-filter>
</activity>
从这些声明代码可以看出,Calculator只有一个过滤器,在该过滤器中有1个Action和3个Category。读者可以对照6.5节介绍的过滤机制,想一想如何调用Calculator,然后再看下面的内容。
可能通常认为android.intent.action.MAIN是给系统用的,与第三方程序无关。实际上,该Action不仅系统可以调用,第三方程序同样也可以调用。后面定义的3个Category指定一个就可以,不过指定前两个,系统中肯定有重复的定义。所以通常使用下面的代码调用Calculator。
源代码文件:src/ch06/AndroidSrcFilter/src/mobile/android/src/filter/AndroidSrcFilterActivity.java
public void onClick_Calculator(View view)
{
Intent intent = new Intent("android.intent.action.MAIN");
intent.addCategory("android.intent.category.APP_CALCULATOR");
startActivity(intent);
}
当然,android.intent.action.MAIN 和android.intent.category.APP_CALCULATOR在Intent类中已经定义了,所以也可以使用下面的代码调用Calculator。
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_APP_CALCULATOR);
startActivity(intent);
如果Intent类中已经为某些Action和Category定义了常量,应尽量使用这些常量,而不要直接使用字符串形式的Action和Category。因为如果应用程序中的相应Action和Category改变,可能会影响窗口的调用者,尽管发生这种情况的可能性并不大。在不知道具体的Action或Category定义的常量名的情况下,可以通过查看Android源代码获取相应的字符串形式的Action和Category,然后在Eclipse中跟踪进Intent类,并查找这些字符串,这样就可以很容易找到相应的常量。例如,Intent.ACTION_MAIN和Intent.CATEGORY_APP_CALCULATOR的定义代码如下:
public static final String ACTION_MAIN = "android.intent.action.MAIN";
public static final String CATEGORY_APP_CALCULATOR =
"android.intent.category.APP_CALCULATOR";
在6.4节的例子中曾使用了显式的方式调用Calculator,这一点从本节给出的Calculator类的声明代码中更容易理解。不过笔者仍然建议尽可能使用隐式方式调用系统的窗口,除非别无选择,才使用显式的方式调用窗口。
注意
Calculator只能简单地显示主窗口,并不能向主窗口传递任何值。由于声明Calculator时未指定Data,所以自然无法传递Uri和Mime Type了。当然还可以通过Extra向主窗口传递数据,不过这就需要直接查看Calculator的源代码了。经过笔者查看,Calculator并没有处理Extra,所以自然也无法通过Extra传递数据了。
6.6.3 用浏览器(Browser)显示网页
在5.7.4小节曾使用下面的代码调用浏览器显示指定的页面。
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://blog.csdn.net/nokiaguy"));
startActivity(intent);
在代码中使用了Intent.ACTION_VIEW作为Activity Action。Intent.ACTION_VIEW常量的值是android.intent.action.VIEW,在系统中的很多窗口都包含该Action。例如,Browser程序在声明主窗口类(BrowserActivity)时就定义了很多过滤器,这些过滤器有很多都指定了该Action。下面的代码就是其中一个过滤器,也是上面的代码能够匹配的过滤器。
源代码文件:<Android源代码根目录>/packages/apps/Browser/AndroidManifest.xml
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="about" />
<data android:scheme="javascript" />
</intent-filter>
在过滤器中除了前面提到的android.intent.action.VIEW外,还指定了两个Category,以及4个Data。现在先说说android.intent.category.BROWSABLE。由于android.intent.action.VIEW在系统中已经被很多过滤器指定了,这些过滤器可能属于不同的窗口。所以只使用该Action可能会有多个窗口符合条件,这也会增大出现选择列表的可能性。因此,调用浏览器时应加上android.intent.category.BROWSABLE,以便尽可能减小显示选择列表的可能性(因为多一个条件,系统中符合所有条件的过滤器就越少),代码如下:
源代码文件:src/ch06/AndroidSrcFilter/src/mobile/android/src/filter/AndroidSrcFilterActivity.java
public void onClick_Calculator(View view)
{
Intent intent = new Intent("android.intent.action.MAIN");
// Intent.CATEGORY_BROWSABLE是android.intent.category.BROWSABLE对应的常量
intent.addCategory(Intent.CATEGORY_BROWSABLE);
startActivity(intent);
}
最后看一看过滤器中指定的4个Data。通过这4个Data指定了4个scheme:http、https、about和javascript。也就是说Uri必须以如下4个字符串中的一个作为前缀(scheme是以冒号结尾的)。
http:
https:
about:
javascript:
其中about:表示空的内容(什么也不显示),但浏览器地址栏中会显示“about:”,其他3个都需要跟实质性的内容,例如http://www.example.com/abc.html。
我们会发现,使用这个过滤器必须要指定Uri,但如果不想指定Uri该怎么办呢?可能有的读者会将上面代码中Intent类的构造方法第2个参数去掉,不过执行代码后会发现抛出异常,说明声明BrowserActivity类时根本就没有定义这样的过滤器。不过BrowserActivity的过滤器还有很多,经过仔细查找,还是找到了如下的过滤器。
源代码文件:<Android源代码根目录>/packages/apps/Browser/AndroidManifest.xml
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.APP_BROWSER" />
</intent-filter>
这个过滤器并没有指定Data,但却有用于启动程序的Action和Category。说明这个过滤器的功能之一是给系统用于单击浏览器程序图标时显示BrowserActivity窗口的。不过最后又定义了两个Category:android.intent.category.BROWSABLE和android.intent.category.APP_BROWSER。因此可以直接用android.intent.action.MAIN和这两个Category显示BrowserActivity,代码如下:
Intent intent = new Intent("android.intent.action.MAIN");
intent.addCategory(Intent.CATEGORY_BROWSABLE);
// Intent.CATEGORY_APP_BROWSER是android.intent.category.APP_BROWSER对应的常量
intent.addCategory(Intent.CATEGORY_APP_BROWSER);
startActivity(intent);
但要注意最好不要单独只用其中一个Category,否则可能会有多个过滤器满足过滤条件,这就有可能显示选择列表了。例如,只指定Intent.CATEGORY_BROWSABLE,可能会显示如图6-9所示的选择列表。可以看到,Browser、People和Phone程序中都有窗口满足过滤条件。
▲图6-9 多个窗口符合过滤条件下的选择列表
答疑解惑:为什么显式调用浏览器会导致Uri无效
在6.4节中曾在InvokeOtherActivity程序中使用显式的方式调用了WebBrowser中的主窗口用于显示指定的网页,在这个例子中程序可以完美地运行。但如果我们使用这种方法显式调用系统内置的浏览器会发现,尽管浏览器可以成功调用,但往里传入的Uri却无效了。究其原因是因为在我们实现的WebBrowser程序的主窗口类的onCreate方法中不管三七二十一都从Intent.getData方法中获取了Uri,所以无论何种情况,只要主窗口成功显示,就一定会获取传入的Uri。而系统内置的浏览器对Action进行了验证。只有在Action和Category符合要求的情况下才会继续读取Data中的数据。
系统浏览器在启动时会通过Intent.getAction方法返回一个Action,如果要让该方法返回非空值,必须使用下面两种方法调用窗口。
隐式调用窗口。
单击程序图标启动程序。
第1种情况就不需要多讲了,因为已经为窗口指定了一个Action,Intent.getAction方法返回的就是这个Action。而第2种情况Intent.getAction方法返回了android.intent.action.MAIN。读者可以在onCreate方法中加入如下的代码,看看在启动程序时是否会在LogCat视图输出这个Action。
if (getIntent().getAction() != null)
Log.d("action", getIntent().getAction());
如果直接使用显式方法调用窗口,Intent.getAction方法会返回null。而从本节前面给出的过滤器代码可知,要处理Uri,Action必须是android.intent.action.VIEW。所以如果显式调用浏览器的BrowserActivity窗口,根本不可能通过Action检测,因此当然不会执行到接收Uri的代码了,这也是为什么显式调用系统浏览器显示网页后,Uri会被忽略的原因。
6.6.4 拨打电话(Phone)与授权
拨打电话在5.7.1小节已经介绍过了,现在再来回顾一下拨号的过程,代码如下:
Intent callIntent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:12345678"));
startActivity(callIntent);
其中Intent.ACTION_CALL的值是android.intent.action.CALL。现在来研究一下具体哪个程序中的哪个窗口来响应上面代码的请求。
由于本功能是拨打电话,所以很自然就会想到是调用了Phone程序中的某个窗口。打开该程序的AndroidManifest.xml文件后,根据android.intent.action.CALL会找到如下的过滤器。
源代码文件:<Android源代码根目录>/packages/apps/Phone/AndroidManifest.xml
<intent-filter>
<action android:name="android.intent.action.CALL" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="tel" />
</intent-filter>
从这段过滤器的代码中会看到除了指定Action和Category外,还指定了一个叫tel的scheme。所以通过Uri指定电话号时一定要指定“tel:”前缀(由于Uri区分大小写,所以不能写成TEL、Tel等形式)。
到这里还不算完,我们会发现只执行前面两行代码会抛出异常,异常的大概意思是没有权限调用拨号窗口。出现这个问题的关键是拨号盘的权限设置,也就是上面过滤器所在的窗口类,该类的声明代码如下:
<activity android:name="OutgoingCallBroadcaster"
android:theme="@style/OutgoingCallBroadcasterTheme"
android:permission="android.permission.CALL_PHONE"
android:screenOrientation="portrait"
android:configChanges="orientation screenSize keyboardHidden">
……
</activity>
从拨号盘类(OutgoingCallBroadcaster)的声明代码可以看出,在<activity>标签中设置了android:permission属性,并指定了android.permission.CALL_PHONE权限,所以在调用者的AndroidManifest.xml文件中要加入如下的代码才可以成功进行拨号。
<uses-permission android:name="android.permission.CALL_PHONE"/>
注意
前面已经有很多代码涉及Uri。Uri的标准格式是“xxx://host:port/path”。但有的Uri并未指定“//”,例如“tel:12345678”。实际上这是由相应的程序进行处理的,因为如果不指定“//”,是无法通过Uri.getHost方法获取Host的,自然也就无法获取电话号的。不过处理来去电的程序(Phone)会从“tel:12345678”中解析出电话号。如果某些程序只能从Uri的host中获取数据,那就必须加“//”了。