之前我们实现了拦截短信和拦截电话,现在需要完成删除通话记录的功能。通话记录的数据库一般放置在data/data/com.android.providers.contacts/contacts2.db
,打开该数据库中的calls
表就是记录通话记录的。
该功能需要按照以下步骤进行实现:
content://call_log/calls
;calls
表中有一个名为number
的字段,可以作为删除的依据。修改BlackNumberService,修改endCall()方法,添加内容解析器的相关逻辑,并且完善删除功能,代码如下:
/**
* 挂断电话的方法
* @param phoneNumber 要挂断的电话
*/
private void endCall(String phoneNumber) {
int mode = mDao.queryModeByPhone(phoneNumber);
if (mode == 2 || mode == 3){
// 拦截电话,由于ServiceManager此类Android对开发者隐藏,所以不能直接调用其方法,需要反射调用
try {
// 1.获取ServiceManger字节码文件
Class<?> clazz = Class.forName("android.os.ServiceManager");
// 2.获取反射方法
Method method = clazz.getMethod("getService", String.class);
// 3.反射调用此方法
IBinder iBinder = (IBinder) method.invoke(null,Context.TELEPHONY_SERVICE);
// 4.调用获取aidl文件对象方法
// ITelePhoney iTelePhoney = ITelePhoney.stub.asInterface(iBinder);
// 5.调用在aidl中隐藏的endcall方法
// iTelePhoney.endCall();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
// 6.删除此被拦截的电话号码的通信记录(要加读写日志权限)
getContentResolver().delete(Uri.parse("content://call_log/calls"),"number = ?",new String[]{phoneNumber});
}
}
因为这个操作涉及到系统日志的读写,所以要在清单文件中声明权限,代码如下:
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
注意,这次的删除功能,只能删除过去的通话记录,而不能删除刚刚通话时的通话记录。原因是:数据库中本次通话记录的电话号码还没有插入,就进行了删除操作。
为了解决这个问题,需要引入内容观察者的概念:即观察数据库的变化,一旦数据发生改变,就调用相应方法。
通过内容观察者,观察数据库的插入,一旦有插入,则做删除此条插入数据的操作,修改过后的endCall()方法以及自定义的ContentObserver内部类的代码如下:
/**
* 挂断电话的方法
* @param phoneNumber 要挂断的电话
*/
private void endCall(String phoneNumber) {
int mode = mDao.queryModeByPhone(phoneNumber);
if (mode == 2 || mode == 3){
// 拦截电话,由于ServiceManager此类Android对开发者隐藏,所以不能直接调用其方法,需要反射调用
try {
// 1.获取ServiceManger字节码文件
Class<?> clazz = Class.forName("android.os.ServiceManager");
// 2.获取反射方法
Method method = clazz.getMethod("getService", String.class);
// 3.反射调用此方法
IBinder iBinder = (IBinder) method.invoke(null,Context.TELEPHONY_SERVICE);
// 4.调用获取aidl文件对象方法
// ITelePhoney iTelePhoney = ITelePhoney.stub.asInterface(iBinder);
// 5.调用在aidl中隐藏的endcall方法
// iTelePhoney.endCall();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
// 6.删除此被拦截的电话号码的通信记录(要加读写日志权限)(方式一)
// getContentResolver().delete(Uri.parse("content://call_log/calls"),"number = ?",new String[]{phoneNumber});
// 6.通过内容解析器去注册内容观察者,然后观察数据库的变化(方式二)
mContentObserver = new MyContentObserver(new Handler(), phoneNumber);
getContentResolver().registerContentObserver(Uri.parse("content://call_log/calls"),true,mContentObserver);
}
}
/**
* 自定义的ContentObserver类
*/
private class MyContentObserver extends ContentObserver {
private String phoneNumber;
public MyContentObserver(Handler handler,String phoneNumber) {
super(handler);
this.phoneNumber = phoneNumber;
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
// 数据库中指定表在发生改变时会去调用的方法
getContentResolver().delete(Uri.parse("content://call_log/calls"),"number = ?",new String[]{phoneNumber});
}
}
最后,记得在onDestory()
方法中注销这个自定义的内容观察者,顺便注销掉电话状态的监听,代码如下:
@Override
public void onDestroy() {
super.onDestroy();
// 注销广播
if (mInnerSmsReceiver != null){
unregisterReceiver(mInnerSmsReceiver);
}
// 注销内容管擦着
if (mContentObserver != null){
getContentResolver().unregisterContentObserver(mContentObserver);
}
// 取消对电话状态的监听
if (mPhoneStateListener != null){
mSystemService.listen(mPhoneStateListener,PhoneStateListener.LISTEN_NONE);
}
}
前面我们完成了“通信卫士”模块中的黑名单号码管理功能,,现在需要完成另一个模块——“高级工具”中的第二个功能,即短信备份,如图中的红框所示:
点击该按钮后会弹出一个带有进度条的对话框,表明正在将短信进行备份。在系统中存放短信的数据库所对应的表为:data/data/com.android.providers.telephony/databases/mmssms.db/sms
,需要获取其中的四个字段:
address
:电话号码date
:接收/发送短信的时间type
:接收/发送,短信类型body
:短信内容跟通话记录一样,访问短信同样需要使用内容观察者,URL内容为:content://sms/
首先修改activity_a.tool.xml,添加一个这样的条目,代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.AToolActivity"
android:orientation="vertical">
<TextView
style="@style/TitleStyle"
android:text="高级工具"/>
<TextView
android:id="@+id/tv_query_phone_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="归属地查询"
android:padding="5dp"
android:background="@drawable/selector_atool_item_bg"
android:gravity="center"
android:drawableLeft="@android:drawable/btn_star"/>
<TextView
android:id="@+id/tv_sms_backup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="短信备份"
android:padding="5dp"
android:background="@drawable/selector_atool_item_bg"
android:gravity="center"
android:drawableLeft="@android:drawable/btn_star"/>
LinearLayout>
在dao下新建SmsBackUpDao,作为将短信的查询过程封装起来的工具类,并将信息解析到xml文件中,代码如下:
package com.example.mobilesafe.dao;
import android.app.ProgressDialog;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Xml;
import org.xmlpull.v1.XmlSerializer;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
public class SmsBackUpDao {
// 进度条的进度值
private static int index = 0;
/**
* 短信备份方法
* @param context 上下文环境
* @param path 备份文件夹路径
* @param progressDialog 进度条所在的对话框对象
*/
public static void backUp(Context context, String path, ProgressDialog progressDialog) {
FileOutputStream fos = null;
Cursor cursor = null;
try {
// 1.获取短信备份写入的文件
File file = new File(path);
// 2.获取内容解析器,对数据库进行查询
cursor = context.getContentResolver().query(Uri.parse("content://sms/"), new String[]{"address", "date", "type", "body"}, null, null, null);
// 3.文件相应的输出流
fos = new FileOutputStream(file);
/*
* 4.序列化数据库中读取的数据,放置到xml中,xml应维护成如下格式:
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
XmlSerializer xmlSerializer = Xml.newSerializer();
// 5.给xml做相应的设置
xmlSerializer.setOutput(fos,"utf-8");
xmlSerializer.startDocument("utf-8",true); // DTD(xml规范)
xmlSerializer.startTag(null,"smss"); // 命名空间起始
// 6.备份短信总数的指定给进度条,设置进度条的进度值
progressDialog.setMax(cursor.getCount());
// 7.读取数据库中的每一行的数据,写入到xml中
while (cursor.moveToNext()){
xmlSerializer.startTag(null,"sms");
xmlSerializer.startTag(null,"address");
xmlSerializer.text(cursor.getString(0));
xmlSerializer.endTag(null,"address");
xmlSerializer.startTag(null,"date");
xmlSerializer.text(cursor.getString(1));
xmlSerializer.endTag(null,"date");
xmlSerializer.startTag(null,"type");
xmlSerializer.text(cursor.getString(2));
xmlSerializer.endTag(null,"type");
xmlSerializer.startTag(null,"body");
xmlSerializer.text(cursor.getString(3));
xmlSerializer.endTag(null,"body");
xmlSerializer.endTag(null,"sms");
// 8.每进行一次循环,就要让进度条变化
index++;
// 9.ProgressDialog可以在子线程中更新相应的进度条的转变
progressDialog.setProgress(index);
}
xmlSerializer.endTag(null,"smss");
xmlSerializer.endDocument();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (cursor != null && fos != null) {
cursor.close();
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
修改AToolActivity,添加initSmsBackUp(),作为初始化短信加载的方法,代码如下:
package com.example.mobilesafe.activity;
import androidx.appcompat.app.AppCompatActivity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.TextView;
import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.SmsBackUpDao;
import java.io.File;
public class AToolActivity extends AppCompatActivity {
private TextView tv_query_phone_address;
private TextView tv_sms_backup;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_a_tool);
// 1.电话归属地查询方法
initPhoneAddress();
// 2.短信备份方法
initSmsBackUp();
}
/**
* 1.电话归属地查询方法
*/
private void initPhoneAddress() {
tv_query_phone_address = findViewById(R.id.tv_query_phone_address);
tv_query_phone_address.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(getApplicationContext(),QueryAddressActivity.class));
}
});
}
/**
* 2.短信备份方法
*/
private void initSmsBackUp() {
tv_sms_backup = findViewById(R.id.tv_sms_backup);
tv_sms_backup.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showSmsBackUpDialog();
}
});
}
/**
* 展示备份短信的进度条的对话框
*/
private void showSmsBackUpDialog() {
// 1.创建一个带进度条的对话框
final ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.setIcon(R.drawable.ic_launcher);
progressDialog.setTitle("短信备份");
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); // 指定进度条的样式为水平
progressDialog.show(); // 展示进度条
// 2.获取短信
new Thread(){
@Override
public void run() {
String path = getApplicationContext().getExternalFilesDir(null) + File.separator + "smsbackup.xml"; // 将短信信息存储到SD卡中
SmsBackUpDao.backUp(getApplicationContext(),path,progressDialog);
progressDialog.dismiss();
}
}.start();
}
}
注意,因为涉及到读取短信的操作,需要到清单文件中声明相应权限,代码如下:
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.WRITE_SMS"/>
回调方法,是Android中常见的基于观察者模式的一个编程思想,体现了代码的解耦和多态性,回调需要实现以下步骤:
假设现在不需要使用ProgreessDialog,而是要使用ProgressBar,就需要修改相应代码,这样会比较麻烦。如果采用回调的思想的话,就利于代码更好地进行维护。
现在就来重构一下代码,新建一个callback包,在该包下新建名为SmsBackUpCallBack的回调接口,在其中定义两个方法,代码如下:
package com.example.mobilesafe.callback;
public interface SmsBackUpCallBack {
/**
* 设置短信总数(使用对话框和进度条调用均可)
* @param max 短信总数
*/
public void setMax(int max);
/**
* 备份过程中更新进度条的百分比(使用对话框和进度条调用均可)
* @param index
*/
public void setProgress(int index);
}
修改SmsBackUpDao,修改backup()方法,将接口作为参数传递,并且完善相应逻辑,代码如下:
package com.example.mobilesafe.dao;
import android.app.ProgressDialog;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Xml;
import com.example.mobilesafe.callback.SmsBackUpCallBack;
import org.xmlpull.v1.XmlSerializer;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
public class SmsBackUpDao {
// 进度条的进度值
private static int index = 0;
/**
* 短信备份方法
* @param context 上下文环境
* @param path 备份文件夹路径
* @param smsBackUpCallBack 回调接口
*/
public static void backUp(Context context, String path, SmsBackUpCallBack smsBackUpCallBack) {
FileOutputStream fos = null;
Cursor cursor = null;
try {
// 1.获取短信备份写入的文件
File file = new File(path);
// 2.获取内容解析器,对数据库进行查询
cursor = context.getContentResolver().query(Uri.parse("content://sms/"), new String[]{"address", "date", "type", "body"}, null, null, null);
// 3.文件相应的输出流
fos = new FileOutputStream(file);
/*
* 4.序列化数据库中读取的数据,放置到xml中,xml应维护成如下格式:
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
XmlSerializer xmlSerializer = Xml.newSerializer();
// 5.给xml做相应的设置
xmlSerializer.setOutput(fos,"utf-8");
xmlSerializer.startDocument("utf-8",true); // DTD(xml规范)
xmlSerializer.startTag(null,"smss"); // 命名空间起始
// 6.备份短信总数的指定给进度条,设置进度条的进度值(进度条/对话框进度条)
if (smsBackUpCallBack != null){
smsBackUpCallBack.setMax(cursor.getCount());
}
// 7.读取数据库中的每一行的数据,写入到xml中
while (cursor.moveToNext()){
xmlSerializer.startTag(null,"sms");
xmlSerializer.startTag(null,"address");
xmlSerializer.text(cursor.getString(0));
xmlSerializer.endTag(null,"address");
xmlSerializer.startTag(null,"date");
xmlSerializer.text(cursor.getString(1));
xmlSerializer.endTag(null,"date");
xmlSerializer.startTag(null,"type");
xmlSerializer.text(cursor.getString(2));
xmlSerializer.endTag(null,"type");
xmlSerializer.startTag(null,"body");
xmlSerializer.text(cursor.getString(3));
xmlSerializer.endTag(null,"body");
xmlSerializer.endTag(null,"sms");
// 8.每进行一次循环,就要让进度条变化
index++;
// 9.ProgressDialog可以在子线程中更新相应的进度条的转变(进度条/对话框进度条)
if (smsBackUpCallBack != null){
smsBackUpCallBack.setProgress(index);
}
}
xmlSerializer.endTag(null,"smss");
xmlSerializer.endDocument();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (cursor != null && fos != null) {
cursor.close();
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
接着,修改ATooaActivity,主要是修改showSmsBackUpDialog()方法,需要传入一个实现了刚刚编写的回调接口的对象,代码如下:
package com.example.mobilesafe.activity;
import androidx.appcompat.app.AppCompatActivity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.TextView;
import com.example.mobilesafe.R;
import com.example.mobilesafe.callback.SmsBackUpCallBack;
import com.example.mobilesafe.dao.SmsBackUpDao;
import java.io.File;
public class AToolActivity extends AppCompatActivity {
private TextView tv_query_phone_address;
private TextView tv_sms_backup;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_a_tool);
// 1.电话归属地查询方法
initPhoneAddress();
// 2.短信备份方法
initSmsBackUp();
}
/**
* 1.电话归属地查询方法
*/
private void initPhoneAddress() {
tv_query_phone_address = findViewById(R.id.tv_query_phone_address);
tv_query_phone_address.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(getApplicationContext(),QueryAddressActivity.class));
}
});
}
/**
* 2.短信备份方法
*/
private void initSmsBackUp() {
tv_sms_backup = findViewById(R.id.tv_sms_backup);
tv_sms_backup.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showSmsBackUpDialog();
}
});
}
/**
* 展示备份短信的进度条的对话框
*/
private void showSmsBackUpDialog() {
// 1.创建一个带进度条的对话框
final ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.setIcon(R.drawable.ic_launcher);
progressDialog.setTitle("短信备份");
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); // 指定进度条的样式为水平
progressDialog.show(); // 展示进度条
// 2.获取短信
new Thread(){
@Override
public void run() {
String path = getApplicationContext().getExternalFilesDir(null) + File.separator + "smsbackup.xml"; // 将短信信息存储到SD卡中
SmsBackUpDao.backUp(getApplicationContext(), path, new SmsBackUpCallBack() {
@Override
public void setMax(int max) {
progressDialog.setMax(max);
// 进度条.setMax(max);
}
@Override
public void setProgress(int index) {
progressDialog.setProgress(index);
// 进度条.setProgress(index);
}
});
progressDialog.dismiss();
}
}.start();
}
}
我们实现了前面提到的功能,现在需要实现另一个模块——“软件管理”,如图所示:
该模块应该实现以下功能:
首先修改HomeActivity,修改initData()方法,添加进入“软件管理”模块的功能,代码如下:
/**
* 2.初始化数据
*/
private void initData() {
// 1.初始化每个图标的标题
mTitleStrs = new String[]{"手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","缓存清理","高级工具","设置中心"};
// 2.初始化每个图标的图像
mDrawableIds = new int[]{R.drawable.home_safe,R.drawable.home_callmsgsafe,R.drawable.home_apps,R.drawable.home_taskmanager,R.drawable.home_netmanager,R.drawable.home_trojan,R.drawable.home_sysoptimize,R.drawable.home_tools,R.drawable.home_settings};
// 3.为GridView设置数据适配器
gv_home.setAdapter(new MyAdapter());
// 4.注册GridView中单个条目的点击事件
gv_home.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
switch (position){
case 0:
// 手机防盗
showDialog();
break;
case 1:
// 通信卫士
startActivity(new Intent(getApplicationContext(),BlackNumberActivity.class));
break;
case 2:
// 软件管理
startActivity(new Intent(getApplicationContext(),AppManagerActivity.class));
break;
case 7:
// 高级工具
startActivity(new Intent(getApplicationContext(),AToolActivity.class));
break;
case 8:
// 设置中心
Intent intent = new Intent(getApplicationContext(), SettingActivity.class);
startActivity(intent);
break;
default:
break;
}
}
});
}
在activity包下新增名为AppManagerActivity的Activity,作为“软件管理”模块下的Activity,首先修改其布局activity_app_manager.xml,代码如下:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.AppManagerActivity"
android:orientation="vertical">
<TextView
style="@style/TitleStyle"
android:text="软件管理"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_memory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="磁盘可用"/>
<TextView
android:id="@+id/tv_sd_memory"
android:layout_alignParentRight="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="sd卡可用"/>
RelativeLayout>
<ListView
android:id="@+id/lv_app_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
LinearLayout>
修改AppManagerActivity,添加initTitle()方法,完成计算磁盘和SD卡的可用空间的业务逻辑,代码如下:
package com.example.mobilesafe.activity;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Environment;
import android.os.StatFs;
import android.text.format.Formatter;
import android.widget.TextView;
import com.example.mobilesafe.R;
public class AppManagerActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_manager);
// 初始化磁盘 & SD卡可用大小
initTitle();
}
/**
* 初始化磁盘 & SD卡可用大小
*/
private void initTitle() {
// 0.初始化控件
TextView tv_memory = findViewById(R.id.tv_memory);
TextView tv_sd_memory = findViewById(R.id.tv_sd_memory);
// 1.获取磁盘(内存,区分于手机运行内存)可用大小,磁盘路径
String path = Environment.getDataDirectory().getAbsolutePath();
// 2.获取SD卡可用大小,SD卡路径
String sdpath = Environment.getExternalStorageDirectory().getAbsolutePath();
// 3.获取以上两个路径下文件夹的可用大小
long space = getAvailSpace(path);
long sdspace = getAvailSpace(sdpath);
// 4.对bytes为单位的数值格式化
String strSpace = Formatter.formatFileSize(this, space);
String strSdSpace = Formatter.formatFileSize(this, sdspace);
// 5.给控件赋值
tv_memory.setText("磁盘可用:" + strSpace);
tv_sd_memory.setText("sd卡可用:" + strSdSpace);
}
/**
* 计算文件夹的可用大小
* @param path 路径名
* @return 可用空间大小,单位为byte=8bit
*/
private long getAvailSpace(String path) {
// 获取可用磁盘大小的对象
StatFs statFs = new StatFs(path);
// 获取可用区块的个数
long availableBlocks = statFs.getAvailableBlocks();
// 获取区块的大小
long blockSize = statFs.getBlockSize();
// 可用空间大小 = 区块大小 * 可用区块个数
return availableBlocks * blockSize;
}
}
上一节中我们完成了“软件管理”模块中获取内存大小和SD卡大小的功能,这一节我们要实现获取应用相关信息集合的功能。
在ListView
控件中,每个条目都应该拥有以下信息:
为了将这些信息进行一个封装,在domain包下新建AppInfo,作为封装条目中这些信息的一个Java Bean
,代码如下:
package com.example.mobilesafe.domain;
import android.graphics.drawable.Drawable;
public class AppInfo {
private Drawable icon;
private String name;
private String packagename;
private boolean isSdCard;
private boolean isSystem;
public AppInfo() {
}
public Drawable getIcon() {
return icon;
}
public void setIcon(Drawable icon) {
this.icon = icon;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPackagename() {
return packagename;
}
public void setPackagename(String packagename) {
this.packagename = packagename;
}
public boolean isSdCard() {
return isSdCard;
}
public void setSdCard(boolean sdCard) {
isSdCard = sdCard;
}
public boolean isSystem() {
return isSystem;
}
public void setSystem(boolean system) {
isSystem = system;
}
}
为了将获取应用相关信息集合的一系列操作进行复用,在dao下新增AppInfoDao,添加静态方法getAppInfoList(),这里使用到了状态机制,该机制一般应用在手机游戏里的RPG元素当中,例如(某样属性(0001) & 某样道具(1111) = 0001,说明该道具具有该属性)代码如下:
package com.example.mobilesafe.dao;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import com.example.mobilesafe.domain.AppInfo;
import java.util.ArrayList;
import java.util.List;
public class AppInfoDao {
/**
* 获取安装在手机上应用相关信息集合的方法
* @param context 上下文环境
*/
public static List<AppInfo> getAppInfoList(Context context){
// 0.声明集合
List<AppInfo> appInfoList = new ArrayList<>();
// 1.获取包管理对象
PackageManager packageManager = context.getPackageManager();
// 2.通过包管理对象来获取手机上所有应用信息,直接传递0即可
List<PackageInfo> installedPackages = packageManager.getInstalledPackages(0);
// 3.循环遍历上述集合,获取每一个安装在手机上的应用相关信息(包名,名称,路径,系统,图标)
for (PackageInfo packageInfo : installedPackages) {
AppInfo appInfo = new AppInfo();
// 4.包名
appInfo.setPackagename(packageInfo.packageName);
// 5.获取应用名称、图标,其信息存储在application节点中
appInfo.setName(packageInfo.applicationInfo.loadLabel(packageManager).toString());
appInfo.setIcon(packageInfo.applicationInfo.loadIcon(packageManager));
// 6.判断应用是否为系统应用(状态机)
if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == ApplicationInfo.FLAG_SYSTEM){
// 系统应用
appInfo.setSystem(true);
}else {
// 非系统应用
appInfo.setSystem(false);
}
// 7.判断应用是否为sd卡应用
if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) == ApplicationInfo.FLAG_EXTERNAL_STORAGE){
// sd卡应用
appInfo.setSdCard(true);
}else {
// 非sd卡应用
appInfo.setSdCard(false);
}
// 8.加入集合中
appInfoList.add(appInfo);
}
// 9.返回集合
return appInfoList;
}
}
修改AppManagerActivity,添加intiListView()方法,对布局中的列表进行初始化和数据适配,代码如下:
package com.example.mobilesafe.activity;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.os.StatFs;
import android.text.format.Formatter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.AppInfoDao;
import com.example.mobilesafe.domain.AppInfo;
import java.util.ArrayList;
import java.util.List;
public class AppManagerActivity extends AppCompatActivity {
private ListView lv_app_list;
private List<AppInfo> mAppInfoList;
private MyAdapter mAdapter;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
// 4.使用数据适配器
mAdapter = new MyAdapter();
lv_app_list.setAdapter(mAdapter);
}
};
class MyAdapter extends BaseAdapter{
@Override
public int getCount() {
return mAppInfoList.size();
}
@Override
public AppInfo getItem(int position) {
return mAppInfoList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
// 1.判断ConverView是否为空
if (convertView == null){
convertView = View.inflate(getApplicationContext(),R.layout.list_app_item,null);
// 2.获取控件实例赋值给ViewHolder
holder = new ViewHolder();
holder.iv_icon_application = convertView.findViewById(R.id.iv_icon_application);
holder.tv_app_name = convertView.findViewById(R.id.tv_app_name);
holder.tv_app_location = convertView.findViewById(R.id.tv_app_location);
// 3.将Holder放到ConverView上
convertView.setTag(holder);
}else {
holder = (ViewHolder) convertView.getTag();
}
// 4.获取Holder中的控件实例,赋值
holder.iv_icon_application.setBackground(getItem(position).getIcon());
holder.tv_app_name.setText(getItem(position).getName());
// 5.显示应用安装的位置
if (getItem(position).isSdCard()){
holder.tv_app_location.setText("sd卡应用");
}else {
holder.tv_app_location.setText("内存应用");
}
// 6.返回现有条目填充上数据的View对象
return convertView;
}
}
static class ViewHolder{
ImageView iv_icon_application;
TextView tv_app_name;
TextView tv_app_location;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_manager);
// 初始化磁盘 & SD卡可用大小
initTitle();
// 初始化ListView
intiListView();
}
/**
* 初始化磁盘 & SD卡可用大小
*/
private void initTitle() {
// 0.初始化控件
TextView tv_memory = findViewById(R.id.tv_memory);
TextView tv_sd_memory = findViewById(R.id.tv_sd_memory);
// 1.获取磁盘(内存,区分于手机运行内存)可用大小,磁盘路径
String path = Environment.getDataDirectory().getAbsolutePath();
// 2.获取SD卡可用大小,SD卡路径
String sdpath = Environment.getExternalStorageDirectory().getAbsolutePath();
// 3.获取以上两个路径下文件夹的可用大小
long space = getAvailSpace(path);
long sdspace = getAvailSpace(sdpath);
// 4.对bytes为单位的数值格式化
String strSpace = Formatter.formatFileSize(this, space);
String strSdSpace = Formatter.formatFileSize(this, sdspace);
// 5.给控件赋值
tv_memory.setText("磁盘可用:" + strSpace);
tv_sd_memory.setText("sd卡可用:" + strSdSpace);
}
/**
* 计算文件夹的可用大小
* @param path 路径名
* @return 可用空间大小,单位为byte=8bit
*/
private long getAvailSpace(String path) {
// 获取可用磁盘大小的对象
StatFs statFs = new StatFs(path);
// 获取可用区块的个数
long availableBlocks = statFs.getAvailableBlocks();
// 获取区块的大小
long blockSize = statFs.getBlockSize();
// 可用空间大小 = 区块大小 * 可用区块个数
return availableBlocks * blockSize;
}
/**
* 初始化ListView
*/
private void intiListView() {
// 1.初始化控件
lv_app_list = findViewById(R.id.lv_app_list);
new Thread(){
@Override
public void run() {
// 2.准备填充ListView中数据适配器的数据
mAppInfoList = AppInfoDao.getAppInfoList(getApplicationContext());
for (AppInfo appInfo : mAppInfoList) {
Log.i("应用信息为:",appInfo.toString());
}
// 3.发送空消息进行数据绑定
mHandler.sendEmptyMessage(0);
}
}.start();
}
}
在res/layout下新建list_app_item,作为列表中每个条目的布局,代码如下:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv_icon"
android:background="@drawable/ic_launcher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tv_app_name"
android:layout_toRightOf="@id/iv_icon"
android:text="应用名称"
android:textColor="#000"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tv_app_location"
android:layout_toRightOf="@id/iv_icon"
android:layout_below="@id/tv_app_name"
android:text="安装位置"
android:textColor="#000"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
RelativeLayout>
前面我们已经完成了ListView
展示数据,现在需要实现另一种条目类型的数据适配器指定。
目前列表中需要用到两种条目:
效果应该如下图所示:
默认的ListView
指定了一种类型的条目,现在想让ListView
的数据适配器多一种类型的条目,需要重写方法getViewTypeCount()
,并且区分索引指定条目的展示类型(根据索引值来决定展示哪种类型的条目),需要重写getViewItemType()
。与此同时,还需要在重写方法中修改一些关于集合的属性。
自然,为了达到区分的目的,需要将集合分成两部分:
修改AppManagerActivity,修改适配器的业务,完善相应逻辑,代码如下:
package com.example.mobilesafe.activity;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.os.StatFs;
import android.text.format.Formatter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.AppInfoDao;
import com.example.mobilesafe.domain.AppInfo;
import java.util.ArrayList;
import java.util.List;
public class AppManagerActivity extends AppCompatActivity {
private ListView lv_app_list;
private List<AppInfo> mAppInfoList;
private MyAdapter mAdapter;
// 系统应用所在的集合
private List<AppInfo> mSystemList;
// 用户应用所在的集合
private List<AppInfo> mCustomerList;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
// 4.使用数据适配器
mAdapter = new MyAdapter();
lv_app_list.setAdapter(mAdapter);
}
};
class MyAdapter extends BaseAdapter{
// 在ListView中多添加一种类型条目,条目总数有2种
@Override
public int getViewTypeCount() {
return super.getViewTypeCount() + 1;
}
// 根据索引值决定展示哪种类型的条目
@Override
public int getItemViewType(int position) {
if (position == 0 || position == mCustomerList.size() + 1){
// 纯文本条目
return 0;
}else {
// 图文条目
return 1;
}
}
@Override
public int getCount() {
return mCustomerList.size() + mSystemList.size() + 2;
}
@Override
public AppInfo getItem(int position) {
if (position == 0 || position == mCustomerList.size() + 1){
// 纯文本条目
return null;
}else {
// 图文条目
if (position < mCustomerList.size() + 1){
// 用户应用
return mCustomerList.get(position - 1);
}else {
// 系统应用
return mSystemList.get(position - mCustomerList.size() - 2);
}
}
// return mAppInfoList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 判断当前索引指向的条目类型状态码
int itemViewType = getItemViewType(position);
if (itemViewType == 0){
// 纯文本条目
ViewTitleHolder holder = null;
if (convertView == null){
convertView = View.inflate(getApplicationContext(),R.layout.list_app_item_text,null);
holder = new ViewTitleHolder();
holder.tv_title_des = convertView.findViewById(R.id.tv_title_des);
convertView.setTag(holder);
}else {
holder = (ViewTitleHolder) convertView.getTag();
}
if (position == 0){
holder.tv_title_des.setText("用户应用("+mCustomerList.size()+")");
}else {
holder.tv_title_des.setText("系统应用("+mSystemList.size()+")");
}
return convertView;
}else {
// 图文条目
ViewHolder holder = null;
// 1.判断ConverView是否为空
if (convertView == null){
convertView = View.inflate(getApplicationContext(),R.layout.list_app_item,null);
// 2.获取控件实例赋值给ViewHolder
holder = new ViewHolder();
holder.iv_icon_application = convertView.findViewById(R.id.iv_icon_application);
holder.tv_app_name = convertView.findViewById(R.id.tv_app_name);
holder.tv_app_location = convertView.findViewById(R.id.tv_app_location);
// 3.将Holder放到ConverView上
convertView.setTag(holder);
}else {
holder = (ViewHolder) convertView.getTag();
}
// 4.获取Holder中的控件实例,赋值
holder.iv_icon_application.setBackground(getItem(position).getIcon());
holder.tv_app_name.setText(getItem(position).getName());
// 5.显示应用安装的位置
if (getItem(position).isSdCard()){
holder.tv_app_location.setText("sd卡应用");
}else {
holder.tv_app_location.setText("内存应用");
}
// 6.返回现有条目填充上数据的View对象
return convertView;
}
}
}
static class ViewHolder{
ImageView iv_icon_application;
TextView tv_app_name;
TextView tv_app_location;
}
static class ViewTitleHolder{
TextView tv_title_des;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_manager);
// 初始化磁盘 & SD卡可用大小
initTitle();
// 初始化ListView
intiListView();
}
/**
* 初始化磁盘 & SD卡可用大小
*/
private void initTitle() {
// 0.初始化控件
TextView tv_memory = findViewById(R.id.tv_memory);
TextView tv_sd_memory = findViewById(R.id.tv_sd_memory);
// 1.获取磁盘(内存,区分于手机运行内存)可用大小,磁盘路径
String path = Environment.getDataDirectory().getAbsolutePath();
// 2.获取SD卡可用大小,SD卡路径
String sdpath = Environment.getExternalStorageDirectory().getAbsolutePath();
// 3.获取以上两个路径下文件夹的可用大小
long space = getAvailSpace(path);
long sdspace = getAvailSpace(sdpath);
// 4.对bytes为单位的数值格式化
String strSpace = Formatter.formatFileSize(this, space);
String strSdSpace = Formatter.formatFileSize(this, sdspace);
// 5.给控件赋值
tv_memory.setText("磁盘可用:" + strSpace);
tv_sd_memory.setText("sd卡可用:" + strSdSpace);
}
/**
* 计算文件夹的可用大小
* @param path 路径名
* @return 可用空间大小,单位为byte=8bit
*/
private long getAvailSpace(String path) {
// 获取可用磁盘大小的对象
StatFs statFs = new StatFs(path);
// 获取可用区块的个数
long availableBlocks = statFs.getAvailableBlocks();
// 获取区块的大小
long blockSize = statFs.getBlockSize();
// 可用空间大小 = 区块大小 * 可用区块个数
return availableBlocks * blockSize;
}
/**
* 初始化ListView
*/
private void intiListView() {
// 1.初始化控件
lv_app_list = findViewById(R.id.lv_app_list);
new Thread(){
@Override
public void run() {
// 2.准备填充ListView中数据适配器的数据
mAppInfoList = AppInfoDao.getAppInfoList(getApplicationContext());
mSystemList = new ArrayList<>();
mCustomerList = new ArrayList<>();
// 分割集合
for (AppInfo appInfo : mAppInfoList) {
if (appInfo.isSystem()){
mSystemList.add(appInfo);
}else {
mCustomerList.add(appInfo);
}
}
// 3.发送空消息进行数据绑定
mHandler.sendEmptyMessage(0);
}
}.start();
}
}
在res/layout下新建list_app_item_text.xml,作为纯文本条目的布局,代码如下:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_title_des"
android:background="#ccc"
android:text="纯文本条目"
android:padding="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
LinearLayout>
我们已经完成了“软件管理”模块中的大部分功能了,现在需要完成——常驻悬浮框的效果。该效果实现了当列表从“用户应用”拖拽到“系统应用时”,悬浮框中的文字也会发生相应变化。
要想实现该效果,则需要创建一个父布局中的某个控件盖住原来的条目,即ListView
控件。
修改activity_app_manager.xml,根据思路来进行修改,代码如下:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.AppManagerActivity"
android:orientation="vertical">
<TextView
style="@style/TitleStyle"
android:text="软件管理"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_memory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="磁盘可用"/>
<TextView
android:id="@+id/tv_sd_memory"
android:layout_alignParentRight="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="sd卡可用"/>
RelativeLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ListView
android:id="@+id/lv_app_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tv_title_type"
android:background="#ccc"
android:textColor="#fff"
android:text="条目类型的说明"
android:padding="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
FrameLayout>
LinearLayout>
修改AppManagerActivity,修改intiListView()方法,获取到刚刚创建的文本控件的实例,并设置列表的滚动事件,以此来执行相应逻辑,代码如下:
package com.example.mobilesafe.activity;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.os.StatFs;
import android.text.format.Formatter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.AppInfoDao;
import com.example.mobilesafe.domain.AppInfo;
import java.util.ArrayList;
import java.util.List;
public class AppManagerActivity extends AppCompatActivity {
private ListView lv_app_list;
private List<AppInfo> mAppInfoList;
private MyAdapter mAdapter;
// 系统应用所在的集合
private List<AppInfo> mSystemList;
// 用户应用所在的集合
private List<AppInfo> mCustomerList;
private TextView tv_title_type;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
// 4.使用数据适配器
mAdapter = new MyAdapter();
lv_app_list.setAdapter(mAdapter);
}
};
class MyAdapter extends BaseAdapter{
// 在ListView中多添加一种类型条目,条目总数有2种
@Override
public int getViewTypeCount() {
return super.getViewTypeCount() + 1;
}
// 根据索引值决定展示哪种类型的条目
@Override
public int getItemViewType(int position) {
if (position == 0 || position == mCustomerList.size() + 1){
// 纯文本条目
return 0;
}else {
// 图文条目
return 1;
}
}
@Override
public int getCount() {
return mCustomerList.size() + mSystemList.size() + 2;
}
@Override
public AppInfo getItem(int position) {
if (position == 0 || position == mCustomerList.size() + 1){
// 纯文本条目
return null;
}else {
// 图文条目
if (position < mCustomerList.size() + 1){
// 用户应用
return mCustomerList.get(position - 1);
}else {
// 系统应用
return mSystemList.get(position - mCustomerList.size() - 2);
}
}
// return mAppInfoList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 判断当前索引指向的条目类型状态码
int itemViewType = getItemViewType(position);
if (itemViewType == 0){
// 纯文本条目
ViewTitleHolder holder = null;
if (convertView == null){
convertView = View.inflate(getApplicationContext(),R.layout.list_app_item_text,null);
holder = new ViewTitleHolder();
holder.tv_title_des = convertView.findViewById(R.id.tv_title_des);
convertView.setTag(holder);
}else {
holder = (ViewTitleHolder) convertView.getTag();
}
if (position == 0){
holder.tv_title_des.setText("用户应用("+mCustomerList.size()+")");
}else {
holder.tv_title_des.setText("系统应用("+mSystemList.size()+")");
}
return convertView;
}else {
// 图文条目
ViewHolder holder = null;
// 1.判断ConverView是否为空
if (convertView == null){
convertView = View.inflate(getApplicationContext(),R.layout.list_app_item,null);
// 2.获取控件实例赋值给ViewHolder
holder = new ViewHolder();
holder.iv_icon_application = convertView.findViewById(R.id.iv_icon_application);
holder.tv_app_name = convertView.findViewById(R.id.tv_app_name);
holder.tv_app_location = convertView.findViewById(R.id.tv_app_location);
// 3.将Holder放到ConverView上
convertView.setTag(holder);
}else {
holder = (ViewHolder) convertView.getTag();
}
// 4.获取Holder中的控件实例,赋值
holder.iv_icon_application.setBackground(getItem(position).getIcon());
holder.tv_app_name.setText(getItem(position).getName());
// 5.显示应用安装的位置
if (getItem(position).isSdCard()){
holder.tv_app_location.setText("sd卡应用");
}else {
holder.tv_app_location.setText("内存应用");
}
// 6.返回现有条目填充上数据的View对象
return convertView;
}
}
}
static class ViewHolder{
ImageView iv_icon_application;
TextView tv_app_name;
TextView tv_app_location;
}
static class ViewTitleHolder{
TextView tv_title_des;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_manager);
// 初始化磁盘 & SD卡可用大小
initTitle();
// 初始化ListView
intiListView();
}
/**
* 初始化磁盘 & SD卡可用大小
*/
private void initTitle() {
// 0.初始化控件
TextView tv_memory = findViewById(R.id.tv_memory);
TextView tv_sd_memory = findViewById(R.id.tv_sd_memory);
// 1.获取磁盘(内存,区分于手机运行内存)可用大小,磁盘路径
String path = Environment.getDataDirectory().getAbsolutePath();
// 2.获取SD卡可用大小,SD卡路径
String sdpath = Environment.getExternalStorageDirectory().getAbsolutePath();
// 3.获取以上两个路径下文件夹的可用大小
long space = getAvailSpace(path);
long sdspace = getAvailSpace(sdpath);
// 4.对bytes为单位的数值格式化
String strSpace = Formatter.formatFileSize(this, space);
String strSdSpace = Formatter.formatFileSize(this, sdspace);
// 5.给控件赋值
tv_memory.setText("磁盘可用:" + strSpace);
tv_sd_memory.setText("sd卡可用:" + strSdSpace);
}
/**
* 计算文件夹的可用大小
* @param path 路径名
* @return 可用空间大小,单位为byte=8bit
*/
private long getAvailSpace(String path) {
// 获取可用磁盘大小的对象
StatFs statFs = new StatFs(path);
// 获取可用区块的个数
long availableBlocks = statFs.getAvailableBlocks();
// 获取区块的大小
long blockSize = statFs.getBlockSize();
// 可用空间大小 = 区块大小 * 可用区块个数
return availableBlocks * blockSize;
}
/**
* 初始化ListView
*/
private void intiListView() {
// 1.初始化控件
tv_title_type = findViewById(R.id.tv_title_type);
lv_app_list = findViewById(R.id.lv_app_list);
new Thread(){
@Override
public void run() {
// 2.准备填充ListView中数据适配器的数据
mAppInfoList = AppInfoDao.getAppInfoList(getApplicationContext());
mSystemList = new ArrayList<>();
mCustomerList = new ArrayList<>();
// 分割集合
for (AppInfo appInfo : mAppInfoList) {
if (appInfo.isSystem()){
mSystemList.add(appInfo);
}else {
mCustomerList.add(appInfo);
}
}
// 3.发送空消息进行数据绑定
mHandler.sendEmptyMessage(0);
}
}.start();
// 4.给ListView注册滚动事件
lv_app_list.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// 在滚动过程中调用方法
if (mSystemList != null && mCustomerList != null){
if (firstVisibleItem >= mCustomerList.size() + 1){
// 滚动到了系统应用
tv_title_type.setText("系统应用("+mSystemList.size()+")");
}else {
// 滚动到了用户应用
tv_title_type.setText("用户应用("+mCustomerList.size()+")");
}
}
}
});
}
}