├── README.md └── servicebestpractice ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── servicebestpractice.iml └── src ├── androidTest └── java │ └── com │ └── example │ └── servicebestpractice │ └── ApplicationTest.java ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── example │ │ └── servicebestpractice │ │ ├── DownloadListener.java │ │ ├── DownloadService.java │ │ ├── DownloadTask.java │ │ └── MainActivity.java └── res │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── test └── java └── com └── example └── servicebestpractice └── ExampleUnitTest.java /README.md: -------------------------------------------------------------------------------- 1 | # OKHttp_DownloadFile 2 | (1)用OKHttp实现大文件下载(2)OKHttp实现文件下载的断点续传(3)取消下载时删除已下载的文件 Microstrong Microstrong&&you&&me 3 | -------------------------------------------------------------------------------- /servicebestpractice/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /servicebestpractice/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion '23.0.1' 6 | 7 | defaultConfig { 8 | applicationId "com.example.servicebestpractice" 9 | minSdkVersion 17 10 | targetSdkVersion 24 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:24.2.1' 26 | compile 'com.squareup.okhttp3:okhttp:3.4.1' 27 | } 28 | -------------------------------------------------------------------------------- /servicebestpractice/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\androidDeveloper\android-studio-bundle-143.2821654-windows\sdk1/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /servicebestpractice/servicebestpractice.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /servicebestpractice/src/androidTest/java/com/example/servicebestpractice/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.example.servicebestpractice; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /servicebestpractice/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /servicebestpractice/src/main/java/com/example/servicebestpractice/DownloadListener.java: -------------------------------------------------------------------------------- 1 | package com.example.servicebestpractice; 2 | 3 | /** 4 | * Created by Administrator on 2017/2/23. 5 | */ 6 | public interface DownloadListener { 7 | 8 | 9 | /** 10 | * 通知当前的下载进度 11 | * @param progress 12 | */ 13 | void onProgress(int progress); 14 | 15 | /** 16 | * 通知下载成功 17 | */ 18 | void onSuccess(); 19 | 20 | /** 21 | * 通知下载失败 22 | */ 23 | void onFailed(); 24 | 25 | /** 26 | * 通知下载暂停 27 | */ 28 | void onPaused(); 29 | 30 | /** 31 | * 通知下载取消事件 32 | */ 33 | void onCanceled(); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /servicebestpractice/src/main/java/com/example/servicebestpractice/DownloadService.java: -------------------------------------------------------------------------------- 1 | package com.example.servicebestpractice; 2 | 3 | import android.app.Notification; 4 | import android.app.NotificationManager; 5 | import android.app.PendingIntent; 6 | import android.app.Service; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.graphics.BitmapFactory; 10 | import android.os.Binder; 11 | import android.os.Environment; 12 | import android.os.IBinder; 13 | import android.support.v4.app.NotificationCompat; 14 | import android.widget.Toast; 15 | 16 | import java.io.File; 17 | 18 | /** 19 | * 为了保证DownloadTask可以一直在后台运行,我们还需要创建一个下载的服务。 20 | */ 21 | public class DownloadService extends Service { 22 | 23 | private DownloadTask downloadTask; 24 | 25 | private String downloadUrl; 26 | 27 | private DownloadListener listener=new DownloadListener() { 28 | 29 | /** 30 | * 构建了一个用于显示下载进度的通知 31 | * @param progress 32 | */ 33 | @Override 34 | public void onProgress(int progress) { 35 | //NotificationManager的notify()可以让通知显示出来。 36 | //notify(),接收两个参数,第一个参数是id:每个通知所指定的id都是不同的。第二个参数是Notification对象。 37 | getNotificationManager().notify(1,getNotification("Downloading...",progress)); 38 | } 39 | 40 | /** 41 | * 创建了一个新的通知用于告诉用户下载成功啦 42 | */ 43 | @Override 44 | public void onSuccess() { 45 | downloadTask=null; 46 | //下载成功时将前台服务通知关闭,并创建一个下载成功的通知 47 | stopForeground(true); 48 | getNotificationManager().notify(1,getNotification("Download Success",-1)); 49 | Toast.makeText(DownloadService.this,"Download Success",Toast.LENGTH_SHORT).show(); 50 | } 51 | 52 | /** 53 | *用户下载失败 54 | */ 55 | @Override 56 | public void onFailed() { 57 | downloadTask=null; 58 | //下载失败时,将前台服务通知关闭,并创建一个下载失败的通知 59 | stopForeground(true); 60 | getNotificationManager().notify(1,getNotification("Download Failed",-1)); 61 | Toast.makeText(DownloadService.this,"Download Failed",Toast.LENGTH_SHORT).show(); 62 | } 63 | 64 | /** 65 | * 用户暂停 66 | */ 67 | @Override 68 | public void onPaused() { 69 | downloadTask=null; 70 | Toast.makeText(DownloadService.this,"Download Paused",Toast.LENGTH_SHORT).show(); 71 | } 72 | 73 | /** 74 | * 用户取消 75 | */ 76 | @Override 77 | public void onCanceled() { 78 | downloadTask=null; 79 | //取消下载,将前台服务通知关闭,并创建一个下载失败的通知 80 | stopForeground(true); 81 | Toast.makeText(DownloadService.this,"Download Canceled",Toast.LENGTH_SHORT).show(); 82 | } 83 | }; 84 | 85 | private DownloadBinder mBinder=new DownloadBinder(); 86 | 87 | @Override 88 | public IBinder onBind(Intent intent) { 89 | return mBinder; 90 | } 91 | 92 | /** 93 | * 为了要让DownloadService可以和活动进行通信,我们创建了一个DownloadBinder对象 94 | */ 95 | class DownloadBinder extends Binder{ 96 | 97 | /** 98 | * 开始下载 99 | * @param url 100 | */ 101 | public void startDownload(String url){ 102 | if(downloadTask==null){ 103 | downloadUrl=url; 104 | downloadTask=new DownloadTask(listener); 105 | //启动下载任务 106 | downloadTask.execute(downloadUrl); 107 | startForeground(1,getNotification("Downloading...",0)); 108 | Toast.makeText(DownloadService.this, "Downloading...", Toast.LENGTH_SHORT).show(); 109 | } 110 | } 111 | 112 | /** 113 | * 暂停下载 114 | */ 115 | public void pauseDownload(){ 116 | if(downloadTask!=null){ 117 | downloadTask.pauseDownload(); 118 | } 119 | } 120 | 121 | /** 122 | * 取消下载 123 | */ 124 | public void cancelDownload(){ 125 | if(downloadTask!=null){ 126 | downloadTask.cancelDownload(); 127 | }else { 128 | if(downloadUrl!=null){ 129 | //取消下载时需要将文件删除,并将通知关闭 130 | String fileName=downloadUrl.substring(downloadUrl.lastIndexOf("/")); 131 | String directory= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath(); 132 | File file=new File(directory+fileName); 133 | if(file.exists()){ 134 | file.delete(); 135 | } 136 | getNotificationManager().cancel(1); 137 | stopForeground(true); 138 | Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show(); 139 | } 140 | } 141 | 142 | } 143 | 144 | 145 | 146 | } 147 | 148 | /** 149 | * 获取NotificationManager的实例,对通知进行管理 150 | * @return 151 | */ 152 | private NotificationManager getNotificationManager(){ 153 | return (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 154 | } 155 | 156 | /** 157 | * 158 | * @param title 159 | * @param progress 160 | * @return 161 | */ 162 | private Notification getNotification(String title,int progress){ 163 | Intent intent=new Intent(this,MainActivity.class); 164 | //PendingIntent是等待的Intent,这是跳转到一个Activity组件。当用户点击通知时,会跳转到MainActivity 165 | PendingIntent pi=PendingIntent.getActivity(this,0,intent,0); 166 | /** 167 | * 几乎Android系统的每一个版本都会对通知这部分功能进行获多或少的修改,API不稳定行问题在通知上面凸显的尤其严重。 168 | * 解决方案是:用support库中提供的兼容API。support-v4库中提供了一个NotificationCompat类,使用它可以保证我们的 169 | * 程序在所有的Android系统版本中都能正常工作。 170 | */ 171 | NotificationCompat.Builder builder=new NotificationCompat.Builder(this); 172 | //设置通知的小图标 173 | builder.setSmallIcon(R.mipmap.ic_launcher); 174 | //设置通知的大图标,当下拉系统状态栏时,就可以看到设置的大图标 175 | builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher)); 176 | //当通知被点击的时候,跳转到MainActivity中 177 | builder.setContentIntent(pi); 178 | //设置通知的标题 179 | builder.setContentTitle(title); 180 | if(progress>0){ 181 | //当progress大于或等于0时,才需要显示下载进度 182 | builder.setContentText(progress+"%"); 183 | builder.setProgress(100,progress,false); 184 | } 185 | return builder.build(); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /servicebestpractice/src/main/java/com/example/servicebestpractice/DownloadTask.java: -------------------------------------------------------------------------------- 1 | package com.example.servicebestpractice; 2 | 3 | import android.os.AsyncTask; 4 | import android.os.Environment; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.RandomAccessFile; 10 | 11 | import okhttp3.OkHttpClient; 12 | import okhttp3.Request; 13 | import okhttp3.Response; 14 | 15 | /** 16 | * Created by Administrator on 2017/2/23. 17 | */ 18 | 19 | /** 20 | * String 在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。 21 | * Integer 后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。 22 | * Integer 当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。 23 | */ 24 | public class DownloadTask extends AsyncTask { 25 | 26 | public static final int TYPE_SUCCESS=0; 27 | 28 | public static final int TYPE_FAILED=1; 29 | 30 | public static final int TYPE_PAUSED=2; 31 | 32 | public static final int TYPE_CANCELED=3; 33 | 34 | private DownloadListener listener; 35 | 36 | private boolean isCanceled=false; 37 | 38 | private boolean isPaused=false; 39 | 40 | private int lastProgress; 41 | 42 | public DownloadTask(DownloadListener listener) { 43 | this.listener = listener; 44 | } 45 | 46 | /** 47 | * 这个方法中的所有代码都会在子线程中运行,我们应该在这里处理所有的耗时任务。 48 | * @param params 49 | * @return 50 | */ 51 | @Override 52 | protected Integer doInBackground(String... params) { 53 | InputStream is=null; 54 | RandomAccessFile savedFile=null; 55 | File file=null; 56 | long downloadLength=0; //记录已经下载的文件长度 57 | //文件下载地址 58 | String downloadUrl=params[0]; 59 | //下载文件的名称 60 | String fileName=downloadUrl.substring(downloadUrl.lastIndexOf("/")); 61 | //下载文件存放的目录 62 | String directory= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath(); 63 | //创建一个文件 64 | file=new File(directory+fileName); 65 | if(file.exists()){ 66 | //如果文件存在的话,得到文件的大小 67 | downloadLength=file.length(); 68 | } 69 | //得到下载内容的大小 70 | long contentLength=getContentLength(downloadUrl); 71 | if(contentLength==0){ 72 | return TYPE_FAILED; 73 | }else if(contentLength==downloadLength){ 74 | //已下载字节和文件总字节相等,说明已经下载完成了 75 | return TYPE_SUCCESS; 76 | } 77 | OkHttpClient client=new OkHttpClient(); 78 | /** 79 | * HTTP请求是有一个Header的,里面有个Range属性是定义下载区域的,它接收的值是一个区间范围, 80 | * 比如:Range:bytes=0-10000。这样我们就可以按照一定的规则,将一个大文件拆分为若干很小的部分, 81 | * 然后分批次的下载,每个小块下载完成之后,再合并到文件中;这样即使下载中断了,重新下载时, 82 | * 也可以通过文件的字节长度来判断下载的起始点,然后重启断点续传的过程,直到最后完成下载过程。 83 | */ 84 | Request request=new Request.Builder() 85 | .addHeader("RANGE","bytes="+downloadLength+"-") //断点续传要用到的,指示下载的区间 86 | .url(downloadUrl) 87 | .build(); 88 | try { 89 | Response response=client.newCall(request).execute(); 90 | if(response!=null){ 91 | is=response.body().byteStream(); 92 | savedFile=new RandomAccessFile(file,"rw"); 93 | savedFile.seek(downloadLength);//跳过已经下载的字节 94 | byte[] b=new byte[1024]; 95 | int total=0; 96 | int len; 97 | while((len=is.read(b))!=-1){ 98 | if(isCanceled){ 99 | return TYPE_CANCELED; 100 | }else if(isPaused){ 101 | return TYPE_PAUSED; 102 | }else { 103 | total+=len; 104 | savedFile.write(b,0,len); 105 | //计算已经下载的百分比 106 | int progress=(int)((total+downloadLength)*100/contentLength); 107 | //注意:在doInBackground()中是不可以进行UI操作的,如果需要更新UI,比如说反馈当前任务的执行进度, 108 | //可以调用publishProgress()方法完成。 109 | publishProgress(progress); 110 | } 111 | 112 | } 113 | response.body().close(); 114 | return TYPE_SUCCESS; 115 | } 116 | } catch (IOException e) { 117 | e.printStackTrace(); 118 | }finally { 119 | try{ 120 | if(is!=null){ 121 | is.close(); 122 | } 123 | if(savedFile!=null){ 124 | savedFile.close(); 125 | } 126 | if(isCanceled&&file!=null){ 127 | file.delete(); 128 | } 129 | }catch (Exception e){ 130 | e.printStackTrace(); 131 | } 132 | } 133 | return TYPE_FAILED; 134 | } 135 | 136 | /** 137 | * 当在后台任务中调用了publishProgress(Progress...)方法之后,onProgressUpdate()方法 138 | * 就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以 139 | * 对界面进行相应的更新。 140 | * @param values 141 | */ 142 | protected void onProgressUpdate(Integer...values){ 143 | int progress=values[0]; 144 | if(progress>lastProgress){ 145 | listener.onProgress(progress); 146 | lastProgress=progress; 147 | } 148 | } 149 | 150 | /** 151 | * 当后台任务执行完毕并通过Return语句进行返回时,这个方法就很快被调用。返回的数据会作为参数 152 | * 传递到此方法中,可以利用返回的数据来进行一些UI操作。 153 | * @param status 154 | */ 155 | @Override 156 | protected void onPostExecute(Integer status) { 157 | switch (status){ 158 | case TYPE_SUCCESS: 159 | listener.onSuccess(); 160 | break; 161 | case TYPE_FAILED: 162 | listener.onFailed(); 163 | break; 164 | case TYPE_PAUSED: 165 | listener.onPaused(); 166 | break; 167 | case TYPE_CANCELED: 168 | listener.onCanceled(); 169 | break; 170 | default: 171 | break; 172 | } 173 | } 174 | 175 | public void pauseDownload(){ 176 | isPaused=true; 177 | } 178 | 179 | public void cancelDownload(){ 180 | isCanceled=true; 181 | } 182 | 183 | /** 184 | * 得到下载内容的大小 185 | * @param downloadUrl 186 | * @return 187 | */ 188 | private long getContentLength(String downloadUrl){ 189 | OkHttpClient client=new OkHttpClient(); 190 | Request request=new Request.Builder().url(downloadUrl).build(); 191 | try { 192 | Response response=client.newCall(request).execute(); 193 | if(response!=null&&response.isSuccessful()){ 194 | long contentLength=response.body().contentLength(); 195 | response.body().close(); 196 | return contentLength; 197 | } 198 | } catch (IOException e) { 199 | e.printStackTrace(); 200 | } 201 | return 0; 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /servicebestpractice/src/main/java/com/example/servicebestpractice/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.servicebestpractice; 2 | 3 | import android.Manifest; 4 | import android.content.ComponentName; 5 | import android.content.Intent; 6 | import android.content.ServiceConnection; 7 | import android.content.pm.PackageManager; 8 | import android.os.Bundle; 9 | import android.os.IBinder; 10 | import android.support.v4.app.ActivityCompat; 11 | import android.support.v4.content.ContextCompat; 12 | import android.support.v7.app.AppCompatActivity; 13 | import android.view.View; 14 | import android.widget.Button; 15 | import android.widget.Toast; 16 | 17 | public class MainActivity extends AppCompatActivity implements View.OnClickListener{ 18 | 19 | private DownloadService.DownloadBinder downloadBinder; 20 | 21 | private ServiceConnection connection=new ServiceConnection() { 22 | 23 | @Override 24 | public void onServiceConnected(ComponentName name, IBinder service) { 25 | downloadBinder=(DownloadService.DownloadBinder) service; 26 | } 27 | 28 | @Override 29 | public void onServiceDisconnected(ComponentName name) { 30 | 31 | } 32 | }; 33 | 34 | @Override 35 | protected void onCreate(Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | setContentView(R.layout.activity_main); 38 | Button startDownload=(Button) findViewById(R.id.start_download); 39 | startDownload.setOnClickListener(this); 40 | Button pauseDownload=(Button) findViewById(R.id.pause_download); 41 | pauseDownload.setOnClickListener(this); 42 | Button cancelDownload=(Button)findViewById(R.id.cancel_download); 43 | cancelDownload.setOnClickListener(this); 44 | Intent intent=new Intent(this,DownloadService.class); 45 | //这一点至关重要,因为启动服务可以保证DownloadService一直在后台运行,绑定服务则可以让MaiinActivity和DownloadService 46 | //进行通信,因此两个方法的调用都必不可少。 47 | startService(intent); //启动服务 48 | bindService(intent,connection,BIND_AUTO_CREATE);//绑定服务 49 | /** 50 | *运行时权限处理:我们需要再用到权限的地方,每次都要检查是否APP已经拥有权限 51 | * 下载功能,需要些SD卡的权限,我们在写入之前检查是否有WRITE_EXTERNAL_STORAGE权限,没有则申请权限 52 | */ 53 | if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED){ 54 | ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1); 55 | } 56 | } 57 | 58 | public void onClick(View v){ 59 | if(downloadBinder==null){ 60 | return; 61 | } 62 | switch (v.getId()){ 63 | case R.id.start_download: 64 | //String url="http://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe"; 65 | String url="http://10.0.2.2:8080/ChromeSetup.exe"; 66 | downloadBinder.startDownload(url); 67 | break; 68 | case R.id.pause_download: 69 | downloadBinder.pauseDownload(); 70 | break; 71 | case R.id.cancel_download: 72 | downloadBinder.cancelDownload(); 73 | break; 74 | default: 75 | break; 76 | } 77 | } 78 | 79 | /** 80 | * 用户选择允许或拒绝后,会回调onRequestPermissionsResult 81 | * @param requestCode 请求码 82 | * @param permissions 83 | * @param grantResults 授权结果 84 | */ 85 | @Override 86 | public void onRequestPermissionsResult(int requestCode,String[] permissions,int[] grantResults) { 87 | switch (requestCode){ 88 | case 1: 89 | if(grantResults.length>0&&grantResults[0]!= PackageManager.PERMISSION_GRANTED){ 90 | Toast.makeText(this,"拒绝权限将无法使用程序",Toast.LENGTH_SHORT).show(); 91 | finish(); 92 | } 93 | break; 94 | } 95 | } 96 | 97 | @Override 98 | protected void onDestroy() { 99 | super.onDestroy(); 100 | //解除绑定服务 101 | unbindService(connection); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /servicebestpractice/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 |