Android实现多线程下载并显示通知

1、前言

相信大家都使用过一些可以下载文件的 App,在下载列表中通常都会显示一个进度条实时地更新下载进度,现在的下载都是断点重传的,也就是在暂停后,重新下载会依照之前进度接着下载。

我们这个下载的案例是有多个线程同时下载一个任务,并能提供多个文件同时下载,在下载的同时会显示通知,因为下载线程是放在 Service 中的,所以就算程序运行在后台也可以继续下载。

Android实现多线程下载并显示通知_第1张图片

当启动下载时,就会发送通知提示开始下载,下载完成后在列表和通知栏中都会移除这个任务。有下载和停止两个 Button 控制下载。

Android实现多线程下载并显示通知_第2张图片

2、软件结构

我们这个下载的案例也算一个小型的软件,结构具有一定程度的复杂性,在展示代码前,我先来分析一下这个软件的结构。

Android实现多线程下载并显示通知_第3张图片

这就是整个工程的分类,我们将控制下载的进程信息以数据库的形式记录下来,这样可以避免重复下载,而且就算程序在后台被杀死,重新打开后也可以继续下载。

我们之前演示 APP 时,大家已经看到了布局,就是一个很简单的 RecyclerView,因为这个程序的重点是在多线程等后台操作上,所以在 UI 设计就随便了些,现在一般的 APP 都已经用一个 Button 来实现开始和暂停,有兴趣的朋友也可以自己用 selector 来设计一下,这里就不做了。

Android实现多线程下载并显示通知_第4张图片

这些类之间的关系大致如上图,FileInfo 和 ThreadInfo 是两个实体类。FileInfo 是记录要下载的文件的信息,之前说过我们是多线程下载,所以 ThreadInfo 对应的就是一个 FileInfo 对象所需要的下载时的线程信息。

一个 FileInfo 对应一个 DownloadTask 下载任务,下载肯定是放在后台的,所以我们要使用 Service。用 DownloadService 来启动每个 DownloadTask。DownloadTask 中就处理线程去进行下载和传递消息更新 UI 并将数据存入数据库,DownloadThread 类放在 DownloadTask 里,每一个 DownloadThread 处理一个 ThreadInfo 对应的信息。数据库中存储的就是 ThreadInfo 信息。

3、实体类

FileInfo.java:

public class FileInfo implements Serializable {

    private int id;
    private int length;
    private String url;
    private String name;
    private int finished;

    public FileInfo() {
    }

    public FileInfo(int id, int length, String url, String name, int finished) {
        this.id = id;
        this.length = length;
        this.url = url;
        this.name = name;
        this.finished = finished;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setFinished(int finished) {
        this.finished = finished;
    }

    public int getFinished() {
        return finished;
    }
}

FileInfo 的属性一目了然,文件 id,文件长度,下载地址,文件名,下载进度。因为我们要把这个对象在 Activity 和 Service 之间传递,所以我们要让它实现序列化,用 Serializable 操作比 Parcelable 简单。

ThreadInfo.java:

public class ThreadInfo {

    private int id;
    private String url;
    private int start;
    private int end;
    private int finished;

    public ThreadInfo() {
    }

    public ThreadInfo(int id, String url, int start, int end, int finished) {
        this.id = id;
        this.url = url;
        this.start = start;
        this.end = end;
        this.finished = finished;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getStart() {
        return start;
    }

    public void setStart(int start) {
        this.start = start;
    }

    public int getEnd() {
        return end;
    }

    public void setEnd(int end) {
        this.end = end;
    }

    public int getFinished() {
        return finished;
    }

    public void setFinished(int finished) {
        this.finished = finished;
    }
}

ThreadInfo 的属性与 FileInfo 大同小异,其中标识这个线程是属于哪个任务的是 url,因为 url 是唯一的,后面也要根据 url 确定是哪个 FileInfo。

start 和 end 是两个关键的属性,代表这个线程要完成从 start 开始到 end 这一区间的下载,然后配合 finished 就能知道这个线程下载到哪里啦。像一个 100 KB 的文件,我们用三个线程对它进行下载,三个线程分别完成 0KB - 33KB,33KB - 66KB,66KB - 100KB。

4、数据库

我建立了一个常数类用来保存一些经常用到的常数,Constant.java:

public class Constant {

    public static final String DATABASE_NAME = "info.db"; //数据库名称
    public static final int DATABASE_VERSION = 1; //数据库版本
    public static final String TABLE_NAME = "threadInfo"; //表名

    public static final String _ID = "_id";
    public static final String THREAD_ID = "thread_id";
    public static final String URL = "url";
    public static final String START = "start";
    public static final String END = "end";
    public static final String FINISHED = "finished";

    public static final String DOWNLOAD_PATH = Environment.getExternalStorageDirectory()
            + File.separator + "download";

    public static final String ACTION_START = "ACTION_START";
    public static final String ACTION_STOP = "ACTION_STOP";

    public static final int MSG_INIT = 0x1;
    public static final int MSG_BIND = 0x2;
    public static final int MSG_START = 0x3;
    public static final int MSG_UPDATE = 0x4;
    public static final int MSG_FINISH = 0x5;
}

前面都是数据库相关的定义,相信有一定的朋友都能理解。后面的 Action 和 MSG 是我们用来开启服务和进行 Activity 和 Service 通信的标识。这个程序用的是 Handler 来传递数据,这个后面再介绍。

DBHelper.java:

public class DBHelper extends SQLiteOpenHelper {

    private volatile static DBHelper helper;

    public static DBHelper getInstance(Context context) {
        if (helper == null) {
            synchronized (DBHelper.class) {
                if (helper == null) {
                    helper = new DBHelper(context);
                }
            }
        }
        return helper;
    }

    private DBHelper(Context context) {
        super(context, Constant.DATABASE_NAME, null, Constant.DATABASE_VERSION);
    }

    public void onCreate(SQLiteDatabase db) {
        String sql = "create table " + Constant.TABLE_NAME + " (" +
                Constant._ID + " Integer primary key autoincrement, " +
                Constant.THREAD_ID + " Integer," +
                Constant.URL + " text," +
                Constant.START + " Integer," +
                Constant.END + " Integer," +
                Constant.FINISHED + " Integer)";
        db.execSQL(sql);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

我们的数据库中只有一张表,所以并不复杂,这些基础的使用我在我的博客Android上SQLite的基本应用(一)中有介绍。

因为对数据库的使用不能有太多入口,所以我们对数据库的访问需要使用单例模式,这里用双重校验锁。

DBManager.java:

public class DBManager {

    private DBHelper helper;

    public DBManager(Context context) {
        helper = DBHelper.getInstance(context);
    }

    public synchronized void insertThread(ThreadInfo threadInfo) {

        SQLiteDatabase db = helper.getWritableDatabase();

        ContentValues values = new ContentValues();

        values.put(Constant.THREAD_ID, threadInfo.getId());
        values.put(Constant.URL, threadInfo.getUrl());
        values.put(Constant.START, threadInfo.getStart());
        values.put(Constant.END, threadInfo.getEnd());
        values.put(Constant.FINISHED, threadInfo.getFinished());

        db.insert(Constant.TABLE_NAME, null, values);

        db.close();
    }

    public synchronized void updateData(String url, int thread_id, int finished) {
        SQLiteDatabase db = helper.getWritableDatabase();

        ContentValues values = new ContentValues();
        values.put(Constant.FINISHED, finished);

        db.update(Constant.TABLE_NAME, values, Constant.URL + "=? and " +
                Constant.THREAD_ID + "=?", new String[]{url, thread_id + ""});

        db.close();
    }

    public synchronized void deleteThread(String url) {
        SQLiteDatabase db = helper.getWritableDatabase();

        db.delete(Constant.TABLE_NAME, Constant.URL + "=?",
                new String[]{url});

        db.close();
    }

    public List getThreads(String url) {
        SQLiteDatabase db = helper.getReadableDatabase();

        Cursor cursor = db.query(Constant.TABLE_NAME, null, Constant.URL + "=?",
                new String[]{url}, null, null, Constant._ID + " asc");

        List list = cursorTolist(cursor);
        cursor.close();
        db.close();
        return list;
    }

    public static List cursorTolist(Cursor cursor) {
        List list = new ArrayList();

        while (cursor.moveToNext()) {

            int _id = cursor.getInt(cursor.getColumnIndex(Constant.THREAD_ID));
            String url = cursor.getString(cursor.getColumnIndex(Constant.URL));
            int start = cursor.getInt(cursor.getColumnIndex(Constant.START));
            int end = cursor.getInt(cursor.getColumnIndex(Constant.END));
            int finished = cursor.getInt(cursor.getColumnIndex(Constant.FINISHED));

            ThreadInfo threadInfo = new ThreadInfo(_id, url, start, end, finished);
            list.add(threadInfo);
        }

        return list;
    }
}

我们通常在开发中都是把数据库定义和数据库功能实现分开处理,这样使得结构更加清晰,也方便管理。

实例化 DBManager 的同时也获得了 DBHelper 对象,数据库帮助类就是一个打开数据库的钥匙,有了它就能进行插入、更新、删除、查询等数据操作。

因为插入、更新、删除都是对数据的修改,而我们是多线程,可能同时有几个线程对数据进行修改,所以我们要给这个方法加上同步。

5、Activity与Service通信

前面介绍常数类,说过我们的数据传输是使用 Handler,其实也可以使用广播。虽然使用起来很简单,只要注册了在任何地方都能使用,但因为广播是系统组件,所以效率肯定不及 Handler,它是专门用来做线程通信的,对于我们这个下载案例使用它是很合适的。

要使用 Handler 进行跨组件通信,我们需要使用到 Messenger,也就是信使。比如要从 Service 向 Activity 发送消息,我们要先获得带有 Activity 的 Handler 的信使,然后就可以通过这个信使从 Service 发送消息到 Activity 的 Handler 中了。

而怎么获得 Messenger,这就要用到 Service 的绑定了,我们可以在 onBind() 方法中返回 Service 的 Messenger,那么在 Activity 绑定服务的时候就可以获得这个信使,于是实现了 Activity 到 Service 的单向通信。通过这个 Messenger 就可以向 Service 发送消息,消息里包含 Activity 的 Messenger,这样 Serivce 也获得了 Activity 的信使,就实现了两者的双向通信。

DownloadService.java:

public class DownloadService extends Service {

    private Map mTasks =
            new LinkedHashMap();
    private Messenger mActivityMessenger;

    @SuppressLint("HandlerLeak")
    private Handler mHandle = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case Constant.MSG_INIT :
                    FileInfo fileInfo = (FileInfo) msg.obj;
                    DownloadTask task = new DownloadTask(DownloadService.this, mActivityMessenger, fileInfo, 3);
                    task.download();
                    mTasks.put(fileInfo.getId(), task);
                    Message startMsg = new Message();
                    startMsg.what = Constant.MSG_START;
                    startMsg.obj = fileInfo;
                    try {
                        mActivityMessenger.send(startMsg);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
                case Constant.MSG_BIND :
                    mActivityMessenger = msg.replyTo;
                    break;
            }
        }
    };

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        if (Constant.ACTION_START.equals(intent.getAction())) {
            FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("FileInfo");

            DownloadTask task = mTasks.get(fileInfo.getId());

            if (task == null || task.isPause || task.end) {
                new InitThread(fileInfo).httpConnection();
            }

        } else if (Constant.ACTION_STOP.equals(intent.getAction())) {
            FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("FileInfo");
            DownloadTask task = mTasks.get(fileInfo.getId());

            if (task != null) {
                task.isPause = true;
            }
        }

        return super.onStartCommand(intent, flags, startId);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {

        Messenger messenger = new Messenger(mHandle);

        return messenger.getBinder();
    }

    private class InitThread {

        private FileInfo mFileInfo;

        public InitThread(FileInfo fileInfo) {
            this.mFileInfo = fileInfo;
        }

        public void httpConnection() {
            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder()
                    .url(mFileInfo.getUrl())
                    .build();
            client.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {

                }

                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    InputStream is = null;
                    RandomAccessFile raf = null;
                    try {
                        is = response.body().byteStream();

                        int length = (int) response.body().contentLength();
                        File dir = new File(Constant.DOWNLOAD_PATH);
                        if (!dir.exists()) {
                            dir.mkdir();
                        }
                        File file = new File(dir, mFileInfo.getName());
                        raf = new RandomAccessFile(file, "rwd");
                        mFileInfo.setLength(length);
                        mHandle.obtainMessage(Constant.MSG_INIT, mFileInfo).sendToTarget();
                    } catch (IOException e) {
                        e.printStackTrace();
                    } finally {
                        raf.close();
                        is.close();
                    }
                }
            });
        }
    }
}

DownloadService 有两个全局变量,mTasks 是一个键对,用来确认每个下载任务的当前状态,每次启动一个新的任务,就会放入 mTasks 中。mActivityMessenger 是我们从 Activity 中获得的信使,它的初始化是在 onCreate() 中绑定服务时发生,绑定的时候会发送标识为 MSG_BIND 的消息给 Service,我们可以利用 Message 对象的 replyto 获得信使。

mHandler 是处理由 Activity 传来的消息,MSG_INIT 代表点击下载按钮时会触发初始化,DownloadService 中有个 InitThread,我们要先获得文件的长度,如果不知道这个我们很难实时更新下载进度。我访问网络是用的 OKHttp,对它不了解的朋友可以看我的博客Android网络框架OKHttp初解。

在 onResponse() 中做的操作是在其它线程中,所以我们的 InitThread 不用继承线程也可以让网络请求运行在其它线程中。我们获得了文件长度后,就发送消息通知 Handler 初始化完成了,可以开启 Task,进行下载。这时就要同步发送消息给 Activity 下载开始了。

onStartCommand() 是在使用 startService() 时调用,我用 Intent 来控制两个 Button 的点击效果。Action 为 START 时,说明要开始下载。这时从 intent 中获取 FileInfo 对象,然后在 mTasks 中找到对应的 DownloadTask,这时要避免多次点击,只有下载没有开始,暂停下载和下载结束这几种情况下进行初始化;当 Action 为 STOP 时,设置这个下载任务为暂停。

onBinder() 方法则是在 Service 与 Activity 绑定时调用的方法,我们可以把它看作 Service 与 Activity 的连接点,就是通过这个方法把 Service 的 Messenger 传递给 Activity。

MainActivity.java:

public class MainActivity extends AppCompatActivity {

    private RecyclerView mRecycler;
    private List list;
    private SimpleAdapter mAdapter;
    private NotificationUtil mUtil;
    private Messenger mServiceMessenger;

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

        getPermission();

        list = new ArrayList();

        FileInfo fileInfo = new FileInfo(0, 0, "http://m.down.sandai.net/MobileThunder/Android_5.29.2.4520/XLWXguanwang.apk",
                "xunlei.apk", 0);
        FileInfo fileInfo2 = new FileInfo(1, 0, "",
                "view.jpg", 0);
        FileInfo fileInfo3 = new FileInfo(2, 0, "http://gdown.baidu.com/data/wisegame/d28f2315db2b6f97/UCliulanqi_682.apk",
                "UC.apk", 0);

        list.add(fileInfo);
        list.add(fileInfo2);
        list.add(fileInfo3);

        mRecycler = (RecyclerView) findViewById(R.id.recycler);
        mAdapter = new SimpleAdapter(this, list);
        mRecycler.setAdapter(mAdapter);
        LinearLayoutManager mLayoutManager = new LinearLayoutManager(this,
                LinearLayoutManager.VERTICAL, false);
        mRecycler.setLayoutManager(mLayoutManager);

        mUtil = new NotificationUtil(this);

        Intent intent = new Intent(this, DownloadService.class);
        bindService(intent, sc, Service.BIND_AUTO_CREATE);
    }

    private ServiceConnection sc = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mServiceMessenger = new Messenger(service);
            Messenger messenger = new Messenger(mHandler);

            Message msg = new Message();
            msg.what = Constant.MSG_BIND;
            msg.replyTo = messenger;
            try {
                mServiceMessenger.send(msg);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    public void getPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
        }
    }

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {

            Bundle bundle;
            FileInfo fileInfo;

            switch (msg.what) {

                case Constant.MSG_START :
                    fileInfo = (FileInfo) msg.obj;
                    mUtil.showNotification(fileInfo);
                    break;

                case Constant.MSG_UPDATE :
                    bundle = msg.getData();
                    int progress = (int) bundle.get("finished");
                    String url = (String) bundle.get("url");
                    int id = (int) bundle.get("id");
                    mAdapter.updateProgress(url, progress);
                    mUtil.updateNotification(id, progress);
                    break;

                case Constant.MSG_FINISH :
                    fileInfo = (FileInfo) msg.obj;
                    mAdapter.removeDownload(fileInfo.getUrl());
                    Toast.makeText(MainActivity.this, fileInfo.getName() + "已下载完成!",
                            Toast.LENGTH_SHORT).show();
                    mUtil.cancelNotification(fileInfo.getId());
                    break;
            }

            super.handleMessage(msg);
        }
    };

    @Override
    protected void onDestroy() {
        unbindService(sc);
        super.onDestroy();
    }
}

MainActivity 的作用有设置布局,权限,设置 Handler 绑定服务。在 Handler 处理各种消息触发的逻辑。

布局是 RecyclerView,大家不了解的可以看我的博客Android控件–RecyclerView。

SimpleAdapter.java:

public class SimpleAdapter extends RecyclerView.Adapter {

    private Context context;
    private LayoutInflater mInflater;
    private List mDatas;
    private List isFirsts;

    public SimpleAdapter(Context context, List datas) {
        this.context = context;
        this.mDatas = datas;
        mInflater = LayoutInflater.from(context);
        isFirsts = new ArrayList();
        for (int i = 0; i < datas.size(); i++) {
            isFirsts.add(true);
        }
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        View view = mInflater.inflate(R.layout.item_layout, parent, false);
        MyViewHolder viewHolder = new MyViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {

        final FileInfo fileInfo = mDatas.get(position);
        if (isFirsts.get(position)) {
            holder.tv.setText(fileInfo.getName());
            holder.progress.setMax(100);
            holder.start.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent intent = new Intent(context, DownloadService.class);
                    intent.setAction(Constant.ACTION_START);
                    intent.putExtra("FileInfo", fileInfo);
                    context.startService(intent);
                }
            });
            holder.stop.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent intent = new Intent(context, DownloadService.class);
                    intent.setAction(Constant.ACTION_STOP);
                    intent.putExtra("FileInfo", fileInfo);
                    context.startService(intent);
                }
            });
            isFirsts.set(position, false);
        }
        holder.progress.setProgress(fileInfo.getFinished());
    }

    @Override
    public int getItemCount() {
        return mDatas.size();
    }

    class MyViewHolder extends RecyclerView.ViewHolder {

        TextView tv;
        Button start, stop;
        ProgressBar progress;

        public MyViewHolder(View itemView) {
            super(itemView);
            tv = (TextView) itemView.findViewById(R.id.text);
            start = (Button) itemView.findViewById(R.id.start);
            stop = (Button) itemView.findViewById(R.id.stop);
            progress = (ProgressBar) itemView.findViewById(R.id.progress);
        }
    }

    public void removeDownload(String url) {

        for (int i = 0; i < mDatas.size(); i++) {
            if (mDatas.get(i).getUrl().equals(url)) {
                mDatas.remove(i);
                isFirsts.remove(i);
                for (int j = 0; j < isFirsts.size(); j++) {
                    isFirsts.set(j, true);
                }
                notifyItemChanged(i);
                notifyDataSetChanged();
                break;
            }
        }
    }

    public void updateProgress(String url, int progress) {
        for (int i = 0; i < mDatas.size(); i++) {
            if (mDatas.get(i).getUrl().equals(url)) {
                FileInfo fileInfo = mDatas.get(i);
                fileInfo.setFinished(progress);
                notifyDataSetChanged();
                break;
            }
        }
    }
}

在适配器中有几个要注意的,控件的初始化比较简单,两个 Button 就是设置 Intent 传递文件信息启动服务,前面已经提到了。这里要看到的是 updateProgress() 和 removeDownload() 两个方法,前面的演示大家可以看见当下载任务完成后就会把 item 从列表中移除,而移除后 Item 在集合中的 id 就会发生变化,所以我们要找到对应的要更新的 item 就要用可以唯一标识的属性,这里用的是文件的 url。因为在列表中不会有相同的 url 的 item。

这里有个集合 isFirsts,因为对控件的初始化的操作有很多只用做一次,而 onBindViewHolder() 方法每次刷新都会调用一次,只是不必要的,所以用这个集合来确定是否是第一次调用这个方法。在移除 Item 后,要重新设置 isFirsts,否则就不会绘制好要变化的列表。

NotificationUtil 是用来设置通知的,NotificationUtil.java:

public class NotificationUtil {

    private NotificationManager manager;
    private Map notifications;
    private Context mContext;

    public NotificationUtil(Context context) {
        this.mContext = context;
        manager = (NotificationManager) context
                .getSystemService(context.NOTIFICATION_SERVICE);
        notifications = new HashMap<>();
    }

    public void showNotification(FileInfo fileInfo) {

        if (!notifications.containsKey(fileInfo.getId())) {

            NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)
                    .setSmallIcon(R.mipmap.ic_launcher)
                    .setTicker(fileInfo.getName() + "开始下载");

            builder.setAutoCancel(true);

            Intent intent = new Intent(mContext, MainActivity.class);

            PendingIntent pIntent = PendingIntent.getActivity(mContext, fileInfo.getId(), intent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            builder.setContentIntent(pIntent);

            RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(),
                    R.layout.notification_layout);

            //TextView
            remoteViews.setTextViewText(R.id.text, fileInfo.getName());

            //开始按钮
            Intent intentStart = new Intent(mContext, DownloadService.class);
            intentStart.setAction(Constant.ACTION_START);
            intentStart.putExtra("FileInfo", fileInfo);
            PendingIntent pStart = PendingIntent.getService(mContext, fileInfo.getId(), intentStart,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            remoteViews.setOnClickPendingIntent(R.id.startN, pStart);

            //停止按钮
            Intent intentStop = new Intent(mContext, DownloadService.class);
            intentStop.setAction(Constant.ACTION_STOP);
            intentStop.putExtra("FileInfo", fileInfo);
            PendingIntent pStop = PendingIntent.getService(mContext, fileInfo.getId(), intentStop,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            remoteViews.setOnClickPendingIntent(R.id.stopN, pStop);

            Notification notification = builder.setContent(remoteViews).build();

            manager.notify(fileInfo.getId(), notification);

            notifications.put(fileInfo.getId(), notification);
        }
    }

    public void cancelNotification(int id) {

        manager.cancel(id);
        notifications.remove(id);
    }

    public void updateNotification(int id, int progress) {
        Notification notification = notifications.get(id);
        if (notification != null) {

            notification.contentView.setProgressBar(R.id.progressN, 100, progress, false);
            manager.notify(id, notification);

        }
    }
}

Notification 的设置很简单,不过这里要注意 PendingIntent 的 getActivity() 方法,里面有一个参数 requestCode,这也是一个标识码。如果我们给每个 PendingIntent 都设置了相同的 requestCode,就会导致当发送多个 Notification 的时候,点击通知每次获取的都是第一个通知对应的内容。所以如果要显示多个布局相同的通知,就要给每个通知设置不同的 requestCode。

回到 MainActivity,要绑定服务需要 ServiceConnection 对象,onServiceConnected() 方法中的 Service 就有我们在 onBinder() 返回的 Messenger,利用这个信使把 Activity 的 Messenger 发送过去,就实现了双向通信了。

在 Handler 中共处理下载开始、更新和完成三种情况时的对 UI 的更新。

6、下载逻辑

DownloadTask.java:

public class DownloadTask {

    private Context mContext;
    private FileInfo mFileInfo;
    private DBManager manager;
    private long mFinished = 0;
    public boolean isPause = false;
    private int mThreadCount = 1;
    private List mThreadList = null;
    private Messenger mMessenger;
    public boolean end = false;

    public DownloadTask(Context context, Messenger messenger, FileInfo fileInfo, int threadCount) {
        this.mContext = context;
        this.mFileInfo = fileInfo;
        this.mMessenger = messenger;
        manager = new DBManager(mContext);
        this.mThreadCount = threadCount;
    }

    public void download() {

        List threads = manager.getThreads(mFileInfo.getUrl());

        if (threads.size() == 0) {
            int length = mFileInfo.getLength() / mThreadCount;
            for (int i = 0; i < mThreadCount; i++) {
                ThreadInfo threadInfo = new ThreadInfo(i, mFileInfo.getUrl(),
                        i * length, (i + 1) * length, 0);
                if (i == mThreadCount - 1) {
                    threadInfo.setEnd(mFileInfo.getLength());
                }
                threads.add(threadInfo);
                manager.insertThread(threadInfo);
            }
        }

        mThreadList = new ArrayList();

        for (ThreadInfo info: threads) {
            DownloadThread thread = new DownloadThread(info);
            mThreadList.add(thread);
            thread.httpConnection();
        }
    }

    private synchronized void checkAllThreadsFinished() {
        Boolean allFinished = true;
        for (DownloadThread thread : mThreadList) {
            if (!thread.isFinished) {
                allFinished = false;
                break;
            }
        }
        if (allFinished) {
            Message msg = new Message();
            msg.what = Constant.MSG_FINISH;
            msg.obj = mFileInfo;
            try {
                mMessenger.send(msg);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            manager.deleteThread(mFileInfo.getUrl());
            end = true;
        }
    }

    private class DownloadThread {

        private ThreadInfo mThreadInfo;
        public boolean isFinished = false;

        public DownloadThread (ThreadInfo threadInfo) {
            this.mThreadInfo = threadInfo;
        }

        public void httpConnection() {

            OkHttpClient client = new OkHttpClient();
            int start = mThreadInfo.getStart() + mThreadInfo.getFinished();
            mFinished += mThreadInfo.getFinished();
            Request request = new Request.Builder()
                    .url(mFileInfo.getUrl())
                    .header("RANGE", "bytes=" + start +
                            "-" + mThreadInfo.getEnd())
                    .build();

            client.newCall(request).enqueue(new MyCallBack(mThreadInfo));
        }

        private class MyCallBack implements Callback {

            private ThreadInfo mThreadInfo;

            public MyCallBack(ThreadInfo threadInfo) {
                this.mThreadInfo = threadInfo;
            }

            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {

                File dir = new File(Constant.DOWNLOAD_PATH);

                int start = mThreadInfo.getStart() + mThreadInfo.getFinished();

                if (!dir.exists()) {
                    dir.mkdir();
                }
                final File file = new File(dir, mFileInfo.getName());

                InputStream is = null;
                RandomAccessFile raf = null;
                try {

                    is = response.body().byteStream();
                    byte[] buf = new byte[1024];

                    raf = new RandomAccessFile(file, "rwd");
                    raf.seek(start);

                    int len;
                    int progress = (int)(mFinished * 100 / mFileInfo.getLength());
                    while ((len = is.read(buf)) != -1) {
                        raf.write(buf, 0, len);
                        mFinished += len;
                        int nowProgress = (int)(mFinished * 100 / mFileInfo.getLength());
                        mThreadInfo.setFinished(mThreadInfo.getFinished() + len);
                        if (nowProgress - progress >= 2) {
                            Message msg = new Message();
                            msg.what = Constant.MSG_UPDATE;
                            Bundle bundle = new Bundle();
                            bundle.putInt("finished", nowProgress);
                            bundle.putString("url", mFileInfo.getUrl());
                            bundle.putInt("id", mFileInfo.getId());
                            msg.setData(bundle);
                            mMessenger.send(msg);
                            progress = nowProgress;
                        }

                        if (isPause) {
                            manager.updateData(mThreadInfo.getUrl(), mThreadInfo.getId(),
                                    mThreadInfo.getFinished());
                            return;
                        }

                    }
                    isFinished = true;
                    checkAllThreadsFinished();

                } catch (IOException e) {
                    e.printStackTrace();
                } catch (RemoteException e) {
                    e.printStackTrace();
                } finally {
                    raf.close();
                    is.close();
                }
            }
        }
    }
}

首先介绍 DownloadTask 的几个全局变量,messenger 就是 DownloadService 中的 mActivityMessenger,用来向 Activity 传递消息。threadCount 是使用几个线程对这个文件进行下载。isPause 和 end 是判断这个下载任务是否是暂停或是完成状态,mFinished 是下载完成的字节总数。

download() 方法中是先判断这个 FileInfo 是否存在线程信息于数据库中, 存在说明已经下载过但没有完成,不存在 size 为0则创建线程。ThreadInfo 的信息之前说过如何设置。之后遍历线程组,开始每个线程的任务。

我们依然用的是 OKHttp,在 header 中设置下载的区间。下载过程中需要注意的就是如何更新进度,我们要传递过去的 progress 是一个 0 - 100 的数,所以我们要小小计算一下,因为 int 的范围如果我们要下载比较大的文件时乘上 100 就不够了,所以使用 long。

每当进度变化大于等于 2 时再做更新,以免 UI 阻塞。

在暂停的时候,更新数据库,因为每次点击下载都会调用 download() 方法,所以集合 threads 的信息会一直更新,这就不用重新下载了。要想重程序意外被杀死也能保存进度的话,可以实时更新,也可以在 onDestroy() 中更新。

checkAllThreadsFinished() 方法就是用来检查是否下载完成,我们为每个 DownloadThread 都设置了一个 isFinished,只要所有线程的属性值都是 true 就说明下载完成,这时就可以用 Messenger 通知 Activity 了。

这个案例来自慕课网的视频,因为是好几年前的,所以有很多内容需要修改,我的代码使用现在流行的 OKHttp 和 RecyclerView,整理了一些功能,有兴趣的朋友可以看看。

程序虽然简单,但涉及到的知识还是很多的,如果能够熟练运用这些内容,相信一般的开发已经难不到你啦。

结束语:本文仅用来学习记录,参考查阅。

你可能感兴趣的:(Android进阶学习,android)