1.点击ADM不能查看data文件夹,android device monitor不能查看/data目录。
2.不要忘记china后面是有/。
String address="http://guolin.tech/api/china/"+provincecode+"/"+citycode;
3.ProgressDialog在24以上的是用不了的。
4.setselection(int position)的作用就是把第position位置的数据显示在listview的最上面一项。来自ListView的setSelection用法。下文代码就是把第一项放在最上面一项。假如你设置1,就是把第二位的数据显示在listview的最上面一项,那么第一项呢,向上滑动就可以看到。
listView.setSelection(0);
5.OkHttp 建立一个新方法,使程序可以多次调用,并带有回调参数callback,同时在client.newCall(request)没有调用excuter()方法,而是调用enqueue()方法,该方法的内部已经开好子线程了,在子线程中去执行HTTP请求,并将最终的请求结果回调OKhttp.Callback当中,在调用它的时候要重写onResponse(),得到服务器返回的具体内容,重写onFailure()方法,在这里对异常情况进行处理。不可以再这里进行UI操作。
public class HttpUtil {
public static void sendOkHttpRequest(String address,okhttp3.Callback callback){
OkHttpClient client=new OkHttpClient();
Request request=new Request.Builder().url(address).build();
client.newCall(request).enqueue(callback);
}
那么我们在调用sendOkHttpRequest()方法的时候就可以这样写
HttpUtil。sendOkHttpRequest("http://www.baidu.com",new okhhtp3.Callback())
{
@Override
public void onResponse(Call call,Response response)throws IOException{
//得到服务器返回的具体内容
String responseData =response.body().string();
}
@Override
public void onFailure(Call call,IOException e){
//在这里对异常情况进行处理
}
}
在coolweather中,重写了onFailure()方法,onResponse()方法。
onFailure()方法
如果加载失败,要返回UI线程,去关闭加载框,并提示加载失败字样。
onResponse()方法
首先去判断要去服务器查询什么type的数据,包含三个种类,去调用相应解析json的方法。之后数据库中已经包含数据了,所以调用对应type的读取数据库数据的方法。
HttpUtil.sendOkHttpRequest(address, new Callback() {
@Override
public void onFailure(Call call, IOException e) {
//失败了,返回UI线程处理逻辑
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
//关闭加载框
// closeProgressBar();
closeProgressDialog();
Toast.makeText(getContext(),"加载失败",Toast.LENGTH_SHORT).show();
}
});
}
//得到服务器返回的具体内容
@Override
public void onResponse(Call call, Response response) throws IOException {
String responseText=response.body().string();
boolean result=false;
if("province".equals(type))
result = Utility.handleProvinceResponse(responseText);
else if("city".equals(type)){
result=Utility.handleCityResponse(responseText,selectedProvince.getId());
}
else if("county".equals(type)){
result=Utility.handleCountyResponse(responseText,selectedCity.getId());
}
//解析完数据牵涉到UI操作,因此必须要在主线程中调用。
//数据库中有了,可以显示了。
if(result){
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
///关闭进度框
// closeProgressBar();
closeProgressDialog();
if("province".equals(type))
queryProvinces();
else if("city".equals(type)){
queryCities();
}
else if("county".equals(type)){
queryCounties();
}
}
});
}
}
});
回顾gson的使用,并找一些json格式的数据进行解析。
String response="[{img=0.0,name=非会员,qualify_amount=0.0}," +
"{img=1.0,name=一级会员,qualify_amount=100.0}," +
"{img=2.0, name=二级会员,qualify_amount=300.0}]";
由于是数组中有多个对象,所以可以用List的方法进行解析,解析方法如下:
Gson gson=new Gson();
//数组的话可以这么做
List<User>users=gson.fromJson(response,new TypeToken<List<User>>(){}.getType());
for (User user:users){
System.out.println(user.img+" "+user.name+" "+user.qualify_amount);
这里参考自Gson用法详解。当我们的接口返回的数据与我们期望的数据不同时,我们可以使用属性重命名。或者下述的json数据中有一个tz,我们怕忘记它的意思想给他改成time,就需要使用@SerializedName 来建立与java字段的联系。
/**
* admin_area : 北京
* tz : +8.00
* location : 北京
* lon : 116.4052887
* parent_city : 北京
* cnty : 中国
* lat : 39.90498734
* cid : CN101010100
*/
private String admin_area;
@SerializedName("tz")
private String time;
SerializeName远比你想象的更强大,假如,json数据如下:userName,user_name,Name,都是指向一个值,此时我们可以使用alternate属性。很奇怪的是,我翻看了我的SerializedName类发现竟然没有alternate这个方法,黑人问号脸,为什么问号它围绕着我?
{\"userName\":\"leavesC\",\"age\":24,\"sex\":true}
{\"user_name\":\"leavesC\",\"age\":24,\"sex\":true}
{\"Name\":\"leavesC\",\"age\":24,\"sex\":true}
@SerializedName(value = "userName", alternate = {"user_name", "Name"})
private String name;
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.google.gson.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface SerializedName {
String value();
}
参考自Gson 解析复杂数据。首先有一个Json对象,表示一本书的基本信息。
{
'title': 'Java Puzzlers: Traps, Pitfalls, and Corner Cases',
'isbn-10': '032133678X',
'isbn-13': '978-0321336781',
'authors': ['Joshua Bloch', 'Neal Gafter']
}
java的变量名不容许有‘-’符号的,我们可以使用@SerializedName注解来处理,不过对于一些复杂情况是难以处理的,所以我们使用JsonDeserializer。我们需要自定义一个JsonDeserializer,然后注册到GsonBuilder,GsonBuilder在解析时就会自动使用我们自定义的JsonDeserializer。
首先我们需要创建一个BookJsonDeserializer。它负责把Book对应的Json对象,解析为Java对象。
public class BookDeserializer implements JsonDeserializer<Book> {
@Override
public Book deserialize(final JsonElement jsonElement, final Type typeOfT, final JsonDeserializationContext context)
throws JsonParseException {
//todo 解析字段
final Book book = new Book();
book.setTitle(title);
book.setIsbn10(isbn10);
book.setIsbn13(isbn13);
book.setAuthors(authors);
return book;
}
}
简单介绍下JsonDeserializer,它需要有一个Type,那肯定是我们的Book。我们要重写deserialize()函数,并在其中解析json数据,最后返回一个Book对象。这里比较重要的是,Gson解析Json对象在内部表示为JsonElement,虽然element是元素的意思,但JsonElement可以表示为下列的任何一种:
1. JsonPrimitive :Java 基本类型的包装类,以及 String
2. JsonObject:类比 Js 中 Object的表示,或者 Java 中的 Map
3. JsonArray:JsonElement 组成的数组,注意:这里是 JsonElement,说明这个数组是混合类型的。
4. JsonNull:值为 null
完整代码如下,根据上述所讲,就可以解释,为什么要用getAsString和getAsJsonArray了。使用getAsString时是这个JsonElement表示为JsonPrimitive。使用getAsJsonArray时,虽然读取的是[‘Joshua Bloch’, ‘Neal Gafter’]这个数组,但表示为JsonElement所以,要给他转化一下。
class BookDeserializer implements JsonDeserializer<Book>{
@Override
public Book deserialize(final JsonElement jsonElement, final Type typeOfT,
final JsonDeserializationContext context)throws JsonParseException{
final JsonObject jsonObject=jsonElement.getAsJsonObject();
JsonElement titleElement=jsonObject.get("title");
final String title=titleElement.getAsString();
JsonElement isbn10Element=jsonObject.get("isbn-10");
final String isbn10=isbn10Element.getAsString();
JsonElement isbn13Element=jsonObject.get("isbn-13");
final String isbn13=isbn13Element.getAsString();
//虽然是个数组,但Gson解析json对象并在内部表示为JsonElement,
// 这里的其实是表示为JsonElement的JsonArray,所以要在get之后用getAsJsonArray();
JsonArray authors=jsonObject.get("authors").getAsJsonArray();
String[]Authors=new String[authors.size()];
//Array中存放的是element
int i=0;
for(JsonElement jsonElement1:authors){
Authors[i++]=jsonElement1.getAsString();
}
//todo 解析字段
final Book book=new Book();
book.setTitle(title);
book.setIsbn10(isbn10);
book.setIsbn13(isbn13);
book.setAuthors(Authors);
return book;
}
这里要注意一下,Book类我们要重写一下toString(),否则的话无法得到我们想要的结果,直接System.out.println(book)就会输出book对应的地址。我们的检验代码如下:
public static void main(String[]args){
String response="{\"title\":\"Java Puzzlers: Traps, Pitfalls, and Corner Cases\"," +
"\"isbn-10\":\"032133678X\"," +
"\"isbn-13\":\"978-0321336781\"," +
"\"authors\":[\"Joshua Bloch\"," +
"\"Neal Gafter\"]}";
//使用GsonBuilder来注册BookDeserializer,并创建Gson对象,这样的话,
// 需要解析Book的时候,就会使用BookDeserializer来解析
GsonBuilder gsonBuilder=new GsonBuilder();
gsonBuilder.registerTypeAdapter(Book.class,new BookDeserializer());
Gson gson=gsonBuilder.create();
Book book = gson.fromJson(response, Book.class);
System.out.println(book);
}
最后是解析的过程:
1.将输入的字符串解析为JsonElement
2.找到对应的JsonDeserializer来解析,就找到我们的BookDeserializer
3.传入参数进deserialize()函数,讲JsonElement转换成Book对象
4.将Book对象返回fromJson()
假如数据的结构如下,大类是Book类,其中还包括Author类
{
'title': 'Java Puzzlers: Traps, Pitfalls, and Corner Cases',
'isbn': '032133678X',
'authors':[
{
'id': 1,
'name': 'Joshua Bloch'
},
{
'id': 2,
'name': 'Neal Gafter'
}
]
}
public class Author{
long id;
String name;
}
我们有三种方法去解析这个Author类,
1.我们更新BookDeserializer,同时在其中解析authors字段。
2.可以使用默认的Gson实现,因为Author类的数据域和author的Json是一一对应的。
3.新写一个AuthorDeserializer来处理author的解析问题。
第二种方式:JsonDeserializer 的 deserialize() 方法提供了一个 JsonDeserializationContext 对象,这个对象基于 Gson 的默认机制, 我们可以选择性的将某些对象的反序列化工作委托给这个JsonDeserializationContext。JsonDeserializationContext 会解析 JsonElement 并返回对应的对象实例,并在BookDserializer中set一下值即可。
Author author = jsonDeserializationContext.deserialize(jsonElement, Author.class);
Book.setauthor(author);
第三种方式:新写一个AuthorDeserializer,这个类很简单,就不放代码了,要记得在主函数中,给GsonBuilder注册。
// Configure GSON
final GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Book.class, new BookDeserializer());
gsonBuilder.registerTypeAdapter(Author.class, new AuthorDeserializer());
final Gson gson = gsonBuilder.create();
首先要说明JSONObject和JsonObject是不同的,前者是json类下的,后者是gson类下的,我们尝试全部使用gson来解析和风天气数据。下面是json数据类型。其实解析起来很简单,我们定义三个类,分别是basic,update,now,三个类。这里主要就是想练习一下JsonDeserializer。
{
HeWeather6: [
{
basic: {
cid: "CN101010100",
location: "北京",
parent_city: "北京",
admin_area: "北京",
cnty: "中国",
lat: "39.90498734",
lon: "116.4052887",
tz: "+8.00"
},
update: {
loc: "2019-03-21 15:55",
utc: "2019-03-21 07:55"
},
status: "ok",
now: {
cloud: "0",
cond_code: "100",
cond_txt: "晴",
fl: "8",
hum: "11",
pcpn: "0.0",
pres: "1021",
tmp: "12",
vis: "16",
wind_deg: "3",
wind_dir: "北风",
wind_sc: "2",
wind_spd: "11"
}
}
]
}
我们定义的WeatherDeserializer定义如下,主要就是练习对象与数组的转换,以及context的默认方法。注意的就是可以使用gsonformat插件来生成类,非常的便捷。
class WeatherDeserializer implements JsonDeserializer<Weather>{
@Override
public Weather deserialize(final JsonElement jsonElement, final Type typeOfT,
final JsonDeserializationContext context)throws JsonParseException {
final JsonObject jsonObject = jsonElement.getAsJsonObject();
final JsonArray jsonArray=jsonObject.get("HeWeather6").getAsJsonArray();
final JsonObject jsonObject1=jsonArray.get(0).getAsJsonObject();
Basic basic=context.deserialize(jsonObject1.get("basic"), Basic.class);
Update update=context.deserialize(jsonObject1.get("update"),Update.class);
String status=context.deserialize(jsonObject1.get("status"),String.class);
Now now=context.deserialize(jsonObject1.get("now"),Now.class);
Weather weather=new Weather();
weather.basic=basic;
weather.update=update;
weather.status=status;
weather.now=now;
return weather;
}
}
最初想深入学习Gson是由于看了这篇文章,你真的会用Gson么。这篇文章有一个评论,有人问这个json形式数据该如何解析。我打算解析下这个数据练下手。
{0={img=0.0,name=非会员,qualify_amount=0.0}," +
"1={img=1.0,name=一级会员,qualify_amount=100.0}," +
"2={img=2.0, name=二级会员,qualify_amount=300.0}}
我们的类定义如下,数据存储形式为map的键值对类型。
class User{
Map<Integer,Data> map;
class Data {
public double img;
public String name;
public double qualifty_amount;
@Override
public String toString() {
return "Data{" +
"img=" + img +
", name='" + name + '\'' +
", qualifty_amount=" + qualifty_amount +
'}';
}
}
定义一个MapDeserializer来方便我们解析json数据。首先我们的数据是一个object,所以要将其变为JsonObject,最初的想法是去get(),但这里数据量较小,只有0,1,2,但真实数据一定很大,我看了一下JsonObject的函数,惊喜的发现有entrySet()方法,我们利用这个方法就可以实现所有数据的遍历,也节省了代码量。
class MapDeserializer implements JsonDeserializer<User> {
@Override
public User deserialize(final JsonElement jsonElement, final Type typeOfT,
final JsonDeserializationContext context)throws JsonParseException {
JsonObject jsonObject=jsonElement.getAsJsonObject();
Set<Map.Entry<String,JsonElement>> set=jsonObject.entrySet();
Map<Integer, User.Data>map=new HashMap<>();
//User.Data data0=context.deserialize(jsonObject.get("0"),User.Data.class);
//map.put(0,data0);
for(Map.Entry<String,JsonElement>s:set){
User.Data data=context.deserialize(s.getValue().getAsJsonObject(), User.Data.class);
map.put(Integer.valueOf(s.getKey()),data);
}
User user=new User();
user.map=map;
return user;
}
}
读取数据:
服务与活动
SharedPreferences prefs=PreferenceManager.getDefaultSharedPreferences(this);
String times=prefs.getString("times",null);
碎片
SharedPreferences prefs= PreferenceManager.getDefaultSharedPreferences(getContext());
String test=prefs.getString("times",null);
写入数据:
在碎片中写入:
SharedPreferences.Editor editor=PreferenceManager.
//不能使用getDefaultSharedPreferences(WeatherActivity.this)
getDefaultSharedPreferences(getContext())
.edit();
editor.putString("weather_id",weatherid);
editor.apply();
在活动或服务中写入:
SharedPreferences.Editor editor=PreferenceManager.
getDefaultSharedPreferences(AutoUpdateService.this)
.edit();
editor.putString("weather",responseText);
editor.apply();
SharedPreferences.Editor editor = PreferenceManager.
getDefaultSharedPreferences(WeatherActivity.this).edit();
editor.putString("weather", responseText);
editor.apply();
使用handler可以解决在子线程中进行UI操作,首先创建一个Handler对象,重写handleMessage()方法,在这里对具体的Message进行处理,这里可以判断Message的what字段,然后进行操作。同时,在子线程中的run()中创建一个Message对象,并将该对象的what字段指定为指定值,并使用Handler的sendMessage去发送创建的Message对象。这里handleMessage方法在主线程,所以可以使用UI操作。
public static final int UPDATE_TEXT =1;
private Handler handler=new Handler(){
public void handleMessage(Message msg){
switch (msg.what){
case UPDATE_TEXT:
text.setText("Nice to meet you");
break;
default:
break;
}
}
};
new Thread(new Runnable() {
@Override
public void run() {
Message message=new Message();
message.what=UPDATE_TEXT;
handler.sendMessage(message);
}
}).start();
Android中的异步消息处理主要包括:Message、Handler、MessageQueue和Looper。
整体流程:
首先需要在主线程中创建一个Handler对象,并重写handleMessage()方法。然后当子线程中需要进行UI操作,就创建一个Message对象,并通过Handler将这条消息发送出去。之后这条消息就会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理消息,最后分发会Handler的handleMessage()方法中。
AsyncTask是一个抽象类,我们在继承时,需要指定3个泛型参数。
AsyncTask一般需要重写四个函数:
1 服务一般要重写onCreate(),onStartCommand(),onDestroy()这三个方法。同时需要注意服务需要注册。
2 服务要启动都需要使用Intent。
Intent startIntent=new Intent(this,MyService.class);
startService(startIntent);
3 活动与服务通信,需要使用onBind()方法。
我们在服务中新建一个DownloadBinder类,并在其中写一些方法以便我们使用,然后新建一个该类的对象,在onBind()方法返回这个对象。
//这是在服务类
private DownloadBinder mBinder=new DownloadBinder();
class DownloadBinder extends Binder{
public void startDownload(){
Log.d("MyService","startDownload executed");
}
public int getProgress(){
Log.d("MyService","getProgress executed");
return 0;
}
}
public MyService() {
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
return mBinder;
}
我们在活动中,调用Binder。我们在活动中创建一个ServiceConnection的匿名类,并在里面重写onServiceConnected()方法和onServiceDisconnected()方法,分别在活动与服务成功绑定以及接触绑定时调用。在onServiceConnected我们通过向下转型得到了DownloadBinder的是咧,之后就可以调用DownloadBinder中任何public()的方法。
//这是在活动中
private MyService.DownloadBinder downloadBinder;
private ServiceConnection connection=new ServiceConnection() {;
//活动与服务成功绑定时调用
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder=(MyService.DownloadBinder)service;
downloadBinder.startDownload();
downloadBinder.getProgress();
}
//活动与服务成功解绑时调用
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
4 使用startForeground()方法可以使通知一直显示,前台服务。
5. 使用IntentService开启新线程。
首先是定义一个回调接口,用于对下载过程中的各种状态进行监听和回调。
public interface DownLoadListener {
//通知当前的下载进度
void onProgress(int progress);
//通知下载成功事件
void onSuccess();
//通知下载失败事件
void onFailed();
//通知下载暂时时间
void onPaused();
//通知下载取消事件
void onCanceled();
}
新写一个DownloadTask完成下载功能.。首先是传入的三个泛型参数,第一个泛型参数(Param)为String,这个是要传入的Url数据,会传入doInbackground()方法。第二个泛型参数(Progress)是进度显示单位,会通过publishProgress()更新,并回调给onProgressUpdate()方法。第三个(Result)是返回的结果的类型,这个参数用于doInbackground()方法返回,以及onPostExcute()方法执行一些操作。
首先有四个整形常量。代表了当前下载任务的不同状态。之后有一个我们定义的接口的实例对象,我们要在onPostExcute()方法中根据不同的result来执行该对象的不同方法。最后有两个boolean型变量,因为我们在doInbackground()中要时刻判断是否点击取消或暂停。
在这个方法要完成文件的下载,以及更新下载的进度。
文件的下载中需要实现断点下载。我们将其存在sd卡的download目录,我们首先要判断文件是否存在,如果存在就要记录已下载文件长度,我们通过okhttp来获取下载文件的长度,判断已下载文件长度是否等于下载文件长度,相等就可以直接返回Type_SUCCESS,否则以已下载长度开始下载。增加断点的方法如下。
OkHttpClient client=new OkHttpClient();
Request request=new Request.Builder()
//增加断点
.addHeader("RANGE","bytes="+downloadedLength+"-")
.url(downloadUrl)
.build();
Response response=client.newCall(request).execute();
接下来通过java的文件流方式,不断从网络上读取数据,并写入本地,同时计算出progress,并使用publishProgress()方法来传递给onProgressUpdate()。在这个过程我们要时刻判断那两个boolean变量是否为true,为true就返回相应的result。正常完成循环后返回TYPE_SUCCESS,如果出现任何错误最终会返回TYPE_FAILED。
这个方法在publishProgress()后就会回调,虽然一直用progress代替,但传入的是values,从values【0】中取出progress,与一个对象lastProgress进行比较,如果有更改就去调用listener的onProgress()函数。我们在listener的onProgress()中使用新的progress来创建新的通知,就可以达到实时更新下载进度的功能。
@Override
protected void onProgressUpdate(Integer...values){
int progress=values[0];
if (progress>lastProgress)
listener.onProgress(progress);
lastProgress=progress;
}
这个函数是用来通知下载结果的,由于在doInbackground()不可以进行UI操作,所以需要使用这个方法来通知下载结果,这个函数在doInbackground()返回结果后会调用。
@Override
protected void onPostExecute(Integer status){
switch (status){
case TYPE_CANCELED:
listener.onCanceled();
break;
case TYPE_PAUSED:
listener.onPaused();
break;
case TYPE_FAILED:
listener.onFailed();
break;
case TYPE_SUCCESS:
listener.onSuccess();
break;
default:
break;
}
}
首先是两个设置函数,会在Activity中按下相应button时调用,将boolean设置为true,就会在doInBackground中暂停下载或取消下载。最后的这个通用函数是使用OkHttp来获取文件的总长度。
public void pauseDownload(){
isPaused=true;
}
public void cancelDownload(){
isCanceled=true;
}
private long getContentLength(String downloadUrl)throws IOException{
OkHttpClient client=new OkHttpClient();
Request request=new Request.Builder()
.url(downloadUrl)
.build();
Response response=client.newCall(request).execute();
if (response!=null&&response.isSuccessful()){
long contentLength=response.body().contentLength();
response.close();
return contentLength;
}
return 0;
}
完整代码如下:
public class DownloadTask extends AsyncTask<String,Integer,Integer> {
public static final int TYPE_SUCCESS = 0;
public static final int TYPE_FAILED = 1;
public static final int TYPE_PAUSED = 2;
public static final int TYPE_CANCELED = 3;
private DownLoadListener listener;
private boolean isCanceled=false;
private boolean isPaused=false;
private int lastProgress;
public DownloadTask(DownLoadListener listener){
this.listener = listener;
}
//在后台做的东西
@Override
protected Integer doInBackground(String...params){
InputStream is = null;
RandomAccessFile savedFile = null;
File file=null;
try{
long downloadedLength=0;//记录已下载的文件。
String downloadUrl = params[0];
String fileName=downloadUrl.substring(downloadUrl.lastIndexOf("/"));
//下载到sd卡的Download目录
String directory= Environment.getExternalStoragePublicDirectory
(Environment.DIRECTORY_DOWNLOADS).getPath();
file = new File(directory+fileName);
//已经下载了一部分了
if (file.exists()){
downloadedLength=file.length();
}
long contentLength=getContentLength(downloadUrl);
if (contentLength==0)
return TYPE_FAILED;
else if(contentLength==downloadedLength)
//已下载等于文件总字节,说明下载完毕
return TYPE_SUCCESS;
OkHttpClient client=new OkHttpClient();
Request request=new Request.Builder()
//增加断点
.addHeader("RANGE","bytes="+downloadedLength+"-")
.url(downloadUrl)
.build();
Response response=client.newCall(request).execute();
if (response!=null){
is=response.body().byteStream();
savedFile=new RandomAccessFile(file,"rw");
savedFile.seek(downloadedLength);
byte[]b=new byte[1024];
int total=0;
int len;
while((len = is.read(b))!=-1)
{
//随时判断是否被取消或终止,
if (isCanceled){
return TYPE_CANCELED;
}
else if (isPaused)
return TYPE_PAUSED;
else
{
total+=len;
savedFile.write(b,0,len);
int progress=(int)((total+downloadedLength)*100/contentLength);
//通知下载进度变化,之后很快回调用onProgressUpdate()方法
publishProgress(progress);
}
}
response.body().close();
return TYPE_SUCCESS;
}
}catch (Exception e)
{e.printStackTrace();}
finally {
try {
if (is!=null)
is.close();
if (savedFile!=null)
savedFile.close();
if (isCanceled&&file!=null)
file.delete();
}catch (Exception e)
{
e.printStackTrace();
}
}
return TYPE_FAILED;
}
//更新下载数据。在这里去调用listener的onProgress()方法,
//listener的onProgress()方法中时刻创建新通知,去更新数据。
@Override
protected void onProgressUpdate(Integer...values){
int progress=values[0];
if (progress>lastProgress)
listener.onProgress(progress);
lastProgress=progress;
}
//根据参数去回调
@Override
protected void onPostExecute(Integer status){
switch (status){
case TYPE_CANCELED:
listener.onCanceled();
break;
case TYPE_PAUSED:
listener.onPaused();
break;
case TYPE_FAILED:
listener.onFailed();
break;
case TYPE_SUCCESS:
listener.onSuccess();
break;
default:
break;
}
}
public void pauseDownload(){
isPaused=true;
}
public void cancelDownload(){
isCanceled=true;
}
private long getContentLength(String downloadUrl)throws IOException{
OkHttpClient client=new OkHttpClient();
Request request=new Request.Builder()
.url(downloadUrl)
.build();
Response response=client.newCall(request).execute();
if (response!=null&&response.isSuccessful()){
long contentLength=response.body().contentLength();
response.close();
return contentLength;
}
return 0;
}
}
在该类中,我们主要重写了之前定义的回调接口的匿名类;新写了一个DownloadBinder与Activity绑定,最后写了使用通知的函数。
正常的通知是如下去写的。首先要有一个NotificationManger,之后为了防止不同版本的问题,会使用NotificationCompat.Builder(this)来创建,之后包括了不同的set函数。
值得一提的是下面的setstyle是为了完成内容很多的时候不被屏幕挡住,内容完全显示出来的功能。这里使用PendingIntent的作用是当我们点击顶端通知栏可以跳转到其他acitivity。传入四个参数分别是Context,第二个和第四个一般传入0,第三个参数是一个Intent对象,可以通过这个对象构建出PendingIntent的“意图”。也就是跳转到哪个Activity。Intent倾向于立即执行某个操作,而PendingIntent更加倾向于在某个合适的时机去执行某个操作。
Intent intent=new Intent(this,Main2Activity.class);
PendingIntent pi=PendingIntent.getActivity(this,0,intent,0);
NotificationManager manager=(NotificationManager)getSystemService
(NOTIFICATION_SERVICE);
//防止不同版本
Notification notification=new NotificationCompat.Builder(this)
.setContentTitle("this is content title")
.setContentText("This is content text")
//会覆盖text
.setStyle(new NotificationCompat.BigTextStyle().bigText
("abcdefgauwohewqkehwqkehwqkewkqejqwkejqwkejwqkekwqehwqke" +
"hwqkehqwkehwqweqwewqeqwewqewq"))
.setPriority(NotificationCompat.PRIORITY_MAX)
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pi)
//不能是id。要求是传入 bitmap。
.setLargeIcon(BitmapFactory.decodeResource(getResources()
,R.mipmap.ic_launcher))
.setAutoCancel(true)
.build();
manager.notify(1,notification);
我们在这个类中将其分开成两个函数,方便我们去多次调用。第一个函数没什么说的,就是去获取一个NotificationManger,第二个函数有一个跳转。跳转回主活动。这里有一个setProgress函数,第一个参数传入通知的最大进度,第二个参数传入通知的当前进度,第三个参数表示是否使用模糊进度条。
private NotificationManager getNotificationManager(){
return (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
}
private Notification getNotification(String title,int progress){
Intent intent=new Intent(this,MainActivity.class);
PendingIntent pi= PendingIntent.getActivity(this,0,intent,0);
NotificationCompat.Builder builder=new NotificationCompat.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),
R.mipmap.ic_launcher));
builder.setContentIntent(pi);
builder.setContentTitle(title);
if (progress>0){
//当progress大于或等于0时才显示下载进度。
builder.setContentText(progress+"%");
//第一个参数传入通知的最大进度,第二个为当前进度,第三个为是否使用
//模糊进度条。
builder.setProgress(100,progress,false);
}
return builder.build();
}
主要重写了以下几个函数。
private DownLoadListener listener=new DownLoadListener() {
@Override
public void onProgress(int progress) {
//建立一个通知
getNotificationManager().notify(1,getNotification("Downloading...",
progress));
}
@Override
public void onSuccess() {
downloadTask=null;
//下载成功时将前台服务通知关闭,并创建一个下载成功的通知
stopForeground(true);
getNotificationManager().notify(1,getNotification("Download Success",
-1));
Toast.makeText(DownloadService.this,"Download Success",
Toast.LENGTH_SHORT).show();
}
@Override
public void onFailed() {
downloadTask=null;
//下载失败时将前台服务通知关闭,并创建一个下载失败的通知
stopForeground(true);
getNotificationManager().notify(1,getNotification("Download Failed",
-1));
Toast.makeText(DownloadService.this,"Download Failed",
Toast.LENGTH_SHORT).show();
}
@Override
public void onPaused() {
downloadTask=null;
Toast.makeText(DownloadService.this,"Paused",Toast.LENGTH_SHORT)
.show();
}
@Override
public void onCanceled() {
downloadTask=null;
stopForeground(true);
Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_SHORT)
.show();
}
};
我们通过自定义DownloadBinder类使活动可以与服务通信。这里包含三个方法,分别用于开始下载,暂停下载和取消下载的。
private DownloadBinder mBinder=new DownloadBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
//服务与活动通信
class DownloadBinder extends Binder{
public void startDownload(String url){
if (downloadTask==null)
{
downloadUrl=url;
downloadTask=new DownloadTask(listener);
//execute传入AsyncTask的第一个参数。
downloadTask.execute(downloadUrl);
startForeground(1,getNotification("Downloading...",0));
Toast.makeText(DownloadService.this,"Downloading...",
Toast.LENGTH_SHORT).show();
}
}
public void pauseDownload(){
if (downloadTask!=null)
{
downloadTask.pauseDownload();
}
}
public void cancelDownload(){
if(downloadTask!=null)
downloadTask.cancelDownload();
else{
if (downloadUrl!=null){
//取消下载时需将文件删除
String fileName=downloadUrl.substring(
downloadUrl.lastIndexOf("/"));
String directory = Environment.getExternalStoragePublicDirectory
(Environment.DIRECTORY_DOWNLOADS).getPath();
File file=new File(directory+fileName);
if (file.exists())
file.delete();
getNotificationManager().cancel(1);
stopForeground(true);
Toast.makeText(DownloadService.this, "Canceled",
Toast.LENGTH_SHORT).show();;
}
}
}
}
在主活动中,我们要实现服务的绑定,app的打开时授权,button的点击函数。layout文件很简单就是包含三个button:分别是startdownload,canceldownload,pausedownload。
我们首先创建了ServiceConnection的匿名类,并在onServiceConnected方法中获取到DownloadBinder的实例,这样在与服务连接上的时候就可以调用服务中提供的方法了(startDownload,cancelDownload和pauseDownload)。调用startService和bindService方法来启动和绑定服务,启动服务就可以保证DownloadService一直在后台运行,绑定服务可以让MainActivity和DownloadService进行通信。
private DownloadService.DownloadBinder downloadBinder;
private ServiceConnection connection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder=(DownloadService.DownloadBinder)service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
...
Intent intent=new Intent(this,DownloadService.class);
startService(intent);
//使活动与服务可以通信
bindService(intent,connection,BIND_AUTO_CREATE);
}
我们在onCreate函数中,使用checkSelfPermiison来判断是否被许可使用权限。未被允许的话,就会调用requestPermissions方法,我们想要被用户允许的权限要放在一个String数组里,最后的1就是随便传入的一个id值。
@Override
protected void onCreate(Bundle savedInstanceState) {
...
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.
permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED)
{
ActivityCompat.requestPermissions(MainActivity.this,new
String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
}
}
当弹窗出现提示用户是否允许后,不管结果如何,都会回调这个onRequestPemissionsResult()方法,首先判断的是requestCode,这个就是刚刚的那个id1,我们的结果存储在grantResults数组中。我们要在不允许的情况下,弹出一个Toast提示用户。
//用户选择是否接受权限后回调的函数
@Override
public void onRequestPermissionsResult(int requestCode,String[]permissions,
int[]grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] != PackageManager.
PERMISSION_GRANTED) {
Toast.makeText(this, "拒绝权限将无法使用程序", Toast.LENGTH_SHORT)
.show();
finish();
}
break;
default:
}
}
这里要说明的我们不能为每个button去写他的clickLinstener,这样很麻烦,我们可以使用实现View.OnClickListener接口。
public class MainActivity extends AppCompatActivity implements View.OnClickListener
在onCreate中我们直接setOnclickLinstener(this)即可。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button startDownload=(Button)findViewById(R.id.start_download);
Button pauseDownload=(Button)findViewById(R.id.pause_download);
Button cancelDownload=(Button)findViewById(R.id.cancel_download);
startDownload.setOnClickListener(this);
pauseDownload.setOnClickListener(this);
cancelDownload.setOnClickListener(this);
}
在onClick中我们使用v.getId()去判断究竟是哪个button即可。根据不同button我们去使用服务(DownloadBinder实例)提供的不同方法。
@Override
public void onClick(View v){
if (downloadBinder==null)
return;
switch (v.getId()){
case R.id.start_download:
String url="https://raw.githubusercontent.com/guolindev/eclipse/master" +
"/eclipse-inst-win64.exe";
downloadBinder.startDownload(url);
break;
case R.id.pause_download:
downloadBinder.pauseDownload();
break;
case R.id.cancel_download:
downloadBinder.cancelDownload();;
break;
}
}
最后运行效果如下:最初是一个运行时权限,之后可以看到,点击开始后可以开始下载,点击暂停后再次点击开始会从之前暂停处继续下载,而点击取消下载后再次点击开始下载会从0开始,说明文件已被删除。