Android5.x新API介绍(二)

Android5.x新API介绍(二)

作者:李旺成
时间:2016年4月9日


接上篇 Android5.x新API介绍(一)

八、MediaProjection

在 Android 5.0 之前做屏幕截图,首先需要获取 root 权限(当然说的截取包括状态栏在内,任意界面的屏幕,就如同按下截图快捷键那种)。现在不用那么辛苦了,在 Android 5.0 中提供了新的接口 android.media.projection,使用该包下的一些接口,无需 root 即可截屏。看类图:

android.media.projection 包

MediaProjectionManager 类

查询官方文档可以发现,这个接口主要用来“屏幕截图”操作和“音频录制”操作。由于这里使用的是媒体映射技术手段,所以截取的屏幕不是真正的设备屏幕,而是截取通过映射出来的“虚拟屏幕”。当然,这不影响效果啦!我们截图不就是要得到一张完整的屏幕图片,而这个“映射”出来的图片与系统屏幕完全一致,所以,对于普通截屏,用这个完全可以胜任了。

看效果图:

截屏功能开启

截屏效果图

MediaProjection 使用步骤

  1. 获取 MediaProjectionManager 类实例*

    1
    2
    mMediaProjectionManager = (MediaProjectionManager)getApplication()
    .getSystemService(Context.MEDIA_PROJECTION_SERVICE);
  2. 发出截屏意图

    1
    2
    3
    startActivityForResult(
    mMediaProjectionManager.createScreenCaptureIntent(),
    REQUEST_MEDIA_PROJECTION);
  3. 得到返回结果后获取 MediaProjection

    1
    2
    mMediaProjection = mMediaProjectionManager
    .getMediaProjection(resultCode, resultData);
  4. 创建虚拟屏幕

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private void createVirtualDisplay() {
    mVirtualDisplay = mMediaProjection.createVirtualDisplay(
    "screen-mirror",
    mWindowWidth,
    mWindowHeight,
    mScreenDensity,
    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
    mImageReader.getSurface(),
    null,
    null);
    }
  5. 获取屏幕图片

    1
    2
    3
    Image image = mImageReader.acquireLatestImage();
    // 处理成 Bitmap,存到 SD 卡中
    ...

具体实例

这里参考了路上的脚印的一个 Demo —— 分享一种截屏方法。在我的 Demo 中直接使用该示例的代码,只是修改了一下命名,在这先表示下感谢。下面大致分析下,该截屏 Demo 的思路,首先看下整体流程:

截屏流程示意图

截屏 Demo 解析
这里面有个思路值得赞赏:通过共享类给 Service 提供实现截屏所需要的,截屏请求所返回的数据(我这里说的有点抽象,有点拗口,其实就上面流程图所示,具体可参考源码或者查看 分享一种截屏方法 一文)
1. 创建一个 Application —— DIYApp.java 用来共享数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DIYApp extends Application {

private int mResult;
private Intent mIntent;
private MediaProjectionManager mMediaProjectionManager;

public int getmResult(){
return mResult;
}

public Intent getmIntent(){
return mIntent;
}
// 就是一些 Getter / Setter 方法
}

不要忘记了,得在 Manifest.xml 文件中注册啊!
2. 创建一个 Activity —— MediaProjectionActivity.java 用来发起请求

1
2
3
4
5
6
7
8
9
10
// onCreate() 方法
// 获取 MediaProjectionManager 实例
mMediaProjectionManager = (MediaProjectionManager) getApplication()
.getSystemService(Context.MEDIA_PROJECTION_SERVICE);

// 利用MediaProjectionManager 的 createScreenCaptureIntent() 方法生成intent,发起截屏请求
startActivityForResult(
mMediaProjectionManager.createScreenCaptureIntent(),
REQUEST_MEDIA_PROJECTION);
((DIYApp) getApplication()).setMediaProjectionManager(mMediaProjectionManager);

3. 创建一个 Service —— CaptureScreenService.java
先创建出来,其作用就是完成真正的截屏功能,稍后讲解。
不要忘记了,得在 Manifest.xml 文件中注册啊!
4. 在 onActivityResult() 中 获取并处理返回结果

1
2
3
4
5
6
7
8
9
10
// 缓存返回结果
result = resultCode;
intent = data;
((DIYApp) getApplication()).setmResult(resultCode);
((DIYApp) getApplication()).setmIntent(data);
// 启动截图使用的 Service
Intent intent = new Intent(
getApplicationContext(),
CaptureScreenService.class);
startService(intent);

5. 在 Service 中创建虚拟环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void createVirtualEnvironment() {
mDateFormat = new SimpleDateFormat("yyyy_MM_dd_hh_mm_ss");
mDateStr = mDateFormat.format(new java.util.Date());
mImagePath = Environment.getExternalStorageDirectory().getPath() + "/Pictures/";
mImageName = mImagePath + mDateStr + ".png";
sMediaProjectionManager = (MediaProjectionManager) getApplication().getSystemService(Context.MEDIA_PROJECTION_SERVICE);
WindowManager windowManager = (WindowManager) getApplication().getSystemService(Context.WINDOW_SERVICE);
mWindowWidth = windowManager.getDefaultDisplay().getWidth();
mWindowHeight = windowManager.getDefaultDisplay().getHeight();
DisplayMetrics metrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(metrics);
mScreenDensity = metrics.densityDpi;
mImageReader = ImageReader.newInstance(mWindowWidth, mWindowHeight, 0x1, 2); //ImageFormat.RGB_565
Log.i(TAG, "prepared the virtual environment");
}

6. 在 Service 中获取虚拟屏幕

1
2
3
4
5
6
7
8
9
10
private void createVirtualDisplay() {
mVirtualDisplay = mMediaProjection.createVirtualDisplay(
"screen-mirror",
mWindowWidth,
mWindowHeight,
mScreenDensity,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mImageReader.getSurface(), null, null);
Log.i(TAG, "virtual displayed");
}

7. 获取截图,转化后保存截取的屏幕数据到文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private void startCapture() {
mDateStr = mDateFormat.format(new java.util.Date());
mImageName = mImagePath + mDateStr + ".png";

Image image = mImageReader.acquireLatestImage();
int width = image.getWidth();
int height = image.getHeight();
final Image.Plane[] planes = image.getPlanes();
final ByteBuffer buffer = planes[0].getBuffer();
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPadding = rowStride - pixelStride * width;
Bitmap bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(buffer);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height);
image.close();
Log.i(TAG, "image data captured");

if (bitmap != null) {
try {
File fileImage = new File(mImageName);
if (!fileImage.exists()) {
fileImage.createNewFile();
Log.i(TAG, "image file created");
}
FileOutputStream out = new FileOutputStream(fileImage);
if (out != null) {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
out.flush();
out.close();
Intent media = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Uri contentUri = Uri.fromFile(fileImage);
media.setData(contentUri);
this.sendBroadcast(media);
Log.i(TAG, "screen image saved");
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

8. 使用说明
启动后会生成 一个浮动小图标,该图标为截屏按键。在任意界面点击该图标,即可以截取当前屏幕。
注意:截屏图像保存为 PNG 格式,存储路径为手机 sdcard 的 Pictures 文件夹。
点击下载原始 Demo

好了,关于截屏到这里了。

九、JobScheduler

Android 5.0 提供了一个新的 JobScheduler API,它允许你通过为系统定义要在以后的某个时间或在指定的条件下(例如,当设备在充电时)异步运行的作业来优化电池寿命。

使用场景

作业调度在下列情况下非常有用:

  • 应用具有您可以推迟的非面向用户的工作。
  • 应用具有当插入设备时您希望优先执行的工作。
  • 应用具有需要访问网络或 Wi-Fi 连接的任务。
  • 应用具有您希望作为一个批次定期运行的许多任务。

当一系列预置的条件被满足时,JobScheduler API 为你的应用执行一个操作。JobScheduler 与 AlarmManager 不同的是这个执行时间是不确定的。除此之外,JobScheduler API 允许同时执行多个任务。这允许你的应用执行某些指定的任务时不需要考虑时机控制引起的电池消耗。(引自:在Android 5.0中使用 JobScheduler

JobScheduler 简介

先来看一下 JobScheduler 所在的包,以及几个关键的类:

Job 包

JobService 类

JobInfo 类

JobInfo.Builder 类

Android 文档上介绍的比较详细,这里就不翻译文档了,以介绍如何使用为主。

简单使用

先看一个简单的演示,效果如下图:
简单演示

1. 在 AndroidStudio 中创建 API 最低为21 的项目

创建项目

2. 创建 JobService

这里仅为了演示,所以一切以最简单的方式。新建一个继承自 JobService 的 Service。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class JobSchedulerSimpleService extends JobService {

private static final String TAG = "JobSchedulerService";

private Handler mJobHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
// 模拟用 Handlder 来执行在 JobSchedulerService 中定义的任务(耗时操作)
Toast.makeText(getApplicationContext(), "JobSchedulerService task running", Toast.LENGTH_SHORT).show();
jobFinished((JobParameters) msg.obj, false);
return true;
}
});

// 需要注意的是这个job service运行在主线程
public JobSchedulerSimpleService() {
}

@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "JobSchedulerService created");
}

// 当任务开始时会执行onStartJob(JobParameters params)方法
@Override
public boolean onStartJob(JobParameters params) {
mJobHandler.sendMessage(Message.obtain(mJobHandler, 1, params));
// 返回true,表示让系统知道你会手动地调用jobFinished
return true;
}

/*
当系统接收到一个取消请求时,
系统会调用onStopJob(JobParameters params)方法取消正在等待执行的任务
*/

@Override
public boolean onStopJob(JobParameters params) {
mJobHandler.removeMessages(1);
// 可以试一下这里分别返回 true 与 false 的区别
return false;
}

@Override
public void onDestroy() {
super.onDestroy();
Log.i(TAG, "JobSchedulerService destroyed");
}

}

说明:在示例 Demo 中的代码有比较详细的注释,这里为减少篇幅,进行了删减。(示例参考自:Using the JobScheduler API on Android Lollipop

3. 在 Manifest 中注册 JobService

这里有个比较关键的地方,那就是必须添加权限,否则报错。代码如下:

1
2
3
4
5
<service
android:name=".newapi2.JobSchedulerSimpleService"
android:enabled="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<!-- 一定不要忘记添加权限 -->

4. 创建 JobScheduler

JobService 已准备妥当,剩下的就是和 JobScheduler API 交互了。这里我们需要创建一个 JobScheduler 对象,直接看代码:

1
// 获取 JobScheduler 对象mJobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);

5. 封装任务并执行

JobScheduler 的定时任务都是使用 JobInfo 对象封装的,而 JobInfo 是通过 JobInfo.Builder 来构建的(以后会看到越来越多的 Builder 的)。该 JobInfo 创建好之后就可以交给 Service 去执行了。看代码:

1
2
3
4
5
6
7
8
9
10
11
12
// JobInfo.Builder接收两个参数,
// 第一个参数是你要运行的任务的标识符,
// 第二个是这个Service组件的类名。
JobInfo.Builder builder = new JobInfo.Builder(
1,
new ComponentName(
getPackageName(),
JobSchedulerSimpleService.class.getName()));
// builder允许你设置很多不同的选项来控制任务的执行
// 这里设置的是让任务每隔三秒运行一次
builder.setPeriodic(3000);
mJobScheduler.schedule(builder.build());

上面的代码很简单,只是演示怎么实现一个使用 Handler 对象来运行后台任务的JobService 子类,以及如何使用 JobInfo.Builder 来设置 JobService。尝试使用一下,你可以在减少资源消耗的同时提升应用的效率。当然还有很多地方没有涉及到,好在 Google 给了一个 Simple Demo,已经集成在示例代码里面的,用到的时候大家可以参考下,运行效果如下图:

Google Simple JobScheduler 演示

JobScheduler 就介绍到这里了,这里稍微提一下,老版本上可以试试 Trigger

十、Heads-Up 风格通知

Android 5.0 中提供了个通知新样式 Heads-Up,现在Android 也有推送卡片了。可以去看看 Google 的介绍,低版本的手机可以使用 HeadsUp提醒 APP 来尝试下。

Android 5.0 对通知的改动还是挺大的,主要有如下两个方面。

锁定屏幕通知

Android 5.0 中的锁定屏幕能够呈现通知。用户可以通过“设置”来选择是否允许在安全的锁定屏幕上显示敏感的通知内容。

应用可以控制通知在安全的锁定屏幕上显示时的具体公开程度。要控制公开程度的级别,可以调用 setVisibility())
并指定下列值之一:
VISIBILITY_PRIVATE
:显示基本信息(例如通知图标),但隐藏通知的全部内容。
VISIBILITY_PUBLIC
:显示通知的全部内容。
VISIBILITY_SECRET
:不显示任何内容,甚至连通知图标也不显示。

如果公开程度级别为 VISIBILITY_PRIVATE
,还可以提供隐藏了个人详细信息的通知内容修改版本。例如,短信应用的通知可能会显示“您有 3 条新短信”,但隐藏短信内容和发送者。要提供此备用通知,请先使用 Notification.Builder
创建替代通知。当创建不公开的通知对象时,可以通过 setPublicVersion())
方法为其附加替代通知。

通知元数据

Android 5.0 使用与应用通知关联的元数据更智能地对通知进行排序。要设置元数据,需要在构建通知时调用 Notification.Builder 中的以下方法:

setCategory()):告诉系统当设备处于“优先”**模式时如何处理您的应用通知(例如,当通知表示来电、即时消息或警报时)。

setPriority()):将通知标记为重要性高于或低于普通通知。如果还带有声音或振动,则优先级字段设置为 PRIORITY_MAXPRIORITY_HIGH 的通知将出现在一个小的浮动窗口中。

addPerson()):允许向通知添加一个或多个相关的人员。利用此方法,应用可指示系统将来自指定人员的通知归成一组,或者将来自这些人员的通知归类为重要性高于普通通知。

简单使用

先看下效果:

各手机上的新通知

上图所用的手机都是 Android 6.0 的系统(基本上都是在 Andoird 6.0 上定制的),可以看到很多定制过的 ROM 对 Heads-Up 的支持有很大差别。

在 Support V4 包和 V7 包中都提供了 NotificationCompat,这里就不探讨这些,直接看看怎么使用,看代码:

1
2
3
4
5
6
7
8
9
Notification notification = new NotificationCompat.Builder(this)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setSmallIcon(R.mipmap.ic_logo)
.setFullScreenIntent(pendingIntent, false)
.setContentTitle("这是标题")
.setContentText("这是内容")
.addAction(R.mipmap.ic_logo, "菜单1", pendingIntent1)
.build();
notificationManager.notify(1, notification);

关于通知就介绍到这里。

十一、PdfRenderer

PdfRenderer —— 使用位图来呈现 PDF 文件。

PdfRenderer 简介

Andorid 5.0 中可以使用新的 PdfRenderer 类将 PDF 文档页呈现为位图图片以便展示或打印。必须指定系统将可打印内容写入其中的一个可查找的(也就是说,可以随机访问内容)ParcelFileDescriptor

应用可以通过 openPage()) 获取页面进行呈现,然后调用 render()) 将已打开的 PdfRenderer.Page 转变为位图。如果只希望将文档的一部分转变为位图图片(例如,要实施平铺渲染以放大文档),则还可以设置其他参数。

看一下 Andorid 5 添加的 pdf 相关 API:

Pdf 包

PdfRenderer 类文档

PdfRenderer 类

PdfRenderer 简单使用

先看效果图:

PdfRenderer 演示

  1. 打开 PDF 文件,获取 PdfRenderer 实例,看代码:

    1
    2
    3
    4
    5
    6
    private void openRenderer(Context context) throws IOException {
    // In this sample, we read a PDF from the assets directory.
    mFileDescriptor = context.getAssets().openFd("sample.pdf").getParcelFileDescriptor();
    // This is the PdfRenderer we use to render the PDF.
    mPdfRenderer = new PdfRenderer(mFileDescriptor);
    }
  2. 确定显示第几页

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    private void showPage(int index) {
    if (mPdfRenderer == null) return;
    if (mPdfRenderer.getPageCount() <= index) {
    return;
    }
    // Make sure to close the current page before opening another one.
    if (null != mCurrentPage) {
    mCurrentPage.close();
    }
    // Use `openPage` to open a specific page in PDF.
    mCurrentPage = mPdfRenderer.openPage(index);
    // Important: the destination bitmap must be ARGB (not RGB).
    Bitmap bitmap = Bitmap.createBitmap(mCurrentPage.getWidth(), mCurrentPage.getHeight(),
    Bitmap.Config.ARGB_8888);
    // Here, we render the page onto the Bitmap.
    // To render a portion of the page, use the second and third parameter. Pass nulls to get
    // the default result.
    // Pass either RENDER_MODE_FOR_DISPLAY or RENDER_MODE_FOR_PRINT for the last parameter.
    mCurrentPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
    // We are ready to show the Bitmap to user.
    mImageView.setImageBitmap(bitmap);
    }
  3. 退出的时候不要忘记关闭

    1
    2
    3
    4
    5
    6
    7
    8
    private void closeRenderer() throws IOException {
    if (null != mCurrentPage) {
    mCurrentPage.close();
    }
    if (mPdfRenderer == null) return;
    mPdfRenderer.close();
    mFileDescriptor.close();
    }

说明:上面的代码来自 Google Simple,直接使用 Google 提供的 Demo 运行没有问题,但是继承到现在这示例项目中的时候会出现文件读取失败的问题,目前还没有找到原因。鉴于上述原因,这里提供一下 Google 的示例程序,有需要的可以使用这个项目: PdfRendererBasic

好了,PdfRenderer 就简单的介绍到这了。

十二、android.app.usage API

这是 Android 5.0 提供的获取应用使用情况统计信息的 API。在此之前 Android 本身有 PkgUsageStats 等相关类来统计应用使用情况,但这些类在 SDK 不公开,只能通过反射或者在源码环境下才能访问到。

android.app.usage 简介

可以通过新的 android.app.usage API 访问 Android 设备上的应用使用情况历史记录。此 API 提供了比被弃用的 getRecentTasks() 方法更详细的使用情况信息。要使用此 API,必须先在清单文件中声明 “android.permission.PACKAGE_USAGE_STATS” 权限。用户还必须通过“设置”>“安全性”>“应用”使用“使用情况访问”启用对此应用的访问权限。

系统将以每个应用为单位收集使用情况数据,并按每天、每周、每月和每年时间间隔对数据进行汇总。系统保留此数据的最大持续时间如下所述:

每天数据:7 天每周数据:4 周每月数据:6 个月每年数据:2 年

对于每个应用,系统将记录以下数据:
上次使用应用的时间应用在该时间间隔内(按天、周、月或年)处于前台的总时间长度组件(由程序包和活动名称予以标识)在一天中移动到前台或后台时的时间戳捕获设备配置更改时(例如当设备配置因为旋转而更改时)的时间戳捕获。
(上述内容参考自:知乎用户

看一下官方文档的介绍:

android.app.usage 包

UsageStatsManager 类

UsageStats 类

UsageEvents 类

android.app.usage 简单使用

先看一下运行效果:

UsageStats 演示

UsageStats Log 输出

简单说下使用步骤:
1. 获取 UsageStatsManager 实例

1
2
3
4
private static UsageStatsManager getUsageStatsManager(Context context){
UsageStatsManager usm = (UsageStatsManager) context.getSystemService("usagestats");
return usm;
}

2. 设置查询时间获取查询结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Calendar calendar = Calendar.getInstance();
long endTime = calendar.getTimeInMillis();
calendar.add(Calendar.YEAR, -1);
long startTime = calendar.getTimeInMillis();

Log.d(TAG, "Range start:" + dateFormat.format(startTime));
Log.d(TAG, "Range end:" + dateFormat.format(endTime));

List<UsageStats> usageStatsList = usm.queryUsageStats(
UsageStatsManager.INTERVAL_DAILY,
startTime,
endTime);

// 或者
UsageEvents uEvents = usm.queryEvents(startTime,endTime);
while (uEvents.hasNextEvent()){
UsageEvents.Event e = new UsageEvents.Event();
uEvents.getNextEvent(e);
if (e != null){
Log.d(TAG, "Event: " + e.getPackageName() + "\t" + e.getTimeStamp());
}
}

3. 展示查询结果

1
2
3
4
5
6
7
8
9
private void printUsageStats(List<UsageStats> usageStatsList) {
StringBuilder sb = new StringBuilder();
for (UsageStats u : usageStatsList){
sb.append("Pkg: " + u.getPackageName() + "\t" + "ForegroundTime: "
+ u.getTotalTimeInForeground());
Log.d(TAG, "Pkg: " + u.getPackageName() + "\t" + "ForegroundTime: "
+ u.getTotalTimeInForeground()) ;
}
}

上面的演示代码参考了一个开源项目 UsageStatsSample,在这里对作者 ColeMurray 表示感谢了。

注意:
这里有几个要注意的地方:

  1. 不要忘记添加权限

    1
    2
    3
    <uses-permission
    android:name="android.permission.PACKAGE_USAGE_STATS"
    tools:ignore="ProtectedPermissions" />
  2. 检测是否给我们的 App 打开了权限

    1
    2
    3
    4
    if (UStats.getUsageStatsList(this).isEmpty()){
    Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
    startActivity(intent);
    }

好了,关于 usage 的介绍就到这里了。

小结

历时两天,总算是勉强弄完了,前后两篇都只是简要的介绍了 Android 5 上的新 API。由于内容实在是有点多,所以只挑了几个重点的来演示。介绍的也很简略,只是简单地演示了基本使用方法,有很多不足的地方,请见谅。

这一段开始很忙了,所以更新会慢下来,但是这个系列一定会坚持写完的,下一篇开始介绍 Android 5 出的新控件,这个没这么枯燥了。

好了,附上 Demo 地址,该项目会集成到 AndroidStudyDemo 项目中去,你可以根据自己的需要选择。
GitHub

参考

Android 5.0 API新增和改进
Android 5.0的调度作业JobScheduler
Google最新截屏案例详解
关于Android5.0以上屏幕截图探索总结
Android实战技巧之三十三:android.hardware.camera2使用指南
分享一种截屏方法
在Android 5.0中使用JobScheduler
Using the JobScheduler API on Android Lollipop
Android Job框架–Trigger
Android 状态栏通知Notification、NotificationManager详解
android Heads-Up风格通知

坚持原创技术分享,您的支持将鼓励我继续创作!