Android开发实战《手机安全卫士》——5.“高级工具”模块实现 & 获取经纬度 & 锁屏 & 卸载

1.手机防盗——接收短信播放音乐

之前我们已经“手机防盗”模块中的界面和跳转逻辑编写完毕,现在需要着手实现该模块的4种防盗功能,我们先实现较为简单的功能——即播放报警音乐的功能,如图中的红框所示:

在这里插入图片描述
由于涉及到短信接收,可以联想到以下几个步骤:

  1. 接收短信的时候,会发送广播,对系统的广播进行监听;
  2. 监听短信内容,如果内容中包含关键字#*alarm*#,播放报警音乐

在receiver包下新建SmsReceiver,继承BroadcastReceiver,作为接收到短信发送时的广播接收器,代码如下:

package com.example.mobilesafe.receiver;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.media.MediaPlayer;
import android.telephony.SmsMessage;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.utils.SharedPreferencesUtil;

public class SmsReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 1.判断是否开启了防盗保护
        boolean open_security = SharedPreferencesUtil.getBoolean(context, ConstantValue.OPEN_SECURITY, false);
        // 2.获取短信内容
        if (open_security){
            Object[] pdus = (Object[])intent.getExtras().get("pdus");
            // 3.循环遍历短信的过程
            for (Object object : pdus) {
                // 4.获取短信对象
                SmsMessage sms = SmsMessage.createFromPdu((byte[]) object);
                // 5.获取短信对象的基本信息
                String originatingAddress = sms.getOriginatingAddress(); // 短信地址
                String messageBody = sms.getMessageBody(); // 短信内容
                // 6.判断是否包含播放音乐的关键字
                if (messageBody.contains("#*alarm*#")){
                    // 7.播放音乐(准备音乐,MediaPlay,音乐提前准备好放在Raw文件夹中)
                    MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.ylzs);
                    mediaPlayer.setLooping(true); // 一直循环音乐
                    mediaPlayer.start();
                }
            }
        }
    }
}

逻辑代码写完后,记得在manifest.xml中添加接收短信的权限,代码如下:

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

2.手机防盗——定位方式

完成了“接收短信播放音乐的功能后”,接下来需要完成第二个功能,即“GPS追踪”,涉及到经纬度的获取,根绝精确程度,获取手机经纬度共有三种方式:

  1. 网络定位:IP地址对应一个真实的物理地址,但是只能定位到大体位置;
  2. 基站定位:通常由三个基站进行定位,确认物理地址;
  3. 卫星定位:顾名思义,采用卫星进行定位。

获取经纬度的相关类有LocationListenerLocationManager等,在获取坐标时,还需要在清单文件下声明以下权限:

    <!-- 允许模拟器模拟位置坐标的权限 -->
    <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>
    <!-- 获取粗略坐标的权限(网络定位时使用)   -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <!-- 获取精准坐标的权限   -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

默认获取的坐标,是真实正确的坐标,但是由于地图偏振,所以无法在偏振地图上定位到真实的位置上。但是已经有人破解了地图偏振算法,即将获取到的坐标转换成**火星坐标(即偏振坐标)**放到偏振地图上,就能够得到坐标对应的真实位置。

在util包下新建ModifyOffsetUtil,作为获取火星坐标的工具类,代码如下:

package com.example.mobilesafe.utils;

import java.io.InputStream;
import java.io.ObjectInputStream;

/**
 * 火星地球坐标转化.地图坐标修偏
 * 
 */
public class ModifyOffsetUtil {
	private static ModifyOffsetUtil modifyOffset;
	static double[] X = new double[660 * 450];
	static double[] Y = new double[660 * 450];


	private ModifyOffsetUtil(InputStream inputStream) throws Exception {
		init(inputStream);
	}

	public synchronized static ModifyOffsetUtil getInstance(InputStream is) throws Exception {
		if (modifyOffset == null) {
			modifyOffset = new ModifyOffsetUtil(is);
		}
		return modifyOffset;
	}

	public void init(InputStream inputStream) throws Exception {
		ObjectInputStream in = new ObjectInputStream(inputStream);
		try {
			int i = 0;
			while (in.available() > 0) {
				if ((i & 1) == 1) {
					Y[(i - 1) >> 1] = in.readInt() / 100000.0d;
					;
				} else {
					X[i >> 1] = in.readInt() / 100000.0d;
					;
				}
				i++;
			}
		} finally {
			if (in != null)
				in.close();
		}
	}

	// standard -> china
	public PointDouble s2c(PointDouble pt) {
		int cnt = 10;
		double x = pt.x, y = pt.y;
		while (cnt-- > 0) {
			if (x < 71.9989d || x > 137.8998d || y < 9.9997d || y > 54.8996d)
				return pt;
			int ix = (int) (10.0d * (x - 72.0d));
			int iy = (int) (10.0d * (y - 10.0d));
			double dx = (x - 72.0d - 0.1d * ix) * 10.0d;
			double dy = (y - 10.0d - 0.1d * iy) * 10.0d;
			x = (x + pt.x + (1.0d - dx) * (1.0d - dy) * X[ix + 660 * iy] + dx
					* (1.0d - dy) * X[ix + 660 * iy + 1] + dx * dy
					* X[ix + 660 * iy + 661] + (1.0d - dx) * dy
					* X[ix + 660 * iy + 660] - x) / 2.0d;
			y = (y + pt.y + (1.0d - dx) * (1.0d - dy) * Y[ix + 660 * iy] + dx
					* (1.0d - dy) * Y[ix + 660 * iy + 1] + dx * dy
					* Y[ix + 660 * iy + 661] + (1.0d - dx) * dy
					* Y[ix + 660 * iy + 660] - y) / 2.0d;
		}
		return new PointDouble(x, y);
	}

	// china -> standard
	public PointDouble c2s(PointDouble pt) {
		int cnt = 10;
		double x = pt.x, y = pt.y;
		while (cnt-- > 0) {
			if (x < 71.9989d || x > 137.8998d || y < 9.9997d || y > 54.8996d)
				return pt;
			int ix = (int) (10.0d * (x - 72.0d));
			int iy = (int) (10.0d * (y - 10.0d));
			double dx = (x - 72.0d - 0.1d * ix) * 10.0d;
			double dy = (y - 10.0d - 0.1d * iy) * 10.0d;
			x = (x + pt.x - (1.0d - dx) * (1.0d - dy) * X[ix + 660 * iy] - dx
					* (1.0d - dy) * X[ix + 660 * iy + 1] - dx * dy
					* X[ix + 660 * iy + 661] - (1.0d - dx) * dy
					* X[ix + 660 * iy + 660] + x) / 2.0d;
			y = (y + pt.y - (1.0d - dx) * (1.0d - dy) * Y[ix + 660 * iy] - dx
					* (1.0d - dy) * Y[ix + 660 * iy + 1] - dx * dy
					* Y[ix + 660 * iy + 661] - (1.0d - dx) * dy
					* Y[ix + 660 * iy + 660] + y) / 2.0d;
		}
		return new PointDouble(x, y);
	}

}

class PointDouble {
	double x, y;

	PointDouble(double x, double y) {
		this.x = x;
		this.y = y;
	}

	public String toString() {
		return "x=" + x + ", y=" + y;
	}
}

3.手机防盗——获取经纬度坐标

经过上面小节的分析,我们来正式完成获取经纬度的业务实现。为了方便起见,这里我们使用Service来绑定获取经纬度坐标的业务,达到实时获取坐标的功能。

修改SmsReceiver,加入判断开启位置服务的代码,代码如下:

package com.example.mobilesafe.receiver;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.media.MediaPlayer;
import android.telephony.SmsMessage;

import com.example.mobilesafe.service.LocationService;
import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.utils.SharedPreferencesUtil;

public class SmsReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 1.判断是否开启了防盗保护
        boolean open_security = SharedPreferencesUtil.getBoolean(context, ConstantValue.OPEN_SECURITY, false);
        // 2.获取短信内容
        if (open_security){
            Object[] pdus = (Object[])intent.getExtras().get("pdus");
            // 3.循环遍历短信的过程
            for (Object object : pdus) {
                // 4.获取短信对象
                SmsMessage sms = SmsMessage.createFromPdu((byte[]) object);
                // 5.获取短信对象的基本信息
                String originatingAddress = sms.getOriginatingAddress(); // 短信地址
                String messageBody = sms.getMessageBody(); // 短信内容
                // 6.判断是否包含播放音乐的关键字
                if (messageBody.contains("#*alarm*#")){
                    // 7.播放音乐(准备音乐,MediaPlay,音乐提前准备好放在Raw文件夹中)
                    MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.ylzs);
                    mediaPlayer.setLooping(true); // 一直循环音乐
                    mediaPlayer.start();
                }
                // 8.判断是否包含定位的关键字
                if (messageBody.contains("#*location*#")){
                    // 9.开启获取位置的服务
                    context.startService(new Intent(context, LocationService.class));
                }
            }
        }
    }
}

新建service包,在包下新建名为LocationService的类,作为获取位置的服务,记得在manifest下声明这个Service,代码如下:

package com.example.mobilesafe.service;

import android.Manifest;
import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.IBinder;
import android.telephony.SmsManager;

import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;

import com.example.mobilesafe.activity.SettingActivity;

public class LocationService extends Service {

    @Override
    public void onCreate() {
        super.onCreate();
        // 获取手机的经纬度坐标
        // 1.获取位置管理者对象
        LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);
        // 2.以最优的方式获取经纬度
        Criteria criteria = new Criteria();
        criteria.setCostAllowed(true); // 允许使用流量
        criteria.setAccuracy(Criteria.ACCURACY_FINE); // 指定获取经纬度的精确度
        String bestProvider = lm.getBestProvider(criteria, true);
        // 3.在一定时间间隔,移动一定距离后获取经纬度坐标,注意动态申请权限
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            return;
        }
        lm.requestLocationUpdates(bestProvider, 0, 0, new LocationListener() {
            @Override
            public void onLocationChanged(Location location) {
                // 经度
                double longitude = location.getLongitude();
                // 纬度
                double latitude = location.getLatitude();

                // 4.发送短信给另一台手机报警
                SmsManager sms = SmsManager.getDefault();
                sms.sendTextMessage("5556",null,"longtituede change!!" + longitude,null,null);
            }

            @Override
            public void onStatusChanged(String provider, int status, Bundle extras) {
                // GPS状态发生切换的事件监听
            }

            @Override
            public void onProviderEnabled(String provider) {
                // GPS开启的时候的事件监听
            }

            @Override
            public void onProviderDisabled(String provider) {
                // GPS关闭的时候的事件监听
            }
        });
    }

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

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

    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}

4.手机防盗——设备管理器使用

我们在上面的小节中完成了防盗模块的两个功能,接下来完成剩下的两个功能——数据销毁和远程锁屏。由于这两个功能都会涉及到设备管理器,所以这一节中简单介绍一下。

设备管理器本质上是一个广播接收器BroadCastReceiver,其中涉及到的api主要为:DeviceAdminReceiverComponentName,要使用设备管理器,大概分为以下步骤:

  1. 查看Google文档 Administration -> device polices -> 清单文件的配置
  2. 将清单文件中对应的广播接受者子类创建出来,需要继承DeviceAdminReceiver
  3. 将清单文件中的错误进行修复(创建对应xml文件),对应代码如下:
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
		  <uses-policies>
		    <limit-password />
		    <watch-login />
		    <reset-password />
		    <force-lock />
		    <wipe-data />
		    <expire-password />
		    <encrypted-storage />
		    <disable-camera />
		  </uses-policies>
		</device-admin>
  1. 开启激活界面的Activity,对应代码如下:
ComponentName mDeviceAdminSample = new ComponentName(context, DeviceAdmin.class);
Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, mDeviceAdminSample);
intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION,mActivity.getString(R.string.add_admin_extra_app_text));
startActivity(intent);

5.手机防盗——锁屏 & 数据清除 & 卸载

要想实现锁屏功能,需要在开启设备管理器下(即需要激活)的前提下调用api:DevicePolicyManager来获取对象,锁屏的api为lockNow(),重置密码的api为resetPassword("密码",0),整体流程为:

// 1.创建对象
DevicePolicyManager mDpm = (DevicePolicyManager)getSystemService(Context.DEVICE_POLICY_SERVICE);
// 2.锁屏
mDpm.lockNow();
// 3.重置密码为空
mDpm.resetPassword("密码",0);

要想实现数据清除功能,即将数据全部清除(同时取消激活),同样需要调用api:DevicePolicyManager来获取对象,清除手机数据的api为:wipeData(0),而清除手机SD卡数据的api为:wipeData(DevicePolicyManager.WIPE_EXTERNAL_STORAGE)

// 1.创建对象
DevicePolicyManager mDpm = (DevicePolicyManager)getSystemService(Context.DEVICE_POLICY_SERVICE);
// 2.清除手机数据
mDpm.wipeData(0);

要想实现卸载功能,需要注意以下两点:

  • 在设备管理器中没有激活的应用,可以卸载;
  • 在设备管理器中有激活的应用,不可以卸载,系统会提示取消在设备管理器中激活,然后才可以卸载;
  • 卸载是Android系统中自带的功能,可以调用固有api(查看PackageInstaller源码,找到uninstallActivity源码,调用隐式Intent匹配对应的Action、CateGory、Data(即应用的包名)就可以卸载指定应用)。

对应的调用代码如下:

Intent intent = new Intent("android.intent.action.DELETE");
intent.addCategory("android.intent.category.DEFAULT");
intent.setData("package:" + Uri.parse(getPackageName()));
startActivity(intent);

最后,整体修改过后的SmsReceiver代码如下:

package com.example.mobilesafe.receiver;

        import android.app.admin.DevicePolicyManager;
        import android.content.BroadcastReceiver;
        import android.content.Context;
        import android.content.Intent;
        import android.media.MediaPlayer;
        import android.telephony.SmsMessage;

        import com.example.mobilesafe.service.LocationService;
        import com.example.mobilesafe.R;
        import com.example.mobilesafe.constant.ConstantValue;
        import com.example.mobilesafe.utils.SharedPreferencesUtil;

public class SmsReceiver extends BroadcastReceiver {
    
    @Override
    public void onReceive(Context context, Intent intent) {

        // 0.创建DevicePolicyManager对象
        DevicePolicyManager mDpm = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
        
        // 1.判断是否开启了防盗保护
        boolean open_security = SharedPreferencesUtil.getBoolean(context, ConstantValue.OPEN_SECURITY, false);
        // 2.获取短信内容
        if (open_security){
            Object[] pdus = (Object[])intent.getExtras().get("pdus");
            // 3.循环遍历短信的过程
            for (Object object : pdus) {
                // 4.获取短信对象
                SmsMessage sms = SmsMessage.createFromPdu((byte[]) object);
                // 5.获取短信对象的基本信息
                String originatingAddress = sms.getOriginatingAddress(); // 短信地址
                String messageBody = sms.getMessageBody(); // 短信内容
                // 6.判断是否包含播放音乐的关键字
                if (messageBody.contains("#*alarm*#")){
                    // 7.播放音乐(准备音乐,MediaPlay,音乐提前准备好放在Raw文件夹中)
                    MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.ylzs);
                    mediaPlayer.setLooping(true); // 一直循环音乐
                    mediaPlayer.start();
                }
                // 8.判断是否包含定位的关键字
                if (messageBody.contains("#*location*#")){
                    // 9.开启获取位置的服务
                    context.startService(new Intent(context, LocationService.class));
                }
                // 10.判断是否包含锁屏的关键字
                if (messageBody.contains("#*lockscreen*")){
                    // 锁屏
                    mDpm.lockNow();
                    // 重置密码为空
                    mDpm.resetPassword("密码",0);
                }
                // 11.判断是否包含清除数据的关键字
                if (messageBody.contains("#*wipedate*")){
                    // 清除手机数据
                    mDpm.wipeData(0);
                }
            }
        }
    }
}

写到这里,手机防盗的模块我们已经完全实现了,接下来需要实现高级工具的模块功能。

6.高级工具——归属地查询

“高级工具”模块总共具有四个功能,如图所示:

在这里插入图片描述

这一节我们需要完成该模块下的第一个功能——归属地查询,即查询某个号码对应的相关信息,如图所示:
在这里插入图片描述

该功能需要实现:

  1. 实时查询 & 点击按钮查询
  2. 输入框的抖动效果
  3. 手机的震动效果

我们先修改HomeActivity中initData()方法中跳转到“高级工具”的跳转逻辑,代码如下:

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 7:
                        // 高级工具
                        startActivity(new Intent(getApplicationContext(),AToolActivity.class));
                        break;
                    case 8:
                        // 设置中心
                        Intent intent = new Intent(getApplicationContext(), SettingActivity.class);
                        startActivity(intent);
                        break;
                    default:
                        break;
                }
            }
        });
    }

与此同时,创建名为AToolActivity的Activity,作为“高级中心”的Activity,套用Empty Activity即可。随后,我们编写一个名为selector_atool_item_bg.xml的按钮点击的选择器,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 选中状态:灰色  -->
    <item android:state_pressed="true" android:drawable="@android:color/darker_gray"/>
    <!-- 未选中状态:粉红色  -->
    <item android:drawable="@android:color/holo_red_light"/>
</selector>

然后修改activity_a_tool.xml,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<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"/>

</LinearLayout>

修改AToolActivity,添加initPhoneAddress()方法,该方法要跳转到查询电话归属地页面,代码如下:

package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class AToolActivity extends AppCompatActivity {

    private TextView tv_query_phone_address;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_a_tool);

        // 1.电话归属地查询方法
        initPhoneAddress();
    }

    /**
     * 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));
            }
        });
    }
}

创建对应的QueryAddressActivity,同样套用Empty Activity模板,我们先修改其布局文件,即activity_query_address.xml,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<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.QueryAddressActivity"
    android:orientation="vertical">

    <TextView
        style="@style/TitleStyle"
        android:text="归属地查询"/>

    <EditText
        android:id="@+id/et_phone"
        android:hint="请输入电话号码"
        android:inputType="phone"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <Button
        android:id="@+id/btn_query"
        android:text="查询"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:id="@+id/tv_query_result"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

7.高级工具——导入数据库

为了实现归属地查询功能,我们需要使用到已经存储了海量手机号信息的数据库来进行关联查询,以此来获取到电话号码对应的归属地信息。

这里需要用到一个名为address.db的数据库,其中主要有两张表:data1和data2。这两张表的信息如下:

  • data1:主键id代表电话号码的前7位,外键outkey做关联查询使用;
  • data2:主键id和data1表中outkey关联外键,location代表电话号码的归属地信息,area代表区号(省略第一位0)。

将address.db放到main/assets目录下,然后再进行读取,将其转换在工程中的Files/Cache/SD卡,做好资源准备。

修改SplashActivity,添加initDB()方法,作为初始化数据库的方法。再创建initAddressDB()方法,当打开应用时,就将数据库载入进去,代码如下:

/**
     * 12.初始化数据库
     */
    private void initDB(){
        // 归属地数据拷贝过程
        initAddressDB("address.db");
    }

    /**
     * 13.初始化归属地数据库
     * @param dbName 数据库名称
     */
    private void initAddressDB(String dbName) {
        // 1.在files文件夹下创建同名数据库
        File filesDir = getFilesDir();
        File file = new File(filesDir, dbName);
        if (file.exists()){
            return;
        }
        InputStream stream = null;
        FileOutputStream fileOutputStream = null;
        // 2.读取第三方资产目录下的文件
        try {
            stream = getAssets().open(dbName);
            // 3.将读取的内容写入到指定文件夹的文件中
            fileOutputStream = new FileOutputStream(file);
            // 4.每次的读取内容大小
            byte[] bytes = new byte[1024];
            int temp = -1;
            while ( (temp = stream.read(bytes)) != -1){
                fileOutputStream.write(bytes,0,temp);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (stream != null && fileOutputStream != null){
                try {
                    stream.close();
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

重新运行项目,可以发现address.db文件已经存储到相应位置了,即data/data/工程包名/files/address.db,如图所示:

在这里插入图片描述

8.高级工具——编写查询过程

上一节中我们完成了数据库的导入,现在需要编写SQL查询过程。为了代码的复用性,这里将查询过程整合成Dao操作类。首先在包下创建Dao层,然后创建AddressDao作为查询地址的功能封装类,使用SQLite原生的查询语句进行管理查询,代码如下:

package com.example.mobilesafe.dao;

import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

public class AddressDao {

    // 1.指定访问数据库的路径
    public static String path = "data/data/com.example.mobilesafe/files/address.db";

    private static String mAddress;

    /**
     * 开启数据库连接,进行访问
     * @param phone 电话号码
     */
    public static String getAddress(String phone){
        
        mAddress = "未知号码";

        // 0.定义SQLite数据库对象
        SQLiteDatabase db = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY);;

        // 1.对手机号码进行正则表达式处理
        String regularExpression = "^1[3-8]\\\\d{9}";
        if (phone.matches(regularExpression)){
            phone = phone.substring(0,7); // 取电话号的前7位

            // 2.数据库查询
            Cursor cursor = db.query("data1", new String[]{"outkey"}, "id = ?", new String[]{phone}, null, null, null);
            // 3.查到即可
            if (cursor.moveToNext()){
                // 4.从data1表中获取外键outkey
                String outkey = cursor.getString(0);
                // 5.从data2表中通过outkey来获取location
                Cursor indexCursor = db.query("data2", new String[]{"location"}, "id = ?", new String[]{outkey}, null, null, null);
                if (indexCursor.moveToNext()){
                    // 6.获取查询到的电话归属地
                    mAddress = indexCursor.getString(0);
                }
            }else {
                mAddress = "未知号码";
            }
        }else {
            int length = phone.length();
            switch (length){
                case 3: // 119,110,112,114
                    mAddress = "报警电话";
                    break;
                case 4:
                    mAddress = "模拟器";
                    break;
                case 5:// 10086,99555
                    mAddress = "服务电话";
                    break;
                case 7:
                    mAddress = "固定电话";
                    break;
                case 8:
                    mAddress = "固定电话";
                    break;
                case 11: // (3 + 8) 区号 + 座机号码
                    String area = phone.substring(1, 3);
                    Cursor cursor2 = db.query("data2", new String[]{"location"}, "area = ?", new String[]{area}, null, null, null);
                    if (cursor2.moveToNext()){
                        mAddress = cursor2.getString(0);
                    }else {
                        mAddress = "未知号码";
                    }
                    break;
                case 12: // (4 + 8) 区号 + 座机号码
                    String area2 = phone.substring(1, 4);
                    Cursor cursor3 = db.query("data2", new String[]{"location"}, "area = ?", new String[]{area2}, null, null, null);
                    if (cursor3.moveToNext()){
                        mAddress = cursor3.getString(0);
                    }else {
                        mAddress = "未知号码";
                    }
                    break;
            }
        }
        return mAddress;
    }
}

注意,这里要匹配电话号码,为了避免空指针异常,需要使用到正则表达式。

9.高级工具——查询号码归属地

我们将查询逻辑编写完成了,接下来需要实现输入电话号码,点击按钮后显示归属地

修改QueryAddressActivity,完善相应逻辑,代码如下:

package com.example.mobilesafe.activity;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.AddressDao;

public class QueryAddressActivity extends AppCompatActivity {

    private EditText et_phone;

    private Button btn_query;

    private TextView tv_query_result;

    private String mAddress;

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            // 4.控制使用查询结果
            tv_query_result.setText(mAddress);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_query_address);

        // 初始化UI
        initUI();
    }

    private void initUI() {
        et_phone = findViewById(R.id.et_phone);
        btn_query = findViewById(R.id.btn_query);
        tv_query_result = findViewById(R.id.tv_query_result);

        // 1.点击查询功能,注册按钮的点击事件
        btn_query.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String phone = et_phone.getText().toString();

                // 2.查询是耗时操作,需要开启子线程
                query(phone);
            }
        });

    }

    /**
     * 查询操作,在子线程中
     * @param phone 查询电话号码
     */
    private void query(final String phone) {
        new Thread(){
            @Override
            public void run() {
                mAddress = AddressDao.getAddress(phone);
                // 3.消息机制,告知主线程查询结束,可以去使用查询结果
                mHandler.sendEmptyMessage(0);
            }
        }.start();
    }
}

为了实现文本变化时实时更新归属地,我们调用EditText组件中的addTextChangedListener方法注册一个监听器,再次修改QueryAddressActivity,代码如下:

package com.example.mobilesafe.activity;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.AddressDao;

public class QueryAddressActivity extends AppCompatActivity {

    private EditText et_phone;

    private Button btn_query;

    private TextView tv_query_result;

    private String mAddress;

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            // 4.控制使用查询结果
            tv_query_result.setText(mAddress);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_query_address);

        // 初始化UI
        initUI();
    }

    private void initUI() {
        et_phone = findViewById(R.id.et_phone);
        btn_query = findViewById(R.id.btn_query);
        tv_query_result = findViewById(R.id.tv_query_result);

        // 1.点击查询功能,注册按钮的点击事件
        btn_query.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String phone = et_phone.getText().toString();

                // 2.查询是耗时操作,需要开启子线程
                query(phone);
            }
        });

        // 5.实时查询
        et_phone.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                // 文本发生改变前
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                // 文本发生改变时
            }

            @Override
            public void afterTextChanged(Editable s) {
                // 文本发生改变后
                String phone = et_phone.getText().toString();
                query(phone);
            }
        });

    }

    /**
     * 查询操作,在子线程中
     * @param phone 查询电话号码
     */
    private void query(final String phone) {
        new Thread(){
            @Override
            public void run() {
                mAddress = AddressDao.getAddress(phone);
                // 3.消息机制,告知主线程查询结束,可以去使用查询结果
                mHandler.sendEmptyMessage(0);
            }
        }.start();
    }
}

10.高级工具——抖动效果

我们实现了查询号码归属地的功能,现在需要实现——让EditText控件在内容为空时,点击按钮后具有抖动的效果,修改QueryAddressActivity,完善按钮的点击事件逻辑,注意这里的抖动效果由官方的api来提供,代码如下:

package com.example.mobilesafe.activity;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.AddressDao;

public class QueryAddressActivity extends AppCompatActivity {

    private EditText et_phone;

    private Button btn_query;

    private TextView tv_query_result;

    private String mAddress;

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            // 4.控制使用查询结果
            tv_query_result.setText(mAddress);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_query_address);

        // 初始化UI
        initUI();
    }

    private void initUI() {
        et_phone = findViewById(R.id.et_phone);
        btn_query = findViewById(R.id.btn_query);
        tv_query_result = findViewById(R.id.tv_query_result);

        // 1.点击查询功能,注册按钮的点击事件
        btn_query.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String phone = et_phone.getText().toString();
                if (!TextUtils.isEmpty(phone)){
                    // 2.查询是耗时操作,需要开启子线程
                    query(phone);
                }else {
                    Animation animation = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.shake);
                    et_phone.startAnimation(animation);
                }
            }
        });

        // 5.实时查询
        et_phone.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                // 文本发生改变前
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                // 文本发生改变时
            }

            @Override
            public void afterTextChanged(Editable s) {
                // 文本发生改变后
                String phone = et_phone.getText().toString();
                query(phone);
            }
        });

    }

    /**
     * 查询操作,在子线程中
     * @param phone 查询电话号码
     */
    private void query(final String phone) {
        new Thread(){
            @Override
            public void run() {
                mAddress = AddressDao.getAddress(phone);
                // 3.消息机制,告知主线程查询结束,可以去使用查询结果
                mHandler.sendEmptyMessage(0);
            }
        }.start();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赈川

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值