Android 基础复习

2018/5/18 posted in  Android

AndroidManifest

  • uses-sdk 这个节点用于定义要想正确地运行应用程序,设备上必须具有的最低和最高SDK版本。
  • uses-configuration 指定应用程序支持的每个输入机制的组合。一般不需要,适合有特殊输入控制的游戏。
  • uses-feature Android可以在各种各样的硬件平台上运行。可以使用多个uses-feature节点来指定应用程序需要的每个硬件功能。这可以避免将应用程序安装到不包含必要的硬件功能的设备上。
    • 音频 用于要求低延迟音频管道的应用程序
    • 蓝牙 用于需要蓝牙传输的应用程序
    • 摄像头 用于要求有摄像头的应用程序。还可以要求具有自动聚焦功能、闪光灯或前摄像头
    • 位置 用于需要基于位置的服务的应用程序。还可以显式指定要求网络或GPS支持
    • 麦克风 用于需要音频输入的应用程序
    • NFC 要求NFC支持
    • 传感器 指定对任何潜在可用的硬件传感器的要求
    • 电话服务 指定需要一般性的电话服务
    • 触摸屏 指定应用程序需要的触摸屏类型
    • USB 用于需要支持USB host或accessory模式的应用程序
    • Wi-Fi 用于需要支持Wi-Fi网络的应用程序
  • supports-screens 应用程序支持的屏幕
  • smallScreens 分辨率比传统的HVGA小的屏幕
  • normalScreens 用于指定典型的手机屏幕
  • largeScrees 比普通屏幕大的屏幕
  • xlargeScreens 比普通大屏幕更大的屏幕
  • requiresSmallestWidthDp 允许使用设备无关的像素指定支持的最小屏幕宽度
  • compatibleWidthLimitDp 指定一个上限,超出此值后应用程序可能无法扩展。使用该属性可以使系统在屏幕分辨率大于你指定的值得设备上启动兼容模式
  • largestWidthLimitDp 指定一个绝对上限。在屏幕分辨率大于你指定的值得设备上,这回导致系统强制应用程序在兼容模式下运行。
  • supports-texture 用于声明应用程序能够提供以一种特定的GL纹理压缩格式压缩的纹理资源。
  • uses-permission 声明应用程序所需要的权限,并告诉给用户。
  • permission 应用程序组件也可以创建权限来限制对共享应用程序组件的访问
  • instrumentation instrumentation类提供一个测试框架,用来在应用程序运行时测试应用程序组件。对于应用程序所创建的每一个测试类,都需要创建一个新的节点。
  • application 一个Manifest只能包含一个application节点。它使用各种属性来指定应用程序的各种元数据。包含了Activity、Service、Content Provider和Broadcst Receiver节点的容器。
    • activity 应用程序内的每一个Activity都要求有一个activity标签
      • 运行时配置更改添加(android:configChanges属性),添加该属性可以阻止由于特定配置改变而造成的重启,并会触发Activity中的onConfigurationChanged处理程序。可以通过重写这个方法来处理配置的改变,并使用传入Configuration对象来确定新的配置值。
        • mcc和mnc 检测到SIM,并且预支关联的国家或网络的代码发生了变化
        • locale 用于改变了设备的语言设置
        • keyboardHidden 显示或者隐藏了键盘、d-pad或其他输入设置
        • keyboard 对键盘的类型进行了更改。
        • fontScale 用户修改了首选的字体大小
        • uiMode 整体UI模式发生了变化。
        • orientation 屏幕在纵向和横向之间进行了旋转
        • screenLayout 屏幕布局发生了变化,比如激活了另外一个屏幕
        • screenSize 当可用屏幕尺寸改变
        • smallestScreenSize当物理屏幕尺寸改变
    • service
    • provider 指定应用程序中的每一个Content Provider。
    • receiver 注册Broadcast Receiver。
    • uses-library 用于指定应用程序需要的共享库。

应用程序的进程

  • Active 进程 指那些有组件正在和用户进行交互的应用程序的进程。只有在最后关头才会被系统终止回收。
    • 处于活动状态的Activity
    • 正在执行onReceive事件处理程序的Broadcast Receiver
    • 正在执行onStart、onCreate或者onDestroy事件处理程序的Service
    • 正在运行、且已被标记为在前台运行的Service
  • 可见进程 当一个Activity被部分遮挡时就会出现这种情况
  • 启动Service进程 已经启动的Service进程。因为后台Service没有直接和用户交互,所以优先级比可见Activity或前台Service低一些。当系统终止一个运行的Service后,会在资源可用时尝试重新启动Service
  • 后台进程 不可见、并且没有任何正在运行的Service的Activity进程。
  • 空进程 为了提高系统整体性能。Android经常在应用程序的生存期结束之后仍然把它们保存在内存中。Android通过维护这个缓存来减少应用程序被再次启动时的启动时间。通常这些进程会根据需要被定期终止

Application

每次应用程序运行时,应用程序的Application类都保持实例化状态。与Activity不同,配置改变并不会导致应用程序重启。

扩展Application类,可以完成以下3项工作

  • 在Android运行时广播的应用程序级事件做出响应
  • 在应用程序组件之间传递对象
  • 管理和维护多个应用程序组件使用的资源

重写应用程序的生命周期事件

  • onCreate 在创建应用程序时调用。可以重写这个方法来实例化应用程序单态,以及创建和实例化任何应用程序状态变量或共享资源
  • onLowMemory 当系统处于资源匮乏的状态时,具有良好行为的应用程序可以释放额外的内存。这个方法一般只会在后台进程已经终止,但是前台应用程序仍然缺少内存时调用。可以重写这个处理程序来清空缓存或释放不必要的资源
  • onTrimMemory 当运行时决定当前应用程序应该尝试减少其内存开销时(通常在它进入后台时)调用
  • onConfigurationChanged 与Activity不同,在配置改变时,应用程序对象不会被终止或重启个。如果应用程序使用的值依赖于特定的配置,则重写这个方法来重新加载这些值,或者在应用程序级别处理配置改变

Activity

生命周期

20180813153414769138796.png
20180813153414769138796.png

  • onCreate() ———— 当活动首次被创建时调用。此时Activity还在后台,不可见。用于创建和实例化将在应用程序中使用的对象。
  • onRestoreInstanceState() ———— 用于恢复UI状态
  • onStart() ———— 当活动对用户可见时调用,启动动画、线程、传感器监听器、GPS查找、定时器、Service或其他用于更新用户界面的进程,注册Broadcast Receiver。
  • onResume() ———— 当活动与用户开始交互时调用。启动当活动位于前台时需要运行的任何服务或代码。
  • onSaveInstanceState() ———— 把UI状态改变保存到savedInstanceState
  • onPause() ———— 当当前活动被暂停并恢复以前的活动时调用。停止当活动不在前台时不需要运行的任何服务或代码。
  • onStop() ———— 当活动不再对用户可见时调用,用来暂停或停止动画、线程、传感器监听器、GPS查找、定时器、Service或其他用于更新用户界面的进程,注销Broadcast Receiver。
  • onDestroy() ———— 当活动被系统销毁时调用。在活动销毁前释放资源。
  • onRestart() ———— 在活动已停止并要再次启动时调用。

Activity的状态

  • 活动状态 当一个Activity位于栈顶的时候,它是可见的、具有焦点的前台Activity,这时它可以接收用户输入。Android将会不惜一切代价来保持它处于活动状态,并根据需要来销毁栈下面部分的Activity,以保证这个Activity拥有它所需要的资源。
  • 暂停状态 当Activity是可见的,但是没有获得焦点,此时它就处于暂停状态。
  • 停止状态 当一个Activity不可见的时候,它就处于停止状态。此时,Activity仍然会停留在内存中,保存所有的状态信息,然而当系统的其他地方要求使用内存的时候,它们就会成为被终止的首要候选对象。
  • 非活动状态 当一个Activity被终止之后,在启动之前它就处于非活动状态。处于非活动状态的Activity已经从Activity栈中移除了。
    ### 管理屏幕发生变化
    当设备的屏幕方向发生改变时,会把活动销毁,并重建。所以需要确保采取必要的措施来保持方向改变之前活动的状态。在包含视图的活动被销毁时,只有那些在活动中被命名的视图(android:id属性)才能保持它们的状态。(例:命名过id的EditText视图中的任何文本都会在活动创建时自动恢复,而没有命名id的EditText视图,不会保持视图中当前所含文本)

当终止一个活动,将在以下两个方法中保存数据:

  • onPause() ———— 利用数据库、内部或外部的文件存储器存储持久化数据。
  • onSaveInstanceState() ———— 提供Bundle对象作为一个参数,可以用它保存活动的状态。可以在onCreate或随后的onRestoreInstanceState()方法中恢复Bundle中保存的状态。但通过Bundle对象保存状态信息具有局限性,无法保存更复杂的数据结构。
  • onRetainNonConfigurationInstance()方法。当一个活动由于配置改变将要销毁时会触发这一方法。可以在方法中返回保存当前的数据。并在onCreate()使用getLastNonCOnfigurationInstance()方法进行提取。

检测方向改变

WindowManager wm = getWindowManager();
Display dp = wm.getDefaultDisplay();
if (dp.getWidth() > dp.getHeight()) {
    Log.d("Orientation","Landscape mode");
} else {
    Log.d("Orientation","Portrait mode");
}

控制活动方向

  • Activity类 setRequestOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE/ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
  • AndroidManifest.xml中元素上使用android:screenOrientation属性。

侦听用户界面通知

  • onKeyDown 当一个按键按下并且没有被活动中的任何视图处理时调用
  • onKeyUp 当一个键被释放并且没有被活动中的任何视图处理时调用
  • onMenuItemSelected 当用户选择了面板的菜单时调用
  • onMenuOpened 当用户打开了面板的菜单时调用

Fragment

生命周期

20180813153415635189141.png
20180813153415635189141.png

  • onAttached() ———— 当碎片与活动建立关联时调用
  • onCreateView() ———— 用于创建碎片的视图
  • onActivityCreated() ———— 当活动的onCreate()方法被返回时调用
  • onDestroyView() ———— 当碎片的视图被移除时调用
  • onDetach() ———— 当碎片与活动的关联被移除时调用

Intent

Intent是一种消息传递机制,可以在应用程序内使用,也可以在应用程序间使用。

  • 使用类名显式启动一个特定的Service或Activity
  • 启动Activity或Service来执行一个动作的Intent,通常需要使用特定的数据,或者对特定的数据执行动作
  • 广播某个事件已经发生

显式启动新Activity

要显式地选择要启动的Activity类,可以创建一个新的Intent来指定当前Activity的上下文以及要启动的Activity的类,然后把这个Intent传递给startActivity

Intent intent = new Intent(MyActivity.this,MyOtherActivity.class);
startActivity(intent);

在调用startActivity之后,新的Activity将会被创建、启动和恢复运行,他会移动到Activity栈的顶部

调用新的Activity的finish或按下设备的返回按钮将关闭该Activity,并把它从栈中移除。开发人员可以通过调用startActivity导航到其他Activity。每次调用startActivity时,会有一个新的Activity添加到栈中,而按下后退按钮则依次删除每个Activity。

隐式的Intent

隐式的Intent提供了一种机制,可以让匿名的应用程序组件响应动作请求。这意味着可以要求系统启动一个可执行给定动作的Activity,而不必知道需要启动哪个应用程序或Activity。

当构建一个新的隐式的Intent时,需要指定一个要执行的动作,也可以提供执行那个动作需要的数据的URI。还可以通过向Intent添加extra来向目标Activity发送额外的数据。

Extra是一种向Intent附加基本类型值得机制。可以在热河Intent上使用重载后的putExtra方法来附加一个新的名称/值对,以后在启动的Activity中使用对应的getExtra方法来检索它。Extra作为一个Bundle对象存储在Intent中,可以使用getExtras方法检索。如果多个Activity都能够执行指定的动作,则会向用户呈现各种选项。

使用Intent调用内置应用程序

打开Web浏览器

Intent i = new Intent(android.content.Intent.ACTION_VIEW,Uri.parse("http://www.amazon.com"));
startActivity(i);

打开拨号界面

Intent i = new Intent(android.content.Intent.ACTION_DIAL,Uri.parse("tel:+651234567"));
startActivity(i);

打开Maps应用

Intent i = new Intent(android.content.Intent.ACTION_VIEW,Uri.parse("geo:37.827500,-122.481670"));
startActivity(i);

打开联系人列表

Intent i = new Intent(android.content.Intent.ACTION_VIEW,Uri.parse("content://contacts"));
startActivity(i);

选择联系人

Intent i = new Intent(android.content.Intent.ACTION_PICK,Uri.parse("content://contacts"));
startActivity(i);

注意: 当有多个活动匹配Intent对象时会出现Complete action using对话框,通过使用Intent类的createChooser()方法来对该对话框进行自定义。

Intent i = new Intent(android.content.Intent.ACTION_VIEW,Uri.parse("http://www.amazon"));
startActivity(Intent.createChooser(i,"Open URL Using..."));

使用了createChooser()方法将对话框的标题改为“Open URL using...”,没有Use by default for this action选项,的另一个好处是当没有活动与您的Intent对象匹配时,不会崩溃。

确定Intent能否解析

通过调用Intent的resolveActivity方法,并向该方法传入包管理器,可以对包管理器进行查询,确定是否有Activity能够启动以响应该Intent。

//Create the impliciy Intent to use to start a new Activity
Intent intent = new Intent(Intent.ACTION_DIAL,Uri.parse("tel:555-2368"));

//Check if an Activity exists to perform this action
PackageManager pm = getPackageManager();
ComponentName cn = intent.resolveActivity(pm);
if (cn == null) {
    //If there is no Activity available to perform the action
    //Check to see if the Google Play Store is available
    Uri marketUri = Uri.parse("market://search?q=pname:com.myapp.packagename");
    Intent marketIntent = new Intent(Intent.ACTION_VIEW).setData(marketUri);
    
    //If the Google Play Store is available,use it to download an application
    //capable of performing the required action.Otherwise log an error.
    if (marketIntent.resolveActivity(pm) != null) {
        startActivity(marketIntent);
    } else {
        Log.d(TAG,"Market client not available");
    }
} else {
    startActivity(intent);
}

如果没有找到Activity,可以选择禁用相关的功能,也可以引导用户找到Google Play Store中合适的应用程序。要注意Google Play并不是在所有的设备和模拟器上都可用的,所以最好也对此进行检查。

原生Android动作

Action 说明 输入
ACTION_ALL_APPS 打开一个列出所有已安装应用程序的Activity。通常,此操作由启动器处理
ACTION_ANSWER 打开一个处理来电的Activity,通常这个动作是由本地电话拨号程序进行处理的。
ACTION_BUG_REPORT 显示一个可以报告BUG的Activity,通常由本地bug报告机制处理。
ACTION_CALL 打开一个电话拨号程序,并立即使用Intent的数据URI所提供的号码拨打一个电话,直接进入系统拨打电话界面,开始拨打电话。此动作只应用于替代本地拨号程序的Activity。大多数情况下,使用ACTION_DIAL是一种更好的方式。 tel:// +phone number
ACTION_CALL_BUTTON 当用户按下硬件的“拨打按钮”时触发,通常会调用拨号Activity
ACTION_DELETE 启动一个Activity,允许删除Intent的数据URI中指定的数据
ACTION_DIAL 打开一个拨号程序,要拨打的号码由Intent的数据URI预先提供,直接进入系统拨号界面。默认情况下,这是由本地Android电话拨号程序进行处理的。拨号程序可以规范化大部分号码样式。 tel: +phone number
ACTION_EDIT 请求一个Activity,要求该Activity可以编辑Intent的数据URI中的数据
ACTION_INSERT 打开一个能够在Intent的数据URI指定的游标处插入新项的Activity,当作为子Activity调用的时候,它应该返回一个指向新插入项的URI
ACTION_PICK 启动一个子Activity,它可以让你从Intent的数据URI指定的Content Provider中选择一个项。当关闭的时候,它应该返回所选择的项的URI。启动的Activity与选择的数据有关,例如,传递 content://contacts/people将会调用本地联系人列表 content://contacts/people
ACTION_SEARCH 通常用于启动特定的搜索Activity。如果没有在特定的Activity上触发它,就会提示用户从所有支持搜索的应用程序中做出选择。可以使用SearchManager.QUERY键把搜索词作为一个Intent的extra中的字符串来提供
ACTION_SEARCH_LONG_PRESS 允许截获对硬件搜索键的长按操作。通常由系统处理,以提供语音搜索的快捷方式。
ACTION_SENDTO 启动一个Activity来向Intent的数据URI所指定的联系人发送一条信息
ACTION_SEND 启动一个Activity,该Activity会发送Intent中指定的数据。接收人需要由解析的Activity来选择。使用setType可以设置要传输的数据的MIME类型。数据本身应该根据它的类型,使用EXTRA_TEXT或者EXTRA_STREAM存储为extra。对于E-mail,本地应用程序也可以使用EXTRA_EMAIL、EXTRA_CC、EXTRA_BCC和EXTRA_SUBJECT键来接收extra。应该只使用ACTION_SEND动作向远程接收人发送数据
ACTION_VIEW 这是最常用的通用动作。视图要求以最合适的方式查看Intent的数据URI中提供的数据。不同的应用程序将会根据所提供的数据的URI模式来处理视图请求。一般情况下,http:地址将会打开浏览器,tel:地址将会打开拨号程序以拨打该号码,geo:地址会在Google地图应用程序中显示出来,而联系人将会在联系人管理器中显示出来
ACTION_WEB_SEARCH 打开一个浏览器,根据SearchManager.QUERY键提供的查询执行Web搜索

使用Intent广播事件

Broadcast Intent用于向监听器通知系统的应用程序或应用程序事件,从而可以扩展应用程序间的事件驱动的编程模型。

Broadcast Intent可以使应用程序更加开放,通过使用Intent来广播一个事件,可以在不用修改原始的应用程序的情况下,让你和第三方开发人员对事件作出反应。在应用程序中,可以通过监听Broadcast Intent来对设备状态变化和第三方应用程序事件作出反应。

intent-filter

使用Intent Filter,应用程序组件可以声明它们支持的动作和数据。

action

使用android:name属性指定要为之服务的动作的名称。每个Intent Filter必须要有至少一个action标签。Action应该是一个描述性的唯一的字符串。所以最好的做法使用基于Java的包命名约定的命名系统。

category

使用android:name属性来指定应该在哪种情况下为action提供服务。每个Intent Filter标签可以包含多个category标签。既可以指定自己的category也可以使用以下Android提供的标准值:

  • ALTERNATIVE 可以把这个动作指定为在特定数据类型上执行的默认动作的可选项。
  • SELECTED_ALTERNATIVE 与ALTERNATIVE相似,但是ALTERNATIVE总是使用后面将描述的intent resolution解析为一个动作,而当要求有很多可能性的时候,则可以使用SELECTED_ALTERNATIVE
  • BROWSABLE 指定一个在浏览器内部可用的动作。如果想让应用程序相应浏览器内触发的动作,那么必须包含BROWSABLE类别。
  • DEFAULT 通过设置这个类型可以使一个组件成为Intent Filter内指定的数据类型的默认动作。对于那些使用一个显式的Intent启动的Activity,这个类别是很有必要的。
  • HOME 通过讲一个Intent Filter的类别设置为HOME,而不指定一个action,就可以把它作为本地屏幕的可选项。
  • LAUNCHER 使用这个类别会让一个Activity出现在应用程序的启动器中。

data

data标签允许指定组件可以执行的数据类型;根据情况,也可以包含多个数据标签。可以使用以下属性的任意组合来指定你的组件所支持的数据:

  • android:host 指定一个幼小的主机名
  • android:mimetype 指定组件可以执行的数据类型
  • android:path 指定URI的有效路径值
  • android:port 指定主机的有效端口
  • android:scheme 指定一种特定的模式
<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"
            android:host="bolg.radioactiveyak.com"/>
</intent-filter>

在Android设备上点击YouTubu视频的链接时,会提示使用YouTube应用。这是通过在Intent Filter的data标签下指定scheme、host和path属性来实现。

Android 如何解析 Intent Filter

  • Android将已安装包的可用的Intent Filter放到一个列表中
  • 那些鱼解析Intent时相关联的动作或者类别不匹配的Intent Filter将会从列表中移除
    • 如果Intent Filter包含了指定的动作,那么就认为动作匹配了。
    • 对于category匹配来说,Intent Filter必须包含待解析的Intent中的所有category,但可以包含Intent中所不包含的其他的category。一个没有指定的category的Intent Filter只能和没有任何category的Intent相匹配。
  • Intent的数据URI的每一个部分都和Intent Filter的data标签进行比较。如果Intent Filter指定了scheme、host/authority、path或者MIME类型,呢么这些值都要和Intent的URI比较。任意一个不匹配都会把Intent Filter从列表中衣橱。没有指定数据值的Intent Filter将会和所有的Intent数据值匹配。
    • MIME类型是指要匹配的数据的数据类型。如果不指定数据类型,它会和所有的Intent匹配。
    • scheme是URI的“协议”部分(例如,http: mailto: 或者 tel:)
    • hostname 或者 data authority是URI位于scheme和path之间的部分
    • 数据path是authority之后的内容。只有数据的scheme和hostname都匹配的时候,path才匹配。
  • 当隐式启动一个Activity时,如果这个进程解析出多个组件,那么所有可能匹配的组件都会呈现给用户。对于Broadcast Receiver,每个匹配的接收器将接收Broadcast Intent。

使用Intent Filter作为插件和扩展

Android 提供一个插件模型,可以让你的应用程序利用由你自己或者第三方应用程序组件所匿名提供的功能。

想应用程序提供匿名的动作

Category 必须是ALTERNATIVE 或者 SELECTED_ALETERNATIVE

<activity android:name=".NOstromoController">
    <intent-filter
        android:label="@string/Nuke_From_Orbit">
        <action android:name="com.pad.nostromo.NUMKE_FROM_ORBIT"/>
        <data android:mimeType="vnd.moonbase.cursor.item/*"/>
        <category android:name="android.intent.category. ALTERNATIVE"/>
        <category android:name="android.intent.category. SELECTED_ALETERNATIVE"/>
</activity>
从第三方Intent Receiver中发现新的动作

通过Package Manager,可以创建一个指定了数据类型和action类别的Intent,让系统能返回一个能够在该数据上执行该动作的Activity的列表。

PackageManager packageManager = getPackageManager();

//创建一个Intent用来解析哪个动作应该出现在菜单中
Intent intent = new Intent();
intent.setData(MoonBaseProvider.CONTENT_URI);
intent.addCategory(Intent.CATEGORY_SELECTED_SLTERNSTIVE);

//指定标识。
int flags = PackageManager.MATCH_DEFAULT_ONLY;

//生成列表
List<ResolveInfo> actions;
actions = packageManager.queryIntentActivities(intent,flags);

//获取动作名称的列表
ArrayList<String> labels = new ArrayList<String>();
Resources r = getResources();
for (ResolveInfo action : actions) {
    labels.add(r.getString(action.labelRes));
}
把匿名的动作作为菜单项集成

Menu类中可用的addIntentOptions方法可以指定一个描述了在Activity中所操作的数据的Intent。然而,和只简单地返回一个可用的Receiver的列表不同,会为每个动作创建一个新的菜单项,并使用匹配的Intent Filter的标签填充其文本。

@Override
public boolean onCreateOptionMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    
    //创建一个Intent用来解析哪个动作应该出现在菜单中
    Intent intent = new Intent();
    intent.setData(MoonBaseProvider.CONTENT_URI);
    intent.addCategory(Intent.CATEGORY_SELECTED_ALTERNATIVE);
    
    //正常的菜单选项用来为要添加的菜单项设置group和ID值
    int menuGroup = 0;
    int menuItemId = 0;
    int menuItemOrder = Menu.NONE;
    
    //提供调用动作的组建的名称————通常为当前的Activity
    ComponentName caller = getComponentName();
    
    //首先定义应该添加的一些Intent
    Intent[] specificIntents = null;
    
    //通过前面Intent创建的菜单项将填充这个数组
    MenuItem[] outSpecificItems = null;
    
    //填充菜单
    menu.addIntentOptions(menuGroup,menuItemId,menuItemOrder,caller,specificIntents,intent,flags,outSpecificItems);
    return true;
}

Pending Intent

PendingIntent类提供了一种创建可由其他应用程序在稍晚的时候触发的Intent的机制。

Pending Intent通常用于包装在响应将来的事件时触发的Intent。

int requestCode = 0;
int flags = 0;

//启动一个Activity
Intent startActivityIntent = new Intent(this,MyOtherActivity.class);
PendingIntent.getActivity(this,requestCode,startActivityIntent,flags);

//启动一个Service
Intent startServiceIntent = new Intent(this,MyService.class);
PendingIntent.getService(this, requestCode, startServiceIntent,flags);

//广播一个Intent
Intent broadcastIntent = new Intent(NEW_LIFEORM_DETECTED);
PendingIntent.getBroadcast(this, requestCode, broadcastIntent,flags);

显示通知Notification

视图

基础视图

Chronometer

一个TextView的扩展,实现了一个简单的计时器

ViewFlipper

允许将一组View定义为一个水平行的ViewGroup,其中任意时刻只有一个View可见,并且可见View之间切换会通过动画形式表现出来,可以自动切换

QuickContactBadge

显示一个徽标,该徽标显示了一个图片,该图片关联了通过电话号码、姓名、电子邮件或URI所指定的联系人信息。单击图片会显示一个快速联系人栏,它提供了联系选中的联系人的多种快捷方式————包括打电话和发送短消息、电子邮件及IM等。

复合控件

复合控件即是指不可分割的,自包含的视图组,其中包含了多个排列和连接在一起的子视图。当创建复合控件时,必须对它包含的视图的布局、外观和交互进行定义。复合控件时通过扩展一个ViewGroup来创建。

<LinearLayout xmlns:android="http:/schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <Button
        android:id="@+id/clearButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Clear"/>
</LinearLayout>


public class ClearableEditText extends LinearLayout {
    EditText editText;
    Button clearButton;
    
    public ClearableEditText(Context context) {
        super(context);
        
        LayoutInflater.inflate(R.layout.clearable_edit_text,this,true);
        
        //获得对子控件的引用
        editText = (EditText) findViewById(R.id.editText);
        clearButton = (Button) findViewById(R.id.clearButton);
    }
}

自定义视图

一般分别对onMeasure和onDraw方法进行重写

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }
    
    public MyView(Context context,AttributeSet ats,int defaultStyle) {
        super(context,ats,defaultStyle);
    }
    
    public MyView(Context context,AttributeSet attrs) {
        super(context,attrs);
    }
    
    @Override
    protected void onMeasure(int wMeasureSpec,int hMeasureSpec) {
        int measuredHeight = measureHeight(hMeasureSpec);
        int measuredWidth = measureWidth(wMeasureSpec);
        
        //必须调用setMeasuredDimension
        //否则在布局控件的时候
        //会造成运行时异常
        setMeasuredDimension(measuredWidth, measuredHeight);
    }
    
    private int measureHeight(int measureSpec) {
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        
        //如果不指定限制,就是默认大小
        int result = 500;
        
        if (specMode == MeasureSpec.AT_MOST) {
            //Calculate the ideal size of your
            //计算控件在这个最大尺寸范围内的理想大小
            //如果控件填充了可用空间,则返回外边界
            result = specSize;
        } else if (specMode == MeasureSpec.EXCATLY) {
            //如果控件可以放置在这个边界内,则返回该值
            result = specSize;
        }
        
        return specSize;
    }
    
    private int measureWidth(int measureSpec) {
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        
        //如果不指定限制,就是默认大小
        int result = 500;
        
        if (specMode == MeasureSpec.AT_MOST) {
            //Calculate the ideal size of your
            //计算控件在这个最大尺寸范围内的理想大小
            //如果控件填充了可用空间,则返回外边界
            result = specSize;
        } else if (specMode == MeasureSpec.EXCATLY) {
            //如果控件可以放置在这个边界内,则返回该值
            result = specSize;
        }
        
        return specSize;
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        //绘制背景
        super(canvas)
        //绘制表面内容
    }
}

处理用户交互事件

  • onKeyDown 当任何设备按键被按下时,就会调用它
  • onKeyUp 当用户释放一个按键时调用
  • onTrackballEvent 当设备的轨迹球被移动的时候调用
  • onTouchEvent 当触摸屏被按下或者释放时调用,或者当检测到运动时调用
@Override
public boolean onKeyDown(int keyCode,KeyEvent keyEvent) {
    //如果事件得到处理,返回true
    return true;
}

@Override
public boolean onKeyUp(int keyCode,KeyEvent keyEvent) {
    //如果事件得到处理,返回true
    return true;
}

@Override
public boolean onTrackballEvent(MotionEvent event) {
    //获得这个事件代表的动作类型
    int actionPerformed = event.getAction();
    //如果事件得到处理,返回true
    return true;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    //获得这个事件代表的动作类型
    int actionPerformed = event.getAction();
    //如果事件得到处理,返回true
    return true;
}

Broadcast Receiver

广播事件

在应用程序组件中,可以构建希望广播的Intent,然后使用sendBroadcast方法来发送它。

可以对Intent的动作、数据和分类进行设置,从而使Broadcast Receiver能够精确地确定它们的需求。在这种方案中,Intent动作字符串可以用来标识要广播的事件,所以它应该是能够标识事件的唯一的字符串。习惯上,动作字符串使用于Java包名相同的构建方式。public static final String NEW_LIFEFORM_DETECTED = "com.paad.action.NEW_LIFEFORM";

如果希望在Intent中包含数据,那么可以使用Intent的data属性指定一个URI,也可以包含extras来添加额外的基本值。

Intent intent = new Intent(LifeformDetectedReceiver.NEW_LIFEFORM);
intent.putExtra(LifeformDetectedReceiver.EXTRA_LIFEFORM_NAME,detectedLifeform);
intent.putExtra(LifeformDetectedReceiver.EXTRA_LONGTUDE,currentLongitude);
intent.putExtra(LifeformDetectedReceiver.EXTRA_LATITUDE,currentLatitude);
sendBroadcast(intent);

监听广播

Boradcast Receiver 可以用来监听Broadcast Intent。要使Broadcast Receiver能够接收广播,就需要对其进行注册,既可以使用diamante,也可以在应用程序的manifest文件中注册。但无论怎么注册,都需要使用一个Intent Filter来指定它要监听哪些Intent和数据。

对于包含manifest接收器的应用程序,在Intent被广播出去的时候,应用程序不一定非要处于运行状态才能执行接收。当匹配的Intent被广播出去的时候,它们会被自动地启动。即使被关闭或销毁了,也仍然能够对广播事件作出响应。

要创建一个新的Brocast Receiver,需要扩展BroadcastReceiver类并重写onReceive事件处理程序。

public class MyBoradcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context,Intent intent) {
        //响应接收到的Intent
    }
}

当接收到一个与在注册接收器时使用的Intent Filter相匹配的Broadcast Intent的时候,就会执行onReceive方法。onReceive处理程序必须在5秒钟内完成,否则就会显示Force Close对话框

Broadcast Receiver将会更新内容、启动Service、更新Activity UI,或者使用Notification Manager来通知用户。

public class LifeformDetectedReceiver extends BroadcastReceiver {
    public final static String EXTRA_LIFEFORM_NAME = “EXTRA_LIFEFORM_NAME”;
    public final static String EXTRA_LATITUDE = "EXTRA_LATITUDE";
    public final static String EXTRA_LONGTUDE = "EXTRA_LONGTUDE";
    public final static String ACTION_BURN = "com.paad.alien.action.BURN_IT_WITH_FIRE";
    public final static String NEW_LIFEFORM = "com.paad.alien.action.NEW_LIFEFORM";
    
    @Override
    public void onReceive(Context context,Intent intent) {
        //从Intent获得lifeform的细节
        Uri data = intent.getData();
        String type = intent.getStringExtra(EXTRA_LIFEFORM_NAME);
        double lat = intent.getDoubleExtra(EXTRA_LATITUDE,0);
        double lng = intent.getDoubleExtra(EXTRA_LONGTUDE,0);
        Location loc = new Location("gps");
        loc.setLatitude(lat);
        loc.setLongitude(lng);
        if (type.equals("facehugger")) {
            Intent startIntent = new Intent(ACTION_BURN,data);
            startIntent.putExtra(EXTRA_LATITUDE,lat);
            startIntent.putExtra(EXTRA_LONGTUDE,lng);
            
            context.startService(startIntent);
        }
    }
}

在manifest中注册Broadcast Receiver

在application节点中添加一个receiver标签,以指定要注册的Broadcast Receiver的类名。接收器节点需要包含一个intent-filter标签来指定要监听的动作字符串。

<receiver android:name=".LifeformDetectedReceiver">
    <intent-filter>
        <action android:name="com.paad.alien.action.NEW_LIFEFORM"/>
    </intent-filter>
</receiver>

通过这种方式注册的Broadcast Receiver总是活动的,并且即时当应用程序被终止或未启动时,也可以接收Broadcast Receiver。

在代码中注册Broadcast Receiver

影响特定Activity的UI的Broadcast Receiver通常在代码中注册。在代码中注册的接收器只会在包含它的应用程序组件运行时响应Broadcast Intent。

在接收器用来更新一个Activity中的UI元素时,这样做很有帮助。在这种情况下,在onResume处理程序中注册接收器,并在onPause中注销它。

private IntentFilter filter = new IntentFilter(LifeformDetectedReceiver.NEW_LIFEFORM);
private LifeformDetectedReceiver receiver = new LifeformDetectedReceiver();

@Override
public void onResume() {
    super.onResume();
    //注册Broadcast Receiver
    registerReceiver(receiver,filter);
}

@Override
public void onPause() {
    //注销Broadcast Receiver
    unregisterReceiver(receiver);
    super.onPause();
}

广播有序的Intent

当Broadcast Receiver接收Intent的顺序十分重要时,特别是当需要接收器能够影响将来的接收器收到的Broadcast Intent时,可以使用sendOrderedBroadcast方法。

String requiredPermission = "com.paad.MY_BROADCAST_PERMISSION";
sendOrderedBroadcast(intent,requiredPermission);

使用这个方法时,Intent将会按照优先级顺序被传递给所有具有合适权限的已注册的接收器,可以在Broadcast Recevier的Intent Filter manifest节点中使用android:priority属性指定其权限,值越大,优先级越高。

<receiver
    android:name=".MyOrderedReceiver"
    android:permission="com.paad.MY_BROADCAST_PERMISSION">
    <intent-filter
        android:priority="100">
        <action android:name="com.paad.action.ORDERED_BROADCAST"/>
    </intent-filter>
</receiver> 

广播Sticky Intent

Sticky Intent是Broadcast Intent的有用变体,可以保存它们最后一次广播的值,并且当有一个新的接收器被注册为接收该广播时,它们会把这些值作为Intent返回。

当调用registerReceiver来指定一个匹配Sticky Broadcast Intent的Intent Filter时,返回值将是最后一次Intent广播,例如电池电量变化的广播:

IntentFilter battery = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent currentBatteryCharge = registerReceiver(null,battery);

不是必须指定一个接收器来获得Sticky Intent当前值。要广播自己的Sticky Intent,应用程序必须具有BROADCAST_STICKY用户权限,然后需要调用sendStickyBroadcast并传入相关的Intent:
sendStickyBroadcast(intent);

要删除一个Sticky Intent,可以调用removeStickyBroadcast,并传入要删除的Sticky Intent:

removeStickyBroadcast(intent);

监听本地Broadcast Intent

很多的系统Service都会广播Intent来指示一种变化。可以使用这些信息来向自己的项目中添加基于系统事件的功能。下列动作用来跟踪设备状态的变化:

  • ACTION_BOOT_COMPLETED 一旦系统完成了它的启动序列之后,就会触发这个动作。要想接收这个广播,应用程序需要具有RECEIVE_BOOT_COMPLETED权限
  • ACTION_CAMERA_BUTTON 当单机拍照按键的时候触发
  • ACTION_DATE_CHANGED 和 ACTION_TIME_CHANGED 如果设备的日期和时间被手动修改了,这些动作就会被广播。
  • ACTION_MEDIA_MOUNTED 和 ACTION_MEDIA_UNMOUNTED 任何时候,当新的外部存储介质被成功地添加或者从设备移除的时候,都会触发这两个事件。
  • ACTION_NEW_OUTGOING_CALL 当将要向外拨打电话的时候就会进行广播。监听这个广播可以截获播出的电话呼叫。拨打的电话号码存储在EXTRA_PHONE_NUMBER extra中,而返回的Intent中的resultData则是实际拨打的号码。要为这个动作注册一个Broadcast Receiver,应用程序必须声明PROCESS_OUTGOING_CALLS使用权限。
  • ACTION_SCREEN_OFF 和 ACTION_SCREEN_ON 当屏幕关闭或者打开时就分别对其广播。
  • ACTION_TIMEZONE_CHANGED 当手机当前的时区发生改变的时候就会广播这个动作。

使用Broadcast Intent监控设备的状态变化

监控设备状态是创建高效和动态的应用程序的重要的一部分,该应用程序的行为根据连接性、电量状态和dock状态发生变化。

监听电量变化
IntentFilter batIntentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent battery = context.registerReceiver(null,batIntentFilter);
int status = battery.getIntExtra(BatteryManager.EXTRA_STATUS,-1);
boolean is Charging = status == BatteryManager.BATTERY_STATUS_CHANGING || status == BatteryManager.BATTERY_STATUS_FULL;

不能再manifest文件的Receiver中注册电量变化的动作。然而可以使用下面的动作字符串监控和电源的连接情况以及低电量情况。

  • ACTION_BATTERY_LOW
  • ACTION_BATTERY_OKAY
  • ACTION_POWER_CONNECTED
  • ACTION_POWER_DISCONNECTED
监听连接变化

连接的变化,包括贷款、延迟和Intent连接是否可用等信息。想要监听连接的变化,注册一个Broadcast Receiver用来监听 android.net.conn.CONNECTIVITY_CHANGE(ConnectivityManager.CONNECTIVITY_ACTION)动作。

连接变化的广播不是sticky的而且也不包含任何和变化相关的额外信息。想要获得当前连接状态的详细信息,需要使用Connectivity Manager。

String svcName = Context.CONNECTIVITY_SERVICE;
ConnectivityManager cm = (ConnectivityManager)context.getSystemService(svcName);

NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
boolean isConnected = activeNetwork.isConnectedOrConnecting();
boolean isMobile = activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE;
监听Dock变化

通过注册一个Receiver来监听Intent.ACTION_DOCK_EVENT,可以确定docking的状态和类型。

和电池状态一样,dock事件的Broadcast Intent也是sticky的。

IntentFilter dockIntentFilter = new IntentFilter(Intent.ACTION_DOCK_EVENT);
Intent dock = registerReceiver(null,dockIntentFilter);
int dockState = dock.getIntExtra(Intent.EXTRA_DOCK_STATE,Intent.EXTRA_DOCK_STATE_UNDOCKED);
boolean isDocked = dockState != Intent.EXTRA_DOCK_STATE_UNDOCKED;
在运行时管理Manifest Receiver

使用Package Manager的setComponentEnabledSetting方法,可以在运行时启用和禁用应用程序的manifest Receiver。

想要减少应用程序的开销,当应用程序不需要响应一些系统事件时,最好禁用监听这些常见系统事件的manifest Receiver。这项技术也能够让你定时执行一个基于系统事件的动作,如当设备连接到Wi-Fi时去下载一个大文件。

ComponentName myReceiverName = new ComponentName(this,MyReceiver.class);
PackageManager pm = getPackageManager();

//启用一个manifest receiver
pm.setComponentEnabledSetting(myReceiverName,PackageManager.COMPONENT_ENABLED_STATE_ENABLED,PackageManager.DONT_KILL_APP);

//禁用一个manifest receiver
pm.setComponentEnabledSetting(myReceiverName,PackageManager.COMPONENT_ENABLED_STATE_DISABLED,PackageManager.DONT_KILL_APP);

Local Broadcast Manager

用于简化注册Broadcast Intent,以及在应用程序内的组件之间发送Broadcast Intent的工作。因为局部广播的作用域要小一些,所以使用Local Broadcast Manager比发送全局广播更加高效。而且使用Local Broadcast Manager也确保了应用程序外部的任何组件都收不到你广播的Intent,所以不会有私人数据或敏感数据泄露出去的风险。

LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);

要注册一个局部Broadcast Receiver,与注册全局接收器时类似。需要使用Local Broadcast Manager的registerReceiver方法,并传入一个Broadcast Receiver 和一个 Intent Filter

lbm.registerReceiver(new BroadcastReceiver() {
    @Override
    public void onReceive(Context context,Intent intent) {
        
    }
}, new IntentFilter(LOCAL_ACTION));

要发送一个局部Broadcast Intent,可以使用Local Broadcast Manager的sendBroadcast方法,并传入要广播的Intent:lbm.sendBroadcast(new Intent(LOCAL_ACTION));

Content Provider

在Android中推荐使用Content Provider来实现跨包的数据共享。其行为方式和数据库很像,可以进行增删改查等操作。但是其可以将数据存储在数据库、文件甚至是网络上。

Content Provider提供了一个接口用来发布数据,通过Content Resolver来使用数据。它们允许将使用数据的应用程序组件和底层的数据源分离开来,并提供了一种通用的机制来允许一个应用程序共享它们的数据或者使用其他应用程序提供的数据。

Android附带了许多有用的Content Provider

  • Browser ———— 存储诸如浏览器书签、浏览器历史记录等数据
  • CallLog ———— 存储诸如未接电话、通话详细信息等数据
  • Contacts ———— 存储联系人详细信息
  • MediaStore ———— 存储媒体文件,如音频、视频和图像
  • Settings ———— 存储设备的设置和首选项

查询URL的格式: :////

  • standard_prefix始终是content://
  • authority 指定了Provider的名称,如内置的Contacts 内容提供者的名称为contacts。对于第三方内容提供者,将采用其完全限定的名称。
  • data path 指定了请求数据的类型。如果从Contacts获取联系人,那么data path就是people
  • id 指定了请求的特定记录。 如果从Contacts获取二号联系人,那么id值为2

代码实现

Uri allContacts = Uri.parse("content://contacts/people");
Cursor c;
CursorLoader cursorLoader = new CursorLoader (
    this,allContacts,null,null,null,null
);
c = cursorLoader.loadInBackground();
if (c.moveToFirst) {
    do {
        String contactID = c.getString(c.getColumnIndex(ContactsContract.Contacts._ID));
        String contactDisplayName = c.getString(c.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
    } while (c.moveToNext());
}

创建自己的ContentProvider

扩展ContentProvider抽象类public class MyContentProvider extends ContentProvider

注册Content Provider

同Activity和Service一样,Content Resolver必须在应用程序清单文件中进行注册,注册是通过provider标记实现的。使用authorities标记来设定Content Provider的基本URI,找到想要交互的数据库。每个Content Resolver的授权必须是唯一的,因此最好用包名作为URI的基本路径com.{CompanyName}.provider.<ApplicationName>

发布Content Provider的URI地址

每个Content Provider都应该使用一个公有的静态CONTENT_URI属性来公开他们的授权。这个CONTENT_URI应该包含一个主要内容的数据路径。public static final Uri CONTENT_URI = Uri.parse("content://com.paad.skeletondatabaseprovider/elements");直接使用这种形式的查询表示请求所有行,而在结尾附加/的查询请求表示请求一条记录。最好同时支持使用这两种形式的提供程序,可以通过UriMatcher实现。

Content Provider的实现框架

public class MyContentProvider extends ContentProvider {
    public static final Uri CONTENT_URI = Uri.parse("content://com.paad.skeletondatabaseprovider/elements")
    
    //创建两个常量来区分不同的URI请求
    private static final int ALLROWS = 1;
    private static final int SINGLE_ROW = 2;
    
    private static final UriMatcher uriMatcher;
    
    //填充UriMatcher对象,以'element'结尾的URI对应请求全部数据
    //以'elements/[rowID]'结尾的URI代表请求单行数据
    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI("com.paad.skeletondatabaseprovider","elements",ALLROWS);
        uriMatcher.addURI("com.paad.skeletondatabaseprovider","elements/#",SINGLE_ROW);
    }
    
    //where 子句中使用的索引列的名称
    public static final String KEY_ID = "_id";
    
    //数据库中每个列的列名和索引。这些内容应该是描述性的。
    public static final String KEY_COLUMN_1_NAME = "KEY_COLUMN_1_NAME";
    
    //SQLiteOpenHelper变量
    private MySQLiteOpenHelper myOpenHelper;
    
    @Override
    public boolean onCreate() {
        //构造底层的数据库
        //延迟打开数据库,直到需要执行一个查询或者事务时再打开
        myOpenHelper = new MySQLiteOpenHelper(getContext(),MySQLiteOpenHelper.DATABASE_NAME,null, MySQLiteOpenHelper.DATABASE_VERSION);
        return true;
    }
    
    @Override
    public Cursor query(Uri uri,String[] projection,String selection,String[] selectionArgs,String sortOrder) {
        //打开数据库
        SQLiteDatabase db = myOpenHelper.getWriteableDatabase();
        
        //必要的话,使用有效的SQL语句替换这些语句
        String groupBy = null;
        String having = null;
        
        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
        queryBuilder.setTables(MySQLiteOpenHelper.DATABASE_TABLE);
        
        //如果是行查询,用传入的行限制结果
        switch(uriMatcher.match(uri)) {
            case SINGLE_ROW:
                String rowID = uri.getPathSegments().get(1);
                queryBuilder.appendWhere(KEY_ID + "=" + rowID);
                break;
            default:
                break;
        }
        
        Cursor cursor = queryBuilder.query(db,projection,selection,selectionArgs,groupBy,having,sortOrder);
        return cursor;
    }
    
    @Override
    public int delete(Uri uri,String selection,String[] selectionArgs) {
        //打开一个可读/可写的数据库来支持事务
        SQLiteDatabase db = myOpenHelper.getWritableDatabase();
        
        //如果是行URI,限定删除的行为指定的行
        switch(uriMatcher.match(uri)) {
            case SINGLE_ROW:
                String rowID = uri.getPathSegments().get(1);
                selection = KEY_ID + "=" + rowID + (!TextUtils.isEmpty(selection) ? "AND (" + selection + ')' : "");
                break;
            default:
                break;
        }
        
        //想要返回删除的项的数量,必须指定一条where子句。删除所有的行并返回一个值,同时传入"1"。
        if (selection == null) {
            selection = "1";
        }
        
        int deleteCount = db.delete(MySQLiteOpenHelper.DATABASE_TABLE,selection,selectionArgs);
        
        //通知所有观察者,数据集已经改变
        getContext().getContentResolver().notifyChange(uri,null);
        return deleteCount;
    }
    
    @Override
    public Uri insert(Uri uri,ContentValues values) {
        //打开一个可读/可写的数据库来支持事务
        SQLiteDatabase db = myOpenHelper.getWritableDatabase();
        
        //要想通过传入一个空Content Value对象的方式向数据库中添加一个空行
        //必须使用nullColumnHack参数来指定可以设置为null的列名
        String nullColumnHack = null;
        
        long id = db.insert(MySQLiteOpenHelper.DATABASE_TABLE,nullColumnHack,values);
        
        //构造并返回新插入行的URI
        if (id > -1) {
            //构造并返回新插入行的URI
            Uri insertedId = ContentUris.withAppendedId(CONTENT_URI,id);
            
            //通知所有的观察者,数据集已经改变
            getContext().getContentResolver().notifyChange(insertId,null);
            return insertedId;
        } else {
            return null;
        }
    }
    
    @Override
    public int update(Uri uri,ContentValues values,String selection,String[] selectionArgs) {
        //打开一个可读/可写的数据库来支持事务
        switch(uriMatcher.match(uri)) {
            case SINGLE_ROW:
                String rowID = uri.getPathSegments().get(1);
                selection = KEY_ID + "=" + rowID + (!TextUtils.isEmpty(selection) ? "AND (" + selection + ')' : "");
                break;
            defalut:
                break;
        }
        
        //执行更新
        int updateCount = db.update(MySQLiteOpenHelper.DATABASE_TABLE,values,selection,selectionArgs);
        
        //通知所有的观察者,数据集已经改变
        getContext().getContentResolver().notifyChange(uri,null);
        
        return updateCount;
    }
    
    @Override
    public ParcelFileDescriptor openFile(Uri uri,String mode) throws FileNotFoundException{
        //找到行ID并把它作为一个文件名使用
        String rowID = uri.getPathSegments().get(1);
        
        //在应用程序的外部文件目录中创建一个文件对象
        String picsDir = Environment.DIRECTORY_PICTURES;
        File file = new File(getContext().getExternalFilesDir(picsDir),rowID);
        
        //如果文件不存在,则直接创建它
        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                Log.d(TAG,"File creation failed:" + e.getMessage());
            }
        }
        
        //将mode参数转换为对应的ParcelFileDescriptor打开模式
        int fileMode = 0;
        if (mode.contains("w")) {
            fileMode |= ParcelFileDescriptor.MODE_WRITE_ONLY;
        }
        if (mode.contains("r")) {
            fileMode |= ParcelFileDescriptor.MODE_READ_ONLY;
        }
        if (mode.contains("+")) {
            fileMode |= ParcelFileDescriptor.MODE_APPEND;
        }
        
        //返回一个代表了文件的ParcelFileDescriptor
        return ParcelFileDescriptor.open(file,fileMode);
    }
    
    @Override
    public String getType(Uri uri) {
        //为一个Content Provider URI返回一个字符串,它标识了MIME类型
        switch(uriMatcher.match(uri)) {
            case ALLROWS:
                return "vnd.android.cursor.dir/vnd.paad.elemental";
            case SINGLE_ROW:
                return "vnd.android.cursor.item/vnd.paad.elemental";
            default:
                throw new IllegalArgumentException("Unsupported URI:" + uri);
        }
    }
    
    private static class MySQLiteOpenHelper extends SQLiteOpenHelper {
    
    }
}

使用Content Provider

Content Resolver简介

每一个程序都有一个ContentResolver实例,可以使用getContentResolver方法来对其进行访问。当使用Content Provider公开数据时,Content Resolver是用来在这些Content Provider上进行查询和执行事务的对应类。Content Resolver包含了一些查询和事务方法,它们与Content Provider中定义的查询和事务方法相对应。

查询Content Provider

查询结果是作为结果集的Cursor返回的。对ContentResolver对象使用query方法并传递给它以下参数:

  • 希望查询的Content Provider的URI
  • 一个投影,列出了希望包含在结果集中的列
  • 一条where子句,定义了要返回的行。可以在其中包含"?"通配符,它将会被传入选择参数的值代替
  • 将代替where子句中"?"通配符的选择参数字符串数组
  • 一个字符串,用来描述返回的行的顺序

查询特定行可以使用ContentUris类的withAppendedId

//获得Content Resolver
ContentResolver cr = getContentResolver();

//指定结果列投影
//返回满足要求所需的最小列表
String[] result_columns = new String[] {
    MyHoardContentProvider.KEY_ID,
    MyHoardContentProvider.KEY_GOLD_HOARD_NAME_COLUMN,
    MyHoardContentProvider.KEY_GOLD_HOARD_COLUMN,
}

//将一个行ID附加到URI以定位特定的行
Uri rowAddress = ContentUris.withAppendedId(MyHoardContentProvider.CONTENT_URI,rowId);

//由于我们在请求单独的一行,因此下列变量的取值都为null
String where = null;
String whereArgs[] = null;
String order = null;

//返回指定的行
Cursor resultCursor = cr.query(rowAddress,result_columns,where,whereArgs,order);

数据库查询的执行时间很长。默认,Content Resolver将在应用程序主线程上执行查询和其他一些事务。可以使用Cursor Loader异步查询内容

使用Cursor Loader异步查询内容

Cursor Loader能够处理在Activity或者Fragment中使用Cursor所需的所有管理任务。

实现 Cursor Loader Callback

要使用Cursor Loader,可创建一个新的LoaderManager.LoaderCallbacks实现。

LoaderManager.LoaderCallbacks<Cursor> loaderCallback = new LoaderManager.LoaderCallbacks<Cursor>(){}

如果需要在Fragment或者Activity中实现一个Loader,通常通过让该组件实现LoaderCallback接口来实现

  • onCreateLoader 当Loader被初始化后,调用onCreateLoader,该处理程序应该创建并返回一个新的Cursor Loader对象。Cursor Loader构造函数的参数与使用Content Resolver执行查询所需的参数是相同的。
  • onLoadFinished 当Loader Manager已经完成了异步查询后,onLoadFinished处理程序会被调用,并把结果Cursor作为参数传入。使用这个Cursor来更新适配器和其他UI元素。
  • onLoaderReset 当Loader Manager重置Cursor Loader的时候,会调用onLoaderReset处理程序。
public Loader<Cursor> onCreateLoader(int id,Bundle args) {
    String[] projection = null;
    String where = null;
    String[] whereArgs = null;
    String sortOrder = null;
    
    Uri queryUri = MyContentProvider.CONTENT_URI;
    
    return new CursorLoader(DatabaseSkeletonActivity.this,queryUri,projection,where,whereArgs,sortOrder);
}

public void onLoadFinished(Loader<Cursor> loader,Cursor cursor) {
    
}

public void onLoadReset(Loader<Cursor> loader) {
    
}

初始化和重新启动Cursor Loader

每个Activity和Fragment都提供了getLoaderManager方法 ,可以调用该方法来访问Loader Manager。

LoaderManager loaderManager = getLoaderManager();

初始化新的Loader

Bundle args = null;
loaderManager.initLoader(LOADER_ID,args,myLoaderCallbacks);

这个过程通常是在Activity的onCreate方法或者Fragment中的onActivityCreated中完成的。

添加、删除和更新内容Cursor Loader

要在Content Provider上执行事务操作,需要使用Content Resolver的insert、delete和update方法。与查询一样,Content Provider的事务会在应用程序主线程上执行,除非把它们移动到一个工作线程。

本地Android Content Provider

  • Media Store 对设备上的多媒体信息、包括音频、视频和图像提供了集中、托管的访问。
  • Browser 读取或者修改浏览器和浏览器搜索历史记录
  • Contacts Contract 检索、修改或者存储联系人的详细信息以及相关的社交流更新
  • Call Log 查看或者更新通话记录
  • Calender 创建新事件,删除或更新现有的日历项。

使用Media Store Content Provider

要从Media Store访问媒体文件,可以使用MediaStore类包含的Audio,Video和Images子类,这些子类有分别包含它们的子类,用来为每个媒体提供程序提供列名和内容URI。

//获得外部卷上每一个音频的Cursor,并提取歌曲名称和专辑名称
String[] projection = new String[] {
    MediaStore.Audio.AudioColumns.ALBUM,
    MediaStore.Audio.AudioColumns.TITLE
}

Uri contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;

Cursor cursor = getContentResolver().query(contentUri,projection,null,null,null);

//获得所需列的索引
int albumIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM);
int titleIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE);

//创建一个数组来存储结果集
String[] result = new String[cursor.getCount()];

//迭代Cursor,提取每个专辑名称和歌曲名称
while(cursor.moveToNext()) {
    //提取歌曲的名称
    String title = cursor.getString(titleIdx);
    //提取专辑的名称
    String album = cursor.getString(albumIdx);
    
    result[cursor.getPosition()] = title + " (" + album + ")";
}

//关闭Cursor
cursor.close();

使用 Contacts Contract Content Provider

Android 向所有被赋予READ_CONTACTS权限的应用程序提供了联系人信息数据库的完全访问权限。

其使用了一个三层数据模型来存储数据,将数据与联系人关联起来,并把同一个人的数据聚集起来,这是通过使用下面的ContactsContract 子类实现的

  • Data 在底层的表中,每个行定义了一组个人数据,并使用MIME类型分离这些数据
  • RawContacts 用户可以为设备指定多个联系人账户提供程序。RawContacts表中的每一行定义了一个账户,可以向这个账户关联一组Data值。
  • Contacts 可以将RawContacts中描述同一个人的行聚集起来。
读取联系人详情

找到联系人姓名的联系信息

ContentResolver cr = getContentResolver();
String[] result = null;

//使用部分姓名匹配找到联系人
String searchName = "andy";
Uri lookupUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI,searchName);

//创建所需列名的投影
String[] projection = new String[] {
    ContactsContract.Contacts._ID
}

//获得一个Cursor,用于返回匹配的名称的ID
Cursor IdCursor = cr.query(lookupUri,projection,null,null,null);

//如果有匹配的ID,则提取第一个匹配的ID
String id = null;
if (idCursor.moveToFirst()) {
    int idIdx = idCursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID);
    id = idCursor.getString(idIdx);
}

//关闭Cursor
idCursor.close();

//创建一个新Cursor,搜索与返回的联系人ID关联的数据
if (id != null) {
    //返回该联系人的所有PHONE数据
    String where = ContactsContract.Data.CONTACT_ID + " = " + id + " AND" + ContactsContract.Data.MIMETYPE + " = '" + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'";
    
    projection = new String[] {
        ContactsContract.Data.DISPLAY_NAME,
        ContactsContract.CommonDataKinds.Phone.NUMBER
    }
    
    Cursor dataCursor = getContentResolver().query(ContactsContract.Data.CONTENT_URI,projection,where,null,null);
    
    //获得所需列的索引
    int nameIdx = dataCursor.getColumnIndexOrThrow(ContactsContract.Data.DISPLAY_NAME);
    int phoneIdx = dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER);
    
    result = new String[dataCursor.getCount()];
    
    while(dataCursor.moveToNext()) {
        //提取姓名
        String name = dataCursor.getString(nameIdx);
        //提取电话号码
        String number = dataCursor.getString(phoneIdx);
        
        result[dataCursor.getPosition()] = name + " (" + number + ")";
    }
    
    dataCursor.close();
}

找到联系人联系号码的联系信息

String incomingNumber = "(650)253-0000";
String result = "Not Found";

Uri lookupUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,incomingNumber);

String[] projection = new String[] {
    ContactsContract.Contacts.DISPLAY_NAME
};

Cursor cursor = getContentResolver().query(lookupUri,projection,null,null,null);

if(cursor.moveToFirst()) {
    int nameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME);
    result = cursor.getString(nameIdx);
}

cursor.close();
使用Intent创建和选择纤细人

选择一个联系人

private static int PICK_CONTACT = 0;
private void pickContact() {
    Intent intent = new Intent(Intent.ACTION_PICK,ContactsContract.Contacts.CONTENT_URI);
    startActivityForResult(intent,PICK_CONTACT);
}

@Override
protected void onActivityResult(int requestCode,int resultCode,Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if ((requestCode == PICK_CONTACT) && (resultCode == RESULT_OK)) {
        resultTextView.setText(data.getData().toString());
    }
}

插入一个新联系人

Intent intent = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT,ContactsContract.Contacts.CONTENT_URI);
intent.setData(Uri.parse("tel:(650)253-0000"));

intent.putExtra(ContactsContract.Intents.Insert.COMPANY,"Google");
intent.putExtra(ContactsContract.Intents.Insert.POSTAL,"1600 Amphitheatre Parkway,Mountain View,California");
startActivity(intent);

使用Calendar Content Provider

查询日历

必须在应用程序的清单文件中包含 READ_CALENDAR 权限。

  • Calendars Calendar应用程序可以显示多个日历,这些日历关联多个账户。该表存储每个可显示的日历,以及日历的详情,如日历的显示名称/时区和颜色。
  • Events 为每个调度的日历事件包含一项,内容包括名称、描述、地点和开始/结束时间
  • Instances 每个事件都有一个或多个实例。
  • Attendees 表中每一项表示一个给定事件的单个参与者。每个参与者可以包含姓名、电子邮件地址和出席状态
  • Reminders 描述了事件提醒,每一行代表一个特定事件的提醒

查询Events表

//创建一个限制结果Cursor为所需列的投影
String[] projection = {
    CalendarContract.Events._ID,
    CalendarContract.Events.TITLE
};

//获取Events提供程序上的Cursor
Cursor cursor = getContentResolver().query(CalendarContract.Events.CONTENT_URI,projection,null,null,null);

//获取列的索引
int nameIdx = cursor.getColumnIndexOrThrow(CalendarContract.Events.TITLE);
int idIdx = cursor.getColumnIndexOrThrow(CalendarContract.Events._ID);

//初始化结果集
String[] result = new String[cursor.getCount()];

//迭代结果Cursor
while(cursor.moveToNext()) {
    //提取名称
    String name = cursor.getString(nameIdx);
    //提取唯一的ID
    String id = cursor.getString(idIdx);
    
    result[cursor.getPosition()] = name + " (" + id + ")";
}

//关闭Cursor
cursor.close();
使用Intent创建和查询日历项

Calendar Content Provider包含基于Intent的机制,该机制允许你使用Calendar应用程序执行常见的操作,而不需要特殊的权限。使用Intent,可以再指定的时间打开Calendar应用程序,查看事件的而详细信息,插入一个新的事件,或编辑现有的事件。

创建新的日历事件

//创建一个新的插入Intent
Intent intent = new Intent(Intent.ACTION_INSERT,CalendarContract.Events.CONTENT_URI);

//添加日历事件的详细信息
intent.putExtra(CalendarContract.Events.TITLE,"Launch!");
intent.putExtra(CalendarContract.Events.DESCRIPTION,"Professional Android 4 " + "Application Development release!");
intent.putExtra(CalendarContract.Events.EVENT_LOCATION,"Wrox.com");

Calendar startTime = Calendar.getInstance();
startTime.set(2012,2,13,0,30);
intent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME,startTime.getTimeInMillis());
intent.putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY,true);

//使用Calendar应用程序添加新的事件
startActivity(intent);

编辑日历事件

//创建一个URI,通过行ID来访问指定的事件
//使用它来创建一个新的编辑Intent
long rowID = 760;
Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI,rowID);

Intent intent = new Intent(Intent.ACTION_EDIT,uri);

//修改日历事件的详细信息
Calendar startTime = Calendar.getInstance();
startTime.set(2012,2,13,0,30);
intent.putExtra(ClendarContract.EXTRA_EVENT_BEGIN_TIME,startTime.getTimeInMillis());
intent.putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY,true);

//使用Calendar应用程序来编辑事件
startActivity(intent);

显示日历和日历事件

//创建一个URI来指定要查看的特定时间的日历事件
Calendar startTime = Calendar.getInstance();
startTime.set(2012,2,13,0,30);
Uri uri = Uri.parse("content:??com.android.calendar/time/" + String.valueOf(startTime.getTimeInMillis()));
//使用Calendar应用程序查看时间
startActivity(intent);

Cursor

Content Value用来向数据库的表中插入新的行。每一个ContentValues对象都将一个表行表示为列名到值得映射。

数据库查询作为Cursor对象返回。Cursor是底层数据中的结果集的指针。

  • moveToFirst 把游标移动到查询结果中的第一行
  • moveToNext 把游标移动到下一行
  • moveToPrevious 把游标移动到前一行
  • getCount 返回结果集中的行数
  • getColumnIndexOrThrow 返回具有指定名称的列的索引(如果不存在拥有该名称的列,就会抛出异常),索引从0开始计数。
  • getColumnName 返回指定列索引的名称
  • getColumnNames 返回当前Cursor中的所有列名的字符串数组
  • moveToPosition 将游标移动到指定行
  • getPosition 返回当前的游标位置

结束使用Cursor后,关闭它非常重要。这样可以防止内存泄漏,并降低应用程序的资源负载。cursor.close();

消息传递

发送SMS消息

需添加权限<uses-permission android:name="android.permission.SEND_SMS">

SmsManager sms = SmsManager.getDefault();
sms.sendTextMessage(destinationAddress:phoneNumber,scAddress:null,text:message,sentIntent:null,delivertyIntent:null);
  • destinationAddress: 收件人的电话号码
  • scAddress: 服务中心地址,null代表默认的SMSC
  • text: SMS消息的内容
  • sentIntent: 发送消息后调用的挂起的意图
  • delivertyIntent: 消息递送后调用的挂起的意图

在onCreate() 方法中创建两个PendingIntent对象。

sentPI = PendingIntent.getBroadcast(this,0,new Intent(SENT),0);
deliveredPI = PendingIntent.getBroadcast(this,0,new Intent(DELIVERED),0);

两个PendingIntent对象被传递给sendTextMessage()方法的最后两个参数

registerReceiver(smsDeliveredReceiver,new IntentFilter(DELIVERED));
registerReceiver(smsSentReceiver,new IntentFilter(SENT));
sms.sendTextMessage(phoneNumber,null,message,sentPI,deliveredPI);

使用意图发送SMS消息

Intent i = new Intent(android.content.Intent.ACTION_VIEW);
i.putExtra("address","5556;5558;5560");
i.putExtra("sms_body","Hello my friends!");
i.setType("vnd.android-dir/mms-sms");
startActivity(i);

接收SMS消息

可以使用BroadcastReceiver对象接收传入的SMS消息。如果希望应用程序在接收到一条特定的SMS消息时执行一个动作,就很有用了。

在AndroidManifest.xml文件中申明

<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<receiver android:name=".SMSReceiver">
    <intent-filter>
        <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
    </intent-filter>
</receiver>

创建SMSReceiver文件

public class SMSReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context,Intent intent) {
        Bundle bundle = intent.getExtras();
        SmsMessage[] msgs = null;
        String str = "SMS from";
        if (bundle != null) {
            Object[] pdus = (Object[]) bundle.get("pdus");
            msgs = new SmsMessage[puds.length];
            for (int i = 0;i < msgs.length;i++) {
                msgs[i] = SmsMessage.createFromPdu((byte[])pdus[i]);
                if (i == 0) {
                    str += msgs[i].getOriginatingAddress();
                    str += ": ";
                }
                str += msgs[i].getMessageBody().toString();
            }
            Toast.makeText(context,str,Toast.LENGTH_SHORT).show();
        }
    }
}

阻止Messaging应用程序接收消息

在AndroidManifest.xml中BroadcastReceiver的intent-filter中添加android:priority属性,数字越大,Android就会越早执行。并且在onReceive()方法中调用 abortBroadcast() 方法,停止广播。

发送邮件

private void sendEmail(String[] emailAddresses,String[] carbonCpies,String subject,String message) {
    Intent emailIntent = new Intent(Intent.ACTION_SEND);
    emailIntent.setData(Uri.parse("mailto:"));
    String[] to = emailAddresses;
    String[] cc = carbonCopies;
    emailIntent.putExtra(Intent.EXTRA_EMAIL,to);
    emailIntent.putExtra(Intent.EXTRA_CC,cc);
    emailIntent.putExtra(Intent.EXTRA_SUBJECT,subject);
    emailIntent.putExtra(Intent.EXTRA_TEXT,message);
    emailIntent.setType("message/rfc822");
    startActivity(Intent.createChooser(emailIntent, "Email"));
}

定位

使用基于位置的服务

基于位置的服务(LBS)是一个很宽泛的概念,描述了用来查找设备当前位置的不同技术。主要的两个LBS元素是

  • 位置管理器 提供基于位置的服务的挂钩
  • 位置提供器 每一个位置提供器都表示不同的位置查找技术,这些技术用来确定设备的当前位置

使用位置管理器可以

  • 获得当前的位置
  • 追踪移动
  • 设置近距离提醒,在检测到进入或者离开一个指定的区域时发出提醒
  • 找到可用的位置提供器
  • 监视GPS接收器的状态

LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);

并且需要在manifest文件中申请fine权限和coarse权限

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

选择一个位置提供器

LocationManager类包含了一些静态字符串常量,这些常量将返回以下三种位置提供其的名称

  • LocationManager.GPS_PROVIDER
  • LocationManager.NETWORK_PROVIDER
  • LocationManager.PASSIVE_PROVIDER

通过指定条件查找位置提供器

在大部分情况下都不太可能去显式地选择要使用的位置提供器。更常见的情况是指定你的要求。让Android去确定要使用的最优的技术。使用Criteria类来说明对提供器的要求,包括精度、能耗、花费以及返回海拔、速度和朝向的能力。

传入setAccuracy的coarse/fine 值代表一个主管的精确,其中fine代表GPS或更好的技术,而coarse则代表精度低很多的任何技术。

Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_COARSE);
criteria.setPowerRequirement(Criteria.POWER_LOW);
criteria.setAltitudeRequired(false);
criteria.setBearingRequired(false);
criteria.setSpeedRequired(false);
criteria.setCostAllowed(true);
criteria.setHorizontalAccuracy(Criteria.ACCURACY_HIGH);
criteria.setVerticalAccuracy(Criteria.ACCURACY_MEDIA);
criteria.setBearingAccuracy(Criteria.ACCURACY_LOW);
criteria.setSpeedAccuracy(Criteria.ACCURACY_LOW);

List<String> matchingProviders = locationManager.getProviders(criteria,false);

确定当前位置

找出上一次确定的位置

通过使用getLastKnownLocation方法并传入某个Location Provider的名称作为参数

Location location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);

刷新当前位置

通过requestLocationUpdates方法使用LocationListener,可以请求定期更新位置变化。LocationListener还包含一些回调方法,可用于监听提供器的状态和可用性的变化。在为requestLocationUpdates方法指定参数时,可以使用特定Location Provider的名称,也可以提供一组条件来确定应该使用的提供器。为了提高效率并降低花费和电源消耗,还可以指定两次位置更新相隔的最短时间和最短距离。

String provider = LocationManager.GPS_PROVIDER;
int t = 5000;       //毫秒
int distance = 5;   //米
LocationListener myLocationListener = new LocationListener() {
    public void onLocationChanaged(Location location) {
        //基于新位置更新应用程序
    }
    
    public void onProviderDisabled(String provider) {
        //如果提供器被禁用,则更新应用程序
    }
    
    public void onProviderEnabled(String provider) {
        //如果提供器被启用,则更新应用程序
    }
    
    public void onStatusChanged(String provider,int status,Bundle extras) {
        //如果提供器硬件状态改变,则更新应用程序
    }
};
locationManager.requestLocationUpdates(provider,t,distance,myLocationListener);

同样,你可以指定一个Pending Intent,每当位置发生变化,或者Location Provider的状态或可用性发生变化,该Pending Intent就会被广播。新位置存储在一个extra钟,其key为KEY_LOCATION_CHANGED。如果有多个Activity或Service需要使用位置更新,可以通过这种方式监听相同的广播Intent。

String provider = LocationManager.GPS_PROVIDER;
int t = 5000;       //毫秒
int distance = 5;   //米
final int locationUpdateRC = 0;
int flags = PendingIntent.FLAG_UPDATE_CURRENT;

Intent intent = new Intent(this,MyLocationUpdateReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this,locationUpdateRC,intent,flags);
locationManager.requestLocationUpdates(provider,t,distance,pendingIntent);

使用Passive Location Provider可以在其他应用程序请求位置更新的时候接收更新,但是无法控制更新何时发生。

请求单独一次位置更新

并不是每个应用程序都需要定期的位置更新才能保持有用。很多时候,只要确定一次位置就可以为它们提供的功能或显示的信息提供足够的上下文。

Looper looper = null;
locationManager.requestSingleUpdate(criteria,myLocationListener,looper);

当使用Location Listener时,可以指定一个Looper参数,该参数允许你在特定的线程上来定时回调————将该参数设为null将强制在当前调用线程上返回。

位置更新优化

  • 耗电量和精度 Location Provider的精度越高,耗电量越大
  • 启动时间 在移动环境中,得到最初位置所用的时间对用户体验有显著的影响
  • 更新频率 更新越频繁,电源消耗越大
  • 提供器可用性 用户可以切换提供器的可用性,所以应用程序需要检测提供器状态的变化

近距离提醒

可以使用近距离设置一些Pending Intent,当设备移动到为某个固定位置指定的区域内或从该区域移出时他们就会触发。为了针对特定区域设置近距离提醒,需要选择一个中心点,绕该点的半径以及该提醒的超时时间。

private static final String TREASURE_PROXIMITY_ALERT = "com.paad.treasurealert";

private void setProximityAlert() {
    String locDervice = Context.LOCATION_SERVICE;
    LocationManager locationManager = (LocationManager)getSystemService(locService);
    
    double lat = 73.147536;
    double lng = 0.510638;
    float radius = 100f;
    long expiration = -1;   //不会过期
    
    Intent intent = new Intent(TREASURE_PROXIMITY_ALERT);
    PendingIntent proximityIntent = PendingIntent.getBroadcast(this,-1,intent,0);
    locationManager.addProximityAlert(lat,lng,radius,expriation,proximityIntent);
}

Download Manager

作为一个Service来优化长时间下载操作的处理。Download Manager通过处理HTTP连接、监控连接的变化和系统重新启动来确保每一次下载都能成功完成。

要想访问Download Manager,可以使用getSystemService方法请求DOWNLOAD_SERVICE:

String serviceString = Context.DOWNLOAD_SERVICE;
DownloadManager downloadManager = (DownloadManager)getSystemService(serviceString);

下载文件

要想请求一个下载,需要创建一个新的DownloadManager.Request,指定要下载的文件的URI并把它传给Download Manager的enqueue。

String serviceString = Context.DOWNLOAD_SERVICE;
DownloadManager downloadManager = (DownloadManager)getSystemService(serviceString);
Uri uri = Uri.parse("http://developer.android.com/shareables/icon_templates-v4.0.zip");
DownloadManager.Request request = new Request(uri);
long reference = downloadManager.enqueue(request);

根据返回的引用值,可以对某个下载进行进一步的操作或者查询,包括查看状态或者取消下载。

在Request对象中,可以通过分别调用

  • addRequestHeader() 给请求添加HTTP报头
  • setMimeType() 重写服务器返回的MIME类型。
  • setAllowedNetworkTypes 可以限制下载类型为Wi-Fi或者移动网络
  • getReommendedMaxBytesOverMobile 它会返回一个在移动数据连接上传输时推荐的最大字节数来确定是否应该限制下载类型为Wi-Fi
  • enqueue 一旦连接可用且Download Manager空闲,就会开始下载

要想在下载完成后收到一个通知,需要注册一个Receiver来接收ACTION_DOWNLOAD_COMPLETE广播,它将包含一个EXTRA_DOWNLOAD_ID extra,其中包含了已经完成下载的引用ID。

IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
BroadcastReceiver receiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context,Intent intent) {
        long reference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
        if (myDownloadReference == reference) {
            //对下载的文件进行一些操作
        }
    }
};
registerReceiver(receiver,filter);

通过调用Download Manager的openDownloadFile方法,可以获得文件的Parcel File Descriptor,查询Download Manager来得到文件的位置,或者如果已经指定了文件名和位置,那么直接操作这个文件。

最好为ACTION_NOTIFICATION_CLICKED操作注册一个Receiver。每当用户从Notification任务栏或者Downloads应用中选择一个下载,那么这个Intent就会被广播。

IntentFilter filter = new IntentFilter(DownloadManager.ACTION_NOTIFICATION_CLICKED);
BroadcastReceiver receiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context,Intent intent) {
        String exrtaID = DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS;
        long[] references = intent.getLongArrayExtra(extraID);
        for (long reference : references) {
            if (myDownloadReference == reference) {
            //对下载的文件进行一些操作
            }       
        }
    }
};
registerReceiver(receiver,filter);

自定义Download Manager Notification

默认情况下,会为Download Manager管理的每一个下载显示一个持续的Notification。每一个Notification都会显示当亲的下载进度和文件名。

Download Manager可以为每个下载请求自定义Notification,包括把它完全隐藏。

  • request.setTitle("") 设置通知标题
  • request.setDescription("") 设置通知文本

通过setNotificationVisibility方法并使用下面的标识之一来控制何时以及是否应该为请求显示一个Notification:

  • Request.VISIBILITY_VISIBLE 当一个下载正在进行时,将显示一个持续的Notification标识持续时间。下载完成后,Notification将被移除。默认。
  • Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED 在下载期间会显示一个持续的Notification,即使下载完成也会继续显示(直到被选择或取消)
  • Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION 只有下载完成,Notification 才会显示
  • Request.VISIBILITY_HIDDEN 不为此下载显示Notification。为了设置这个标识,必须在manifest中添加DOWNLOAD_WITHOUT_NOTIFICATION权限。

指定下载位置

默认,Download Manager会把下载的文件保存到共享下载缓存中,而且使用系统生成的文件名。每一个请求对象都可以指定一个下载位置,但是所有的下载都必须存储到外部存储器中的某一个地方,而且应用程序必须在其清单中WRITE_EXTERNAL_STORAGE权限。

request.setDestinationUri(Uri.fromFile(f));

如果下载的文件用于你的应用程序,你可能希望把它放在应用程序的外部存储文件夹下。

request.setDestinationInExternalFilesDir(this,Environment.DIRECTORY_DOWNLOADS,"Bugdroid.png");

对于那些都能够或者应该与其他应用程序共享的文件——特别是希望使用媒体扫描器扫描的文件,可以再外部存储器的公共目录下制定一个文件夹来存储该文件。

request.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC,"Android_Rock.mp3");

注意: 默认情况下Download Manager下载的文件不会被媒体扫描器扫描,因此它们可能不会显示在Gallery和Music Player等应用中。为了使下载的文件可以被扫描,可以再Request对象中调用allowScaningByMediaScanner。如果使你的文件对系统的Downloads应用是可见和可管理的,那么需要调用setVisibleInDownloadsUi,并传入true

取消和删除下载

使用remove方法取消一个正在等待的下载,终止一个正在进行的下载,或者删除一个完成的下载。并且允许指定一个或者多个要取消的下载。

查询Download Manager

可以通过query方法查询Download Manager来得到下载请求的状态、进度和详细信息。该方法会返回下载的Curosr对象。

query方法接收DownloadManager.Query对象作为参数,使用setFilterById方法给Query对象指定一个下载引用ID的序列,或者使用setFilterStatus方法来过滤下载的状态,该方法使用某个DownloadManager.STATUS_*常量来指定下载的运行、暂停、失败或成功。

Download Manager包含了很多COLUMN_*静态字符串常量,可以用它们来查询结果Cursor,可以得到每个下载的详细信息,包括状态、文件大小、目前下载的字节数、标题、描述、URI、本地文件名、媒体类型等。

@Override
public void onReceive(Context context,Intent intent) {
    long reference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID,-1); if (myDownloadReference == reference) {
        Query myDownloadQuery = new Query();
        myDownloadQuery.setFilterById(reference);
        
        Cursor myDownload = downloadManager.query(myDownloadQuery);
        if (myDownload.moveToFirst()) {
            int fileNameIdx = myDownload.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENANE);
            int fileUriIDX = myDownload.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI);
            String fileName = myDownload.getString(fileNameIdx);
            String fileUri = myDownload.getString(fileUriIdx);
        
            //对下载文件进行一些操作
        }
        myDownload.close();
    }
}

针对下载暂停或失败的情形,可以通过查询COLUNMN_REASON列来得到其原因,该原因是由一个整数值来表示的。

  • STATUS_PAUSED 通过使用DownloadManager.PAUSED_* 静态变量之一来解释原因。
  • STATUS_FAILED 通过DownloadManager.ERROR_* 来确定失败的原因
//获得Download Manager Service
String serviceString = Context.DOWNLOAD_SERVICE;
DownloadManager downloadManager = (DownloadManager)getSystemService(serviceString);

//为暂停的下载创建一个查询
Query pausedDownloadQuery = new Query();
pausedDownloadQuery.setFilterByStatus(DownloadManager.STATUS_PAUSED);

//查询Download Manager中暂停的下载
Cursor pausedDownloads = downloadManager.query(pasusedDownloadQuery);

//获得我们需要的数据的列索引
int reasonIdx = pausedDownloads.getColumnIndex(DownloadManager.COLUNMN_REASON);
int titleIdx = pausedDownloads.getColumnIndex(DownloadManager.COLUMN_TITLE);
int fileSizeIdx = pausedDownloads.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
int bytesDLIdx = pausedDownloads.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADS_SO_FAR);

//遍历结果Cursor
while(pausedDownloads.moveToNext()) {
    //从Cursor中提取需要的数据
    String title = pausedDownloads.getString(titleIdx);
    int fileSize = pausedDownloads.getInt(fileSizeIdx);
    int bytesDL = pausedDownloads.getInt(bytesDLIdx);
    
    //将暂停原因转化为友好的文本
    int reason = pausedDownloads.getInt(reasonIdx);
    String reasonString = "Unknown";
    switch(reason) {
        case DownloadManager.PAUSEDD_QUEUED_FOR_WIFI:
            reasonString = "Waiting for WiFi";
            break;
        case DownloadManager.PAUSEDD_WAITING_FOR_NETWORK:
            reasonString = "Waiting for connectivity";
            break;
        case DownloadManager.PAUSEDD_WAITING_TO_RETRY:
            reasonString = "Waiting to retry";
            break;
        default:
            break;
    }
    
    //构造一个状态概要
    StringBuilder sb = new StringBuilder();
    sb.append(title).append("\n");
    sb.append(reasonString).append("\n");
    sb.append("Downloaded").append(bytesDL).append(" / ").append(fileSize);
    
    //显示状态
    Log.d("DOWNLOAD",sb.toString());
}

//关闭结果Cursor
pausedDownloads.close();

文件保存

Android在应用程序的上下文中提供了一些专门的实用工具文件管理

  • deleteFile 使用户能够删除由当前应用程序所创建的文件
  • fileList 返回一个字符串数组,其中包含了由当前应用程序所创建的所有应用程序

如果应用程序崩溃或者被意外终止,那么这些方法对于清理遗留的临时文件尤为有用。

特定于应用程序的文件夹存储文件

  • getDir 返回一个指向内部的应用程序文件存储目录路径的File对象
  • getExternalFilesDir 返回一个指向外部的应用程序文件存储目录路径的File对象(Environment.getExternalStorageDirectory)

存储在应用程序文件夹中的文件应该是特定于父应用程序的而且通常不会被媒体扫描仪所侦测到,因此这些文件不会被自动添加到媒体库中。如果应用程序下载或者创建了应该添加到媒体库中的文件或者想要这些文件对其他应用程序也是可用的,可以放到公共的外部存储目录中。

创建私有的应用程序文件

Android提供了openFileInput和openFileOutput方法来简化从应用程序沙箱中的文件读取数据流和向应用程序沙箱中的文件写入数据流的过程。

这些方法只支持那些当前应用程序文件夹中的文件,指定路径分隔符将会导致抛出一个异常。

在创建FileOoutputStream时,如果你指定的文件名不存在,Android会为你创建。对于已经存在的文件的默认行为就是覆盖它;想要在已经存在的文件末尾添加内容,可以指定其模式为Context.MODE_APPEND。默认情况下,使用openFileOutput方法创建的文件对于调用应用程序是私有的————其他应用程序会被拒绝访问。在不同应用程序间共享文件的标准方式是使用一个Content Provider。另外,当创建输出文件时,可以通过指定Context.MODE_WORLD_READABLE 或者 Context.MODE_WORLD_WRITEABLE 让它在其他应用程序中也是可用的。

通过 getFilesDir 可以找到存储在你的沙箱中的文件的位置,这个方法将会返回使用openFileOutput所创建的文件的绝对路径。

使用应用程序文件缓存

Android提供了一个可管理的内存缓存和一个不能管理的外部缓存。分别调用getCacheDir 和 getExternalCacheDir 方法可以从当前的上下文中访问它们。

存储在任何一个该缓存位置中的文件在应用程序被卸载之后都会被删除。当系统运行在低可用存储空间的时候,存储在内部缓存中的文件可能会被系统所删除;存储在外部缓存中的文件则不会被删除,因为系统不会跟踪外部媒介的可用存储空间。

存储公共可读文件

Environment.getExternalStoragePublicDirectory可以用来找到存储应用程序文件的路径。返回的位置为用户通常存放和管理他们自己的各种类型的文件的位置。

File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
File file = new File(path,FILE_NAME);
try {
    path.mkdirs();
} catch (IOException e) {
    
}
  • DIRECTORY_ALARMS 作为用户可选择的警示音的可用的声音文件
  • DIRECTORY_DCIM 设备拍到的图片和视频
  • DIRECTORY_DOWNLOADS 设备下载的文件
  • DIRECTORY_MOVIES 电影
  • DIRECTORY_MUSIC 代表音乐的音频文件
  • DIRECTORY_NOTIFICATIONS 作为用户可选择的通知音的可用的音频文件
  • DIRECTORY_PICTURES 图片
  • DIRECTORY_PODCASTS 代表播客的音频文件
  • DIRECTORY_RINGTONES作为用户可选择的铃声的可用的音频文件

Service

Android 提供Service类来专门创建用来处理生命周期操作的应用程序组件以及包括不需要用户界面的功能。Android赋予Service比处于非活动状态的Activity更高的优先级,因此当系统请求资源时,它们被终止的可能性更小。如果用户运行过早结束了一个已经启动Service,只要有足够的资源可用,则运行时就会重新启动它。必要的时候,一个Service的优先级可以提升到和前台Activity的优先级一样高。这是为了应对一些终止Service会显著影响用户体验的极端情况,如终止正在播放的音乐。通过使用Service,即使在UI不可见的时候也可以保证应用程序的持续运行。

Service简介

创建和控制Service

创建Service

要定义一个Service,需要创建一个扩展Service的新类,需要重写onCreate和onBind方法。

public class MyService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
    }
    
    @Override
    public void onBind(Intent intent) {
        return null;
    }
}

创建一个新的Service后,必须将这个Service在应用程序的清单文件中进行注册,需要在application节点中包含一个service标记。

service android:enabled="true" android:name=".MyService"/>

为了确保你的Service只能由自己的应用程序启动和停止,所需要在它的Service节点下添加一个permission属性

<service 
    android:enabled="true"
    android:name=".MyService"
    android:permission="com.paad.MY_SERVICE_PERMISSION"/>
执行一个Service并控制它的重新启动行为

重写onStartCommand事件处理程序以执行一个由Service封装的任务。在这个处理程序中,也可以指定Service的重新启动行为。当一个Service通过startService启动时,就会调用onStartCommand方法,所以这个方法可能在Service生命周期内被执行很多次。

Service是在应用程序的主线程中启动的,这意味着在onStartCommand处理程序中完成的任何处理都是运行在GUI主线程中的。实现Service的标准模式是从onStartCommand中创建和运行一个新线程,用来在后台执行处理,并在该线程完成后终止这个Service。

@Override
public int onStartCommand(Intent intent,int flags,int startId) {
    startBackgroundTask(intent,startId);
    return Service.START_STICKY;
}

通过以下的Service常量可以控制重新启动行为

  • START_STICKY 描述了标准的重新启动行为。如果返回了这个值,那么在运行时终止Service后,当重新启动Service时,将会调用onStartCommand。注意,当重新启动Service后,传入onStartCommand的Intent参数将是null。这种模式通常用于处理自身状态的Service,以及根据需要通过startService和stopService显式地启动和终止的Service。这些Service包括播放音乐的Service或者处理其他持续进行的后台任务的Service。
  • START_NOT_STICKY 这种模式用于启动以处理特殊的操作和命令的Service。通常当命令完成后,这些Service会调用stopSelf终止自己。当被运行时终止后,只有当存在未处理的启动调用时,设为这个模式的Service才回重新启动。如果在终止Service后没有进行startService调用,那么Service将停止运行,而不会调用onStartCommand。对于处理特殊请求,尤其是诸如更新或者网络轮询这样的定期处理。
  • START_REDELIVER_INTENT 需要确保从Service中请求的命令得以完成。这种模式是前两种模式的组合。如果Service被运行时终止,那么只有当存在未处理的启动调用或进程在调用stopSelf之前被终止时,才会重新启动Service。后一种情况中,将会调用onStartCommand,冰纯如没有正常完成处理的Intent。

由于对startService的调用不能嵌套,因此不管startService被调用了多少次,对stopService的一次调用就会终止它所匹配的运行中的Service。

自终止Service

由于Service具有高优先级,它们通常不会被运行时终止,因此自终止可以显著地改善应用程序中的资源占用情况。通过在处理完成后显示地停止Service,可以避免系统仍然为使该Service继续运行而保留资源。当Service完成操作或处理后,应该调用stopSelf终止它。此时可以不传递参数,从而强制停止Service。也可以传入startId值,确保已经为目前调用的每个startService实例完成了处理。

将Service绑定到Activity

Service可以和Activity绑定,后者会维持一个对前者实例的引用,此引用允许你像对待其他实例化的类那样,对正在运行的Service进行方法调用。允许Service和Activity绑定,这样能够获得更加详细的接口。要让一个Service支持绑定,需要实现onBind方法,并返回被绑定Service的当前实例。

@Override
public IBinder onBind(Intent intent) {
    return binder;
}

public class MyBinder extends Binder {
    MyMusicService getService() {
        return MyMusicService.this;
    }
}

private final IBinder binder = new MyBinder();

Servie和其他组件之间的连接表示为一个ServiceConnection。要想将一个Service和其他组件进行绑定,需要实现一个新的ServiceConnection,建立了一个连接之后,就可以通过重写onServiceConnected和onServiceDisconnected方法来获得对Service实例的引用。

//Service的引用
private MyMusicService serviceRef;

//处理Servie和Activity之间的连接
private ServiceConnection mConnection = new ServiceConnection() {
    public void onServiceConnected(ComponentName className,IBinder service) {
        //当建立连接时调用
        serviceRef = ((MyMusicService.MyBinder)service).getService();
    }
    
    public void onServiceDisconnected(ComponentName className) {
        //当Service意外断开时接收
        serviceRef = null;
    }
}

要执行绑定,需要在Activity中调用bindService,并传递给它一个用于选择要绑定到的Service的Intent以及一个ServiceConnection实现的实例。还可以指定很多的绑定标识。

//绑定一个Service
Intent bindIntent = new Intent(MyActivity.this,MyMusicService.class);
bindService(bindIntent,mConnection,Context.BIND_AUTO_CREATE);
  • BIND_ADJUST_WITH_ACTIVITY 系统可以根据一个Service所绑定的Activity的相对重要重读来调整这个Service的优先级。因此,当Activity处于前台时,系统会提高Service的优先级。
  • BIND_IMPORTANT 和 BIND_ABOVE_CLIENT 对于正在绑定一个Service的客户端来说,这个Service非常重要。以至于客户端处于前台时,Service也应该变为前台进程。BIND_ABOVE_CLENT指定在内存很低的情况下,运行时会在终止绑定的Service之前先终止Activity。
  • BIND_NOT_FOREGROUND 确保绑定的Service永远不会拥有运行于前台的优先级。默认,绑定一个Service会提高它的优先级。
  • BIND_WAIVE_PRIORITY 表示绑定一个指定的Service不应该改变该Service的优先级。一旦Service被绑定,就可以通过从onServiceConnected处理程序获得的serviceBind对象来使用Service所有的共有方法和属性。

如果你想和运行在不同进程中的Service进行通信,可以使用广播Intent的方式或者在启动Service的Intent中添加额外的Bundle数据。如果需要耦合更加紧密的连接,可以使用Android Interface Definition Language(AIDL),使Service可以跨应用绑定。

创建前台Service

在Service需要直接和用户进行交互的情况下,把Service的优先级提升到与前台一样高,可以通过调用Service的startForeground方法以设置该Service在前台运行。由于需要直接进行交互,所以必须指定一个持续工作的Notification。

private void startPlayback(String album,String artist) {
    int NOTIFICATION_ID = 1;
    
    //创建一个当单机通知时将打开主Activity的Intent
    Intent intent = new Intent(this,MyActivity.class);
    PendingIntent pi = PendingIntent.getActivity(this,1,intent,0);
    
    //设置Notification UI参数
    Notificatin notification = new Notification(R.drawable.icon,"Start Playback",System.cureentTimeMillis());
    notification.setLatestEventInfo(this,album,artis,pi);
    
    //设置Notification为持续显示
    notification.flags = notification.flags | Notification.FLAG_ONGOING_EVENT;
    
    //将Service移到前台
    startForeground(NOTIFICATION_ID, notification);
}

当Service不再需要前台运行的优先级时,可以使用stopForeground方法,把它移到后台,并可以选择是否移除通知。

public void pausePlayback() {
    //移到后台并移除Notification
    stopForeground(true);
}

使用后台线程

Android中所有的应用程序组件包括Activity、Servie、Broadcast Receiver都在应用程序的主线程中运行,因此,任何组件中的费时处理都可能阻塞所有其他的组件,包括Service和可见Activity,我们需要避免出现未响应(Activity对一个输入事件在5秒的时间内没有响应,或者Broadcast Receiver在10秒内没有完成它的onReceive处理程序)。所以对于任何不用直接与用户界面进行交互的重要处理,使用后台线程技术是很重要的。将文件操作、网络查找、数据库事务、复杂计算调度到后台线程中完成尤其重要。

使用AsyncTask运行异步任务

AsyncTask类为将耗时的操作移到后台线程并在操作完成后同步更新UI线程实现了最佳实践模式。它有助于将事件处理程序与GUI线程进行同步,允许通过更新视图和其他UI元素来报告进度,或者在任务完成后发布结果。AsyncTask处理线程创建、管理和同步等全部工作,它可以用来创建、管理和同步等全部工作,他可用来创建一个异步任务,该任务由两个部分完成:将在后台执行的处理以及在处理完成后执行的UI更新。

AsyncTask对于生命周期较短且需要在UI上显示进度和结果的后台操作是很好的解决方案。然而当Activity重新启动的时候,这种操作将不会持续进行。AsyncTask在设备的方向变化而导致Activity被销毁和重新创建时会被取消。对于生命周期较长的后台操作,如从Internet下载数据,使用Service组件是更好的选择。

创建新的异步任务

要创建一个新的异步任务,需要扩展AsyncTask类

private class MyAsyncTask extends AsyncTask<String,Integer,String> {
    @Override
    protected String doInBackground(String... parameter) {
        //移动到后台线程
        String result = "";
        int myProgress = 0;
        int inputLength = parameter[0].length();
        
        //执行后台处理任务,更新myProgress
        for (int i = 1; i <= inputLength;i++) {
            myProgress = i;
            result = result + parameter[0].charAt(inputLength - i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }
            publishProgress(myProgress);
        }
        
        //返回一个值,它将传递给onPostExecute
        return result;
    }
    
    @Override
    protected void onProgressUpdate(Integer... progress) {
        //和UI线程同步
        //更新进度条、Notification或者其他UI元素
        asyncProgress.setProgress(progress[0]);
    }
    
    @Override
    protected void onPostExecute(String result) {
        //和UI线程同步
        //通过UI更新,Dialog或者Notification报告结果
        asyncTextView.setText(result);
    }
}
  • doInBackground 这个方法将会在后台线程上执行,所以应该把运行时间较长的代码放到这里,而且不能视图再次处理程序中与UI对象交互。可以在本处理程序中调用publishProgress方法以传递参数值给onProgressUpdate处理程序,当后台任务完成后,可以返回最终的结果并作为参数传递给onPostExecute处理程序。
  • onProgressUpdate 当中间进度更新变化时更新UI。
  • onPostExecute 后台任务结束,更新UI
运行异步任务

调用execute来执行它,每个AsyncTask实例只能执行一次。如果试图第二次调用execute,则会抛出一个异常。

String input = "redrum ... redrum";
new MyAsyncTask().execute(input);

Intent Service简介

Intent Service是一个非常方便的包装类,根据需求执行一组任务的后台Service实现了最佳实践模式。如Internet的循环更新或者数据处理。其他应用程序组件要想通过Intent Service完成一个任务,需要启动Service并传递给它一个包含完成该任务所需的参数的Intent。

Intent Service会将收到的所有请求Intent放到队列中,并在异步后台线程中逐个地处理它们。当处理完每个收到的Intent后,Intent Service就会终止它自己。Intent Service处理所有的复杂工作,如将多个请求放入队列,后台线程的创建、UI线程的同步。

public class MyIntentService  extends IntentService {
    public MyIntentService(String name) {
        super(name);
        //完成任何需要的构造函数任务
    }
    
    @Override
    public void onCreate() {
        super.onCreate();
        //创建Service时要执行的操作
    }
    
    @Override
    protected void onHandleIntent(Intent intent) {
        //这个处理程序发生在一个后台线程中
        //耗时任务应该在此实现
        //传入这个IntentService的每个Intent将被逐个处理,当所有传入的Intent都被处理后,该Service会终止自己。
    }
}

一旦收到Intent请求,onHandleIntent处理程序就会在一个工作线程中执行。对于按需或者固定时间间隔执行一组任务,Intent Service是创建这种Service的最佳方法。

Loader简介

它封装用于在UI元素中进行异步数据加载的最佳实践技术。如果想要创建一个自己的Loader实现,通常最佳实践是扩展AsyncTaskLoader而不是直接扩展Loader

  • 异步加载数据
  • 监控要加载的数据源并自动提供更新结果

手动创建线程和GUI线程同步

Thread
Thread thread = new Thread(null,new Runnable() {
    public void run() {
    }
},"Background");
thread.start();
runOnUiThread(new Runnable() {
    public void run() {
        //更新一个View或者其他Activity UI元素
    }
});
Handler

使用Handler类的post方法将更新从后台线程发布到用户界面上。

//在主线程上初始化一个handler
private Handler handler = new Handler();
//在主UI线程上使用Handler发布doUpdateGUI Runnable
handle.post(doUpdateGUI)
//执行更新UI的Runnable
private Runnable doUpdateUI = new Runnable() {
    public void run() {
        updateGUI();
    }
}

使用Alarm

Alarm是一种在预先确定的时间或时间间隔内激活Intent的方法。和Timer不同,Alarm是在应用程序之外操作的,所以即使应用程序关闭,它们也仍然能够用来激活应用程序事件或操作。当它们和Broadcast Receiver一起使用时会更加强大,允许设置能够激活广播Intent、启动Service、甚至启动Activity的Alarm,而不需要打开或者运行应用程序。

Alarm是降低应用程序资源需求的一种极为有效的方式。可以使用Alarm实现基于网络查找的定时更新,或者把费时的或者成本受约束的操作安排在“非高峰”时期运行,又或者对失败的操作调度重试。Alarm在设备处于休眠状态时依然保持活动状态,可以有选择地设置Alarm来唤醒设备。无论何时重启设备,所有的Alarm都会被取消。

创建、设置和取消Alarm

要创建一个新的只激活一次的Alarm,可以使用set方法指定一个Alarm类型、触发时间和一个要激活的Pending Intent。如果把Alarm的触发时间设置为过去的时间,那么它将会被立即触发。

  • RTC_WAKEUP 在指定的时间唤醒设备,并激活Pending Intent。
  • RTC 在指定的时间点激活Pending Intent,但是不会唤醒设备。
  • ELAPSED_REALTIME 根据设备启动之后经过的时间激活Pending Intent,但是不会唤醒设备。经过的时间包含设备休眠的所有时间。
  • ELAPSED_REALTIME_WAKEUP 在设备启动并经过指定的时间之后唤醒设备和激活Pending Intent。
//获取一个Alarm Manager的引用
AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);

//如果设备处于休眠状态,设置Alarm来唤醒设备
int alarmType = AlarmManager.ELAPSED_REALTIME_WAKEUP;

//10秒钟后触发设备
long timeOrLengthofwait = 10000;

//创建能够广播和操作的Pending Intent
String ALARM_ACTION = "ALARM_ACTION";
Intent intentToFire = new Intent(ALARM_ACTION);
PendingIntent alarmIntent = PendingIntent.getBroadcast(this,0,intentToFire,0);

//设置Alarm
alarmManager.set(alarmType,timeOrLengthofWait,alarmIntent);

当触发Alarm时,就会广播指定的Pending Intent。因此,使用相同的Pending Intent设置第二个Alarm会替代已经存在的Alarm。

要取消一个Alarm,需要调用Alarm Manager的cancel方法,并传递给它不再想触发的Pending Intent。

设置重复Alarm

重复Alarm和一次性的Alarm具有相同的工作方式,不过会在指定的时间间隔内重复触发。

因为Alarm是在应用程序生命周期之外设置的,所以它们十分适合于调度定时更新或者数据查找,从而避免了在后台持续运行Service。

当需要对重复Alarm的精确时间间隔进行细粒度控制时,可以使用setReating方法。传入这个方法的时间间隔可以用于指定Alarm的确切时间间隔,最多可以精确到毫秒。

当按照计划定时唤醒设备来执行更新时会消耗电池的电量,setInexactRepeating方法能够帮助减少这种电量消耗。在运行时,Android会同步多个没有精确指定时间间隔的重复Alarm,并同时触发它们。

  • INTERVAL_FIFTEEN_MINUTES
  • INTERVAL_HALF_HOUR
  • INTERVAL_HOUR
  • INTERVAL_HALF_DAY
  • INTERVAL_DAY
//获取一个Alarm Manager的引用
AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);

//如果设备处于休眠状态,设置Alarm来唤醒设备
int alarmType = AlarmManager.ELAPSED_REALTIME_WAKEUP;

//调度Alarm以每半小时重复一次
long timeOrLengthofWait = AlarmManager.INTERVAL_HAL_HOUR;

//创建能够广播和操作的Pending Intent
String ALARM_ACTION = "ALARM_ACTION"
Intent intentToFire = new Intent(ALARM_ACTION);
PendingIntent alarmIntent = PendiingIntent.getBroadcast(this,0,intentToFire,0);

//每半小时唤醒设备以激活一个Alarm
alarmManager.steInexactRepeating(alarmType,timeOrLengthofWait,timeOrLengthofWait,alarmIntent);

设置定期重复Alarm会对电池电量产生显著的影响。最好将Alarm频率限制为 最低可接受频率,只在必要时唤醒设备。

硬件传感器

受支持的传感器

  • Sensor.TYPE_AMBIENT_TEMPERATURE 这是一个温度计,返回以摄氏度表示的温度。返回的温度表示的是环境的室温。
  • Sensor.TYPE_ACCELEROMETER 一个三轴的加速计传感器,返回三个坐标轴的当前加速度,单位为m/s2
  • Sensor.TYPE_GRAVITY 一个三轴的重力传感器,返回当前的方向和三个坐标轴上的重力分量,单位为m/s2。通常,重力传感器是通过对加速计传感器的结果应用一个低通过滤器,作为一个虚拟传感器实现。
  • Sensor.TYPE_LINEAR_ACCELERATION 一个三轴的线性加速度传感器,返回三个坐标轴上不包括重力的加速度,单位为m/s2。与重力传感器一样,线性加速度通常是加速计输出,作为一个虚拟传感器实现的。只是为了得到线性加速,对加速计输出应用了高通过滤器。
  • Sensor.TYPE_GYROSCOPE 一个陀螺仪传感器,以弧度/秒返回了三个坐标轴上的设备旋转速度。可以对一段时间内的旋转速率求积分,以确定设备的当前方向,但是更好的做法是结合其他传感器来使用这个传感器以得到更加平滑和校正后的结果。
  • Sensor.TYPE_ROTATION_VECTOR 返回设备的方向,表示为三个轴的角度的组合。通常用作传感器管理器的getRotationMatrixFormVector方向的输入,以便将返回的旋转向量转换为旋转矩阵。旋转向量传感器一般被实现为一个虚拟传感器,它可以组合并校正多个传感器得到的结果,以提供更加平滑的矩阵。
  • Sensor.TYPE_MAGNETIC_FIELD 一个磁力传感器,返回三个坐标轴上的当前磁场,单位为microteslas。
  • Sensor.TYPE_PRESSURE 一个气压传感器,返回当前的大气压力,单位为millibars。通过使用传感器管理器的getAltitude方法来比较两个位置的气压值,可以把气压传感器用于确定海拔高度。
  • Sensor.TYPE_RELATIVE_HUMIDITY 一个相对湿度传感器,以百分比的形式返回当前的相对湿度
  • Sensor.TYPE_PROXIMITY 一个近距离传感器,以厘米为单位返回设备和目标对象之间的距离。
  • Sensor.TYPE_LIGHT 一个环境光传感器,返回一个以lux为单位的值,用于描述环境光的亮度。

查找传感器

//查找所有传感器

SensorManager sensorManager = (SensorManager)getSystemService(service_name);
List<Sensor> allSensor = sensorManager.getSensorList(Sensor.TYPE_ALL);

查找可用的特定类型传感器,可以传入指定所需要的传感器类型。如果指定的传感器类型有多个传感器实现,可以通过查询每个返回的Sensor对象来决定使用哪个传感器。每个Sensor对象都会报告其名称、用电量、最小延迟、最大工作范围、分辨率和供应商的类型。

如何选择一个工作范围最大并且耗电量最低的光传感器,以及校正过的陀螺仪

List<Sensor> lightSensors = sensorManager.getSensorList(Sensor.TYPE_LIGHT);
List<Sensor> gyroscopes = sensorManager.getSensorList(Sensor.TYPE_GYROSCOPE);

Sensor bestLightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
Sensor correctedGyro = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);

if (bestLightSensor != null) {
    for (Sensor lightSensor : lightSensors) {
        float range = lighSensor.getMaximumRnage();
        float power = lightSensor.getPower();
        
        if (range >= bestLightSensor.getMaximumRange()) {
            if (power < bestLightSensor.getPower() || range > bestLightSensor.getMaximumRange()) {
                bestLightSensor = lightSensor;
            }
        }
    }
}

if (gyroscope != null && gyroscopes.size() > 1) {
    correctedGyro = gyroscopes.get(gyroscopes.size() - 1);
}

监视传感器

为监视传感器,需要实现一个SensorEventListener,使用onSensorChanged方法监视传感器值,使用onAccuracyChanged方法响应传感器精确度的变化。

final SensorEventListener mySensorEventListener = new SensorEventListener() {
    public void onSensorChanged(SensorEvent sensorEvent) {
        //监视传感器改变
    }
    
    public void onAccuracyChanged(Sensor sensor,int accuracy) {
        //对传感器精确度的改变做出反应
    }
}

onSensorChanged方法中的SensorEvent参数包含以下4中用于描述一个传感器事件的属性

  • sensor 触发该事件的Sensor对象
  • accuracy 当事件发生时传感器的精确度(low,medium,high 或 unreliable)
  • values 包含了已检测到的新值得浮点型数组
  • timestamp 传感器事件发生的时间

onAccuracyChanged方法单独监视传感器精确度的变化,accuracy传感器精确度,表示的常量

  • SensorManager.SENSOR_STATUS_ACCURACY_LOW 表示传感器的精确度很低并且需要校准
  • SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM 表示传感器数据具有平均精确度,并且校准可能会改善报告的结果
  • SensorManager.SENSOR_STATUS_ACCURACY_HIGH 表示传感器使用的是最高精确度
  • SensorManager.SENSOR_STATUS_UNRELIABLE 表示传感器数据不可靠,这意味着需要校准该传感器或者当前不能读数
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
sensorManager.register(mySensorEventListener,sensor,SensorManager.SENSOR_DELAY_NORMAL);
  • SENSOR_DELAY_FASTEST 指定可以实现的最快更新速率
  • SENSOR_DELAY_GAME 指定适合控制游戏的更新速率
  • SENSOR_DELAY_NORMAL 指定默认的更新速率
  • SENSOR_DELAY_UI 指定适合更新UI的速率

应用

  • 使用指南针和加速计确定用户的朝向和设备方向。将它们与地图、摄像头和基于位置的服务一起使用,可以创建出增强现实UI,这种UI可以使用基于位置的数据叠加在摄像头实时播放的画面之上。
  • 创建能够动态调整以适应用户设备的方向的UI。
  • 通过监视快速的加速度来检测设备是否已经在掉落或者被抛掉
  • 测量移动或者振动。
  • 创建能够使用物理动作和移动作为输入的UI控件

找到当前的屏幕旋转方向

WindowManager wm = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
int rotation = display.getRotation();
switch(rotation) {
    case (Surface.ROTATION_0) : break;  //Natural
    case (Surface.ROTATION_90) : break; //On its left side
    case (Surface.ROTATION_180) : break;    //Upside down
    case (Surface.ROTATION_270) : break; //On its right side
    default: break;
}

计算方向

private float[] accelerometerValues;
private float[] magneticFieldValues;

final SensorEventListener myAccelerometerListener = new SensorEventListener() {
    public void onSensorChanged(SensorEvent sensorEvent) {
        if (sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
            accelerometerValues = sensorEvent.values;
        }
    }
    
    public void onAccuracyChanged(Sensor sensor,int accuracy);
};

final SensorEventListener myMagneticFieldListener = new SensorEventListener() {
    public void onSensorChanged(SensorEvent sensorEvent) {
        if (sensorEvent.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
            magneticFieldValues = sensorEvent.values;
        }
    }
    public void onAccuracyChanged(Sensor sensor,int accuracy) {}
}

SensorManager sm = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
Sensor aSensor = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
Sensor mfSensor = sm.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);

sm.registerListener(myAccelerometerListener,aSensor,SensorManager.SEENSOR_DELAY_UI);
sm.registerListener(myMagneticFieldListener,mfSensor,SensorManager.SENSOR_DELAY_UI);

float[] values = new float[3];
float[] R = new float[9];
SensorManager.getRotationMatrix(R,null,accelerometerValues,magneticFieldValues);
SensorManager.getOrientation(R,values);

//Convert from radians to degrees if preferred
values[0] = (float) Math.toDegrees(values[0]);
values[1] = (float) Math.toDegrees(values[1]);
values[2] = (float) Math.toDegrees(values[2]);
  • values[0] 当设备朝向磁北时,方位角为0
  • values[1] 俯仰角,即绕x轴的旋转
  • values[2] 横滚角,即绕y轴的旋转

个性化屏幕

主屏幕Widget

Widget可以使我们的应用程序直接在用户主屏幕上拥有一块交互式的屏幕面板以及一个入口点。一个好的App Widget可以用最少的资源开销提供有用的、精确的和及时的信息。

App Widgets作为BroadcastReceivers实现,它们使用RemoteViews来创建和更新寄存在另一个应用程序进程中的视图层次结构。为了创建一个应用程序的Widget,我们需要建立以下三个组件

  • 一个定义了该WidgetUI的XML布局资源
  • 一个描述了与该Widget相关联的元数据的XML文件
  • 一个定义并控制该Widget的Intent接收器

创建Widget的XML布局资源

最佳做法是使用XML将自己的Widget布局定义为一个外部布局资源,但是在Broadcast Receiver的onCreate方法中通过编程方式布局自己的UI也是同样可行的。

Widget完成支持透明背景,并允许使用NinePatches和部分透明的Drawable资源。

受支持的Widget视图和布局

  • 所有的自定义视图
  • 由允许的视图所派生的视图
  • EditText
  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • GridLayout
  • AnalogClock
  • Button
  • ImageButton
  • ImageView
  • ProgressBar
  • TextView
  • ViewFlipper

定义Widget设置

Widget定义资源作为XML存储在项目的res/xml文件夹中,appwidget-provider标签使我们能够描述Widget元数据。

  • initialLayout 创建Widget UI 时用到的布局资源
  • minWidth/minHeight 分别表示Widget的最小宽度和最小高度
  • resizeMode 通过使用horizontal和vertical的组合来设置resizeMode允许你指定Widget在哪个方向上进行调整。将它设置为none则会禁止调整Widget的大小。
  • label 在Widget选取器中用户Widget所用到的标题
  • updatePreiodMillis 以毫秒为单位表示的Widget更新的最小周期。Android将会以这个速率唤醒设备以便更新用户Widget,,因此应当将其指定为至少一个小时。App Widget Manger最快不能以每30分钟一次的速率进行更新。
  • configure 当将用户Widget添加到主屏幕中时,可以有选择地指定启动一个完全限定的Activity。
  • icon 默认情况下,Android在Widget选取器中呈现Widget时,会使用应用程序的图标。通过指定一个Drawable资源,可以使用一个不同的图标。
  • previewImage 用于显示Widget的预览,而不是显示其图标。
<appwidget-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/my_widget_layout"
    android:minWidth="110dp"
    android:minHeight="110dp"
    android:label="@string/widget_label"
    android:updatePeriodMillis="360000"
    android:resizeMode="horizontal|vertical"
    android:previewImage=“@drawable/widget_preview”>

创建Widget Broadcast Receiver并将其添加到应用程序的manifest文件中

Widget是作为Broadcast Receiver实现的。每个Widget的Broadcast Receiver都指定Intent Filter,用于监听使用AppWidget.ACTION_APPWIDGET_UPDATE、DELETED、ENABLED以及DISABLED动作请求更新的Broadcast Intent

public class SkeletonAppWidget extends AppWidgetProvider {
    @Override
    public void onUpdate(Context context,AppWidgetManager appWidgetManager,int[] appWidgetIds) {
        //更新Widget UI
    }
    
    @Override
    public void onDeleted(Context context,int[] appWidgetIds) {
        //处理删除Widget的操作
        super.onDeleted(context,appWidgetIds);
    }
    
    @Override
    public void onDisabled(Context context) {
        //Widget已被禁用
        super.onDisabled(context);
    }
    
    @Override
    public void onEnabled(Context context) {
        //Widget已被启用
        super.onEnabled(context);
    }
}

Widget必须被添加到应用程序的manifest文件中,像其他Broadcast Receiver一样使用一个receiver标签。为了将一个Broadcast Receiver指定为一个App Widget,需要下面两个标签添加到它的manifest文件节点中。

  • 一个用于android.appwidget.action.APPWIDGET_UPDATE动作的Intent Filter
  • 一个对appwidget-provider 元数据XML资源的引用。
<receiver android:name=".MyAppWidget" android:label="@string/widget_label">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
    </intent-filter>
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/widget_provider_info"/>
</receiver>

AppWidgetManager 和 RemoteView

AppWidgetManager类用于更新App Widget和提供App Widget的相关信息

RemoteView类用于在另一个应用程序的进程中托管的View层次的代理,从而允许修改运行在另一个应用程序中的View的属性。

RemoteView
RemoteViews views = new RemoteViews(context.getPackageName(),R.layout.my_widget_layout);
views.setInt(R.id.widget_image_view,"setImageLevel",2);    //设置ImageView的image level
views.setBoolean(R.id.widget_text_view,"setCursorVisible",true);    //显示TextView的光标
views.setBitmap(R.id.widget_image_button,"setImageBitmap",myBitmap);  //将一个位图分配给一个ImageButton
views.setTextViewText(R.id.widget_text,"Updated Text");
views.setTextColor(R.id.widget_text,Color.BLUE);
views.setImageViewResource(R.id.widget_image,R.drawable.icon);
views.setProgressBar(R.id.widget_progressbar,100,50,false);
views.setChronometer(R.id.widget_chronometer,SystemClock.elapsedRealtime,null,true);
views.setViewVisibility(R.id.widget_text,View.INVISIBLE);
将RemoteView应用到运行中的App Widget

要将对RemoteView所做的修改应用到处于活动状态的Widget,需要使用AppWidgetManager的updateAppWidget方法,并传入一个或更多个要更新的Widget的标识符和要应用的RemoteView作为其参数。

appWidgetManager.updateAppWidget(appWidgetIds,remoteViews);

@Override
public void onUpdate(Context context,AppWidgetManager appWidgetManager,int[] appWidgetIds) {
    //在迭代每个widget的过程中,创建一个RemoteViews对象并将修改后的RemoteViews应用到每个Widget
    final int N = appWidgetIds.length;
    for(int i = 0;i < N;i++) {
        int appWidgetId = appWidgetIds[i];
        
        //创建一个RemoteViews对象
        RemoteViews views = new RemoteViews(context.getPackageName(),R.layout.my_widget_layout);
        
        //更新UI,通知AppWidgetManager使用修改后的过程view更新widget
        appWidgetManager.updateAppWidget(appWidgetId,views);
    }
}

也可以直接从一个Service、Activity或Broadcast Receiver更新Widget。为此,需要调用AppWidgetManager的getInstance静态方法并传入当前上下文,以获得AppWidgetManager的引用

//获得AppWidgetManager
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
//获得所选的Widget的每个实例的标识符
ComponentName thisWidget = new ComponentName(context,MyAppWidget.class);
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
//迭代每个Widget的过程中,创建一个RemoteViews对象并将修改后的RemoteViews应用到每个Widget
for (int i = 0;i < N;i++) {
    int appWidgetId = appWidgetIds[i];
    //创建一个RemoteViews对象
    RemoteViews views = new RemoteViews(context.getPackageName(),R.layout.my_widget_layout);
    
    //使用views对象更新Widget的UI
    //通知AppWidgetManager使用修改后的远程View更新Widget
    appWidgetManager.updateAppWidget(appWidgetId,views);
}
使用RemoteViews为Widget添加交互性

因为大多数主屏幕应用程序都在完整权限下运行,所以潜在的安全风险变得十分严峻。因而,Widget的交互性是被严格控制的。

  • 添加监听一个或更多个View的Click Listener
  • 根据所选项变化改变UI
  • 在Collection View Widget中的View之间过滤

Android不支持直接在App Widget中输入文本。如果需要在Widget中输入文本,最佳实践是添加该Widget的一个Click Listener,让它显示一个用于接受输入的Activity。

使用Click Listener,要向Widget添加交互性,最简单、最强大的方法是添加其View的Click Listener。

Intent intent = new Intent(context,MyActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context,0,intent,0);
views.setOnClickPendingIntent(R.id.widget_text,pendingIntent);

Collection View Widget简介

这是一种新型的Widget,用于将数据集合显示为列表、网格或层叠卡片样式

  • StackView 一个卡片View,以层叠方式显示其子View。这叠“卡片将从集合的头至尾自动循环。用户可以向上或向下滑动手指,进行切换”
  • ListView 集合中的每个项目都作为垂直列表中的一行进行显示
  • GridView 一个二维的可滚动列表,每个项目都显示在网格的一个单元格中。

创建 RemoteViewsService

RemoteViewsService用作一个实例化和管理RemoteViewsFactory的包装器,而RemoteViewsFactory则用来提供在Collection View Widget中显示的每个View。为创建RemoteViewsService需要扩展RemoteViewsService类,并通过重写onGetViewFactory处理程序来返回RemoteViewsFactory的一个新实例。和任何Service一样,需要使用一个service标签把RemoteViewsService添加到应用程序的manifest文件中。为防止其他应用程序访问你的Widget,必须指定android.permission.BIND_REMOTEVIEWS权限。

创建一个RemoteViewsFactory

RemoteViewsFactory是Adapter类的一个包装器,用于创建和填充将在Collection View Widget中显示的View ———— 实际上是将它们与底层的数据集合绑定到一起。

class MyRemoteViewsFactory implements RemoteViewsFactory {
    private ArrayList<String> myWidgetText = new ArrayList<String>();
    private Context context;
    private Intent intent;
    private int widgetId;
    
    public MyRemoteViewsFactory(Context context,Intent intent) {
        this.context = context;
        this.intent = intent;
        widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,AppWidgetManager.INVALID_APPWIDGET_ID);
    }
    
    //设置数据源的任何连接,繁重的工作,例如下载数据,应该推迟到onDataSetChanged()或getViewAt()中执行这个调用的时间超过20秒会导致一个ANR
    public void onCreate() {
        myWidgetText.add("The");
        myWidgetText.add("quick");
        myWidgetText.add("brown");
        myWidgetText.add("fox");
        myWidgetText.add("jumps");
        myWidgetText.add("over");
        myWidgetText.add("the");
        myWidgetText.add("lazy");
        myWidgetText.add("droid");
    }
    
    //当显示的底层数据集合被修改时调用。可以使用AppWidgetManager的notifyAppWidgetViewDataChanged()方法来触发这个处理程序
    public void onDataSetChanged() {
        //底层数据改变时进行处理
    }
    
    //返回正在显示的集合中的项数
    public int getCount() {
        return myWidgetText.size();
    }
    
    //如果每个项提供的唯一ID是稳定的————即它们不会在运行时改变,就返回true
    public boolean hasStableIds() {
        return false;
    }
    
    //返回与位于指定索引位置的项目关联的唯一ID
    public long getItemId(int index) {
        return index;
    }
    
    //不同View定义的数量
    public int getViewTypeCount() {
        return 1;
    }
    
    //可选地制定一个“加载”View进行显示
    public RemoteViews getLoadingView() {
        return null;
    }
    
    //创建并填充将在指定索引位置显示的View
    public RemoteViews getViewAt(int index) {
        //创建将在所需索引位置显示的View
        RemoteViews rv = new RemoteViews(context.getPackageName,R.layout.my_stack_widget_item_layout);
        
        //使用底层数据填充View
        rv.setTextViewText(R.id.widget_title_text,myWidgetText.get(index));
        rv.setTextViewText(R.id.widget_text,"View Number: " + String.valueOf(index));
        
        //创建一个特定于项的填充Intent,用于填充在App Widget Provider中创建的Pending
        Intent filInIntent = new Intent();
        fillInIntent.putExtra(Intent.EXTRA_TEXT,myWidgetText.get(index));
        rv.setOnClickFillInIntent(R.id.widget_title_text,fillInIntent);
        return rv;
    }
    
    //关闭连接、游标或者onCreate中创建的其他任何持久状态
    public void onDestroy() {
        myWidgetText.clear();
    }
}

使用RemoteViewsService填充CollectionViewWidget

Intent intent = new Intent(context,MyRemoteViewsService.class);

RemoteViewsService内的onGetViewFactory处理程序会收到这个Intent,从而使你能够向Service和它包含的Factory传递额外的参数。还需要指定要绑定的Widget的ID,这样可以为不同的Widget实例指定不同的Service。通过使用setEmptyView方法,可以指定一个当且仅当底层数据集合为空时显示的View。在完成绑定之后,使用App Widget Manager的updateAppWidget方法将绑定应用到指定的Widget上

Intent intent = new Intent(context,MyRemoteViewsService.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,appWidgetId);
views.setRemoteAdapter(appWidgetId,R.id.widget_stack_view,intent);

向Collection View Widget中的项添加交互性

出于效率原因,无法向作为Collection View Widget一部分显示的每个项分配唯一的onClickPendingIntent。需要使用setPendingIntentTemplate向Widget分配一个模板Intent

Intent templateIntent = new Intent(Intent.ACTION_VIEW);
templateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,appWidgetId);
PendingIntent templatePendingIntent = PendingIntent.getActivity(context,0,templateIntent,PendingIntent.FLAG_UPDATE_CURRENT);
views.setPendingIntentTemplate(R.id.widget_stack_view,templatePendingIntent);

将Collection View Widget绑定到Content Provider

Collection View Widget最强大的用途之一就是将Content Provider中的数据呈现到主屏幕上。

class MyRemoteViewsFactory implements RemoteViewsFactory {
    private Context context;
    private ContentResolver cr;
    private Cursor c;
    
    public MyRemoteViewsFactory(Context context) {
        //获得对应程序上下文机器Content Resolver的引用
        this.context = context;
        cr = context.getContextResolver();
    }
    
    public void onCreate() {
        //执行一个查询来返回将要显示的数据的游标,任何辅助的查找或解码操作应在onDataSetChanged处理程序中完成
        c = cr.query(MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI,null,null,null,null);
    }
    
    public void onDataSetChanged() {
        // 任何辅助的查找、处理或解码可以在这里同步完成,只有这个方法完成后,Widget才回被更新
    }
    
    public int getCount() {
        //返回游标中的项数
        if (c != null) {
            return c.getCount();
        } else {
            return 0;
        }
    }
    
    public long getItemId(int index) {
        //返回与特定项关联的唯一ID
        if (c != null) {
            return c.getInt(c.getColumnIndex(MediaStore.Images.Thumbnails._ID));
        } else {
            return index;
        }
    }
    
    public RemoteViews getViewAt(int index) {
        //将游标移动到请求的行位置
        c.moveToPosition(index);
        
        //从需要的项中提取数据
        int idIdx = c.getColumnIndex(MediaStore.Images.Thumbnails._ID);
        String id = c.getString(idIdx);
        Uri uri = Uri.withAppendedPath(MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI,"" + id);
        
        //使用合适的项布局创建一个新的RemoteViews对象
        RemoteViews rv = new RemoteViews(context.getPackageName(),R.layout.my_media_widget_item_layout);
        //将从游标中提取的值赋给RemoteViews
        rv.setImageViewUri(R.id.widget_media_thumbnail,uri);
        
        //分配一个特定于项的填充Intent,用于填充在App Widget Provider中指定的Pending Intent模板。在这里,模板指定了一个ACTION_VIEW动作
        Intent fillInIntent = new Intent();
        fillInIntent.setData(uri);
        rv.setOnClickFillInIntent(R.id.widget_media_thumbnail,fillInIntent);
        return rv;
    }
    
    public int getViewTypeCount() {
        //要使用的不同View定义的数量
        //对于Content Provider,这个值几乎总是1
        return 1;
    }
    
    public boolean hasStableIds() {
        //Content Provider的ID应该是唯一并且永久的
        return true;
    }
    
    public void onDestroy() {
        //关闭结果游标
        c.close();
    }
    
    public RemoteViews getLoadingView() {
        //使用默认的加载View
        return null;
    }
}

刷新Collection View Widget

App Widget Manager中包含的notifyAppWidgetViewDataChanged方法允许指定一个要更新的Widget ID(或ID数组),以及该Widget中底层数据源已经发生变化的集合View的资源ID

appWidgetManager.norifyAppWidgetViewDataChanged(appWidgetIds,R.id.widget_stack_view);

这将会导致相关的RemoteViewsFactory的onDataSetChanged处理程序执行,然后进行元数据调用,最后再重新创建每个View。

Media Palyer

Android中的音频和视频的播放通常由MediaPlayer类进行处理。使用Media Player,我们能够播放存储在应用程序资源、本地文件、Content Provider或者来自网络URL的流式传输中的媒体。

准备音频播放

为了使用Media Player播放音频内容,需要创建一个新的Media Player对象,并设置该音频的数据源。为此,可以使用静态create方法,并传入Activity的上下文以及下列音频源中的一种:

  • 一个资源标识符(通常用于存储在res/raw文件夹中的音频文件)
  • 一个本地文件的URI(使用file://模式)
  • 一个在线音频资源的URI(URI格式)
  • 一个本地Content Provider(它应该返回一个音频文件)的行的URI
//从一个包资源加载音频资源
MediaPlayer resourcePlayer = MediaPlayer.create(this,R.raw.my_audio);

//从一个本地文件加载音频资源
MediaPlayer filePlayer = MediaPlayer.create(this,Uri.parse("file:///sdcard/localfile.mp3"));

//从一个在线资源加载音频资源
MediaPlayer urlPlayer = MediaPlayer.create(this,Uri.parse("http://site.com/audio/audio.mp3"));

//从一个Content Provider加载音频资源
MediaPlayer contentPlayer = MediaPlayer.create(this,Settings.System.DEFAULT_RINGTONE_URI);

通过create方法返回的Media Player对象已经调用了prepare。也可以使用现有的MediaPlayer实例的setDataSource方法,必须在开始播放之前调用prepare方法。

MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource("/sdcard/mydopetunes.mp3");
mediaPlayer.perpare();

准备视频播放

视频内容的播放比音频播放稍微复杂一些。为了显示一个视频,首先必须为该视频指定一个Surface。

使用VideoView播放视频

播放视频最简单的方法是使用VideoView类。VideoView包含了一个Surface,用于显示视频,以及封装和管理Media Player以控制视频的播放。

//分配本地文件以进行播放
videoView.setVideoPath("/sdcard/mycatvideo.3gp");
//分配一个远程视频流的URI
videoView.setVideoUri(myAwesomeStreamingSource);

当视频初始化完成后,可以使用start、stopPlayback、pause和seekTo方法控制播放,还可以使用setKeepScreenOn方法以应用一个屏幕Wake Lock。

//配置VideoView并分配一个视频来源
videoView.setKeepScreenOn(true);
videoView.setVideoPath("/sdcard/mycatvideo.3gp");

//附加一个Media Controller
MediaController mediaController = new MediaController(this);
videoView.setMediaController(mediaController);

使用Media Player播放

使用Media Player直接查看视频内容首先使用一个SurfaceView对象显示视频。SurfaceView类是SufaceHolder对象的包装器,后者是Surface的包装器,而Surface用于支持来自后台线程的可视化更新。

SurfaceHolder是异步创建的,因此必须等到surfaceCreated处理程序被触发,然后再通过实现SurfaceHolder.Callback接口将返回的SurfaceHolder对象分配给Media Player。

在创建和分配SurfaceHolder给Media Player之前,使用setDataSource方法来指定要播放的视频资源的路径、URI或Content Provider URI。

在选择了媒体资源后,调用prepare来初始化Media Player,以准备进行播放。

public class SurfaceViewVideoViewActivity extends Activity implements SurfaceHolder.Callback {
    static final String TAG = "SurfaceViewVideoViewActivity";
    
    private MediaPlayer mediaPlayer;
    
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            //创建Surface后,将其作为显示表面,并分配和准备一个数据源
            mediaPlayer.setDisplay(holder);
            mediaPlayer.setDataSource("/sdcard/test2.3gp");
            mediaPlayer.prepare();
        } catch (IllegalArgumentException e) {
            Log.e(TAG,"Illegal Argument Exception", e);
        } catch (IllegalStateException e) {
            Log.e(TAG,"Illegal State Exception", e);
        } catch (SecurityException e) {
            Log.e(TAG,"Security Exception", e);
        } catch (IOException e) {
            Log.e(TAG,"IO Exception", e);
        }
    }
    
    public void surfaceDestroyed(SurfaceHolder holder) {
        mediaPlayer.release();
    }
    
    public void surfaceChanged(SurfaceHolder holder,int format,int width,int height) {
        
    }
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        setContentView(R.layout.surfaceviewvideoviewer);
        
        //创建一个新的Media Player
        mediaPlayer = new MediaPlayer();
        
        //获得对SurfaceView的引用
        final SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surfaceView);
        
        //配置SurfaceView
        surfaceView.setKeepScreenOn(true);
        
        //配置SurfaceHolder并注册回调
        SurfaceHolder holder = surfaceView.getHolder();
        holder.addCallback(this);
        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        holder.setFixedSize(400,300);
        
        //连接播放按钮
        Button playButton = (Button) findViewById(R.id.buttonPlay);
        playButton.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                mediaPlayer.start();
            }
        });
        
        //连接暂停按钮
        Button pauseButton = (Button) findViewById(R.id.buttonPause);
        pauseButton.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                mediaPlayer.pause();
            }
        });
        
        //添加跳过按钮按钮
        Button skipButton = (Button) findViewById(R.id.buttonSkip);
        skipButton.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                mediaPlayer.skip();
            }
        });
    }
}

MediaPlayer提供了getDuration方法以查找所播放媒体的长度,以及getCurrentPosition方法以查找当前的播放位置。可以使用seekTo方法跳转到媒体中的某个特定位置。为了保持一致的媒体控制体验,Android包含了一个MediaController。这是一个标准的控件,提供了常用的媒体控制按钮。

MediaController mediaController = new MediaController(this);
mediaController.setMediaPlayer(new MediaPlayerController() {
    public boolean canPause() {
        return true;
    }
    
    public boolean canSeekBackward() {
        return true;
    }
    
    public boolean canSeekForward() {
        return true;
    }
    
    public int getBufferPercentage() {
        return 0;
    }
    
    public int getCurrentPosition() {
        return mediaPlayer.getCurrentPosition();
    }
    
    public int getDuration() {
        return mediaPlayer.getDuration();
    }
    
    public boolean isPlaying() {
        return mediaPalyer.isPalying();
    }
    
    public void pause() {
        mediaPlayer.pause();
    }
    
    public void seekTo(int pos) {
        mediaPlayer.seekTo(pos);
    }
    
    public void start() {
        mediaPlayer.start();
    }
})

meidaController.setAnchorView(myView);    //设置当MediaController可见时包含在哪个视图
mediaController.show();    //显示
mediaController.hide();     //隐藏

管理媒体播放输出

MediaPlayer提供了一些方法以控制输出音量、锁定播放期间的屏幕亮度以及设置循环状态。

  • setVolum(0.5f,0.5f); 控制播放过程中的每个声道的音量。左右声道采用了一个0到1之间的标量浮点数值
  • setScreenOnWhilePlaying(true); 强制屏幕在视频播放期间不变暗
  • isLooping(); 确定当前的循环状态
  • setLooping(true); 指定所播放的媒体在播放完成时是否应当继续循环播放

响应Media播放控件。

一些设备带有播放、停止、暂停、下一首和前一首媒体播放按键。用户按下这些按键时,系统会广播一个带有ACTION_MEDIA_BUTTON动作的Intent。

<receiver android:name=".MediaControlReceiver">
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_BUTTON"/>
    </intent-filter>
</receiver>

这个BroadcastReceiver在接收到媒体按键被按下的动作时,会创建一个包含相同extra的新Intent,并把该Intent广播给播放音频的Activity

public class MediaControlReceiver extends BroadcastReceiver {
    public static final String ACTION_MEDIA_BUTTON = "com.paad.ACTION_MEDIA_BUTTON";
    
    @Override
    public void onReceive(Context context,Intent intent) {
        if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
            Intent internalIntent = new Intent(ACTION_MEDIA_BUTTON);
            internalIntent.putExtra(intent.getExtras());
            context.sendBroadcast(internalIntent);
        }
    }
}

被按下的媒体按键的代码存储在接收到的Intent的EXTRA_KEY_EVENT extra中

public class ActivityMediaControlReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context,Intent intent) {
        if (MediaControlReceiver.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
            KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
            switch(event.getKeyCode()) {
                case (KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) :
                    if (mediaPlayer.isPlaying()) {
                        pause();
                    }
                    break;
                case (KeyEvent.KEYCODE_MEDIA_PLAY) :
                    play();
                    break;
                case (KeyEvent.KEYCODE_MEDIA_PAUSE) :
                    pause();
                    break;
                case (KeyEvent.KEYCODE_MEDIA_NEXT) :
                    skip();
                    break;
                case (KeyEvent.KEYCODE_MEDIA_PREVIOUS) :
                    previous();
                    break;
                case (KeyEvent.KEYCODE_MEDIA_STOP) :
                    stop();
                    break;
                default:
                    break;
            }
        }
    }
}

如果应用程序希望在Activity不可见时仍在后台播放音频,让Media Player在Service保持运行,并使用Intent来控制媒体播放。

给定的设备上可能安装了多个应用程序,每个应用程序都被配置为接收媒体按键按下动作,因此必须使用AudioManager的registerMediaButtonEventReceiver放阿飞将接收者注册为媒体按键按下动作的唯一处理程序。

//注册媒体按键事件Receiver来监听媒体按钮按下动作
AudioManager am = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
ComponentName component = new ComponentName(this,MediaControlReceiver.class);

am.registerMediaButtonEventReceiver(component);

//注册一个本地Intent Receiver,用于接收在manifest文件中注册的Receiver,媒体按键按下动作。
activityMediaControlReceiver = new ActivityMediaControlReceiver();
IntentFilter filter = new IntentFilter(MediaControlReceiver.ACTION_MEDIA_BUTTON);
registerReceiver(activityMediaControlReceiver,filter);

请求和管理音频焦点

用户的设备上可能有多个媒体播放器,因此当另一个媒体应用程序获得焦点时,让你的应用程序暂停播放并交出媒体按键的控制权。

显示了一个请求音乐流永久占有音频焦点

AudioManager am = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
//请求音频焦点
int result = am.requestAudioFocus(focusChangeListener,AudioManager.STREAM_MUSIC,AudioManager.AUDIOFOCUS_GAIN);
if (result == AduioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    mediaPlayer.start();
}

音频焦点将依次分配给每个请求音频焦点的应用程序。这意味着如果另一个应用程序请求音频焦点,你的应用程序就会失去音频焦点。你在请求音频焦点时注册的Audio Focus Change Listener的onAudioFocusChange处理程序将会通知你焦点丢失的情况。

private OnAudioFocusChangeListener focusChangeListener = new OnAudioFocusChangeListener() {
    public void onAudioFocusChange(int focusChange) {
        AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        switch(focusChange) {
            case (AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK):
                //降低音量
                mediaPlayer.setVolume(0.2f,0.2f);
                break;
            case (AudioManager.AUDIOFOCUS_LOSS_TRANSIENT):
                pause();
                break;
            case (AudioManager.AUDIOFOCUS_LOSS):
                stop();
                ComponentName component = new ComponentName(AudioPlayerActivity.this,MeidaControlReceiver.class);
                am.unregisterMediaButtonEventReceiver(component);
                break;
            case (AudioManager.AUDIOFOCUS_GAIN):
                //将音量恢复到正常大小,并且如果音频流已被暂停,则恢复音频流
                mediaPlayer.setVolume(1f,1f);
                mediaPlayer.start();
                break;
            default:
                break;
        }
    }
}

完成音频播放后,可以选择放弃音频焦点

am.abandonAudioFocus(focusChangeListener);

如果当前的输出流在入耳式耳机上播放,那么拔出耳机时,系统会自动将输出切换到设备的扬声器。这种情况下,暂停音频输出或者减小音量是一个很好的做法。

private class NoisyAudioStreamReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context,Intent intent) {
        if(AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())){
            pause();
        }
    }
}

Remote Control Client

应用程序可以向能够显示元数据、图片和媒体传输控制按键的远程控件提供数据,并响应这些远程控件。

AudioManager am = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
//创建一个将会广播媒体按键按下动作的Pending Intent。将目标组件设为你的Broadcast Receiver
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
ComponentName component = new ComponentName(this,MediaControlReceiver.class);
mediaButtonIntent.setComponent(component);
PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(getApplicationContext(),0,meidaButtonIntent,0);
//使用PendingIntent创建一个新的Remote Control Client,并把它注册到Audio Manager中
myRemoteControlClient = new RemoteControlClient(mediaPendingInent);
am.registerRemoteControlClient(myRemoteControlClient);

操纵原始音频

使用AudioTrack和AudioRecord类可以直接从音频输入硬件录制音频,以及直接将PCM音频缓冲区中的音频流输出到音频硬件来进行播放。使用Audio Track流式传输时,可以接近实时地处理和播放传入的音频,这就允许你操纵传入或传出的音频,以及对原始音频进行信号处理。

使用AudioRecord录制声音

int frequency = 11025;
int channelConfiguration = AudioFromat.CHANNEL_CONFIGURATION_MONO;
int audioEncoding = AudioFromat.ENCODING_PCM_16BIT;

File file = new File(Environment.getExternalStorageDirectory(),"raw.pcm");

//创建新文件
try {
    file.createNewFile();
} catch (IOException e) {
    Log.d(TAG,"IO Exception", e);
}

try {
    OutputStream os = new FileOutputStream(file);
    BufferedOutputStream bos = new BufferedOutputStream(os);
    DataOutputStream dos = new DataOutputStream(bos);
    
    int bufferSize = AudioRecord.getMinBufferSize(frequency,channelConfiguration,audioEncoding);
    short[] buffer = new short[bufferSize];
    
    //创建一个新的AudioRecord对象来录制音频
    AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,frequency,channelConfiguration,audioEncoding,bufferSize);
    audioRecord.startRecording();
    
    while(isRecording) {
        int bufferReadResult = audioRecord.read(buffer,0,bufferSize);
        for (int i = 0;i < bufferReadResult;i++) {
            dos.writeShort(buffer[i]);
        }
    }
    
    audioRecord.stop();
    dos.close();
} catcg (Throwable t) {
    Log.d(TAG,"An error occurred during recording",t);
}

使用AudioTrack播放音频

使用AudioTrack类可以将原始音频直接播放到硬件缓冲区中。

int frequency = 11025 / 2;
int channelConfiguration = AudioFromat.CHANNEL_CONFIGURATION_MONO;
int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;

File file = new File(Environment.getExternalStorageDirectory(),"raw.pom");

//用于存储音轨的short数组
int audioLength = (int)(file.length() / 2);
short[] audio = new short[audioLength];

try {
    InputStream is = new FileInputStream(file);
    BufferedInputStream bis = new BufferedInputStream(is);
    DataInputStream dis = new DataInputStream(bis);
    
    int i = 0;
    while(dis.available() > 0) {
        audio[i] = dis.readShort();
        i++;
    }
    
    //关闭输入流
    dis.close();
    
    //创建和播放新的AudioTrack对象
    AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,frequency,channelConfiguration,audioEncoding,audioLength,AudioTrack.MODE_STREAM);
    audioTrack.play();
    audioTrack.write(audio,0,audioLength);
} catch (Throable t) {
    Log.d(TAG,"An error occurred during playback",t);
}

使用Sound Pool

当应用程序需要低音频延迟并且将同时播放多个音频流时,可以使用SoundPool类来管理音频。创建一个SoundPool会预加载应用程序试用的音轨,并优化它们的资源管理。

创建SoundPool时,可以指定要播放的最大并发流数。当达到这个值时,SoundPool就会自动停止池内最老的、优先级最低的流,从而将音频混合的影响降到最低。

int maxStreams = 10;
SoundPool sp = new SoundPool(maxStreams,AudioManager.STREAM_MUSIC,0);

int strack1 = sp.load(R.raw.track1,0);
int strack2 = sp.load(R.raw.track2,0);
int strack3 = sp.load(R.raw.track3,0);

使用音效

可以修改效果设置和参数,以改变在应用程序内输出的音频的效果。

  • Equalizer 可以修改音频输出的频率响应。使用setBandLevel方法可以为特定的频带指定一个增益值。
  • Virtualizer 使音频的立体声效果更强。它的实现会随输出的设备的配置而发生变化。使用setStrength方法可以将音效的强度设置为0 - 1000
  • BassBoost 增强音频输出的低音音频。使用setStrength方法可以将音效的强度设置为0 - 1000
  • PresetReverb 允许指定多个混声预设值之一。
  • EnvironmentalIReverb 允许通过控制音频输出来模拟不同环境的效果。

摄像头

使用Intent拍摄照片

startActivityForResult(new Intent(MediaStore.ACTION_IMAGE_CAPTURE), TAKE_PICTURE);

这将启动一个Camera应用程序来拍摄照片,不需要你重写原生Camera应用程序,就可以为用户提供全套的摄像头功能。用户对拍摄的照片感到满意后,该照片就会通过onActivityResult处理程序收到的Intent返回给应用程序。默认情况下,拍摄的照片将作为一个缩略图返回。

//创建输出文件
File file = new File(Environment.getExternalStorageDirectory(),"test.jpg");
Uri outputFileUri = Uri.fromFile(file);
//生成Intent
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT,outputFileUri);
//启动摄像头应用程序
startActivityForResult(intent,TAKE_PICTURE);

@Override
protected void onActivityResult(int requestCode, int resultCode,Intent data) {
    if (requestCode == TAKE_PICTURE) {
        //检查结果是否包含缩略图
        if (data != null) {
            if (data.hasExtra("data")) {
                Bitmap thumbnail = data.getParcelableExtra("data");
                imageView.setImageBitmap(thumbnail);
            }
        } else {
            //如果没有缩略图数据,则说明图像存储在目标输出URI中
            int width = imgaeView.getWidth();
            int height = imageView.getHeight();
            
            BitmapFactory.Options factoryOptions = new BitmapFactory.Options();
            factoryOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeFile(outputFileUri.getPath(),factoryOptions);
            int imageWidth = factoryOptions.outWidth;
            int imageHeight = factoryOptions.outHeight;
            
            //确定将图像缩小多少
            int scaleFactor = Math.min(imageWidth / width,imageHeight / height);
            
            //将图像文件解码为图像大小以填充视图
            factoryOptions.inJustDecodeBounds = false;
            factoryOptions.inSampleSize = scaleFactor;
            factoryOptions.inPurgeable = true;
            
            Bitmap bitmap = BitmapFactory.decodeFile(outputFileUri.getPath(), factoryOptions);
            imageView.setImageBitmap(bitmap);
        }
    }
}

直接控制摄像头

为了直接访问摄像头硬件,添加权限<uses-permission android:name="android.permission.CAMERA"/>

Camera camera = Camera.open();
camera.release();

摄像头属性

使用Camera对象的getParameters方法可以得到Camera.Parameters对象,然后可以使用该对象存储摄像头设置,可以获得摄像头的许多属性和当前对焦的场景。

  • SceneMode 使用一个SCENE_MODE_* 静态常量返回或设置所拍摄的场景的类型。每个场景模式都为特定的场景类型(聚会、海滩、日落等)优化了摄像头参数的配置。
  • FlashMode 使用一个FLASH_MODE_*静态常量返回或设置当前的闪光模式(打开、关闭、红眼消减、闪光灯)
  • WhiteBalance 使用一个WHITE_BALANCE_*静态常量返回或设置白平衡校正来校正场景。
  • AutoWhiteBalanceLock 当使用自动白平衡时,启用自动白平衡锁会暂停颜色校正算法,从而确保连续拍摄的多张照片使用相同的颜色平衡设置。当拍摄全景照片或者为高动态光照渲染图像使用包围曝光时,这种做法特别有用,使用isAutoWhiteBalanceLockSupported方法可以确认设备是否支持这种功能。
  • ColorEffect 使用一个EFFECT_*静态常量返回或设置应用到图像的特殊颜色效果。使用getSupportedColorEffects方法可以找出可用的颜色效果
  • FocusMode 使用一个FOCUS_MODE_*静态常量返回或设置摄像头尝试对焦的方式。使用getSupportedFocusModes方法可以找出可用的模式
  • Antibanding 使用一个ANTIBANDING_*静态常量返回或设置用来降低条带效果的屏幕刷新频率。使用getSupportedAntibanding方法可以找出可用的频率

使用CameraParameters来读取或指定图像、缩略图和摄像头预览的大小、质量和格式参数

  • JPEG和缩略图质量 使用setJpegQualitysetJpegThumbnailQuality方法,并传入0到100之间的整型数值
  • 图像、预览和缩略图大小 分别使用setPictureSizesetPreviewSizesetJpegThumbnailSize参数指定图像、预览和缩略图的高度和宽度。
  • 图像和预览像素格式 使用PixelFormat类中的一个静态常量调用setPictureFormatsetPreviewFormat可以设置图像的格式
  • 预览帧速率 setPreviewFpsRange方法可以用来指定预览的首选帧率范围。使用getSupportedPreviewFpsRange方法可以找出所支持的最低和最高帧率

使用摄像头预览

在实现自己的摄像头时,需要显示摄像头捕获的内容的一个预览,以便用户可以选择拍摄什么样的照片。显示摄像头的流式传输视频还意味着我们能够将实时视频融入到应用程序中,例如实现增强现实。摄像头预览是使用SurfaceHoder显示的,所以要在应用程序中查看实时摄像头流,必须在UI层次中包含一个Surface View,需要实现一个SurfaceHolder.Callback来监听有效表面的构建,然后该表面传递给Camera对象的setPreviewDisplay方法。

public class CameraActivity extends Activity implements SurfaceHolder.Callback {
    private static final String TAG = "CameraActivity";
    private Camera camera;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        SurfaceView surface = (SurfaceView)findViewById(R.id.surfaceView);
        SurfaceHolder holder = surface.getHolder();
        holder.addCallback(this);
        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        holder.setFixedSize(400,300);
    }
    
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            camera.setPreviewDisplay(holder);
            camera.startPreview();
            //必要时在预览上进行绘制
        } catch (IOException e) {
            Log.d (TAG,"IO Exception", e);
        }
    }
    
    public void surfaceDestroyed(SurfaceHolder holder) {
        camera.stopPreview();
    }
    
    public void surfaceChanged(SurfaceHolder holder,int format,int width,int height) {
        
    }
    
    @Override
    protected void onPause() {
        super.onPause();
        camera.release();
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        camera = Camera.open();
    }
}

还可以分配一个PreviewCallback,使其在每个预览中触发,以便可以实时操纵或者分析每个预览帧。需要调用Camera对象的setPreviewCallback方法,并传入一个重写了onPreviewFrame方法的新的PreviewCallback实现。

camera.setPreviewCallback(new PreviewCallback() {
    public void onPreviewFrame(byte[] data,Camera camera) {
        int quality = 60;
        Size previewSize = camera.getParameters().getPreviewSize();
        YuvImage image = new YuvImage(data,ImageFormat.NV21,previewSize.width,previewSize.height,null);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        
        image.compressToJpeg (new Rect(0 ,0,previewSize.width,previewSize.height),quality,outputStream);
        //对预览图像执行一些操作
    }
})

面部检测和面部特征

在拍摄以人为主的照片时调整对焦区域、测光区域和确定白平衡,但是它们也可以用来制作一些创造性的效果。

为了确认设备支持面部检测功能,需要使用Camera对象的getMaxNumDetectedFaces方法int facesDetectable = camera.getParameters().getMaxNumDetectedFaces();该方法返回设备的摄像头能够检测的最大人脸数目。如果返回值为0,则说明设备不支持面部检测。

在开始使用摄像头检测人脸之前,需要分配一个新的FaceDetectionListener,使其重写onFaceDetection方法。你将得到一个Face对象的数组,每个Face对相对应在场景中检测到一个人脸。

camera.setFaceDetectionListener(new FaceDetectionListener() {
    public void onFaceDetection(Face[] faces,Camera camera) {
        if (faces.length > 0) {
            Log.d("FaceDetection","face detected:" + faces.length + " Face 1 Location X:" + faces[0].rect.centerX() + “Y: ” + faces[0].rect.centerY());
        }
    }
})

public void surfaceCreated(SurfaceHolder holder) {
    try {
        camera.setPreviewDisplay(holder);
        camera.startPreview();
        camera.startFaceDetection();
    } catch (IOException e) {
        Log.d(TAG, "IO Exception", e);
    }
}

public void surfaceDestroyed(SurfaceHolder holder) {
    camera.stopFaceDetection();
    camera.stopPreview();
}

拍摄照片

在配置好摄像头的设置并看到预览后,就可以拍摄照片了。调用Camera对象的takePicture,并传入一个ShutterCallback和两个PictureCallback实现(一个用于RAW图像,一个用于JPEG编码的图像)。每个图像回调都会收到一个以相应格式表示图像的字节数组,而快门回调则在快门关闭后立即触发。

private void takePicture() {
    camera.takePicture(shutterCallback,rawCallback,jpegCallback);
}

ShutterCallback shutterCallback = new ShutterCallback() {
    public void onShutter() {
        //快门关闭时执行一些操作
    }
}

PictureCallback rawCallback = new PictureCallback() {
    public void onPictureTaken(byte[] data,Camera camera) {
        //对图像的原始数据做一些处理
    }
}

PictureCallback jpegCallback = new PictureCallback() {
    public void onPictureTaken(byte[] data, Camera camera) {
        //将图像的JPEG数据保存到SD卡
        FileOutputStream outStream = null;
        try {
            String path = Environment.getExternalStorageDirectory() + "\test.jpg";
            outStream = new FileOutputStream(path);
            outStream.write(data);
            outStream.close();
        } catch (FileNotFoundException e) {
            Log.e(TAG,"File Not Found",e);
        } catch (IOException e) {
            Log.e(TAG, "IO Exception", e);
        }
    }
}

读取并写入JPEG EXIF图像详细信息

ExifInterface类为读取并修改存储在JPEG文件中的EXIF(可交换图像文件格式)数据提供了一种机制。通过将目标JPEG图像的完整文件名传入ExifInterface构造函数来创建一个新的ExifInterface实例。EXIF数据用于为照片存储各种不同的元数据,包括拍摄日期和时间、摄像头设置(如制造商和型号)、图像设置(如光圈和快门速度)以及图像描述和位置。为了读取EXIF属性,需要调用ExifInterface对象的getAttribute方法,并传入将要读取的属性名。

File file = new File(Environment.getExternalStorageDirectory(),"test.jpg");

try {
    ExifInterface exif = new ExifInterface(file.getCanonicalPath());
    //读取摄像头模型和位置属性
    String model = exif.getAttribute(ExifInterface.TAG_MODEL);
    Log.d(TAG,"Model: " + model);
    //设置摄像头的品牌
    exif.setAttribute(ExifInterface.TAG_MAKE, "My Phone");
} catch (IOException e) {
    Log.e(TAG,"IO Exception", e);
}

录制视频

使用Intent录制视频

使用此Intent启动新Activity将会启动本机视频录制器,允许用户开始、停止、浏览并重新拍摄视频。已录制视频的URI作为返回Intent的数据参数提供给Activity。

  • MediaStore.EXTRA_OUTPUT 默认,由视频捕获操作录制的视频将存储在默认媒体库中。
  • MediaStore.EXTRA_VIDEO_QUALITY 视频捕获操作允许使用一个整型值指定某个图像的质量。
  • MeidaStore.EXTRA_DURATION_LIMIT 所录制视频的最大长度,单位为秒。
private static final int RECORD_VIDEO = 0;

private void startRecording() {
    //生成Intent
    Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
    
    //启动摄像头应用程序
    startActivityForResult(intent,RECORD_VIDEO);
}

@Override
protected void onActivityResult(int requestCode,int resultCode,Intent intent) {
    if (requestCode == RECORD_VIDEO) {
        VideoView videoView = (VideoView)findViewById(R.id.videoView);
        videoView.setVideoURI(data.getData());
        videoView.start();
    }
}

使用MediaRecorder录制视频

可以使用MediaRecorder类录制音频和视频文件,然后在自己的应用程序中使用它们,或者把它们添加到媒体库中。需要添加权限

<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.RECORD_VIDEO"/>
<uses-permission android:name="android.permission.CAMERA"/>
配置Video Recorder

首先解锁摄像头,并使用setCamera方法将其分配给Media Recorder。setAudioSourcesetVideoSource方法可以指定MediaRecorder.AudioSource.* 或者Media Recorder.VideoSource.* 静态常量,它们分别定义了音频和视频源。

//解锁摄像头以允许Meida Recorder拥有它
camera.unlock();

//将摄像头分配给Media Recorder
mediaRecorder.setCamera(camera);

//配置输入源
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

//设置录制配置文件
CamcorderProfile profile = null;

if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_1080P)) {
    profile = CamcorderProfile.get(CamcorderProfile.QUALITY_1080P);
} else if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_720P)) {
    profile = CamcorderProfile.get(CamcorderProfile.QUALITY_720P);
} else if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_480P)) {
    profile = CamcorderProfile.get(CamcorderProfile.QUALITY_480P);
} else if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_HIGH)) {
    profile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
}

if (profile != null) {
    mediaRecorder.setProfile(profile);
}

//指定输出文件
mediaRecorder.setOutputFile("/sdcard/myvideorecording.mp4");

//准备录制
mediaRecorder.prepare();

缩短Media Recorder的启动时间来提高效率。当Activity只是用于录制音频/视频而不是静态图片,可以使用Camera.Parameters.setRecordHint方法告诉摄像头你只想录制音频/视频

Camera.Parameters parameters = camera.getParameters();
parameters.setRecordingHint(true);
camera.setParameters(parameters);
预览视频流

当录制视频时,实时显示传入所录制视频的预览是一种好的做法。与摄像头预览一样,可以使用MediaRecorder对象的setPreviewDisplay方法分配一个Surface来显示视频流。

mediaRecorder.setPreviewDisplay(holder.getSurface());

mediaRecorder.prepare();
控制录制
mediaRecorder.stop();
//重置和释放Meida Recorder
mediaRecorder.reset();
mediaRecorder.release();
camera.lock();

可以使用setVideoStabilization方法修改摄像头参数,并不是所有的摄像头硬件都支持影像稳定,所以一定要用isVideoStabilizationSupported方法进行检查

Camera.Parameters parameters = camera.getParameters();
if (parameters.isVideoStabilizationSupported()) {
    parameters.setVideoStabilization(true);
}
camera.setParameters(parameters);
创建缩时视频
每隔30秒捕获一副图片
mediaRecorder.setCaptureRate(0.03);

使用媒体效果

使用GPU和OpenGL纹理对视频内容应用大量实时的视觉效果。可以将媒体效果应用到位图、视频或实时的摄像头预览,只要源图像绑定到一个GL_TEXTURE_2D纹理图片,并且包含至少一个mipmap级别即可。一般来说,要对图片或视频帧应用一种效果,需要使用OPenGL ES2.0上下文中的EffectContext.createWithCurrentContext创建一个新的EffectContext.媒体效果是使用EffectFactory创建的,而EffectFactory可以通过调用EffectContext的getFactory方法创建。要创建特定的效果,可以调用createEffect方法,并传入一个EffectFactory.EFFECT_*常量,每种效果支持不同的参数,可以调用setParameter 并传入要更改的设置的名称和要应用的值来进行配置。

向媒体库中添加新媒体

使用媒体扫描仪插入媒体

如果已经录制了任何一种媒体,MediaScannerConnection类提供了一个scanFile方法,作为将该媒体添加到媒体库中的一种简单方法,而不需要为媒体库Content Provider创建完整记录。在使用scanFile方法开始扫描文件之前,必须调用connect方法并等待它完成与媒体扫描仪的连接。这个调用是异步的,因此需要实现一个MediaScannerConnectionClient以便在连接建立时进行通知。

private void mediaScan(final String filePath) {
    MediaScannerConnectionClient mediaScannerClient = new MediaScannerConnectionClient() {
        private MediaScannerConnection msc = null;
        msc = new MediaScannerConnection(VideoCameraActivity.this,this);
        msc.connect();
        
        public void onMediaScannerConnected() {
            //可以指定一个MINE类型,或者让Media Scanner根据文件名自己假定一种类型
            String memeType = null;
            msc.scanFile(filePath,mimeType);
        }
        
        public void onScanCompleted(String path,Uri uri) {
            msc.disconnect();
            Log.d(TAG,"File Added at: " + uri.toString());
        }
    };
}

手动插入媒体

通过创建一个新的ContentValues对象并手动将其插入到适当的媒体库Content Provider中,可以将新媒体添加到媒体库中,而不需要依赖媒体扫描仪。

ContentValues content = new ContentValues(3);
content.put(Audio.AudioColumns.TITLE,"TheSoundandtheFury");
content.put(Audio.AudioColumns.DATE_ADDED,System.currentTimeMillis() / 1000);
content.put(Audio.Media.MIME_TYPE,"audio/amr");
content.put(MediaStore.Audio.Media.DATA,"/sdcard/myoutputfile.mp4");

ContentResolver resolver = getContentResolver();
Uri uri = resolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,content);
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,uri));

蓝牙

蓝牙是一种用于短距离、低带宽点对点通信的通信协议。使用蓝牙API可以搜索并连接到一定范围之内的其他设备。

管理本地蓝牙设备适配器

通过BluetoothAdapter 类来控制本地蓝牙设备,该类代表运行应用程序的Android设备。

<uses-permission android:name="android.permission.BLUETOOTH"/>   //读取任何一种本地Bluetooth Adapter属性、启动发现过程或者找到绑定的设备
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>   //修改任何一种本地设备属性
BluetoothAdater bluetooth = BluetoothAdapter.getDefaultAdapter();
if (bluetooth.isEnabled()) {
    String address = bluetooth.getAddress();
    String name = bluetooth.getName();
}

bluetooth.setName("BlackFang");

为了查找关于当前Bluetooth Adapter状态对的更详细描述,可以使用getState方法

  • STATE_TURNING_ON
  • STATE_ON
  • STATE_TURNING_OFF
  • STATE_OFF

启用蓝牙

private static final int ENABLE_BLUETOOTH = 1;
private void initBluetooth() {
    if (!bluetooth.isEnabled()) {
        //如果蓝牙未启用,提示用户打开它
        Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(intent, ENABLE_BLUETOOTH);
    } else {
        //蓝牙已启用,初始化其UI
        initBluetoothUI();
    }
}

protected void onActivityResult(int requestCode,int resultCode,Intent data) {
    if (requestCode == ENABLE_BLUETOOTH) {
        if (resultCode == RESULT_OK) {
            initBluetoothUI();
        }
    }
}

启用和禁用Bluetooth Adapter是比较耗时的异步操作。应用程序不应轮询Bluetooth Adapter,而是应当注册一个Broadcast Receiver用于监听ACTION_STATE_CHANGED。Broadcast Receiver包含两个extra,EXTRA_STATE和EXTRA_PREVIOUS_STATE,它们分别指示了当前和先前的Bluetooth Adapter状态。

BroadcastReceiver bluetoothState = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context,Intent intent) {
        String prevStateExtra = BluetoothAdapter.EXTRA_PREVIOUS_STATE;
        String stateExtra = BluetoothAdapter.EXTRA_STATE;
        int state = intent.getIntExtra(stateExtra, -1);
        int previousState = intent.getIntExtra(prevStateExtra, -1);
        
        String tt = "";
        switch(state) {
            case (BluetoothAdapter.STATE_TURNING_ON) :
                tt = "Bluetooth turning on";
                break;
            case (BluetoothAdapter.STATE_ON) :
                tt = "Bluetooth on";
                break;
            case (BluetoothAdapter.STATE_TURNING_OFF) :
                tt = "Bluetooth turning off";
                break;
            case (BluetoothAdapter.STATE_OFF) :
                tt = "Bluetooth off";
                break;
            default:
                break;
        }
        Log.d(TAG,tt);
    }
};

String actionStateChanged = BluetoothAdapter.ACTION_STATE_CHANGED;
registerReceiver(bluetoothState,new IntentFilter(actionStateChanged));

可发现性和远程设备发现

两个设备相互查找以进行连接的过程叫做发现。

管理设备的可发现性

为了使远程Android设备能够在发现扫描中找到你的本地Bluetooth Adapter,需要确保它是可发现的。可以通过调用getScanMode来找出它的扫描模式

  • SCAN_MODE_CONNECTABLE_DISCOVERABLE 启用查询扫描和页面扫描,意味着该设备可被执行发现扫描的蓝牙设备发现
  • SCAN_MODE_CONNECTABLE 启用页面扫描但是禁用查询扫描。这意味着先前连接并绑定到本地设备的设备可以在发现过程中找到,但找不到新设备。
  • SCAN_MODE_NONE 可发现性被关闭。在发现过程中没有一个远程设备能够找到本地Bluetooth Adapter。
startActivityForResult (new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE),DISCOVETY_REQUEST);
@Override
protected void onActivityResult(int requestCode,int resultCode,Intent data) {
    if (requestCode == DISCOVETY_REQUEST) {
        if (resultCode == RESULT_CANCELED) {
            Log.d(TAG,"Discovery canceled by user");
        }
    }
}

registerReceiver(new BroadcastReceiver() {
    @Override
    public void onReceive(Context context,Intent intent) {
        String prevScanMode = BluetoothAdapter.EXTRA_PREVIOUS_SCAN_MODE;
        String scanMode = BluetoothAdapter.EXTRA_SCAN_MODE;
        
        int currentScanMode = intent.getIntExtra(scanMode, -1);
        int prevMode = intent.getIntExtra(prevScanMode, -1);
        
        Log.d (TAG, "Scan Mode: " + currentScanMode + ". Previous:" + prevMode);
    }
},new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));

发现远程设备

通过使用isDiscovering方法可以检查本地Bluetooth Adapter是否正在执行一次发现扫描。启动发现,调用startDiscovery。取消发现,调用cancelDiscovery。Android使用Broadcast Intent来通知发现过程的启动和结束以及在扫描过程中发现的远程设备。

BroadcastReceiver discoveryMonitor = new BroadcastReceiver() {
    String dStarted = BluetoothAdapter.ACTION_DISCOVER_STARTED;
    String dFinished = BluetoothAdapter.ACTION_DISCOVERY_FINISHED;
    
    @Override
    public void onReceive(Context context,Intent intent) {
        if (dStarted.equals(intent.getAction())) {
            //启动发现过程
            Log.d(TAG,"Discovery Started...");
        } else if (dFinished.equals(intent.getAction())) {
            //发现过程完成
            Log.d(TAG,"Discovery Complete.");
        }
        
        String remoteDeviceName = intent.getStringExtra(BluetoothDevice.EXTRA_NAME);
        BluetoothDevice remoteDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
        deviceList.add(remoteDevice);
    }
};

registerReceiver(discoverMonitor,new IntentFilter(dStarted));
registerReceiver(discoverMonitor,new IntentFilter(dFinished));

蓝牙通信

  • BluetoothServerSocket 用于建立一个监听套接字以启动设备之间的链路。为建立“握手”,需要将其中一台设备充当服务器以监听和接受传入的连接请求。
  • BluetoothSocket 用于创建一个新的客户端来连接到正在监听的BluetoothServerSocket。一旦连接之后,就在服务器和客户端上使用Bluetooth Sockets来传输数据流。

为使Bluetooth Adapter作为服务器,需要调用其listenUsingRfcommWithServiceRecord方法来监听传入的连接请求,并传入用来标识服务器的名称和以UUID。该方法将会返回一个BluetoothServerSocket对象。为了开始监听连接,需要调用该ServerSocket的accept的方法,并可以选择传入一个超时时间。ServerSocket将会保持阻塞,直到具有匹配UUID的远程BluetoothSocket客户端尝试进行连接。

private BluetoothSocket transferSocket;
private UUID startServerSocket(BlurtoothAdapter adapter) {
    UUID uuid = UUID.fromString("ssss");
    String name = "bluetoothserver";
    
    try {
        final BluetoothServerSocket btserver = bluetooth.listenUsingRfcommWithServiceRecord(name,uuid);
        
        Thread acceptThread = new Thread(new Runnable() 「
            public void run() {
                try {
                    //在客户端连接建立以前保持阻塞
                    BluetoothSocket serverSocket = btserver.accept();
                    //开始监听消息
                    listenForMessages(serverSocket);
                    //添加对用来发送消息的套接字的引用
                    transferSocket = serverSocket;
                } catch (IOException e) {
                    Log.e("BLUETOOTH","Server connection IO Exception",e);
                }
            }
        });
        acceptThread.start();
    } catch(IOException e) {
        Log.e("BLUETOOTH","Socket listener IO Exception",e)
    }
    return uuid;
}

选择远程蓝牙设备进行通信

可以在客户端设备上使用BluetoothSocket类,在应用程序中启动与长在监听的Server的通信信道。

蓝牙设备连接需求

  • 远程设备必须是可发现的
  • 远程设备必须使用一个Bluetooth Server Socket接受连接
  • 本地和远程设备必须经过配对。如果设备没有配对,那么当启动连接请求时将提示用户进行配对。

可以使用本地BluetoothAdapter的getRemoteDevice,并指定你想要连接到的远程蓝牙设备的硬件地址。为了查找当前已配对的设备集合,可以调用本地Bluetooth Adapter的getBondedDevices方法。可以通过查询所返回的集合以发现目标蓝牙设备是否与本地BluetoothAdapter进行配对。

final BluetoothDevice knownDevice = bluetooth.getRemoteDevice("01:23:77:35:2F:AA");
Set<BluetoothDevice> bondedDevices = bluetooth.getBondedDevices();
if (bondedDevices.contains(knownDevice)) {
    //目标设备已经与本地设备绑定/配对
}

打开一个客户端BluetoothSocket连接

调用connect方法,使用所返回的Bluetooth Socket来启动连接

private void connectToServerSocket(BluetoothDevice device,UUID uuid) {
    try {
        BluetoothSocket clientSocket = device.createRfcommSocketToServiceRecord(uuid);
        
        //阻塞,直到服务器接受连接
        clientSocket.connect();
        
        //开始监听消息
        listenForMessages(clientSocket);
        
        //添加对用于发送消息的套接字的引用
        transferSocket = clientSocket;
    } catch(IOException e) {
        Log.e("BLUETOOTH","Bluetooth client I/O Wxception",e);
    }
}

使用Bluetooth Socket传输数据

一旦建立连接后,客户端和服务端设备上都会有Bluetooth Socket。两者之间没有显著区别。通过InputStram和OutputStream对象来处理。

private void sendMessage(BluetootnSocket socket,String message) {
    OutputStream outStream;
    try {
        outStream = socket.getOutputStream();
        
        byte[] byteArray = (message + "").getBytes();
        byteArray[byteArray.length - 1] = 0;
        outStream.write(byteArray);
    } catch(IOException e) {
        Log.e(TAG,"Message send failed",e);
    }
}

private boolean listening = false;

private void listenForMessages(BluetoothSocket socket,StringBuilder incoming) {
    listening = true;
    int buggerSize = 1024;
    byte[] buffer = new byte[bufferSize];
    
    try {
        InputStream instream = socket.getInputStream();
        int bytesRead = -1;
        while (listening) {
            bytesRead = instream.read(buffer);
            if (bytesRead != -1) {
                String result = "";
                while((bytesRead == bufferSize) && (buffer[bufferSize - 1] != 0)) {
                    result = result + new String(buffer,0,bytesRead - 1);
                    bytesRead = instream.read(buffer);
                }
                result = result + new String(buffer,0,bytesRead - 1);
                incoming.qppend(result);
            }
            socket.close();
        }
    } catch (IOException e) {
        Log.e(TAG,"Message received failed.",e);
    }
    finally {
    }
}

管理网络

Android 网络主要是通过ConnectivityManager来处理的,该服务使你可以监视连接状态、设置自己的首选网络连接以及管理连接失败转接。

Connectivity Manager

用于监视网络连接状态、配置故障转移设置以及控制网络无线电。需要权限。

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>

查找和监视网络连接

ConnectivityManager connectivity = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
//获得活动网络信息
NetworkInfo activeNetwork = connectivity.getActiveNetworkInfo();
boolean isConnected = ((activeNetwork != null) && (activeNetwork.isConnectedOrConnecting()));
boolean isWiFi = activeNetwork.getType() == ConnectivityManager.TYPE_WIFI;

通过查询连接状态和网络类型,可以根据可用的带宽暂时性地禁用下载和更新,修改刷新频率,或者推迟大文件的下载。

为了监视网络连接,可以创建一个Broadcast Receiver来监听ConnectivityManager.CONNECTIVITY_ACTION

<receiver android:name=".ConnectivityChangedReceiver">
    <intent-filter>
        <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
    </intent-filter>
</receiver>

这些Intent包含了一些extra,它们提供了关于连接状态变化的额外详细信息。可以使用ConnectivityManager类中可用的静态常量访问每个extra。

EXTRA_NO_CONNECTIVITY 布尔类型,当设备未连接到任何网络时返回true。当有连接时,使用getActiveNetworkInfo来获得新连接状态的更多详细信息并根据情况修改下载计划。

管理WiFi

WifiManager代表Android Wi-Fi连接服务。它能够配置Wi-Fi网络连接,管理当前的Wi-Fi连接、扫描接入点以及监视Wi-Fi连接的变化。添加权限

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
WifiManager wifi = (WifiManager)getSystemService(Context.WIFI_SERVICE);
if (!wifi.isWifiEnabled()) {
    if (wifi.getWifiState() != WifiManager.WIFI_STATE_ENABLING) {
        wifi.setWifiEnabled(true);
    }
}

监视Wi-Fi连接

大多数情况下,使用ConnectivityManager监视Wi-Fi连接的变化是一种最佳实践。不过,每当Wi-Fi 网络连接状态发生变化时,Wi-Fi Manager会广播Intent,它会使用在WifiManager类中定义的常量。

  • WIFI_STATE_CHANGED_ACTION 指示Wi-Fi硬件状态已经发生变化,包括enabling、enabled、disabling、disabled和unknown几种状态。
    • EXTRA_WIFI_STATE 新的Wi-Fi状态
    • EXTRA_PREVIOUS_STATE 前一次的Wi-Fi状态。
  • SUPPLICANT_CONNECTION_CHANGE_ACTION 每当与活动的请求方之间的连接状态发生变化时广播该Intent。当新连接建立或者现有连接丢失时就使用EXTRA_NEW_STATE触发它,并且在建立新连接时,该布尔值返回true
  • NETWORK_STATE_CHANGED_ACTION 每当Wi-Fi连接状态发生变化时被触发。
    • EXTRA_NETWORK_INFO 其中包含详细描述了当前网络状态的NetworkInfo对
    • EXTRA_BSSID 包含所连接的接入点的BSSID
  • RSSI_CHANGED_ACTION 通过监听RSSI_CHANGED_ACTION Intent来监视已连接Wi-Fi网络的当前信号强度。
    • EXTRA_NEW_RSSI 保存了当前的信号强度,可以通过使用Wi-Fi Manager的calculateSignalLevel静态方法,以便按照你指定的范围将该信号强度转换成一个整型数值。

监视活动的Wi-Fi连接的相信信息

当建立了一个活动的Wi-Fi连接,就可以使用Wi-Fi Manager的getConnectionInfo方法找出连接的状态信息。所返回的WiFiInfo对象包含当前接入点的SSID、BSSID、Mac地址、IP地址,以及当前的链路速度和信号强度。

WifiInfo info = wifi.getConnectionInfo();
if (info.getBSSID() != null) {
    int strength = WifiManager.calculateSingalLevel(info.getRssi(),5);
    int speed = info.getLinkSpeed();
    String units = WifiInfo.LINK_SPEED_UNITS;
    String ssid = info.getSSID();
    String cSummary = String.format("Connected to %s at %s%s. Strength %s/5",ssid,speed,units,strength);
    Log.d(TAG,cSummary);
}

扫描热点

可以使用Wi-Fi Manager的startScan方法进行接入点扫描。一个带有SCAN_RESULTS_AVAILABLE_ACTION动作的Intent将被广播以便异步宣布扫描完成并且结果可用。

//注册用于监听扫描结果的BroadcastReceiver
registerReceiver(new BroadcastReceiver() {
    @Override
    public void onReceive(Context context,Intent intent) {
        List<ScanResult> results = wifi.getScanResults();
        ScanResult bestSingal = null;
        for (ScanResult result : results) {
            if (bestSignal == null || WifiManager.compareSignalLevel(bestSignal.level,result.level) < 0) {
                bestSignal = result;
            }
        }
        
        String connSummary = String.format("%s networks found. %s is the strongest.",results.size(),bestSignal.SSID);
        Toast.makeText(MyActivity.this,connSummary,Toast.LENGTH_LONG).show();
    }
},new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION));

//开始扫描
wifi.startScan();

管理Wi-Fi配置

可以使用Wi-Fi Manager管理已配置的网络设置并控制将要连接到哪个网络。一旦连接建立,就可以通过查询可用网络连接来获得其配置和设置的更多详细信息。

  • getConfiguredNetworks 可以获得当前网络配置的列表。返回的WifiConfiguration对象列表包含每个配置的网络ID、SSID和其他详细信息
  • enableNetwork 使用某个特定的网络配置
//获得可用配置的一个列表
List<WifiConfiguration> configurations = wifi.getConfiguredNetworks();
//获得第一个配置的网络ID
if (configurations.size() > 0) {
    int netID = configurations.get(0).networkId;
    //启用网络
    boolean disableAllOthers = true;
    wifi.enableNetwork(netID,disableAllOthers);
}

创建Wi-Fi网络配置

为了连接到一个Wi-Fi网络,需要创建并注册一个配置。一般,用户将使用本地Wi-Fi配置设置进行该操作,但是也可以在自己的应用程序中提供相同的功能。网络配置作为WifiConfiguration对象进行存储。

  • BSSID 每个接入点的BSSID
  • SSID 特定网络的SSID
  • networkId 用于在当前设备上标识这个网络配置的唯一标识符
  • priority 当对要连接到的潜在接入点的列表进行排序时用到的网络配置优先级
  • status 该网络连接的当前状态。

addNetwork方法可以指定一个新的配置并将其添加到当前列表中。updateNetworks可以传入只包含一个网络ID和想要更改的值得WifiConfiguration对象来更新网络配置。

使用Wi-Fi Direct传输数据

Wi-Fi Direct是一种通信协议,用于中等距离、高带宽的点对点通信。与蓝牙技术相比,Wi-Fi Direct更加快速可靠,而且工作距离更远。特别适合媒体共享和接收实时媒体流等操作。

初始化Wi-Fi Direct框架

为使用Wi-Fi Direct,必须添加权限。

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>

Wi-Fi Direct连接是使用Wifi2pManager系统服务建立和管理的。

private Wifi2pManager wifiP2pManager;
private Channel wifiDirectChannel;

private void initializeWiFiDirect() {
    wifiP2pManager = (Wifi2pManager)getSystemService(Context.WIFI_P2P_SERVICE);
    wifiDirectChannel = wifiP2pManager.initialize(this,getMainLooper(),new ChannelListener() {
        public void onChannelDisconnected() {
            initializeWiFiDirect();
        }
    })
}

将会使用这个通道与Wi-Fi Direct框架进行交互,因此WiFi P2P Manager的初始化操作通常是在Activity的onCreate处理程序内完成的。使用WiFi P2P Manager执行的大多数动作会使用一个ActionListener立即指出它们是否成功。

private ActionListener actionListener = new ActionListener() {
    public void onFailure(int reason) {
        String errorMessage = "WiFi Direct Failed.";
        switch(reason) {
            case WifiP2pManager.BUSY:
                errorMessage += "Framework busy.";
                break;
            case WifiP2pManager.ERROR:
                errorMessage += "Framework error.";
                break;
            case WifiP2pManager.P2P_UNSUPPORTED:
                errorMessage += "Unsupported.";
                break;
            default:
                errorMessage += "Unknown error.";
                break;
        }
    }
    
    public void onSuccess() {
        //成功,返回值通过一个Broadcast Intent返回
    }
}

启用Wi-Fi Direct并监视其状态

为使一个Android是被能够发现其他Wi-Fi Direct设备或被其他Wi-Fi Direct设备发现,用户首先必须启用Wi-Fi Direct。

startActivity(new Intent(android.provider.Settings.ACTION_WIRELES_SETTINGS));

只有建立连接并传输数据时,Wi-Fi Direct才回一直保持启用状态。如果短时间不用,它就会自动禁用。只有设备上启用了Wi-Fi Direct时,才能够执行Wi-Fi Direct操作。因此,监听Wi-Fi Direct的状态变化,并通过修改UI来禁用不可行操作非常重要。

BroadcastReceiver p2pStatusReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context,Intent intent) {
        int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE,WifiP2pManager.WIFI_P2P_STATE_DISABLED);
        
        switch(state) {
            case (WifiP2pManager.WIFI_P2P_STATE_ENABLED):
                buttonDiscover.setEnabled(true);
                break;
            default:
                buttonDiscover.setEnabled(false);
        }
    }
}

在创建了连接到Wi-Fi Direct框架的通道并启用设备及其对等设备上的Wi-Fi Direct后,就可以开始搜索和连接对等设备。

发现对等设备

为扫描对等设备,需要调用WiFi P2P Manager的discoverPeers方法,并传入一个处于活动状态的通道和一个Action Listener。对等设备列表的变化将作为一个Intent,通过使用WifiP2pManager,WIFI_P2P_PEERS_CHANGED_ACTION 动作广播出去。在建立一个连接或者创建一个组之前,对等设备的搜索过程会一直进行。

private void discoverPeers() {
    wifiP2pManager.discoverPeers(wifiDirectChannel,actionListener);
}

BroadcastReceiver peerDiscoveryReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context,Intent intent) {
        wifiP2pManager.requestPeers(wifiDirectChannel,new PeerListListener() {
            public void onPeersAvailable(WifiP2pDeveiceList peers) {
                deviceList.clear();
                deviceList.addAll(peers.getDeviceList());
                aa.notifyDataSetChanged();
            }
        })
    }
}

连接对等设备

为了与对等设备建立Wi-Fi Direct连接,需要使用WiFi P2P Manager的connect方法,并传入活动的通道,一个Action Listener以及一个指定了要连接的对等设备的地址的WifiP2pConfig对象

private void connectTo(WifiP2pDevice device) {
    WifiP2pConfig config = new WifiP2pConfig();
    config.deviceAddress = device.deviceAddress();
    wifiP2pManager.connect(wifiDirectChannel,config,actionListener);
}

当尝试建立一个连接时,远程设备就会被提示接受连接请求。如果远程设备接受了建立连接的请求,成功的连接将使用WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION Intent动作在两个设备上广播。

BroadcastReceiver connectionChangedReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context,Intent intent) {
        //提取NetworkInfo
        NetworkInfo networkInfo = (NetworkInfo)intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);
        
        //检查是否已经连接
        if (networkInfo.isConnected()) {
            wifiP2pManager.requestConnectionInfo(wifiDirectChannel,new ConnectionInfoListener() {
                public void onConnectionInfoAvailable(WifiP2pInfo info) {
                    //如果建立了连接
                    if (info.groupFormed) {
                        //如果这个设备是服务器
                        if (info.isGroupOwner) {
                            //启动Server Socket
                        }
                        //如果这个设备是客户机
                        else if (info.groupFormed) {
                            //启动Client Socket
                        }
                    }
                }
            });
        } else {
            Log.d(TAG, "Wi-Fi Direct Disconnected");
        }
    }
}

当连接信息可用时,ConnectionInfoListener会触发其onConnectionInfoAvailable处理程序,并传入一个包含这些详细信息的WifiP2pInfo对象。

辅助

Linkify

Linkify会自动地在TextView类中通过RegEx模式匹配来创建超链接。那些匹配一个指定的RegEx模式的文本都将会被转化为一个可以单击的超链接,这些超链接可以隐式使用匹配的文本作为目标URI来出发startActivity(new Intent(Intent.ACTION_VIEW,uri))

原生Linkify链接类型

添加 WEB_URLS、EMAIL_ADDRESSES、PHONE_NUMBERS和ALL 预设值

TextView textView = findViewById(R.id.myTextView);
Linkify.addLinks(textView,Linkify.WEB_URLS|Linkify.EMAIL_ADDRESSES);

xml 文件中autoLink属性包含(none、web、email、phone、all)

创建定制的链接字符串

传入RegEx模式

Pattern p = Pattern.compile("\\bquake[\\s]?[0-9]+\\b",Pattern.CASE_INSENSITIVE);
Linkify.addLinks(myTextView,p,baseUri);

使用Match FilterTransform Filter,通过实现Match Filter 的 acceptMatch 方法来向RegEx模式匹配添加额外的条件。Transform Filter允许修改匹配的链接文本生成的隐式URI。把链接文本和目标URI分开,你能够更自由地决定如何把数据字符串显示给用户。

Linkify.addLinks(myTextView,p,baseUri,new MyMatchFilter(),new MyTransformFilter());

class MyMatchFilter implements MatchFilter {
    public boolean acceptMatch(CharSequence s,int start,int end) {
        return (start == 0 || s.charAt(start - 1) != '!');
    }
}

class MyTransformFilter implements TransformFilter {
    public String transformUrl(Matcher match,String url) {
        return url.toLowerCase().replace(" ","");
    }
}

全屏显示

要想控制手机上导航栏的可见性或者平板电脑上系统栏的外观,可以对Activity层次结构中任何可见的View使用setSystemUiVisibility方法。

  • SYSTEM_UI_FLAG_LOW_PROFILE 和 STATUS_BAR_HIDDEN 一样会遮挡导航按钮。
  • SYSTEM_UI_FLAG_HIDE_NAVIGATION 在手机上移除导航栏,并遮挡平板电脑的系统栏中使用的导航按钮。

当导航的可见性变化时,最好能够和UI中其他变化同步。例如,在进入或者退出”全屏模式“时,可能需要隐藏或者显示操作栏和其他导航操作。

myView.setOnSystemUiVisibilityChangeListener (
    new OnSystemUiVisibilityChangeListener() {
        public void onSystemUiVisibilityChange(int visibility) [
            if (visibility == View.SYSTEM_UI_FLAG_VISIBLE) {
                //显示操作栏和状态栏
            } else {
                //隐藏操作栏和状态栏
            }
        }
    }
)

要想隐藏状态栏,可以向Window中添加LayoutParams.FLAG_FULLSCREEN标志

myView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);

为每个屏幕尺寸和分辨率做设计

当使用不能很好地动态缩放的Drawable资源时,应该创建和包含针对每种像素密度类别进行优化的图像资源

  • res/drawable-ldpi 为120dpi左右的屏幕提供低密度资源
  • res/drawable-mdpi 为160dpi左右的屏幕提供中密度资源
  • res/drawable-hdpi 为240dpi左右的屏幕提供高密度资源
  • res/drawable-xhdpi 为320dpi左右的屏幕提供超高密度资源
  • res/drawable-nodpi 用于不管宿主屏幕密度如何都不能缩放的资源

  • 宽屏优化

    • long 宽屏修饰符
    • notlong 非宽屏修饰符
  • 横竖屏优化

    • land 横屏修饰符
    • port 竖屏修饰符
  • 宽度优化

    • w600dp 宽度修饰符
    • h720dp 高度修饰符
    • sw320dp 最小的可用屏幕宽度
  • 设备大小

    • small 小
    • normal 普通
    • large 大
    • xlarge 超大

指定支持的屏幕尺寸

对于一些应用程序,可能无法通过优化UI来使其支持所有可能的屏幕尺寸。可以通过在清单文件中使用supports-screens元素来指定应用程序可以运行在哪些屏幕上

<supports-screens android:smallScreens="false"
    android:normalScreens="true"
    android:largeScreens="true"
    android:xlargeScreens="true">

复制、粘贴和剪贴板

ClipboardManager clipboard = (ClipboardManager)getSystemService(CLIPBOARD_SERVICE);

剪贴板支持文本字符串、URI和Intent。要想向剪贴板复制一个对象,可以创建一个新的ClipData对象,它包括一个描述了与待复制对象相关的元数据的ClipDescription、任意数量的ClipData.Item对象。使用setPrimaryClip方法把ClipData添加到剪贴板上。

clipboard.setPrimaryClip(newClip);

在任意时刻,剪贴板中只能包含一个ClipData对象。复制一个新的对象会替换之前持有的剪贴板对象。

向剪贴板中复制数据

ClipData类提供了大量方便的静态方法来简化一个标准的ClipData对象的创建过程。使用newPlainText方法创建一个新的ClipData对象。或使用newUri方法,指定一个Content Resolver、标签和待粘贴数据的URI。

ClipData newClip = ClipData.newPlainText("copied text","Hello, Android!");

ClipData newClip = ClipData.newUri(getContentResolver(),"URI",myUri);

粘贴剪贴板数据

可以判断剪贴板上是否已经复制了数据,从而在UI上启用和禁用粘贴选项。

if (!(clipboard.hasPrimaryClip())){}

当然,还可以查询当前剪贴板中的Clip Data对象的数据类型。使用getPrimaryClipDescription方法获得剪贴板数据中的元数据,并使用它的hasMimeTypes方法指定应用程序粘贴所支持的MIME类型:

if (!(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN))) {
    //如果剪贴板中的内容是一个不支持的类型,就禁用粘贴UI选项
} else {
    //如果剪贴板中的内容是一个支持的类型,就启用粘贴UI选项
}

要想访问数据本身,可以使用getItemAt方法,传入你要遍历的项的索引值

ClipDate.Item item = clipboard.getPrimaryCclip().getItemAt(0);

通过分别使用getText、getUri和getIntent方法,可以获取文本、URI和Intent。使用coerceToText方法,可以将ClipData.Item对象的内容转化为一个字符串。CharSequence pasteText = item.coerceToText(this);

使用Wake Lock

为了延长电池的使用寿命,Android设备会在闲置一段时间后使屏幕变暗,然后关闭屏幕显示,最后停止CPU。WakeLock是一个电源管理系统服务功能,应用程序可以使用它来控制主机设备的电源状态。可以保持CPU运行,避免屏幕变暗和关闭,以及避免键盘背光灯熄灭。但是它会显著影响电池寿命,所以在创建它之前,需要请求权限

<uses-permission android:name="android.permission.WAKE_LOCK"/>

为创建一个WakeLock,需要调用Power Manager的newWakeLock方法,并指定下面一种WakeLock类型

  • FULL_WAKE_LOCK 保持屏幕最大亮度、键盘背光灯点亮以及CPU运行
  • SCREEN_BRIGHT_WAKE_LOCK 保持屏幕最大亮度和CPU运行
  • SCREEN_DIM_WAKE_LOCK 保持屏幕亮起和CPU运行(通常用于在用户观看屏幕但是很少与屏幕进行交互期间防止屏幕变暗)
  • PARTIAL_WAKE_LOCK 保持CPU运行

创建WakeLock后,可以通过acquire来获取它。可以有选择地指定一个超时值来确保将在尽可能长时间内保持使用Wake Lock。动作完成后,需要调用release来让系统管理电源状态。

WakeLock wakeLock;
private class MyAsyncTask extends AsyncTask<Void,Void,Void> {
    @Override
    protected Void doInBackground(Void... parameters) {
        PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
        wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"MyWakeLock");
        wakeLock.acquire();
        
        //Do things in the background
        
        return null;
    }
    
    @Override
    protected void onPostExecute(Void parameters) {
        wakeLock.release();
    }
}

参考资料

《Android4 编程入门经典》