diff --git a/Aria/src/main/java/com/arialyy/aria/core/AriaManager.java b/Aria/src/main/java/com/arialyy/aria/core/AriaManager.java index 2bfd5868..3be6dd9f 100644 --- a/Aria/src/main/java/com/arialyy/aria/core/AriaManager.java +++ b/Aria/src/main/java/com/arialyy/aria/core/AriaManager.java @@ -39,6 +39,7 @@ import com.arialyy.aria.orm.DbEntity; import com.arialyy.aria.orm.DbUtil; import com.arialyy.aria.util.CAConfiguration; import com.arialyy.aria.util.Configuration; +import com.arialyy.aria.util.Speed; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -79,6 +80,13 @@ import java.util.Map; return mReceivers; } + /** + * 设置最大下载速度 + */ + public void setMaxSpeed(Speed speed) { + Configuration.getInstance().setMaxSpeed(speed); + } + /** * 设置命令 */ diff --git a/Aria/src/main/java/com/arialyy/aria/core/download/SingleThreadTask.java b/Aria/src/main/java/com/arialyy/aria/core/download/SingleThreadTask.java index 421f2414..71f4836b 100644 --- a/Aria/src/main/java/com/arialyy/aria/core/download/SingleThreadTask.java +++ b/Aria/src/main/java/com/arialyy/aria/core/download/SingleThreadTask.java @@ -18,6 +18,7 @@ package com.arialyy.aria.core.download; import android.util.Log; import com.arialyy.aria.util.BufferedRandomAccessFile; import com.arialyy.aria.util.CommonUtil; +import com.arialyy.aria.util.Configuration; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -31,12 +32,13 @@ import java.util.Properties; * 下载线程 */ final class SingleThreadTask implements Runnable { - private static final int BUF_SIZE = 8192; private static final String TAG = "SingleThreadTask"; private DownloadUtil.ConfigEntity mConfigEntity; private String mConfigFPath; private long mChildCurrentLocation = 0; private static final Object LOCK = new Object(); + private int mBufSize = 8192; + //private int mBufSize = 64; private IDownloadListener mListener; private DownloadStateConstance mConstance; @@ -48,6 +50,7 @@ final class SingleThreadTask implements Runnable { if (mConfigEntity.isSupportBreakpoint) { mConfigFPath = downloadInfo.CONFIG_FILE_PATH; } + //mBufSize = Configuration.getInstance().getMaxSpeed(); } @Override public void run() { @@ -77,10 +80,10 @@ final class SingleThreadTask implements Runnable { is = conn.getInputStream(); //创建可设置位置的文件 BufferedRandomAccessFile file = - new BufferedRandomAccessFile(mConfigEntity.TEMP_FILE, "rwd", BUF_SIZE); + new BufferedRandomAccessFile(mConfigEntity.TEMP_FILE, "rwd", mBufSize); //设置每条线程写入文件的位置 file.seek(mConfigEntity.START_LOCATION); - byte[] buffer = new byte[BUF_SIZE]; + byte[] buffer = new byte[mBufSize]; int len; //当前子线程的下载位置 mChildCurrentLocation = mConfigEntity.START_LOCATION; diff --git a/Aria/src/main/java/com/arialyy/aria/core/upload/UploadTaskEntity.java b/Aria/src/main/java/com/arialyy/aria/core/upload/UploadTaskEntity.java index 07818b8d..7db1ab9c 100644 --- a/Aria/src/main/java/com/arialyy/aria/core/upload/UploadTaskEntity.java +++ b/Aria/src/main/java/com/arialyy/aria/core/upload/UploadTaskEntity.java @@ -29,8 +29,8 @@ public class UploadTaskEntity extends ITaskEntity { public String uploadUrl; //上传路径 public String attachment; //文件上传需要的key public String contentType = "multipart/form-data"; //上传的文件类型 - public String charset = "utf-8"; public String userAgent = "User-Agent"; + public String charset = "utf-8"; /** * 文件上传表单 diff --git a/Aria/src/main/java/com/arialyy/aria/core/upload/UploadUtil.java b/Aria/src/main/java/com/arialyy/aria/core/upload/UploadUtil.java index d946f4d7..309594d7 100644 --- a/Aria/src/main/java/com/arialyy/aria/core/upload/UploadUtil.java +++ b/Aria/src/main/java/com/arialyy/aria/core/upload/UploadUtil.java @@ -35,7 +35,7 @@ import java.util.UUID; * Created by Aria.Lao on 2017/2/9. * 上传工具 */ -public class UploadUtil implements Runnable { +final class UploadUtil implements Runnable { private static final String TAG = "UploadUtil"; private final String BOUNDARY = UUID.randomUUID().toString(); // 边界标识 随机生成 private final String PREFIX = "--", LINE_END = "\r\n"; @@ -46,8 +46,9 @@ public class UploadUtil implements Runnable { private long mCurrentLocation = 0; private boolean isCancel = false; private boolean isRunning = false; + private OutputStream mOutputStream; - public UploadUtil(UploadTaskEntity taskEntity, IUploadListener listener) { + UploadUtil(UploadTaskEntity taskEntity, IUploadListener listener) { mTaskEntity = taskEntity; CheckUtil.checkUploadEntity(taskEntity.uploadEntity); mUploadEntity = taskEntity.uploadEntity; @@ -100,9 +101,9 @@ public class UploadUtil implements Runnable { mHttpConn.setRequestProperty(key, mTaskEntity.headers.get(key)); } - OutputStream outputStream = mHttpConn.getOutputStream(); + mOutputStream = mHttpConn.getOutputStream(); PrintWriter writer = - new PrintWriter(new OutputStreamWriter(outputStream, mTaskEntity.charset), true); + new PrintWriter(new OutputStreamWriter(mOutputStream, mTaskEntity.charset), true); //添加文件上传表单字段 keys = mTaskEntity.formFields.keySet(); @@ -110,7 +111,7 @@ public class UploadUtil implements Runnable { addFormField(writer, key, mTaskEntity.formFields.get(key)); } mListener.onStart(uploadFile.length()); - uploadFile(writer, outputStream, mTaskEntity.attachment, uploadFile); + uploadFile(writer, mTaskEntity.attachment, uploadFile); Log.d(TAG, finish(writer) + ""); } catch (IOException e) { e.printStackTrace(); @@ -149,8 +150,8 @@ public class UploadUtil implements Runnable { * @param attachment 文件上传attachment * @throws IOException */ - private void uploadFile(PrintWriter writer, OutputStream outputStream, String attachment, - File uploadFile) throws IOException { + private void uploadFile(PrintWriter writer, String attachment, File uploadFile) + throws IOException { Log.e(TAG, "uploadFile"); writer.append(PREFIX).append(BOUNDARY).append(LINE_END); writer.append("Content-Disposition: form-data; name=\"") @@ -171,7 +172,7 @@ public class UploadUtil implements Runnable { int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { mCurrentLocation += bytesRead; - outputStream.write(buffer, 0, bytesRead); + mOutputStream.write(buffer, 0, bytesRead); if (isCancel) { break; } @@ -179,7 +180,7 @@ public class UploadUtil implements Runnable { mListener.onProgress(mCurrentLocation); } - outputStream.flush(); + mOutputStream.flush(); //outputStream.close(); //不能调用,否则服务器端异常 inputStream.close(); writer.append(LINE_END); @@ -219,7 +220,7 @@ public class UploadUtil implements Runnable { writer.flush(); writer.close(); - + mOutputStream.close(); return response.toString(); } } diff --git a/Aria/src/main/java/com/arialyy/aria/util/Configuration.java b/Aria/src/main/java/com/arialyy/aria/util/Configuration.java index 3c2ae6f4..05081d1f 100644 --- a/Aria/src/main/java/com/arialyy/aria/util/Configuration.java +++ b/Aria/src/main/java/com/arialyy/aria/util/Configuration.java @@ -29,33 +29,38 @@ import java.util.WeakHashMap; * 信息配置 */ public class Configuration { - private static final String TAG = "Configuration"; - private static final String CONFIG_FILE = "/Aria/ADConfig.properties"; + private static final String TAG = "Configuration"; + private static final String CONFIG_FILE = "/Aria/ADConfig.properties"; /** * 当前调度器最大下载数,默认最大下载数为 “2” */ - private static final String DOWNLOAD_NUM = "DOWNLOAD_NUM"; + private static final String DOWNLOAD_NUM = "DOWNLOAD_NUM"; /** * 失败重试次数,默认最多重试 10 次 */ - private static final String RE_TRY_NUM = "RE_TRY_NUM"; + private static final String RE_TRY_NUM = "RE_TRY_NUM"; /** * 是否打开下载广播,默认 false */ - private static final String OPEN_BROADCAST = "OPEN_BROADCAST"; + private static final String OPEN_BROADCAST = "OPEN_BROADCAST"; /** * 失败重试间隔时间,默认 4000 ms */ - private static final String RE_TRY_INTERVAL = "RE_TRY_INTERVAL"; + private static final String RE_TRY_INTERVAL = "RE_TRY_INTERVAL"; /** * 超时时间,默认 10000 ms */ private static final String DOWNLOAD_TIME_OUT = "DOWNLOAD_TIME_OUT"; + /** + * 设置最大速度 + */ + private static final String MAX_SPEED = "MAX_SPEED"; + public static boolean isOpenBreadCast = false; - private static Configuration INSTANCE = null; - private File mConfigFile = null; - private static final Object LOCK = new Object(); + private static Configuration INSTANCE = null; + private File mConfigFile = null; + private static final Object LOCK = new Object(); public static Configuration getInstance() { if (INSTANCE == null) { @@ -73,7 +78,7 @@ public class Configuration { mConfigFile.getParentFile().mkdirs(); mConfigFile.createNewFile(); init(); - }else { + } else { isOpenBreadCast = isOpenBroadcast(); } } catch (IOException e) { @@ -88,6 +93,7 @@ public class Configuration { config.put(OPEN_BROADCAST, false + ""); config.put(RE_TRY_INTERVAL, 4000 + ""); config.put(DOWNLOAD_TIME_OUT, 10000 + ""); + config.put(MAX_SPEED, 64 + ""); saveConfig(config); } @@ -95,8 +101,8 @@ public class Configuration { if (config == null || config.size() == 0) { return; } - Properties properties = CommonUtil.loadConfig(mConfigFile); - Set keys = config.keySet(); + Properties properties = CommonUtil.loadConfig(mConfigFile); + Set keys = config.keySet(); for (String key : keys) { properties.setProperty(key, config.get(key)); } @@ -109,6 +115,20 @@ public class Configuration { saveConfig(map); } + /** + * 设置最大下载速度 + */ + public void setMaxSpeed(Speed speed) { + save(MAX_SPEED, speed.buf + ""); + } + + /** + * 获取最大速度 + */ + public int getMaxSpeed() { + return Integer.parseInt(CommonUtil.loadConfig(mConfigFile).getProperty(MAX_SPEED, "8192")); + } + /** * 获取下载超时时间 * diff --git a/Aria/src/main/java/com/arialyy/aria/util/MultipartUtility.java b/Aria/src/main/java/com/arialyy/aria/util/MultipartUtility.java deleted file mode 100644 index ccca7870..00000000 --- a/Aria/src/main/java/com/arialyy/aria/util/MultipartUtility.java +++ /dev/null @@ -1,144 +0,0 @@ -package com.arialyy.aria.util; - -import java.io.BufferedReader; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLConnection; -import java.util.ArrayList; -import java.util.List; - -/** - * This utility class provides an abstraction layer for sending multipart HTTP - * POST requests to a web server. - * - * @author www.codejava.net - */ -public class MultipartUtility { - private final String boundary; - private static final String LINE_FEED = "\r\n"; - private HttpURLConnection httpConn; - private String charset; - private OutputStream outputStream; - private PrintWriter writer; - - /** - * This constructor initializes a new HTTP POST request with content type - * is set to multipart/form-data - * - * @throws IOException - */ - public MultipartUtility(String requestURL, String charset) throws IOException { - this.charset = charset; - - // creates a unique boundary based on time stamp - boundary = "===" + System.currentTimeMillis() + "==="; - - URL url = new URL(requestURL); - httpConn = (HttpURLConnection) url.openConnection(); - httpConn.setUseCaches(false); - httpConn.setDoOutput(true); // indicates POST method - httpConn.setDoInput(true); - httpConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); - //httpConn.setRequestProperty("User-Agent", "CodeJava Agent"); - //httpConn.setRequestProperty("Test", "Bonjour"); - outputStream = httpConn.getOutputStream(); - writer = new PrintWriter(new OutputStreamWriter(outputStream, charset), true); - } - - /** - * Adds a form field to the request - * - * @param name field name - * @param value field value - */ - public void addFormField(String name, String value) { - writer.append("--" + boundary).append(LINE_FEED); - writer.append("Content-Disposition: form-data; name=\"" + name + "\"").append(LINE_FEED); - writer.append("Content-Type: text/plain; charset=" + charset).append(LINE_FEED); - writer.append(LINE_FEED); - writer.append(value).append(LINE_FEED); - writer.flush(); - } - - /** - * Adds a upload file section to the request - * - * @param fieldName name attribute in - * @param uploadFile a File to be uploaded - * @throws IOException - */ - public void addFilePart(String fieldName, File uploadFile) throws IOException { - String fileName = uploadFile.getName(); - writer.append("--" + boundary).append(LINE_FEED); - writer.append( - "Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + fileName + "\"") - .append(LINE_FEED); - writer.append("Content-Type: " + URLConnection.guessContentTypeFromName(fileName)) - .append(LINE_FEED); - writer.append("Content-Transfer-Encoding: binary").append(LINE_FEED); - writer.append(LINE_FEED); - writer.flush(); - - FileInputStream inputStream = new FileInputStream(uploadFile); - byte[] buffer = new byte[4096]; - int bytesRead = -1; - while ((bytesRead = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); - } - outputStream.flush(); - inputStream.close(); - - writer.append(LINE_FEED); - writer.flush(); - } - - /** - * Adds a header field to the request. - * - * @param name - name of the header field - * @param value - value of the header field - */ - public void addHeaderField(String name, String value) { - writer.append(name + ": " + value).append(LINE_FEED); - writer.flush(); - } - - /** - * Completes the request and receives response from the server. - * - * @return a list of Strings as response in case the server returned - * status OK, otherwise an exception is thrown. - * @throws IOException - */ - public List finish() throws IOException { - List response = new ArrayList(); - - writer.append(LINE_FEED).flush(); - writer.append("--" + boundary + "--").append(LINE_FEED); - writer.close(); - - // checks server's status code first - int status = httpConn.getResponseCode(); - if (status == HttpURLConnection.HTTP_OK) { - BufferedReader reader = new BufferedReader(new InputStreamReader(httpConn.getInputStream())); - String line = null; - while ((line = reader.readLine()) != null) { - response.add(line); - } - reader.close(); - httpConn.disconnect(); - } else { - throw new IOException("Server returned non-OK status: " + status); - } - - return response; - } -} \ No newline at end of file diff --git a/Aria/src/main/java/com/arialyy/aria/util/Speed.java b/Aria/src/main/java/com/arialyy/aria/util/Speed.java new file mode 100644 index 00000000..4a069336 --- /dev/null +++ b/Aria/src/main/java/com/arialyy/aria/util/Speed.java @@ -0,0 +1,33 @@ +package com.arialyy.aria.util; + +/** + * Created by Aria.Lao on 2017/3/6. + */ +public enum Speed { + /** + * 最大速度为256kb + */ + KB_256(64), + /** + * 最大速度为512kb + */ + KB_512(128), + /** + * 最大速度为1mb + */ + MB_1(256), + /** + * 最大速度为2mb + */ + MB_2(1024), + /** + * 最大速度为10mb + */ + MAX(8192); + int buf; + + Speed(int buf) { + this.buf = buf; + } + +} diff --git a/app/src/main/java/com/arialyy/simple/download/SingleTaskActivity.java b/app/src/main/java/com/arialyy/simple/download/SingleTaskActivity.java index f565283b..6e31ea7f 100644 --- a/app/src/main/java/com/arialyy/simple/download/SingleTaskActivity.java +++ b/app/src/main/java/com/arialyy/simple/download/SingleTaskActivity.java @@ -27,6 +27,7 @@ import android.support.v7.widget.Toolbar; import android.view.View; import android.widget.Button; import android.widget.ImageView; +import android.widget.RadioGroup; import android.widget.TextView; import android.widget.Toast; import butterknife.Bind; @@ -35,6 +36,7 @@ import com.arialyy.aria.core.Aria; import com.arialyy.aria.core.download.DownloadEntity; import com.arialyy.aria.core.download.DownloadTask; import com.arialyy.aria.util.CommonUtil; +import com.arialyy.aria.util.Speed; import com.arialyy.frame.util.show.L; import com.arialyy.simple.R; import com.arialyy.simple.base.BaseActivity; @@ -42,27 +44,27 @@ import com.arialyy.simple.databinding.ActivitySingleBinding; import com.arialyy.simple.widget.HorizontalProgressBarWithNumber; public class SingleTaskActivity extends BaseActivity { - public static final int DOWNLOAD_PRE = 0x01; - public static final int DOWNLOAD_STOP = 0x02; - public static final int DOWNLOAD_FAILE = 0x03; - public static final int DOWNLOAD_CANCEL = 0x04; - public static final int DOWNLOAD_RESUME = 0x05; + public static final int DOWNLOAD_PRE = 0x01; + public static final int DOWNLOAD_STOP = 0x02; + public static final int DOWNLOAD_FAILE = 0x03; + public static final int DOWNLOAD_CANCEL = 0x04; + public static final int DOWNLOAD_RESUME = 0x05; public static final int DOWNLOAD_COMPLETE = 0x06; - public static final int DOWNLOAD_RUNNING = 0x07; + public static final int DOWNLOAD_RUNNING = 0x07; private static final String DOWNLOAD_URL = //"http://kotlinlang.org/docs/kotlin-docs.pdf"; //"https://atom-installer.github.com/v1.13.0/AtomSetup.exe?s=1484074138&ext=.exe"; - "http://1_static.gaoshouyou.com/d/22/94/822260b849944492caadd2983f9bb624.apk"; + "http://static.gaoshouyou.com/d/22/94/822260b849944492caadd2983f9bb624.apk"; @Bind(R.id.progressBar) HorizontalProgressBarWithNumber mPb; - @Bind(R.id.start) Button mStart; - @Bind(R.id.stop) Button mStop; - @Bind(R.id.cancel) Button mCancel; - @Bind(R.id.size) TextView mSize; - @Bind(R.id.toolbar) Toolbar toolbar; - @Bind(R.id.speed) TextView mSpeed; - @Bind(R.id.img) ImageView mImg; - private DownloadEntity mEntity; + @Bind(R.id.start) Button mStart; + @Bind(R.id.stop) Button mStop; + @Bind(R.id.cancel) Button mCancel; + @Bind(R.id.size) TextView mSize; + @Bind(R.id.toolbar) Toolbar toolbar; + @Bind(R.id.speed) TextView mSpeed; + @Bind(R.id.speeds) RadioGroup mRg; + private DownloadEntity mEntity; private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); @@ -167,6 +169,33 @@ public class SingleTaskActivity extends BaseActivity { int p = (int) (target.getCurrentProgress() * 100 / target.getFileSize()); mPb.setProgress(p); } + mRg.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { + @Override public void onCheckedChanged(RadioGroup group, int checkedId) { + switch (checkedId) { + case 1: + Aria.get(this).setMaxSpeed(Speed.KB_256); + break; + case 2: + Aria.get(this).setMaxSpeed(Speed.KB_512); + break; + case 3: + Aria.get(this).setMaxSpeed(Speed.MB_1); + break; + case 4: + Aria.get(this).setMaxSpeed(Speed.MB_2); + break; + case 5: + Aria.get(this).setMaxSpeed(Speed.MAX); + break; + } + stop(); + new Handler().postDelayed(new Runnable() { + @Override public void run() { + start(); + } + }, 2000); + } + }); } public void onClick(View view) { diff --git a/app/src/main/java/com/arialyy/simple/upload/UploadActivity.java b/app/src/main/java/com/arialyy/simple/upload/UploadActivity.java index d3294899..11d91329 100644 --- a/app/src/main/java/com/arialyy/simple/upload/UploadActivity.java +++ b/app/src/main/java/com/arialyy/simple/upload/UploadActivity.java @@ -6,28 +6,18 @@ import android.os.Message; import butterknife.Bind; import butterknife.OnClick; import com.arialyy.aria.core.Aria; -import com.arialyy.aria.core.upload.IUploadListener; -import com.arialyy.aria.core.upload.UploadEntity; import com.arialyy.aria.core.upload.UploadTask; -import com.arialyy.aria.core.upload.UploadTaskEntity; -import com.arialyy.aria.core.upload.UploadUtil; -import com.arialyy.aria.util.MultipartUtility; import com.arialyy.frame.util.FileUtil; -import com.arialyy.frame.util.show.L; import com.arialyy.frame.util.show.T; import com.arialyy.simple.R; import com.arialyy.simple.base.BaseActivity; import com.arialyy.simple.databinding.ActivityUploadMeanBinding; import com.arialyy.simple.widget.HorizontalProgressBarWithNumber; -import java.io.File; -import java.io.IOException; import java.lang.ref.WeakReference; -import java.util.List; /** * Created by Aria.Lao on 2017/2/9. */ - public class UploadActivity extends BaseActivity { @Bind(R.id.pb) HorizontalProgressBarWithNumber mPb; private static final int START = 0; @@ -70,75 +60,6 @@ public class UploadActivity extends BaseActivity { @Override protected void init(Bundle savedInstanceState) { super.init(savedInstanceState); - //test(); - } - - private void test() { - //String charset = "UTF-8"; - //File uploadFile1 = new File("/sdcard/Download/test.zip"); - //String requestURL = "http://172.18.104.50:8080/upload/sign_file"; - ////String requestURL = "http://172.18.104.50:8080/greeting"; - // - //try { - // MultipartUtility multipart = new MultipartUtility(requestURL, charset); - // - // //multipart.addHeaderField("Test-Header", "Header-Value"); - // - // multipart.addFilePart("file", uploadFile1); - // List response = multipart.finish(); - // - // System.out.println("SERVER REPLIED:"); - // - // for (String line : response) { - // System.out.println(line); - // } - //} catch (IOException ex) { - // System.err.println(ex); - //} - - UploadEntity entity = new UploadEntity(); - entity.setFilePath("/sdcard/Download/test.zip"); - entity.setFileName("test.pdf"); - UploadTaskEntity taskEntity = new UploadTaskEntity(entity); - taskEntity.uploadUrl = "http://172.18.104.50:8080/upload/sign_file"; - taskEntity.attachment = "file"; - UploadUtil util = new UploadUtil(taskEntity, new IUploadListener() { - long fileSize = 0; - - @Override public void onPre() { - - } - - @Override public void onStart(long fileSize) { - this.fileSize = fileSize; - } - - @Override public void onResume(long resumeLocation) { - - } - - @Override public void onStop(long stopLocation) { - - } - - @Override public void onProgress(long currentLocation) { - int p = (int) (currentLocation * 100 / fileSize); - mPb.setProgress(p); - } - - @Override public void onCancel() { - - } - - @Override public void onComplete() { - - } - - @Override public void onFail() { - - } - }); - util.start(); } @OnClick(R.id.upload) void upload() { @@ -147,12 +68,6 @@ public class UploadActivity extends BaseActivity { .setUploadUrl("http://172.18.104.50:8080/upload/sign_file") .setAttachment("file") .start(); - //new Thread(new Runnable() { - // @Override public void run() { - // test(); - // } - //}).start(); - //test(); } @OnClick(R.id.stop) void stop() { diff --git a/app/src/main/res/layout/content_single.xml b/app/src/main/res/layout/content_single.xml index 601e27f8..7f5a8997 100644 --- a/app/src/main/res/layout/content_single.xml +++ b/app/src/main/res/layout/content_single.xml @@ -81,10 +81,54 @@ /> - + + + + + + + + + + + + +