Best Practices for Services--Download Example for Full Edition

Posted by robembra on Mon, 10 Jun 2019 18:57:39 +0200

This is mainly the problems and solutions encountered in 10.6 of the second edition of "Android First Line of Code". Write them down in the hope that they can help you, and that you can have better solutions to communicate with each other.

The Android Studio version is as follows:

 

 

Here I'll start by listing the errors that occur when running against the code in the book:

(1),java.net.UnknownServiceException: CLEARTEXT communication to raw.githubusercontent.com not permitted by network security policy

(2),java.lang.SecurityException: Permission Denial: startForeground from pid=9733, uid=10085 requires android.permission.FOREGROUND_SERVICE

(3),java.io.IOException: unexpected end of stream on http://raw.githubusercontent.com/...

Of course, there is also a Channel-missing error, which is relatively simple and will be modified in later code, which is not to be overlooked here.

 

Now let's look at the three errors above.

(1) The first error is mainly the error of using Http for network access. There are three solutions. Interested ones can look at the previous blog I wrote. Here I will only give the solution. Add the following in AndroidManifest.xml:

 

<application

    ......

    android:usesCleartextTraffic="true"

    ......

</application>

 

 

 

 


(2) This major Android 9.0 issue requires permission to use the foreground service, which is added in AndroidManifest.xml as follows:

 

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

 

 

 

 

 

(3) The problem is hidden. I looked for a solution only after looking for a lot of data, adding the following in the android closure of app/build.gradle:

 

compileOptions{
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8

        }

 

 

 

This code is to turn on Java 1.8 and use Lambda. To be honest, I don't understand the underlying reason. If I find it later, I'll come back and update it, and I hope Dallas can point out how it works.

 

Without adding this code, the program will install normally, but there will be problems starting the download. Here are two pictures

 

 

 

 

 

 

Let's write this project in full with the code in the book.

1. Add Dependent Packages

Edit the app/build.gradle file and add the following to the dependencies closure:

 

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.0.0-beta01'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.1.0-alpha4'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha4'
    implementation 'com.squareup.okhttp3:okhttp:3.14.2'
}

Add a red font here.

Note that compiles have been completely replaced by implementations. Because previous projects relied on compiles uniformly, the result is that module coupling is too high to disassemble the project. Although using implementations is complicated, it is a good way to reduce the coupling and improve security.

 

2. Define callback interface

Define a callback interface to listen for and callback various states during download with the following code:

 

//Define a callback interface for monitoring and callback various states during download
public interface DownloadListener {
    void onProgress(int progress);        //Used to notify current download progress

    void onSuccess();                     //Used to notify download success events

    void onFailed();                      //Used to notify download failure events

    void onPaused();                      //Used to notify download success events

    void onCanceled();                    //Used to notify download cancellation events
}

 

 

3. Write download tasks

Use AsyncTask for implementation.The code is as follows:

import android.os.AsyncTask;
import android.os.Environment;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;




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;                                   //Record downloaded file length
            String downloadURL=params[0];
            String fileName=downloadURL.substring(downloadURL.lastIndexOf("/"));
            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){
                //Downloaded bytes are equal to total file bytes, proving download is complete
                return TYPE_SUCCESS;
            }
            OkHttpClient client=new OkHttpClient();
            Request request=new Request.Builder()                                        //Breakpoint download, which byte to start downloading
                    .addHeader("RANGE","bytes="+downloadedLength+"-")
                    .url(downloadURL)
                    .build();
            Response response=client.newCall(request).execute();
            if(request!=null){
                is=response.body().byteStream();
                savedFile=new RandomAccessFile(file,"rw");
                savedFile.seek(downloadedLength);                       //Skip downloaded bytes
                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);

                        //Calculate percentage of Downloads
                        int progress=(int)((total+downloadedLength)*100/contentLength);
                        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;
    }

    @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_SUCCESS:
                listener.onSuccess();
                break;

            case TYPE_FAILED:
                listener.onFailed();
                break;

            case TYPE_PAUSED:
                listener.onPaused();
                break;

            case TYPE_CANCELED:
                listener.onCanceled();
                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.body().close();
            return contentLength;
        }
        return 0;
    }
}

 

This part follows the code in the book.

 

 

4. Create a download service

To keep DownloadTask running in the background, create a download service with the following code:

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Environment;
import android.os.IBinder;
import android.provider.Settings;
import android.widget.Toast;

import java.io.File;

import androidx.core.app.NotificationCompat;

//To ensure DownloadTask Can always be running in the background
public class DownloadService extends Service {

    private DownloadTask downloadTask;
    private String downloadUrl;

    private DownloadListener listener=new DownloadListener() {
        @Override
        public void onProgress(int progress) {
            //Build notifications showing download progress and trigger notifications
            getNotificationManager().notify(1, getNotification("Downloading ...",progress));
        }

        @Override
        public void onSuccess() {
            downloadTask=null;
            //Download successfully shuts down the foreground service and creates a download success notification
            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;
            //Download failure shuts down the foreground service and creates a download failure notification
            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,"Download Pause",Toast.LENGTH_SHORT).show();

        }

        @Override
        public void onCanceled() {
            downloadTask=null;
            stopForeground(true);
            Toast.makeText(DownloadService.this,"Download Canceled",Toast.LENGTH_SHORT).show();

        }
    };

    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);
                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();
            }
            if(downloadUrl!=null){
                //Delete downloaded files and close notifications when cancelling Download
                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();
            }
        }
    }

    //Obtain NotificationManager Example
    private NotificationManager getNotificationManager(){
        return (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
    }

    //Show download progress
    private Notification getNotification(String title,int progress){
        Intent intent=new Intent(this,MainActivity.class);
        PendingIntent pi=PendingIntent.getActivity(this,0,intent,0);

        NotificationManager manager=(NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
        NotificationChannel channel=null;
        Uri uri= Settings.System.DEFAULT_NOTIFICATION_URI;

        //Versions after Android 8.0 require notification channels
        if(android.os.Build.VERSION.SDK_INT>= Build.VERSION_CODES.O){
            channel=new NotificationChannel("Notification","This is 2",NotificationManager.IMPORTANCE_HIGH);
            channel.setDescription("This is 1");
            channel.setSound(uri,Notification.AUDIO_ATTRIBUTES_DEFAULT);
            manager.createNotificationChannel(channel);

        }


        NotificationCompat.Builder builder=new NotificationCompat.Builder(this);
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),
                R.mipmap.ic_launcher));
        builder.setWhen(System.currentTimeMillis());
        builder.setContentIntent(pi);
        builder.setContentTitle(title);
        builder.setChannelId("Notification");
        builder.setAutoCancel(true);
        if(progress>=0){
            //Show download progress when progress is greater than or equal to 0
            builder.setContentText(progress+"%");
            builder.setProgress(100,progress,false);
        }
        return builder.build();
    }
}

 

The code you need to modify is marked in red here. The main reason for this difference is that Channel was introduced after Android 8(API 26). All Notifications specify Channel, which you can set individually for each Channel; such as Notification switch, reminder tone, vibration or importance; so each applicationNotifications for programs are transparent to users.

 

I won't go into details here, but here I've found a more concise summary for interesting reference: https://www.jianshu.com/p/b529e61d220a

 

 

5. Write front-end code

Modify the code in activity_main.xml as follows:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <Button
            android:id="@+id/start_download"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Start Download"
            android:textAllCaps="false"/>

        <Button
            android:id="@+id/pause_download"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Pause download"
            android:textAllCaps="false" />

        <Button
            android:id="@+id/cancel_download"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Cancel download"
            android:textAllCaps="false" />

    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

 

 

Here I use LinearLayout nested directly in Constranintlayout to save time. It has no effect on the interface. It's the same as the code in the book. Finally, let's modify the code in MainActivity. The code is as follows:

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private static final String TAG = "MainActivity";
    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) {
        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);

        //Start Services
        Intent intent=new Intent(this,DownloadService.class);
        startService(intent);

        //Binding Service
        bindService(intent,connection,BIND_AUTO_CREATE);

        //Determine if you have access to memory
        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);
        }
    }

    @Override
    public void onClick(View v) {
        if(downloadBinder==null){
            return;
        }
        switch (v.getId()){
            case R.id.start_download:
                String url="http://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe";
                Log.d(TAG, "onClick: ");
                downloadBinder.startDownload(url);
                break;
            case R.id.pause_download:
                downloadBinder.pauseDownload();
                break;
            case R.id.cancel_download:
                downloadBinder.cancelDownload();
                break;
            default:
                break;
        }
    }

    @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,"Denying permission will make the program unavailable",Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
            default:
        }
    }

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

 

 

 

 

 

6. Profile

 

Add the following code:

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

 

 

This is mainly to apply for network access, access to SD cards and access to foreground services.

Add the following code to the application tag:

<application

    ......

    android:usesCleartextTraffic="true"

    ......

</application>

 

Add the following to the android closure of app/build.gradle:

compileOptions{
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }

 

 

At this point, the download project will be working properly.

In the near future, if there are any errors in Android, please correct them. I hope to communicate with you more.

Topics: PHP Android Java xml network