开发手机安全卫士
此篇笔记记录了android闪屏页面的开发和主页面(静态,功能还未实现)的开发。主要记录闪屏界面中的版本检测和更新。
代码组织结构
-
根据业务逻辑划分
-
办公软件
- 出差 com.itheima.travel
- 工资 com.itheima.money
- 会议 com.itheima.meeting
-
网盘
- 上传 com.vdisk.upload
- 下载 com.vdisk.download
- 分享 com.vdisk.share
-
-
根据功能模块划分(Android开发推荐此方法)
- Activity com.itheima.mobilesafe.activty
- 后台服务 com.itheima.mobilesafe.service
- 广播接受者 com.itheima.mobilesafe.receiver
- 数据库 com.itheima.mobilesafe.db.dao
- 对象(java bean) com.itheima.mobilesafe.domain/bean
- 自定义控件 com.itheima.mobilesafe.view
- 工具类 com.itheima.mobilesafe.utils
- 业务逻辑 com.itheima.mobilesafe.engine
项目创建
-
minimum SDK 要求最低的安装版本, 安装apk前,系统会判断当前版本是否高于(包含)此版本, 是的话才允许安装
-
maxSdkVersion 要求最高的安装版本(一般不用)
-
Target SDK 目标SDK, 一般设置为开发时使用的手机版本, 这样的话,系统在运行我的apk时,就认为我已经在该做了充分的测试, 系统就不会做过多的兼容性判断, 从而提高运行效率
-
Compile With 编译程序时使用的版本
闪屏页面(Splash)
- 展示logo,公司品牌
- 项目初始化
- 检测版本更新
- 校验程序合法性(比如:判断是否有网络,有的话才运行)
签名冲突
如果两个应用程序, 包名相同, 但是签名不同, 就无法覆盖安装
正式签名
1. 有效期比较长,一般大于25年
2. 需要设置密码
3. 正式发布应用时,必须用正式签名来打包
测试签名(debug.keystore)
1. 有效期是1年,很短
2. 有默认的别名,密码, alias=android, 密码是androiddebugkey
3. 在eclipse中直接运行项目是,系统默认采用此签名文件
如果正式签名丢失了怎么办?
1. 修改包名, 发布, 会发现有两个手机卫士, 用户会比较纠结
2. 请用户先删掉原来的版本,再进行安装, 用户会流失
3. 作为一名有经验的开发人员,请不要犯这种低级错误
子类和父类
子类拥有父类的所有方法, 而且可以有更多自己的方法
Activity(token), Context(没有token)
平时,要获取context对象的话, 优先选择Activity, 避免bug出现, 尽量不用getApplicationContext()
项目结构
app构建文件
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
defaultConfig {
applicationId "com.example.administrator.myapplication"
minSdkVersion 15
targetSdkVersion 16
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// implementation 'com.android.support:appcompat-v7:29.+'
implementation 'com.android.support:appcompat-v7:+'
implementation 'com.android.support.constraint:constraint-layout:2.0.1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
主要文件
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.administrator.myapplication" android:versionCode="1" android:versionName="1.0">
<uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<uses-library android:name="android.test.runner"/>
<activity android:name=".activity.SplashActivity" android:label="主界面" android:icon="@mipmap/chrome">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".activity.HomeActivity"/>
</application>
</manifest>
activity_splash.xml
<RelativeLayout 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"
android:background="@drawable/launcher_bg"
>
<TextView
android:text="TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:id="@+id/tv_version" android:layout_weight="1"
android:layout_centerHorizontal="true" android:layout_alignParentBottom="true"
android:textSize="16sp"
android:textColor="#000"
android:shadowColor="#f00"
android:shadowDx="1"
android:shadowDy="1"
android:shadowRadius="1"
android:layout_marginBottom="116dp" tools:text="版本号1.0"/>
<ProgressBar
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/progressBar"
android:layout_centerInParent="true"/>
<TextView
android:text="下载进度"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:textColor="#f00"
android:layout_marginLeft="20dp"
android:textSize="16sp"
android:visibility="gone"
android:id="@+id/tv_progress"/>
</RelativeLayout>
home_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical" >
<ImageView
android:id="@+id/iv_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/home_apps" />
<TextView
android:id="@+id/tv_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textColor="@color/black"
android:textSize="18sp"
android:text="手机防盗" />
</LinearLayout>
StreamUtils.java
package com.example.administrator.myapplication.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 读取流数据
*/
public class StreamUtils {
/**
* 读取输入流返回String
* @param in
* @return
*/
public static String readFromStream(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int len = 0;
byte [] buffer = new byte[1024];
while ((len=in.read(buffer))!=-1){
out.write(buffer,0,len);
}
String result = out.toString();
in.close();
out.close();
return result;
}
}
SplashActivity.java
package com.example.administrator.myapplication.activity;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.example.administrator.myapplication.R;
import com.example.administrator.myapplication.util.StreamUtils;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
public class SplashActivity extends Activity {
private static final int CODE_UPDATE_DIALOG = 0;
private static final int CODE_URL_ERROR = 1;
private static final int CODE_NET_ERROR = 2;
private static final int CODE_JSON_ERROR = 3;
private static final int CODE_ENTER_HOME = 4;
private TextView tvVserion;
private TextView tvProgress; //下载进度展示
private String mVersionName; //版本名
private int mVersionCode; //版本号
private String mDesc;//版本描述
private String mDownloadUrl; //下载地址
private Handler mHandler = new Handler(){
@Override
public void handleMessage( Message msg) {
switch (msg.what){
case CODE_UPDATE_DIALOG:
showUpdateDialog();
break;
case CODE_URL_ERROR:
Toast.makeText(SplashActivity.this,"下载地址错误",Toast.LENGTH_SHORT).show();
enterHome();
break;
case CODE_NET_ERROR:
Toast.makeText(SplashActivity.this,"网络错误",Toast.LENGTH_SHORT).show();
enterHome();
break;
case CODE_JSON_ERROR:
Toast.makeText(SplashActivity.this,"数据解析错误",Toast.LENGTH_SHORT).show();
enterHome();
break;
case CODE_ENTER_HOME:
enterHome();
}
};
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
setContentView(R.layout.activity_splash);
super.onCreate(savedInstanceState);
tvVserion = findViewById(R.id.tv_version);
tvVserion.setText("版本号:"+getVersionName());
tvProgress = findViewById(R.id.tv_progress); //默认隐藏
checkVersion();
}
private String getVersionName(){
PackageManager packageManager = getPackageManager();
try {
PackageInfo packageInfo = packageManager.getPackageInfo(getPackageName(), 0);
return packageInfo.versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return "";
}
/**
* 获取本地app 的versioncode
* @return
*/
private int getVersionCode(){
PackageManager packageManager = getPackageManager();
try {
PackageInfo packageInfo = packageManager.getPackageInfo(getPackageName(), 0);
return packageInfo.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return -1;
}
/**
* 从服务器获取版本信息进行校验
*/
public void checkVersion(){
final long startTime = System.currentTimeMillis();
//启动子线程加载数据
new Thread(){
@Override
public void run(){
Message msg = Message.obtain();
HttpURLConnection conn = null;
try {
URL url = new URL("http://192.168.1.210:8080/update.json");
conn = (HttpURLConnection)url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);//设置连接超时
conn.setReadTimeout(5000);//设置响应超时
conn.connect();
int responseCode = conn.getResponseCode();//获取响应码
if(responseCode==200){
InputStream inputStream = conn.getInputStream();
String result = StreamUtils.readFromStream(inputStream);
System.out.println("网络返回:"+result);
//解析json
JSONObject json = new JSONObject(result);
mVersionName = json.getString("versionName");
mVersionCode = json.getInt("versionCode");
mDesc = json.getString("description");
mDownloadUrl = json.getString("downloadUrl");
if(mVersionCode>getVersionCode()){
//服务器versionCode大于本地versionCode
//弹出升级对话框
msg.what = CODE_UPDATE_DIALOG;
}else {
msg.what = CODE_ENTER_HOME;
}
}
} catch (MalformedURLException e) {
msg.what = CODE_URL_ERROR;
//url 错误异常
e.printStackTrace();
}catch (IOException e){
msg.what = CODE_NET_ERROR;
//网络错误异常
e.printStackTrace();
} catch (JSONException e) {
msg.what = CODE_JSON_ERROR;
e.printStackTrace();
}finally {
long endTime =System.currentTimeMillis();
long timeUsed = startTime - endTime;
//这里就不是很人性化了,本来秒进系统的,你要给人家阻塞,就为了让闪屏页面至少展示2秒
if(timeUsed<2000){
try {
Thread.sleep(2000-timeUsed);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mHandler.sendMessage(msg);
if(conn!=null){
conn.disconnect();
}
}
}
}.start();
}
private void showUpdateDialog(){
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("检测到最新版本:"+mVersionName);
builder.setMessage(mDesc);
builder.setPositiveButton("立即更新", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
System.out.println("立即更新");
download();
}
});
builder.setNegativeButton("以后再说", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
enterHome();
}
});
//设置取消监听,点击返回调到主界面
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
enterHome();
}
});
builder.show();
}
/**
* 进入主页面
*/
private void enterHome(){
Intent intent = new Intent(this,HomeActivity.class);
startActivity(intent);
finish();
}
/**
* 下载apk文件
*/
private void download(){
//判断SD卡有没有挂在,至少要有这个概念,会不会不要紧可以去网上搜
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
tvProgress.setVisibility(View.VISIBLE);//显示下载进度
String target = Environment.getExternalStorageDirectory()+"/update.apk";
HttpUtils utils = new HttpUtils();
utils.download("http://192.168.1.210:8080/app-release.apk", target, new RequestCallBack<File>() {
//文件的下载情况
@Override
public void onLoading(long total, long current, boolean isUploading) {
super.onLoading(total, current, isUploading);
System.out.println("下载进度:"+current+"/"+total);
tvProgress.setText("下载进度:"+current*100/total+"%");
}
//下载成功
@Override
public void onSuccess(ResponseInfo<File> responseInfo) {
System.out.println("下载成功");
//跳转到安装页面
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setDataAndType(Uri.fromFile(responseInfo.result),"application/vnd.android.package-archive");
//startActivity(intent);
startActivityForResult(intent,0);//如果用户取消安装会返回结果,回调方法onActivityResult
}
//下载失败
@Override
public void onFailure(HttpException e, String s) {
Toast.makeText(SplashActivity.this,"下载失败!",Toast.LENGTH_SHORT).show();
}
});
}else {
Toast.makeText(SplashActivity.this,"没有找到SD卡",Toast.LENGTH_SHORT);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
enterHome();
super.onActivityResult(requestCode, resultCode, data);
}
}
HomeActivity.java
package com.example.administrator.myapplication.activity;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;
import com.example.administrator.myapplication.R;
/**
* 主页面
*
* @author Kevin
*
*/
public class HomeActivity extends Activity {
private GridView gvHome;
private String[] mItems = new String[] { "手机防盗", "通讯卫士", "软件管理", "进程管理",
"流量统计", "手机杀毒", "缓存清理", "高级工具", "设置中心" };
private int[] mPics = 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 };
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
gvHome = (GridView) findViewById(R.id.gv_home);
gvHome.setAdapter(new HomeAdapter());
}
class HomeAdapter extends BaseAdapter {
@Override
public int getCount() {
return mItems.length;
}
@Override
public Object getItem(int position) {
return mItems[position];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = View.inflate(HomeActivity.this,
R.layout.home_list_item, null);
ImageView ivItem = (ImageView) view.findViewById(R.id.iv_item);
TextView tvItem = (TextView) view.findViewById(R.id.tv_item);
tvItem.setText(mItems[position]);
ivItem.setImageResource(mPics[position]);
return view;
}
}
}
此外需要在tomcat的webapp/ROOT/下放一个正式的apk文件,用于版本更新时下载。
和一个app.json,用于得到服务器的版本信息
app.json
{"versionName": "2.0", "versionCode": 2, "description": "新增NB功能,赶紧体验!!!", "downloadUrl": "http://www.baidu.com"}
功能演示
这里超过5s请求不到网络,会有一个网络错误
启动tomcat,再试试
安装的时候产生了一个签名错误,原因是,本机开发用的是测试的签名,需要用正式的签名在app上安装一个低版本的,再用一个相同签名的高版本来覆盖才行。
说道这里演示一下怎么生成一个正式签名的apk吧
用的开发工具是idea,
没有签名文件则需要先生成一个签名文件
这里要选V1,V2不能安装,安装的时候会报一个Certificate Error
点击,finish就会在release下生成一个正式的apk文件。