学习手机安全卫士项目源码记录(一)

  从图书馆借了一本《Android项目实战——手机安全卫士开发案例解析》,想通过学习源代码来加深对Android重点知识的理解,以及进一步复习领悟JAVA SE。接下来的两个月,一边学习一遍记录重要的知识点,希望自己能有所收获、有所提高。

  Splash界面的作用:

  1.展现产品的LOGO,提升产品的知名度。

  2.初始化的操作(初始化数据库、文件的复制、配置的读取)。

  3.根据系统的时间或日期做出相应的判断来加载不同的Splash界面(例如,QQ的登陆界面),提升用户体验。

  4.连接服务器,检查获取更新信息,提示用户升级。在此项目中用于连接服务器,检查版本是否需要更新下载,以及初始化数据库。

问题1:PackageManager pm=this.getPackageManager();Android源代码里他是怎么通过getPackageManager()这个方法实例化PackageManager的?

  大概明白点了,在源码里找到了ContextImpl.java,里面就有getPackageManager()方法,此方法源码如下:

 1 @Override
 2 public PackageManager getPackageManager(){
 3     if(mPackageManager !=null){
 4     return mPackageManager;
 5     }
 6     IPackageManager pm=ActivityThread.getPackageManager();
 7     if(pm!=null){
 8     //Doesn't matter if we make more than one instance.
 9     return (mPackageManager=new ApplicationPackageManager(this, pm);
10     }
11     return null;
12 }

  

去掉标题栏以及设置全屏模式
//设置为无标题栏
requestWindowFeature(Window.FEATURE_NO_TITLE);
//设置为全屏模式
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

 
  Splash界面加载时的具体流程:

  1.为Splash界面做一个淡进(由暗变明)的动画效果,动画播放时间为2秒。

  2.在Splash界面加载时,连接服务器检查软件是否需要更新(后面还需要实现对数据库的复制),其流程如下图:

    

 context和getApplicationContext()的区别

   getApplicationContext()返回应用的上下文,生命周期是整个应用,应用摧毁它才摧毁。

  context返回当前Activity的上下文,Activity执行了onDestory()方法后摧毁它就摧毁,如果组件是属于当前Activity的,就应该使用context。

  getBaseContext()返回由构造函数指定或setBaseContext()设置的上下文。

  如果我们要通过一个上下文来执行某个动作,且希望该动作一直处于“活跃”状态,那么应当考虑使用getApplicationContext获取上下文。例如:当使用数据库时,需要传递一个上下文,如果传递的是Activity.this,那么,当Aactivity执行onDestory()方法时,数据库就会被关闭,应用程序就会出现错误。

  Dialog窗体是Activity的一部分,一般当关乎到生命周期时,我们才会仔细分析使用哪个上下文,一般情况下使用Activity.this。

 创建并显示对话框

 1 private void showUpdateDialog(){
 2 // 创建对话框的构造器
 3         AlertDialog.Builder builder=new Builder(this);
 4         //设置对话框提示标题左边的提示标语
 5         builder.setIcon(getResources().getDrawable(R.drawable.xxx));
 6         //设置对话框的标题
 7         builder.setTitle("升级提示");
 8         //设置对话框的提示内容
 9         builder.setMessage("");
10         //设置升级按钮
11         builder.setPositiveButton("升级", new OnClickListener(){
12 
13             @Override
14             public void onClick(DialogInterface dialog, int which) {
15                 
16             }
17         });
18         builder.setNegativeButton("取消", new OnClickListener(){
19             @Override
20             public void onClick(DialogInterface dialog, int which) {
21             }});
22         builder.create().show();
23     }

  private Handler handler=new Handler(){

  public void handleMessage(Message msg){

   switch(msg.what){ 

      case  XXX:

      .......

      break; 

      ......  

   }}}

 该对象是为了接收子类线程发送过来的消息(子线程与主线程进行通信),将子线程发送过来的消息在handleMessage(Message msg)方法中进行处理。

为什么要将URL放在res/values目录下的config.xml中?

  String serverurl=getResources().getString(R.string.serverurl):得到访问服务端配置信息(即服务端的info.xml文件)的URL地址。

  该URL地址存在values文件夹下的config.xml文件中,目的在于,当这个URL需要变更时,不需要在代码中进行修改,只需要修改这个配置文件即可实现,降低了开发成本。配置信息如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="serverurl">http://192.168.0.4:8080/info.xml<string>
<resources>

  问题2:在手机上运行调试时出现了如下图错误

    很明显是空指针异常。而且是在SplashActivity.java源代码中的第95行。去代码中仔细查看,原来是缺少一句

r1_splash=findViewById(R.id.rl_splash);即使前面已经声明了全局变量private View r1_splash;但并没有初始化r1_splash。

解析服务端的info.xml文件时,需要创建一个解析XML的业务方法。代码如下:

  

 1 /**
 2  * 解析XML数据
 3  *
 4  */
 5 public class UpdateInfoParser {
 6     
 7     public static UpdateInfo getUpdateInfo(InputStream is) throws XmlPullParserException, IOException{
 8         //获得一个XmlPullParser的解析实例
 9         XmlPullParser parser=Xml.newPullParser();
10         //将要解析的文件流传入
11         parser.setInput(is,"utf-8");
12         //创建UpdateInfo实例,用于存放解析得到的XML中的数据,最终将该对象返回
13         UpdateInfo info=new UpdateInfo();
14         //获取当前触发的事件类型
15         int type=parser.getEventType();
16         //使用while循环,如果获得的事件码是文档结束,那么就结束解析
17         while(type!=XmlPullParser.END_DOCUMENT){
18             if(type==XmlPullParser.START_TAG){//开始元素
19                 if("version".equals(parser.getName())){
20                 //判断当前元素是否是读者需要检索的元素,下同
21                 //因为内容页相当于一个节点,所以获取内容时需要调用parser对象的nextText()
22                 //方法才可以得到内容
23                     String version=parser.nextText();
24                     info.setVersion(version);
25                 }else if("description".equals(parser.getName())){
26                     String description=parser.nextText();
27                     info.setDescription(description);
28                 }else if("apkurl".equals(parser.getName())){
29                     String apkurl=parser.nextText();
30                     info.setApkurl(apkurl);
31                 }
32             }
33             type=parser.next();//触发下一个事件,并返回事件的类型
34         }
35         return info;
36     }
37 }

  为Splash界面播放一个动画

  AlphaAnimation aa=new AlphaAnimation(0.3f,1.0f)设置了一个透明度由0.3f~1.0f的淡入的动画效果。0.0f表示完全透明,1.0f表示正常显示效果。

  aa.setDuration(2000)为设置动画的执行时间,单位为毫秒。

  r1_splash.startAnimation(aa)为启动动画。

  此外,查阅API还发现Animation类还有这几个子类AnimationSet,RotateAnimation,ScaleAnimation,TranslateAnimation。

new Thread(new CheckVerisonTask()){}.start()

  由于联网的过程是一般是一个耗时的操作,为了避免出现ANR异常,我们在主线程中开启一个子线程用于联网核对版本号信息。此时我们还应该在清单文件中配置网络权限的信息: 

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

Handler

  handler.sendMessage(msg):通过handler对象向主线程中发送消息,然后在Handler的handleMessage(Message msg)方法中可以处理该消息。

  msg.what=GET_INFO_SUCCESS:为msg做一个标记,这样在Handler的handleMessage(Message msg)的Switch中可以获取该标记,以便识别是哪个消息。

下载APK的工具类:

 1 /**
 2  * 下载的工具类:下载文件的路径;下载文件后保存的路径;关心进度条;上、下文
 3  *
 4  */
 5 public class DownLoadUtil {
 6     /**
 7 
 8      * 下载一个文件
 9      * @param urlpath
10      * 路径
11      * @param filepath
12      * 保存到本地的文件路径
13      * @param pd
14      * 进度条对话框
15      * @return
16      * 下载后的apk
17      */
18     public static File getFile(String urlpath,String filepath,ProgressDialog pd){
19     try {
20         URL url=new URL(urlpath);
21         File file=new File(filepath);
22             FileOutputStream fos=new FileOutputStream(file);
23             HttpURLConnection conn=(HttpURLConnection) url.openConnection();
24             //下载的请求是GET方式,conn的默认方式也是GET请求
25             conn.setRequestMethod("GET");
26             //服务端的响应时间
27             conn.setConnectTimeout(5000);
28             //获取服务端的文件总长度
29             int max=conn.getContentLength();
30             //将进度条的最大值设置为要下载的文件的总长度
31             pd.setMax(max);
32             //获取要下载的apk的文件输入流
33             InputStream is=conn.getInputStream();
34             //设置一个缓存区
35             byte[] buf=new byte[1024];
36             int len=0;
37             int process=0;
38             while((len=is.read(buf))!=-1){
39                 fos.write(buf, 0, len);
40                 //没读取一次输入流,就刷新一次下载进度
41                 process+=len;
42                 pd.setProgress(process);
43                 //设置睡眠时间,便于观察下载进度
44                 Thread.sleep(30);
45             }
46             //刷新缓存数据到文件中
47             fos.flush();
48             //关流
49             fos.close();
50             is.close();
51             return file;
52         } catch (Exception e) {
53             // TODO Auto-generated catch block
54             e.printStackTrace();
55             return null;
56         }
57     }

获取一个路径中的文件名的代码:

 1         /**
 2          * 获取一个路径中的文件名。例如:mymobilesafe.apk
 3          * @param urlpath
 4          * @return
 5          */
 6         public static String getFileName(String urlpath){
 7             return urlpath.substring(urlpath.lastIndexOf("/")+1, urlpath.length());
 8             
 9         
10     }

涉及对Sdcard的操作,需要在清单文件中配置相应的权限:

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

安装APK的方法:

 1 /**
 2      * 安装一个apk文件
 3      * @param file 要安装的完整的文件名
 4      */
 5     protected void installApk(File file){
 6         //隐式意图
 7         Intent intent=new Intent();
 8         intent.setAction("android.intent.action.VIEW");//设置意图的动作
 9         intent.addCategory("android.intent.category.DEFAULT");
10         //为意图添加额外的数据
11 intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");//设置意图的类型和数据
12         startActivity(intent);//激活该意图

LayoutInflater:

  布局填充器,通过getSystemService(Context.LAYOUT_INFLATER_SERVICE)方法可以实例化一个LayoutInflater,也可以通过LayoutInflater inflater=getLayoutInflater();来获取。然后使用inflate()方法在填充xml布局文件。

  还可以使用LayoutInflater.from(this)来实例化。

  最简单的方式是 View view=View.inflate(this, R.layout.xxx, null);

  LayoutInflater的作用类似于findViewById(),不同的是LayoutInflater填充的是layout文件夹下的xml布局文件进行实例化,而findViewById()是某个xml布局文件中的某个widget控件。setContentView()一旦调用, layout就会立刻显示UI;而inflate只会把Layout形成一个以View类实现成的对象。

   一般在activity中通过setContentView()将界面显示出来,然后才能使用findViewById()是找xml布局文件下的具体widget控件(如Button、TextView等)

  但是如果还需要对其他布局进行操作,这就需要LayoutInflater动态加载, LayoutInflater是用来找res/layout/下的xml布局文件,并且实例化。

gravity和layout_gravity的区别

  LinearLayout有两个非常相似的属性:gravity和layout_gravity。

区别在于:android:gravity 是对该view中内容的限定。比如一个button里面的text,可以通过gravity设置text相对于button靠左,靠右,居中。

     android:layout_gravity是用来设置该view相对于父view的位置。比如一个button在LinearLayout里,通过layout_gravity就可以设置button相对于LinearLayout是靠左,靠右还是居中。

  此项目中,如果在main_item.xml中缺少android:gravity="center_horizontal",图标底下的文字就无法相对于图标居中显示。

baseAdapter

  baseAdapter主要作用是给Spiner、ListView、GirdView来填充数据的。这个适配器取得了Adapter的最大控制权:程序要创建多少个列表项,每个列表项的组件都由开发者来决定。继承了baseAdapter接口后需要覆写如下四个方法。

  getCount():该方法返回值控制该Adapter将会包含多少个列表项。

  getItem(int position):返回第position处的列表项的对象,如果不对这个返回的对象做相应的操作,可以返回一个null。

  getItemId(int position):返回第position处的列表项的ID。

  getView(int position,View convertView,ViewGroup parent):该方法的返回值决定第position出的列表的组件。

 GridView在绘制的时候,系统首先调用getCount()函数,根据它的返回值得到GridView的长度,然后根据这个长度,调用getView逐一绘制每一个(行)。如果getCount()返回值是0,列表将不显示,若return 1 ,则只显示一行。系统显示列表时,首先实例化一个适配器,在适配器中getView方法中完成数据的映射。系统在绘制GridView中的每一个view时,都会调用getView()方法,getView()有三个参数,position表示显示的是第几个,covertView是从布局文件中通过inflate方法填充的布局,即将item.xml文件变为View实例用来显示,然后将xml文件中各个组件有findViewById()实例化。

适配器对象MainAdapter的代码如下:

 1 public class MainAdapter extends BaseAdapter {
 2     //将tv_name和iv_icon定义为静态的,有利于提高程序运行的效率,
 3     //因为getView()方法会被调用好多次,定义为静态后(在内存中只有一份)就避免了
 4     //多次在栈内存中创建变量tv_name和iv_icon的引用
 5     private static  TextView tv_name;
 6     private static ImageView iv_icon;
 7     //布局填充器
 8     private LayoutInflater inflater;
 9     //接收MainActivity传递过来的上下文对象
10     private Context context;
11     //将9个item的每一个图片对应的id都存入该数组中
12     private  int[] icons={
13         R.drawable.widget01,R.drawable.widget02,R.drawable.widget03,
14         R.drawable.widget04,R.drawable.widget05,R.drawable.widget06,
15         R.drawable.widget07,R.drawable.widget08,R.drawable.widget09
16     };
17     //将9个item的每一个标题都存入该数组中
18     private String[] names={
19             "手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","系统优化","高级工具","设置中心"
20     };
21     public MainAdapter(Context context){
22         this.context=context;
23         //获取系统中的布局填充器
24         inflater=(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
25     }
26     /**
27      * 返回gridview有多少个item
28      */
29     @Override
30     public int getCount() {
31         // TODO Auto-generated method stub
32         return names.length;
33     }
34     /**
35      * 获取每个item对象,如果不对这个返回的item对象做相应的操作
36      * 可以返回一个null,这里我们简单处理一下,返回position
37      */
38     @Override
39     public Object getItem(int position) {
40         // TODO Auto-generated method stub
41         return position;
42     }
43     /**
44      * 返回当前item的id
45      */
46     @Override
47     public long getItemId(int position) {
48         // TODO Auto-generated method stub
49         return position;
50     }
51     /**
52      * 返回每一个gridview条目中的view对象
53      */
54     @Override
55     public View getView(int position, View convertView, ViewGroup parent) {
56         // TODO Auto-generated method stub
57         View view=inflater.inflate(R.layout.main_item, null);
58          tv_name=(TextView) view.findViewById(R.id.tv_main_item_name);
59          iv_icon=(ImageView) view.findViewById(R.id.iv_main_item_icon);
60         tv_name.setText(names[position]);
61         iv_icon.setImageResource(icons[position]);
62         return view;
63     }
64 }

  

SharedPreferences

  提供了一种轻量级的数据存储方式,主要应用在数据量比较少的情况下。它以“key-value”方式将数据保存在一个xml文件中。

  使用SharedPreferences存储数据比较简单,步骤如下:

  1)使用getSharedPreferences()生成SharedPreferences对象。调用getSharedPreferences()方法时,需要指定如下两个参数:

    一是存储数据的xml文件名,这个xml文件存储在"/data/data/包名/shared_prefs/"目录下,其文件名由该参数指定,注意,文件名不需要指定后缀(.xml),系统会在该文件名之后自动添加xml后缀并创建之。

    二是操作模式,其取值有三种:MODE_WORLD_READABLE(可读),MODE_WORLD_WRITEABLE(可写)和MODE_PRIVATE(私有)。

  2)使用SharedPreferences.Editor的putXXX()方法保存数据。

  3)使用SharedPreferences.Editor的commit()方法将上一步保存的数据写到xml文件中。

  4)使用SharedPreference的getXXX()方法获取相应的数据。

源代码中:

1  sp=getSharedPreferences("config",MODE_PRIVATE);
2  boolean autoupdate=sp.getBoolean("autoupdate",true);

  第一行,获取config.xml文件,如果该文件不存在,将会自动创建该文件,文件的操作类型为私有

  第二行,从sp对应的config.xml文件中获取autoupdate所对应的boolean值,如果没有查找到该键,

      将返回默认的boolean值true(即第二个参数)。

Checkbox的勾选状态发生改变时,用setOnCheckedChangeListener(new OnCheckedChangeListener(){});进行监听。代码如下:

 1 /**
 2          * 当Checkbox的状态发生改变时onCheckedChanged()方法被回调
 3          */
 4         cb_setting_autoupdate.setOnCheckedChangeListener(new OnCheckedChangeListener() {
 5             //第一个参数:当前的Checkbox;第二个参数:当前的Checkbox是否处于勾选状态
 6             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
 7                 // TODO Auto-generated method stub
 8                 //获取编辑器
 9                 Editor editor=sp.edit();
10                 //持久化存储当前Checkbox的状态,当下次进入时,已然可以保存当前设置的状态
11                 editor.putBoolean("autoupdate", isChecked);
12                 //将数据真正提交到sp里面
13                 editor.commit();
14                 if(isChecked){//Checkbox处于选中效果
15                 //当Checkbox处于勾选状态时,表示自动更新已经开启,同时修改字体颜色
16                     tv_setting_autoupdate_status.setTextColor(Color.GREEN);
17                     tv_setting_autoupdate_status.setText("自动更新已经开启");
18                 }else{//Checkbox处于未勾选状态
19                     tv_setting_autoupdate_status.setTextColor(Color.RED);
20                     tv_setting_autoupdate_status.setText("自动更新已经关闭");
21                     
22                 }
23             }
24         });

为GridView对象中的item设置单击时的事件要使用setOnClickListener(new OnItemClickListener(){});

代码如下:

 1 //为gv_main对象设置一个适配器,该适配器的作用是为每个item填充对应的数据
 2         gv_main.setAdapter(new MainAdapter(this));
 3         //为GridView对象中的item设置单击时的监听事件
 4         gv_main.setOnItemClickListener(new OnItemClickListener(){
 5             //参数一:item的父控件,也就是GridView
 6             //参数二:当前单击的item
 7             //参数三:当前单击的item在GridView中的位置
 8             //参数四:id的值为单击GridView的哪一项对应的数值,单击GridView第九页,那么id就等于8
 9             @Override
10             public void onItemClick(AdapterView<?> parent, View view,
11                     int position, long id) {
12                 // TODO Auto-generated method stub
13                 switch(position){
14                 case 8://设置中心
15                     //跳转到”设置中心“对应的Activity界面
16                     Intent settingIntent=new Intent(MainActivity.this,SettingCenterActivity.class);
17                     startActivity(settingIntent);
18                     break;
19                 }
20             }
21         });

android.view.View.OnClickListener与content.DialogInterface.OnClickListener()冲突

  View.OnClickListener:  Interface definition for a callback to be invoked when a view is clicked.

  DialogInterface.OnClickListener:  Interface used to allow the creator of a dialog to run some code when an item on the     dialog is clicked..

  在同一个activity中需要用到这个两个监听事件,若同时导入会有冲突。 解决办法是调用时都带上全路径名。例如

new android.content.DialogInterface.OnClickListener();

又一次遇到空指针异常

  View view=View.inflate(this, R.layout.first_entry_dialog, null);
        //查找view对象中的各个控件
        et_first_dialog_pwd=(EditText)view.findViewById(R.id.et_first_dialog_pwd);在这行代码中没有写view,而Activity类也有findViewById方法,如果不加view,则是默认调用this.findViewById,而Activity窗体布局中并没有et_first_dialog_pwd,所以导致了空指针异常。

MD5加密工具类代码(Md5Encoder.java)

 1 public class Md5Encoder {
 2     public static String encode(String password){
 3         try{
 4             //获取到数字消息的摘要器
 5             MessageDigest digest=MessageDigest.getInstance("MD5");
 6             //执行加密操作
 7             byte[] result=digest.digest(password.getBytes());
 8             StringBuilder sb=new StringBuilder();
 9             //将每个byte字节的数据转换成十六进制的数据
10             for(int i=0;i<result.length;i++){
11                 int number=result[i]&0xff;////向int[]赋值,&0xff的作用是消除对int前24位的影响
12 //(计算机中使用补码存储数据,如果直接将一个第一位为“1”的byte值赋给int,则前24为将为“1”
13                 String str=Integer.toHexString(number);//将十进制的number转换成十六进制的数据
14                 if(str.length()==1){//判断加密后的字符长度,如果长度为1,则在该字符前面补0
15                     sb.append("0");
16                     sb.append(str);
17                 }else{
18                     sb.append(str);
19                 }
20             }
21             return sb.toString();//将加密后的字符转成字符串返回
22         }catch(NoSuchAlgorithmException e){//加密器没有被找到,该异常不可能发生,因为填入的“MD5”是正确的
23             e.printStackTrace();
24             return "";
25         }
26     }
27 }

 我发现:书中52页说“然后在EditText文本框中输入”mp3“后,单击”确定“按钮,当再次进入程序主界面时,就可以看到修改后的标题生效了,”

   再次进入程序主界面,需要退出应用后再次打开应用才能看到修改的标题,说明修改的动作发生在onCreate()方法里。但从LostProtectedActivity窗体返回到MainActivity窗体时,并不能看到更新。需要改进一下:还记得Activity从暂停态到运行态所触发的事件是onResume()方法,即当MainActivity重新获得焦点,开始与用户交互时回调此方法。所以我可以在MainActivity中覆写onResume()方法,即可完成更新UI,代码如下:

 1     /**
 2      * 当MainActivity重新获得焦点(即Activity开始与用户交互)时回调此方法
 3      */
 4 @Override
 5     protected void onResume() {
 6             super.onResume();
 7             sp=this.getSharedPreferences("config",MODE_PRIVATE);
 8     if((sp.getString("newname", "")!=null)){
 9            //重新加载GridView的适配器
10         gv_main.setAdapter(new MainAdapter(MainActivity.this));    

  本来考虑到使用Handler更新UI,费了好大劲还是没有成功。。才发现自己对Handler机制还是糊涂。先在这里重新温习一下Handler的基本知识吧。

  Handler:

  它的作用有两个——发送消息和处理消息,程序使用Handler发送消息,被Handler发送的消息必须被送到指定的MessageQueue。也就是说,如果Handler正常工作,必须在当前线程中有一个MessageQueue,否则消息就没有MessageQueue进行保存了。不过MessageQueue是由Looper负责管理的,也就是说,如果希望Handler正常工作,就必须在当前线程中有一个Looper对象。为了保证当前线程中有Looper对象,可以分如下两种情况处理。

  1.主UI线程中,系统已经初始化了一个Looper对象,因此程序直接创建Handler即可,然后就可以通过Handler来发送消息、处理消息。

  2.子线程中,必须由我们创建一个Looper对象,并启动它。创建Looper对象调用它的prepare()方法即可。

 prepare()方法保证每个线程最多只有一个Looper对象。然后调用Looper的静态loop()方法来启动它。loop()方法使用一个死循环不断取出MessageQueue中的消息,并将取出的消息分给该消息对应的Handler进行处理。

  在子线程中使用Handler的步骤如下:

  1.调用Looper的prepare()方法为当前线程创建Looper对象,创建Looper对象时,它的构造器会创建与之配套的MessageQueue。

  2.有了Looper之后,创建Handler子类的实例,覆写handlerMessage()方法,该方法负责处理来自于其他线程的消息。

  3.调用Looper的loop()方法启动Looper。

原文地址:https://www.cnblogs.com/dazuihou/p/3588780.html