6.4 跨应用程序访问窗口
Android开发权威指南(第二版)
在前面曾介绍过可以通过显式和隐式的方式访问窗口,显式方式只能访问应用程序内部的窗口,而隐式方式无论当前应用程序,还是其他应用程序中的窗口都可以访问。不过阅读了本节的内容后读者就会改变这种观念。因为Android足够强大,已经完全打通不同应用程序之间的界限了,也就是说不同应用程序之间的交互几乎和应用程序内部的交互完全一样。因此在一个应用程序中同样可以通过直接指定另外一个应用程序中窗口类的方式访问该窗口,也就是跨应用程序显式调用。所以从现在开始不要认为只有隐式方式才可以调用其他应用程序的窗口,显式方式一样可以。

要了解显式调用窗口精髓需要知道如下几点。

每一个Android应用的唯一标识就Package Name(包名),也就是AndroidManifest.xml文件中标签的package属性值。只要Package Name相同,就可以认为是同一个Android应用。
如果用Intent对象指定一个窗口类,除了需要指定窗口类的class外,还需要指定窗口类所在的应用程序的Context对象(或应用程序的Package Name),所以通过Package Name和Activity Class Name(窗口类名)可以定位当前Android系统中的任意窗口。
了解了这两点后,就可以很容易想到如何通过直接指定窗口类的方式调用另一个应用程序的窗口。在6.2.2小节介绍了通过Intent.setClass、Intent.setClassName和Intent.setComponent方法可以指定Package Name和Activity Class Name。只要Package Name(或与其对应的Context对象)指向了另一个应用程序,那么系统就会认为窗口类是这个应用程序的(当然窗口类必须在该应用程序中存在,否则访问该窗口会抛出异常)。

定位某个应用程序可以有如下3种方式。

直接指定Package Name。
另一个Android应用的Context对象。
ComponentName对象。
直接指定Package Name比较简单,Package Name就是一个字符串,只要知道目标应用程序的Package Name1,直接指定即可。而Context对象就需要使用Context.createPackageContext方法创建了,代码如下:

Context context = createPackageContext(  
           "mobile.android.web.browser", Context.CONTEXT_INCLUDE_CODE
                 | Context.CONTEXT_IGNORE_SECURITY);

创建Context对象时也需要指定Package Name,但最好还指定Context.CONTEXT_INCLUDE _CODE和Context.CONTEXT_IGNORE_SECURITY,其中Context.CONTEXT_INCLUDE_CODE允许代码被装载,即使这些代码可能不安全,而Context.CONTEXT_IGNORE_SECURITY则忽略任何安全限制。

至于ComponentName对象,实际上就是封装了Package Name(或Context对象)和Class Name(或窗口类的class)的对象,只要得到了Context和窗口类的class,就可以直接创建ComponentName对象,并通过ComponentName类的构造方法传入相应的数据即可。

本例演示了跨应用程序访问窗口的各种方法,程序的主界面如图6-4所示。读者可以通过不同的按钮测试相应的功能。




android应用程序开发第二版 android应用程序开发第二版pdf_应用程序



为了测试本节的例子,需要编写一个辅助程序WebBrowser,该程序的功能是接收一个Uri,并在WebView控件2中显示Uri指向的页面。

WebBrowser的窗口类WebBrowserActivity(该程序中只有一个窗口类)的实现代码很简单,只是在onCreate中通过getData方法获取传入的Uri对象,然后用WebView控件显示Uri指向的页面,WebBrowserActivity类的实现代码如下:

public class WebBrowserActivity extends Activity
{  
  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_web_browser);
    WebView webview = (WebView) findViewById(R.id.webview);
    // 获取Uri对象
        Uri uri = getIntent().getData();
    if (uri != null)
    {
      // 在WebView控件中显示Uri指向的页面
            webview.loadUrl(uri.toString());
            // 在窗口标题栏中显示Uri
            setTitle(uri.toString());
    }
  }
}

在声明WebBrowserActivity类时就有一定的说道了。既然是跨应用程序调用,通常认为指定一个Action即可。但为了说明显式和隐式跨应用程序调用的差别,WebBrowserActivity类需要指定与系统自带的浏览器相同的Action。这样在使用该Action显示窗口时就会显示一个如图6-5所示的选择列表,该列表中至少会显示两个候选的程序3。其中右侧的WebBrowser就是WebBrowserActivity窗口,左侧的Browser是系统自带的浏览器应用中的窗口。

要想让某个窗口可以接收Uri,除了指定相应的Action外,还需要指定Data(用标签设置)。对于浏览Web页面,Data只需要设置scheme即可,也就是Web协议,如http、https等。


android应用程序开发第二版 android应用程序开发第二版pdf_java_02



WebBrowserActivity类的声明代码如下:

第X问1 源代码文件:src/ch06/WebBrowser/AndroidManifest.xml

<activity
  android:name="mobile.android.web.browser.WebBro- wserActivity"
  android:label="@string/app_name" >
  <intent-filter>
     <action android:name="android.intent.action.MAIN" />
     <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>
  <intent-filter>
  <!-- android.intent.action.VIEW是Web浏览器的标准Action -->
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <!-- 指定该窗口可以打开以“http://”开头的Uri -->
  <data android:scheme="http" />
  <!-- 指定该窗口可以打开以“http://”开头的Uri -->
  <data android:scheme="https" />
 </intent-filter>
</activity>

从6.2节的内容可知,窗口可以通过Action、Category和Data三种机制的组合来定位,而WebBrowserActivity在声明时这3种过滤机制都使用了,所以在通过Action显示WebBrowserActivity窗口时需要同时指定Action、Category和Data。不过由于Category是android.intent.category.DEFAULT,所以并不需要指定Category。

下面看一下本节的核心程序InvokeOtherActivity(该程序的窗口类也是InvokeOtherActivity)是如何使用各种方式调用WebBrowserActivity的。前面如图6-4所示的界面就是InvokeOtherActivity程序的主界面。除了显示WebBrowserActivity窗口外,最后一个按钮还可以使用显式方式调用系统的计算器程序。

InvokeOtherActivity窗口类的代码如下:

第X问1 源代码文件:src/ch06/InvokeOtherActivity/src/mobile/android/invoke/other/activity/InvokeOtherActivity.java

public class InvokeOtherActivity extends Activity
{  
    private Context mContext;
    private Class mClass;
  // 由于有多处需要Context和Class对象,所以在onCreate方法中提前创建了这两个对象
  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_invoke_other);
    try
    {
      // 创建指向WebBrowser程序的Context对象,其中mobile.android.web.browser就是
      // WebBrowser的Package Name
      mContext = createPackageContext(
                 "mobile.android.web.browser", Context.CONTEXT_INCLUDE_CODE
              | Context.CONTEXT_IGNORE_SECURITY);  
      // 动态装载WebBrowserActivity类
           mClass = mContext.getClassLoader().loadClass(
                 "mobile.android.web.browser.WebBrowserActivity");
    }
    catch(Exception e)
    {
      e.printStackTrace();
    }
  }
  // ①“浏览网页:Action”按钮的单击事件方法,使用Action方式浏览网页
  public void onClick_Action(View view)
  {
    // 指定了Action和要浏览的页面的Uri
    // Intent.ACTION_VIEW的值是android.intent.action.VIEW
    // Uri确定了Data(符合以http://开头的Uri的要求)
        Intent webIntent = new Intent(Intent.ACTION_VIEW,
               Uri.parse(""));
    startActivity(webIntent);
    }
  // ②“浏览网页:setClassName”按钮单击事件方法,直接指定Package Name和Class Name
  public void onClick_SetClassName(View view)
  {
    Intent webIntent = new Intent();
    // 直接指定了WebBrowser应用的Package Name和WebBrowserActivity类的全名
        webIntent.setClassName("mobile.android.web.browser",
               "mobile.android.web.browser.WebBrowserActivity");
    // 指定Uri
    webIntent.setData(Uri.parse("http://nokiaguy.blogjava.net"));
    startActivity(webIntent);
  }
  // ③“浏览网页:setClassName_Context”按钮单击事件方法,指定Context对象和Class Name
  public void onClick_SetClassName_Context(View view)
  {
    if(mContext == null || mClass == null) return;
    Intent webIntent = new Intent();
    // 指定了Context对象和WebBrowserActivity类的全名
    webIntent.setClassName(mContext,
        "mobile.android.web.browser.WebBrowserActivity");
    // 指定Uri
    webIntent.setData(Uri.parse(""));
    startActivity(webIntent);
  }
  // ④“浏览网页:setClass“按钮单击事件方法,指定Context和Class对象
  public void onClick_SetClass(View view)
  {
    if(mContext == null || mClass == null) return;
    Intent webIntent = new Intent();
    // 指定Context和Class对象
        webIntent.setClass(mContext, mClass);
        // 指定Uri 
        webIntent.setData(Uri.parse(""));
    startActivity(webIntent);    
  }
  // ⑤“浏览网页:setComponentName”按钮单击事件,指定了ComponentName对象
  public void onClick_SetComponentName(View view)
  {
    if(mContext == null || mClass == null) return;
    // 通过ComponentName对象指定了Context和Class对象
        ComponentName cn = new ComponentName(mContext, mClass);
    Intent webIntent = new Intent();
    // 指定ComponentName对象
        webIntent.setComponent(cn);
    // 指定Uri
        webIntent.setData(Uri.parse(""));
    startActivity(webIntent);
  }
  // ⑥“显示计算器”按钮单击事件方法,通过指定Package Name和Class Name的方式显示系统计算器
  public void onClick_ShowCalculator(View view)
  {
    Intent intent = new Intent();
    // 指定计算器的Package Name和主窗口类的全名
        intent.setClassName("com.android.calculator2",
        "com.android.calculator2.Calculator");
    startActivity(intent);
  }
}

在①号方法中使用了Action调用浏览器显示网页,如果事先安装了WebBrowser,就会显示如图6-5所示的选择列表。而②至⑤号方法都直接指定了WebBrowser的Package Name和WebBrowserActivity的全名,只是指定的方式不同。这些方式中分别使用了setClassName、setClass和setComponentName方法。尽管在当前的Android系统中至少存在两个程序可以浏览网页(系统内置的浏览器和WebBrowser),但②至⑤号方法只会调用WebBrowser中的WebBrowserActivity(不会出现图6-5所示的选择列表),因为在这些方法中直接指定了WebBrowserActivity类(显式方式调用)。在⑥号方法中仍然通过显式的方式调用系统内置的计算器程序。

通过本例我们可以知道,如果想调用其他应用程序中的窗口,但又不想出现图6-5所示的选择列表(调用之前谁也不知道当前系统中有多少个窗口满足过滤条件),就可以使用显式调用窗口的方式。

答疑解惑:为什么显式调用其他应用程序的窗口时会失败

在显式调用其他应用程序窗口时会发现并不是都好使,有一些情况无法成功调用这些窗口。这其中原因很多,比较常见的原因就是Package Name和Activity Class Name指定错误。不过这也不是故意的,而是不知情所致。例如,如果要调用系统内置的浏览器程序。通过查询Android源代码的Browser程序可知。Package Name是com.android.browser,Activity Class Name(主窗口类名)是com.android.browser.BrowserActivity。这对于Android模拟器是没错的,不过对于真机上的Android系统,可能就会有变化(有的ROM4内置的Browser仍然和Android模拟器中的Browser是一样的),至少在Google官方原生的Android系统已经将Package Name变成了com.google.android. browser,而Activity Class Name并未变化。

既然系统自带程序的Package Name和Activity Class Name有可能发生变化,那么直接访问这些程序的包名和类名就会带来一定的风险。所以在调用系统程序的窗口时应尽量使用Action,而不要直接指定包名和类名。这样即使包名和类名变化了,只要Action不变,仍然可以成功调用窗口。当然,如果调用者和被调用者都是自己编写的程序(有源代码),显式和隐式调用窗口都没什么问题,因为包名和类名的变化在于自己,这一点很容易控制。尤其是在制作可扩展的系统时,往往不希望调用自己的扩展窗口(另一个自己编写的程序中的窗口)时显示一个选择列表(如果使用Action,有可能系统的其他程序会与当前程序的Action、Category和Data相同),而是想直接就显示指定的窗口,这种情况下使用显式调用的方式就比较好。在本例②至⑤号方法中尽管系统中有多个窗口可以打开网页,但由于使用了显式调用,所以就直接显示了WebBrowserActivity窗口。

那么除了包名和类名错误外,还有没有其他原因导致调用其他程序中窗口时失败呢?答案是肯定的。如果调用的窗口不在当前的应用程序中,在AndroidManifest.xml文件中声明窗口时必须允许该窗口被其他应用程序调用,也就是标签的exported属性值为“true”。现在比较下面两段窗口类(MyActivity)的声明代码。

不可以被其他应用程序通过显式方式5调用。

<activity android:name=".MyActivity"/>
可以被其他应用程序通过显式方式调用。

<activity android:name=".MyActivity"android:exported="true"/>

不过我们看到有很多窗口类在声明时并没有设置android:exported属性(该属性的默认值是true),但仍然可以使用显式的方式调用这些窗口。不过好像还不对。既然android:exported属性的默认值为true,那么不设置android:exported属性不也是允许窗口被外部程序调用吗(按第1段代码声明的窗口调用失败)?这对于使用Action调用的方式是没问题的,但对于显式方式必须将android:exported属性设为true才可以。当然,如果为该窗口指定了至少一个Action,就可以不用设置android:exported属性了(当然也不能将该属性设为false,那样外部程序使用任何方式都无法访问该窗口了)。所以显式和隐式跨应用程序调用窗口必须满足如下规则才能成功调用。

显式调用:如果声明窗口时未指定任何Action,android:exported属性必须设置,而且属性值必须为true。如果指定了Action,则并不需要设置android:exported属性,或设置android:exported属性值true也可。
隐式调用:必须指定Action,而且android:exported属性值不需要设置,或设置该属性值为true。

扩展学习:如何得知系统程序的包名和类名

如果要显式调用其他程序的窗口,必须要知道应用的包名和类名。但如果没有APK程序6或无法获得APK程序该如何做呢?例如,如果从Google Play安装某个程序,但自己的手机又没有root权限7,无法提取该程序的APK文件。但我们还想查看该程序中是否有可以处理某一资源的窗口,如可以浏览网页。现在就拿系统内置的Browser程序为例。

Browser的包名很容易获取,只要在系统的正在运行的应用程序中找到Browser(或浏览器),单击进入如图6-6所示(白框中的就是Browser的包名)。如果没有找到浏览器,可以单击右上角的“显示缓存进程”,如图6-7所示。

android应用程序开发第二版 android应用程序开发第二版pdf_android应用程序开发第二版_03

获取Android应用中声明某个Action的窗口的类名也可以使用PackageManager.queryIntent Activities方法,代码如下:

// 查询是否有窗口指定了叫Intent.ACTION_VIEW的Action
Intent intent = new Intent(Intent.ACTION_VIEW);
// 任意指定一个Uri,如果窗口指定了叫http的scheme,系统就会匹配该窗口
intent.setData(Uri.parse(""));
// 列出系统中所有这样的窗口信息
List<ResolveInfo> resolveInfos = packageManager
    .queryIntentActivities(intent,PackageManager.GET_INTENT_ FILTERS);
// 通常第一个程序就是Browser,当然,也可以枚举所有的窗口信息
// 在LogCat视图中输出第一个满足条件的窗口的类名(<activity>标签的android:name属性值)
Log.d("Activity Action", String.valueOf(resolveInfos.get(0).activityInfo.name));

1 Package Name在AndroidManifest.xml文件中定义,如果没有AndroidManifest.xml文件源代码,可以将相应的APK文件解压,并反编译AndroidManifest.xml文件即可,或安装APK,查看/data/data目录中系统为该应用创建的目录名(就是Package Name)即可。
2 WebView就是Android SDK中的浏览器控件,用于显示Web页面。在后面的控件详解部分会详细介绍该控件。
3如果系统中没安装其他浏览器程序的话,就只有系统自带的浏览器和WebBrowser有相同的Action,并且符合浏览网页的过滤条件。
4 ROM就是Android系统的安装包,可以将特定版本的Android系统安装在Android硬件设备上,这一安装过程通常称为刷机。
5当然更不能通过隐式方式调用了,因为没用指定Action。
6有APK程序的情况会在本章后面的部分详细讨论。
7 root权限相当于Windows的Administrator用户。没有root权限会使得很多事都无法做,例如,无法访问手机内存中的系统目录。