前言
最近老是被什么基金原油的电话骚扰,但是手机没显示归属地,搞得我有时以为是快递电话之类的就接了..然后各种烦..所以打算做一个来去电显示归属地的小软件,碰到某些城市的陌生号码就直接挂掉,既然要做就顺便写篇博客把,显示来去电归属地这个功能商业app用得不多,但是权当学习了。
实现原理
网上找一个有号码段的归属地数据库,然后开启个服务监听系统去电广播和来电,然后获取来去电号码,跟数据库的号码字段进行比对,然后显示在手机界面上。
实现步骤
下面一步步来完成这个功能:
主界面布局:
这个布局是自己为了自己设置是否显示归属地的。
在main下面新建一个assets文件夹,并把网上找到的数据库拷贝到该文件夹下面
接下来分析数据库表:
我下的数据库里面有两个表,表结构如下:
data1:
data2:
其中data1的id就是电话号码,前七位就可以确定归属地了,然后根据id对应的outkey去匹配data2的id就可以得到归属地信息了。知道了两个表之间的联系之后就可以开始我们的查询工作了。
新建一个CallAddressDao类,这个类是用来访问数据库的
public class CallAddressDao {
//数据库路径
private static final String PATH = "data/data/com.sjr.calladdress/files/address.db";
/**
* 从数据库中获取手机归属地
* @param number
* @return 手机归属地
*/
public static String getCallAddress(String number){
String callAddress = "未知号码";
//获取数据库对象
SQLiteDatabase database = SQLiteDatabase.openDatabase(PATH, null,
SQLiteDatabase.OPEN_READONLY);
//正则表达式匹配
if (number.matches("^1[3-8]\d{9}$")) {//匹配器11位手机号
Cursor cursor = database.rawQuery("select location from data2 where id=(select outkey from data1 where id=?)",
new String[]{number.substring(0, 7)});//截取前七个
if (cursor.moveToNext())
callAddress = cursor.getString(0);
cursor.close();
}else if (number.matches("^\d+$")){//匹配数字
switch (number.length()){
case 3:
callAddress = "报警电话";//三位数就是报警电话
break;
case 4:
callAddress = "模拟器";
break;
case 5:
callAddress = "客服电话";
break;
case 7:
case 8:
callAddress = "本地电话";
break;
default:
if (number.startsWith("0")&&number.length()>10){//有可能是长途电话
//有些区号是4位,有些区号是3位(包括0)
// 先查询4位区号
Cursor cursor = database.rawQuery("select location from data2 where area =?",
new String[]{number.substring(1,4)});
if (cursor.moveToNext())
callAddress = cursor.getString(0);
else {
cursor.close();
//查询3位区号
cursor = database.rawQuery("select location from data2 where area =?",
new String[]{number.substring(1,3)});
if (cursor.moveToNext())
callAddress = cursor.getString(0);
cursor.close();
}
}
break;
}
}
database.close();//关闭数据库
return callAddress;
}
}
下面是打开一个数据库:
//获取数据库对象
SQLiteDatabase database = SQLiteDatabase.openDatabase(PATH, null,
SQLiteDatabase.OPEN_READONLY);
PATH是数据库路径,第二个是一个可选的选项,直接传null,第三个然后我们只是要读,并不修改数据库,所以设置为只读
这里我设定的数据库路径是:
data/data/com.sjr.calladdress/files/address.db
这不能是assets文件夹下面的,只能是data/data/包名/ 目录下,不然是访问不到的,但是这时这个路径下是没有数据库的,所以我就在第一次进入主界面的时候先把数据库拷到这个目录下
/**
* 把assets目录下的数据库拷贝到data/data目录下
*
* @param dbName
*/
private void copyDB(String dbName) {
// File filesDir = getFilesDir();///data/data/com.sjr.calladdress/files
// Log.d("print","路径:"+filesDir.getAbsolutePath());
File desFile = new File(getFilesDir(), dbName);//要拷贝的目标地址
if (desFile.exists()) {//如果已经存在,就不进行拷贝工作直接返回
Log.d("print", "数据库" + dbName + "已经存在");
return;
}
//否则就进行数据库拷贝
FileOutputStream out = null;
InputStream in = null;
try {
in = getAssets().open(dbName);//读写assets文件夹下的数据库
out = new FileOutputStream(desFile);//写入data目录
int len = 0;
byte[] buffer = new byte[1024];
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (out != null)
out.close();
if (in != null)
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
上面的代码就把数据库从assets文件夹下面拷贝到data目录下了
然后通过数据库查询语句跟正则表达式对手机号进行匹配。
这时我们开启一个服务去监听来电广播和去电状态,我先把整个服务类拷下来然后详细分析:
/**
* Created by 宋家任 on 2016/3/1 15:22.
* 监听来去电服务
*/
public class CallAddressService extends Service {
private TelephonyManager mTM;//电话管理者
private WindowManager mWM;//桌面窗口管理者
private MPhoneStateListener mListener;
private View mView;//桌面展示归属地的view
private OutCallReceiver mReceiver;//监听去电广播
private SharedPreferences mPref;//记录归属地的位置
private int startX;
private int startY;
private WindowManager.LayoutParams params;
private int windowWidth;
private int windowHeight;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
mPref = getSharedPreferences("config", MODE_PRIVATE);
mTM = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
mListener = new MPhoneStateListener();
mTM.listen(mListener, PhoneStateListener.LISTEN_CALL_STATE);
mReceiver = new OutCallReceiver();
//动态注册去电广播
IntentFilter filter = new IntentFilter(Intent.ACTION_NEW_OUTGOING_CALL);
registerReceiver(mReceiver, filter);
}
/**
* 监听来电通话状态
* 需要权限
*
*/
class MPhoneStateListener extends PhoneStateListener {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
switch (state) {
case TelephonyManager.CALL_STATE_RINGING:// 电话铃响
String address = CallAddressDao.getCallAddress(incomingNumber);//把来电号码跟数据库号码进行比对
showView(address);
break;
case TelephonyManager.CALL_STATE_IDLE:// 电话闲置状态
if (mWM != null && mView != null) {
mWM.removeView(mView);// 从window中移除view
mView = null;
}
break;
}
}
}
/**
* 去电广播
* 监听去电广播需要下面的权限
*
*/
class OutCallReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String number = getResultData();//获取去电号码
String address = CallAddressDao.getCallAddress(number);//将去电号码跟数据库的进行比对
showView(address);
}
}
/**
*
* 弹自定义的界面
* 使用电话窗口需要下面的权限
*
* @param address
*/
private void showView(String address) {
//获得一个窗口管理者实例
mWM = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE);
windowWidth = mWM.getDefaultDisplay().getWidth();//获取屏幕宽度
windowHeight = mWM.getDefaultDisplay().getHeight();//获取屏幕高度
//布局参数
params = new WindowManager.LayoutParams();
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
params.format = PixelFormat.TRANSLUCENT;
params.type = WindowManager.LayoutParams.TYPE_PHONE;//电话窗口。用于电话交互(特别是呼入)。它置于所有应用程序之上,状态栏之下。
params.gravity = Gravity.LEFT + Gravity.TOP;// 将重心位置设置为左上方,
// 也就是(0,0)从左上方开始,而不是默认的重心位置
params.setTitle("Toast");
//先获取上次显示的位置
int lastX = mPref.getInt("lastX", 0);
int lastY = mPref.getInt("lastY", 0);
//设置悬浮窗的位置,基于左上方的偏移量
params.x = lastX;
params.y = lastY;
//要展示的view
mView = View.inflate(this, R.layout.view_address, null);
//给view设置背景
mView.setBackgroundResource(R.drawable.call_locate_blue);// 根据存储的样式更新背景
//设置文字
TextView tvText = (TextView) mView.findViewById(R.id.tv_number);
tvText.setText(address);
mWM.addView(mView, params);// 将view添加在屏幕上(Window)
//给这个归属地view设置监听,移动的时候记录坐标
mView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN://按下
// 初始化起点坐标
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE://移动
int endX = (int) event.getRawX();
int endY = (int) event.getRawY();
// 计算移动偏移量
int dx = endX - startX;
int dy = endY - startY;
// 更新浮窗位置
params.x += dx;
params.y += dy;
// 防止坐标偏离屏幕
if (params.x < 0) {
params.x = 0;
}
if (params.y < 0) {
params.y = 0;
}
// 防止坐标偏离屏幕
if (params.x > windowWidth - mView.getWidth()) {
params.x = windowWidth - mView.getWidth();
}
if (params.y > windowHeight - mView.getHeight()) {
params.y = windowHeight - mView.getHeight();
}
mWM.updateViewLayout(mView, params);//更新布局
// 重新初始化起点坐标
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_UP://抬起
// 记录坐标点
SharedPreferences.Editor edit = mPref.edit();
edit.putInt("lastX", params.x);
edit.putInt("lastY", params.y);
edit.commit();
break;
default:
break;
}
return true;//返回true消费这个手势事件
}
});
}
/**
* 服务销毁的时候会回调这个方法
*/
@Override
public void onDestroy() {
mTM.listen(mListener, PhoneStateListener.LISTEN_NONE);//停止来电监听
unregisterReceiver(mReceiver);//注销监听去电广播
}
}
先解决监听去电广播的问题,写一个广播接收者,去接收去电广播,注意监听去电广播是要权限的,权限如下
/**
* 去电广播
* 监听去电广播需要下面的权限
*
*/
class OutCallReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String number = getResultData();//获取去电号码
String address = CallAddressDao.getCallAddress(number);//将去电号码跟数据库的进行比对
showView(address);
}
}
让后因为是要动态监听去电广播,所以就只能动态注册,所以在服务开启的时候就注册这个广播:
//动态注册去电广播
IntentFilter filter = new IntentFilter(Intent.ACTION_NEW_OUTGOING_CALL);
registerReceiver(mReceiver, filter);
在服务关闭的时候就取消注册:
/**
* 服务销毁的时候会回调这个方法
*/
@Override
public void onDestroy() {
mTM.listen(mListener, PhoneStateListener.LISTEN_NONE);//停止来电监听
unregisterReceiver(mReceiver);//注销监听去电广播
}
上面就完成了一个动态监听去电广播了,然后就是监听电话状态了
/**
* 监听来电通话状态
* 需要权限
*
*/
class MPhoneStateListener extends PhoneStateListener {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
switch (state) {
case TelephonyManager.CALL_STATE_RINGING:// 电话铃响
String address = CallAddressDao.getCallAddress(incomingNumber);//把来电号码跟数据库号码进行比对
showView(address);
break;
case TelephonyManager.CALL_STATE_IDLE:// 电话闲置状态
if (mWM != null && mView != null) {
mWM.removeView(mView);// 从window中移除view
mView = null;
}
break;
}
}
}
写一个类去继承PhoneStateListener,然后监听通话状态,注意也是需要权限的,然后同监听去电广播类似,在服务初始化的时候监听,在服务销毁的时候注销监听。
然后写一个方法在通话窗口展示归属地当然使用电话窗口也是需要权限的,权限如下
/**
*
* 弹自定义的界面
* 使用电话窗口需要下面的权限
*
* @param address
*/
private void showView(String address) {
//获得一个窗口管理者实例
mWM = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE);
windowWidth = mWM.getDefaultDisplay().getWidth();//获取屏幕宽度
windowHeight = mWM.getDefaultDisplay().getHeight();//获取屏幕高度
//布局参数
params = new WindowManager.LayoutParams();
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
params.format = PixelFormat.TRANSLUCENT;
params.type = WindowManager.LayoutParams.TYPE_PHONE;//电话窗口。用于电话交互(特别是呼入)。它置于所有应用程序之上,状态栏之下。
params.gravity = Gravity.LEFT + Gravity.TOP;// 将重心位置设置为左上方,
// 也就是(0,0)从左上方开始,而不是默认的重心位置
params.setTitle("Toast");
//先获取上次显示的位置
int lastX = mPref.getInt("lastX", 0);
int lastY = mPref.getInt("lastY", 0);
//设置悬浮窗的位置,基于左上方的偏移量
params.x = lastX;
params.y = lastY;
//要展示的view
mView = View.inflate(this, R.layout.view_address, null);
//给view设置背景
mView.setBackgroundResource(R.drawable.call_locate_blue);// 根据存储的样式更新背景
//设置文字
TextView tvText = (TextView) mView.findViewById(R.id.tv_number);
tvText.setText(address);
mWM.addView(mView, params);// 将view添加在屏幕上(Window)
//给这个归属地view设置监听,移动的时候记录坐标
mView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN://按下
// 初始化起点坐标
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE://移动
int endX = (int) event.getRawX();
int endY = (int) event.getRawY();
// 计算移动偏移量
int dx = endX - startX;
int dy = endY - startY;
// 更新浮窗位置
params.x += dx;
params.y += dy;
// 防止坐标偏离屏幕
if (params.x < 0) {
params.x = 0;
}
if (params.y < 0) {
params.y = 0;
}
// 防止坐标偏离屏幕
if (params.x > windowWidth - mView.getWidth()) {
params.x = windowWidth - mView.getWidth();
}
if (params.y > windowHeight - mView.getHeight()) {
params.y = windowHeight - mView.getHeight();
}
mWM.updateViewLayout(mView, params);//更新布局
// 重新初始化起点坐标
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_UP://抬起
// 记录坐标点
SharedPreferences.Editor edit = mPref.edit();
edit.putInt("lastX", params.x);
edit.putInt("lastY", params.y);
edit.commit();
break;
default:
break;
}
return true;//返回true消费这个手势事件
}
});
}
上面的方法就是在通话界面显示自定义的view,为了便于理解我加了很多注释,然后在这里还是解释下好了,获得一个桌面管理者,然后设置归属地view的相关参数,然后把归属地view添加到桌面,然后给view设置一个监听,让view可以移动,上面用到了SharedPreferences,这是为了移动完归属地view后记录下位置然后下次初始就让这个view显示在那个位置,view布局如下:
背景是从网上找的一张.9图然后图标是Android系统的一个电话图标,效果如下
然后完成这个服务类以后只要在MainActivity里开启服务就好了,MainActivity代码如下:
/**
* Created by 宋家任 on 2016/5/6 13:55.
* 主界面
*/
public class MainActivity extends Activity implements View.OnClickListener {
private Button btnShow;//显示归属地
private Button btnNoShow;//不显示归属地
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
copyDB("address.db");// 拷贝归属地查询数据库
initDatas();
}
/**
* 把assets目录下的数据库拷贝到data/data目录下
*
* @param dbName
*/
private void copyDB(String dbName) {
// File filesDir = getFilesDir();///data/data/com.sjr.calladdress/files
// Log.d("print","路径:"+filesDir.getAbsolutePath());
File desFile = new File(getFilesDir(), dbName);//要拷贝的目标地址
if (desFile.exists()) {//如果已经存在,就不进行拷贝工作直接返回
Log.d("print", "数据库" + dbName + "已经存在");
return;
}
//否则就进行数据库拷贝
FileOutputStream out = null;
InputStream in = null;
try {
in = getAssets().open(dbName);//打开assets下的数据库
out = new FileOutputStream(desFile);
int len = 0;
byte[] buffer = new byte[1024];
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (out != null)
out.close();
if (in != null)
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void initDatas() {
btnNoShow = (Button) findViewById(R.id.btn_show_address);
btnShow = (Button) findViewById(R.id.btn_noshow_address);
btnShow.setOnClickListener(this);
btnNoShow.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_show_address:
startService(new Intent(this, CallAddressService.class));//开启显示归属地服务
break;
case R.id.btn_noshow_address:
stopService(new Intent(this, CallAddressService.class));//关闭显示归属地服务
break;
}
}
}
效果
到这里整个归属地的设置就已经完成了,下面是实际运行结果
源码地址为
http://download.csdn.net/detail/lxzmmd/9512104