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.