diff --git a/README.md b/README.md
index 0ddcfe29..318291db 100644
--- a/README.md
+++ b/README.md
@@ -358,6 +358,34 @@ try {
     }
 ```
 
+### Image Client 样例
+
+> 以下片断来自项目代码里面的文件:example / cn.jpush.api.examples.ImageExample
+* 支持通过URL或者文件来上传图片
+```Java
+    public static void testUploadImageByUrl() throws APIConnectionException, APIRequestException {
+        ImageClient client = new ImageClient(MASTER_SECRET, APP_KEY);
+        ImageUrlPayload payload = ImageUrlPayload.newBuilder()
+        .setImageType(ImageType.LARGE_ICON)
+        .setImageUrl("http://xxx.com/image/a.jpg")
+        .build();
+        ImageUploadResult imageUploadResult = client.uploadImage(payload);
+        String mediaId = imageUploadResult.getMediaId();
+    }
+
+    public static void testUploadImageByFile() {
+        ImageClient client = new ImageClient(MASTER_SECRET, APP_KEY);
+        ImageFilePayload payload = ImageFilePayload.newBuilder()
+        .setImageType(ImageType.BIG_PICTURE)
+        // 本地文件路径
+        .setOppoFileName("/MyDir/a.jpg")
+        .setXiaomiFileName("/MyDir/a.jpg")
+        .build();
+        ImageUploadResult imageUploadResult = client.uploadImage(payload);
+        String mediaId = imageUploadResult.getMediaId();
+    }
+```
+
 ### Weblogic 使用Java SDK
 
 Weblogic在使用jpush-api-java-client时需要注意的一些事项。
diff --git a/example/main/java/cn/jpush/api/examples/ImageExample.java b/example/main/java/cn/jpush/api/examples/ImageExample.java
new file mode 100644
index 00000000..a29ad866
--- /dev/null
+++ b/example/main/java/cn/jpush/api/examples/ImageExample.java
@@ -0,0 +1,56 @@
+package cn.jpush.api.image;
+
+import cn.jiguang.common.resp.APIConnectionException;
+import cn.jiguang.common.resp.APIRequestException;
+import cn.jpush.api.image.model.ImageFilePayload;
+import cn.jpush.api.image.model.ImageType;
+import cn.jpush.api.image.model.ImageUploadResult;
+import cn.jpush.api.image.model.ImageUrlPayload;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ImageExample {
+    protected static final Logger LOG = LoggerFactory.getLogger(ImageExample.class);
+
+    // demo App defined in resources/jpush-api.conf 
+    protected static final String APP_KEY = "e4ceeaf7a53ad745dd4728f2";
+    protected static final String MASTER_SECRET = "1582b986adeaf48ceec1e354";
+    protected static final String GROUP_PUSH_KEY = "2c88a01e073a0fe4fc7b167c";
+    protected static final String GROUP_MASTER_SECRET = "b11314807507e2bcfdeebe2e";
+
+    public static final String TITLE = "Test from API example";
+    public static final String ALERT = "Test from API Example - alert";
+    public static final String MSG_CONTENT = "Test from API Example - msgContent";
+    public static final String REGISTRATION_ID = "0900e8d85ef";
+    public static final String TAG = "tag_api";
+    public static long sendCount = 0;
+    private static long sendTotalTime = 0;
+
+    public static void main(String[] args) throws APIConnectionException, APIRequestException {
+        testUploadImageByFile();
+        testUploadImageByUrl();
+    }
+
+    public static void testUploadImageByUrl() throws APIConnectionException, APIRequestException {
+        ImageClient client = new ImageClient(MASTER_SECRET, APP_KEY);
+        ImageUrlPayload payload = ImageUrlPayload.newBuilder()
+                .setImageType(ImageType.LARGE_ICON)
+                .setImageUrl("http://xxx.com/image/a.jpg")
+                .build();
+        ImageUploadResult imageUploadResult = client.uploadImage(payload);
+        String mediaId = imageUploadResult.getMediaId();
+    }
+
+    public static void testUploadImageByFile() {
+        ImageClient client = new ImageClient(MASTER_SECRET, APP_KEY);
+        ImageFilePayload payload = ImageFilePayload.newBuilder()
+                .setImageType(ImageType.BIG_PICTURE)
+                // 本地文件路径
+                .setOppoFileName("/MyDir/a.jpg")
+                .setXiaomiFileName("/MyDir/a.jpg")
+                .build();
+        ImageUploadResult imageUploadResult = client.uploadImage(payload);
+        String mediaId = imageUploadResult.getMediaId();
+    }
+}
+
diff --git a/pom.xml b/pom.xml
index fdc07020..f8f5a173 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
 
 	<groupId>cn.jpush.api</groupId>
 	<artifactId>jpush-client</artifactId>
-	<version>3.4.7</version>
+	<version>3.4.8</version>
 	<packaging>jar</packaging>
 	<url>https://github.com/jpush/jpush-api-java-client</url>
 	<name>JPush API Java Client</name>
@@ -51,7 +51,7 @@
 		<dependency>
             <groupId>cn.jpush.api</groupId>
             <artifactId>jiguang-common</artifactId>
-            <version>1.1.9</version>
+            <version>1.1.10</version>
         </dependency>
 		<dependency>
 			<groupId>org.apache.httpcomponents</groupId>
@@ -117,6 +117,18 @@
 			<version>2.0.0</version>
 			<scope>test</scope>
 		</dependency>
+		<dependency>
+			<groupId>org.mockito</groupId>
+			<artifactId>mockito-core</artifactId>
+			<version>1.10.19</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.hamcrest</groupId>
+			<artifactId>hamcrest-core</artifactId>
+			<version>2.2</version>
+			<scope>test</scope>
+		</dependency>
 	</dependencies>
 
 	<build>
@@ -294,7 +306,6 @@
 
 		</plugins>
 	</build>
-
 	<distributionManagement>
 		<snapshotRepository>
 			<id>ossrh</id>
diff --git a/src/main/java/cn/jpush/api/file/FileClient.java b/src/main/java/cn/jpush/api/file/FileClient.java
index 601805cb..b7e11a2e 100644
--- a/src/main/java/cn/jpush/api/file/FileClient.java
+++ b/src/main/java/cn/jpush/api/file/FileClient.java
@@ -56,7 +56,7 @@ public FileUploadResult uploadFile(FileType type, String filename)
         String url = _baseUrl + _filesPath + "/" + typeStr;
         Map<String, String> fileMap = new HashMap<>();
         fileMap.put("filename", filename);
-        String response = client.formUpload(url, null, fileMap, null);
+        String response = client.formUploadByPost(url, null, fileMap, null);
         LOG.info("uploadFile:{}", response);
         return _gson.fromJson(response,
                 new TypeToken<FileUploadResult>() {
diff --git a/src/main/java/cn/jpush/api/image/ImageClient.java b/src/main/java/cn/jpush/api/image/ImageClient.java
new file mode 100644
index 00000000..87a2acd4
--- /dev/null
+++ b/src/main/java/cn/jpush/api/image/ImageClient.java
@@ -0,0 +1,162 @@
+package cn.jpush.api.image;
+
+import cn.jiguang.common.ClientConfig;
+import cn.jiguang.common.ServiceHelper;
+import cn.jiguang.common.connection.HttpProxy;
+import cn.jiguang.common.connection.IHttpClient;
+import cn.jiguang.common.connection.NativeHttpClient;
+import cn.jiguang.common.resp.APIConnectionException;
+import cn.jiguang.common.resp.APIRequestException;
+import cn.jiguang.common.resp.ResponseWrapper;
+import cn.jiguang.common.utils.Preconditions;
+import cn.jiguang.common.utils.StringUtils;
+import cn.jpush.api.image.model.ImageFilePayload;
+import cn.jpush.api.image.model.ImageSource;
+import cn.jpush.api.image.model.ImageUploadResult;
+import cn.jpush.api.image.model.ImageUrlPayload;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonSyntaxException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Provide the ability to upload images to the Jiguang server. Only images in JPG, JPEG and PNG format are supported.
+ *
+ * @author fuyx
+ * @version 2020-12-14
+ */
+public class ImageClient {
+
+    protected static final Logger LOG = LoggerFactory.getLogger(ImageClient.class);
+
+    private IHttpClient _httpClient;
+    private String _baseUrl;
+    private String _imagesPath;
+    private Gson _gson = new Gson();
+
+    public ImageClient(String masterSecret, String appKey) {
+        this(masterSecret, appKey, null, ClientConfig.getInstance());
+    }
+
+    public ImageClient(String masterSecret, String appKey, HttpProxy proxy, ClientConfig conf) {
+        _baseUrl = (String) conf.get(ClientConfig.PUSH_HOST_NAME);
+        _imagesPath = (String) conf.get(ClientConfig.V3_IMAGES_PATH);
+        String authCode = ServiceHelper.getBasicAuthorization(appKey, masterSecret);
+        this._httpClient = new NativeHttpClient(authCode, proxy, conf);
+    }
+
+    /**
+     * Upload image by url. Require at least one non-null url.
+     */
+    public ImageUploadResult uploadImage(ImageUrlPayload imageUrlPayload)
+            throws APIConnectionException, APIRequestException {
+        Preconditions.checkArgument(imageUrlPayload.getImageType() != null, "Image type should not be null");
+        checkImageUrlPayload(imageUrlPayload);
+        NativeHttpClient client = (NativeHttpClient) _httpClient;
+        String url = _baseUrl + _imagesPath + "/" + ImageSource.URL.value();
+        JsonElement jsonElement = imageUrlPayload.toJSON();
+        String content = _gson.toJson(jsonElement);
+        ResponseWrapper responseWrapper = client.sendPost(url, content);
+        if (responseWrapper.responseCode != 200) {
+            LOG.error("upload image failed: {}", responseWrapper);
+        }
+        ImageUploadResult imageUploadResult = _gson.fromJson(responseWrapper.responseContent, ImageUploadResult.class);
+
+        LOG.info("upload image result:{}", imageUploadResult);
+        return imageUploadResult;
+    }
+
+    /**
+     * Upload image by file. Require at least 1 non-null fileName. Currently only support Xiaomi and OPPO
+     */
+    public ImageUploadResult uploadImage(ImageFilePayload imageFilePayload) {
+        Preconditions.checkArgument(imageFilePayload.getImageType() != null, "Image type should not be null");
+        checkImageFilePayload(imageFilePayload);
+        NativeHttpClient client = (NativeHttpClient) _httpClient;
+        String url = _baseUrl + _imagesPath + "/" + ImageSource.FILE.value();
+
+        Map<String, String> textMap = new HashMap<>();
+        textMap.put("image_type", String.valueOf(imageFilePayload.getImageType().value()));
+
+        Map<String, String> fileMap = imageFilePayload.toFileMap();
+        LOG.debug("upload fileMap: {}", fileMap);
+        String response = client.formUploadByPost(url, textMap, fileMap, null);
+        LOG.debug("upload image result: {}", response);
+        ImageUploadResult imageUploadResult;
+        try {
+            imageUploadResult = _gson.fromJson(response, ImageUploadResult.class);
+        } catch (JsonSyntaxException e) {
+            LOG.error("could not parse response: {}", response);
+            throw new IllegalStateException("could not parse response", e);
+        }
+        LOG.info("upload image result:{}", imageUploadResult);
+        return imageUploadResult;
+    }
+
+    /**
+     * Modify image by url. Require at least one non-null url.
+     */
+    public ImageUploadResult modifyImage(String mediaId, ImageUrlPayload imageUrlPayload)
+            throws APIConnectionException, APIRequestException {
+        Preconditions.checkArgument(StringUtils.isNotEmpty(mediaId), "mediaId should not be empty");
+        checkImageUrlPayload(imageUrlPayload);
+        NativeHttpClient client = (NativeHttpClient) _httpClient;
+        String url = _baseUrl + _imagesPath + "/" + ImageSource.URL.value() + "/" + mediaId;
+        JsonElement jsonElement = imageUrlPayload.toJSON();
+        String content = _gson.toJson(jsonElement);
+        ResponseWrapper responseWrapper = client.sendPut(url, content);
+        if (responseWrapper.responseCode != 200) {
+            LOG.error("upload image failed: {}", responseWrapper);
+        }
+        ImageUploadResult imageUploadResult = _gson.fromJson(responseWrapper.responseContent, ImageUploadResult.class);
+
+        LOG.info("upload image result:{}", imageUploadResult);
+        return imageUploadResult;
+    }
+
+    /**
+     * Modify image by file. Require at least 1 non-null fileName. Currently only support Xiaomi and OPPO
+     */
+    public ImageUploadResult modifyImage(String mediaId, ImageFilePayload imageFilePayload) {
+        Preconditions.checkArgument(StringUtils.isNotEmpty(mediaId), "mediaId should not be empty");
+        checkImageFilePayload(imageFilePayload);
+        NativeHttpClient client = (NativeHttpClient) _httpClient;
+        String url = _baseUrl + _imagesPath + "/" + ImageSource.FILE.value() + "/" + mediaId;
+
+        Map<String, String> fileMap = imageFilePayload.toFileMap();
+        LOG.debug("upload image fileMap: {}", fileMap);
+        String response = client.formUploadByPut(url, null, fileMap, null);
+        LOG.debug("upload image result: {}", response);
+        ImageUploadResult imageUploadResult;
+        try {
+            imageUploadResult = _gson.fromJson(response, ImageUploadResult.class);
+        } catch (JsonSyntaxException e) {
+            LOG.error("could not parse response: {}", response);
+            throw new IllegalStateException("could not parse response", e);
+        }
+        LOG.info("upload image result:{}", imageUploadResult);
+        return imageUploadResult;
+    }
+
+    private void checkImageUrlPayload(ImageUrlPayload imageUrlPayload) {
+        boolean anyUrlNotEmpty = StringUtils.isNotEmpty(imageUrlPayload.getImageUrl())
+                || StringUtils.isNotEmpty(imageUrlPayload.getFcmImageUrl())
+                || StringUtils.isNotEmpty(imageUrlPayload.getHuaweiImageUrl())
+                || StringUtils.isNotEmpty(imageUrlPayload.getOppoImageUrl())
+                || StringUtils.isNotEmpty(imageUrlPayload.getXiaomiImageUrl())
+                || StringUtils.isNotEmpty(imageUrlPayload.getJiguangImageUrl()) ;
+        Preconditions.checkArgument(anyUrlNotEmpty, "Require at least 1 non-empty url");
+    }
+
+    private void checkImageFilePayload(ImageFilePayload imageFilePayload) {
+        boolean anyFileNotEmpty = StringUtils.isNotEmpty(imageFilePayload.getOppoFileName() )
+                || StringUtils.isNotEmpty(imageFilePayload.getXiaomiFileName() );
+        Preconditions.checkArgument(anyFileNotEmpty, "Require at least 1 non-empty fileName. Currently only support Xiaomi and OPPO");
+    }
+
+
+}
diff --git a/src/main/java/cn/jpush/api/image/model/ImageFilePayload.java b/src/main/java/cn/jpush/api/image/model/ImageFilePayload.java
new file mode 100644
index 00000000..7a4be086
--- /dev/null
+++ b/src/main/java/cn/jpush/api/image/model/ImageFilePayload.java
@@ -0,0 +1,105 @@
+package cn.jpush.api.image.model;
+
+import lombok.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+public class ImageFilePayload {
+    private static final String IMAGE_TYPE = "image_type";
+    private static final String OPPO_IMAGE_FILE = "oppo_file";
+    private static final String XIAOMI_IMAGE_FILE = "xiaomi_file";
+    private static final String HUAWEI_IMAGE_FILE = "huawei_file";
+    private static final String FCM_IMAGE_FILE = "fcm_file";
+    private static final String JIGUANG_IMAGE_FILE = "jiguang_file";
+
+    private ImageType imageType;
+    private String oppoFileName;
+    private String xiaomiFileName;
+    private String huaweiFileName;
+    private String fcmFileName;
+    private String jiguangFileName;
+
+    public Map<String, String> toFileMap() {
+        HashMap<String, String> fileMap = new HashMap<>();
+        if (null != oppoFileName) {
+            fileMap.put(OPPO_IMAGE_FILE, oppoFileName);
+        }
+        if (null != xiaomiFileName) {
+            fileMap.put(XIAOMI_IMAGE_FILE, xiaomiFileName);
+        }
+        if (null != huaweiFileName) {
+            fileMap.put(HUAWEI_IMAGE_FILE, huaweiFileName);
+        }
+        if (null != fcmFileName) {
+            fileMap.put(FCM_IMAGE_FILE, fcmFileName);
+        }
+        if (null != jiguangFileName) {
+            fileMap.put(JIGUANG_IMAGE_FILE, jiguangFileName);
+        }
+        return fileMap;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+        private ImageType imageType;
+        private String oppoFileName;
+        private String xiaomiFileName;
+        private String huaweiFileName;
+        private String fcmFileName;
+        private String jiguangFileName;
+
+        private Builder() {
+        }
+
+        public static Builder builder() {
+            return new Builder();
+        }
+
+        public Builder setImageType(ImageType imageType) {
+            this.imageType = imageType;
+            return this;
+        }
+
+        public Builder setOppoFileName(String oppoFileName) {
+            this.oppoFileName = oppoFileName;
+            return this;
+        }
+
+        public Builder setXiaomiFileName(String xiaomiFileName) {
+            this.xiaomiFileName = xiaomiFileName;
+            return this;
+        }
+
+        public Builder setHuaweiFileName(String huaweiFileName) {
+            this.huaweiFileName = huaweiFileName;
+            return this;
+        }
+
+        public Builder setFcmFileName(String fcmFileName) {
+            this.fcmFileName = fcmFileName;
+            return this;
+        }
+
+        public Builder setJiguangFileName(String jiguangFileName) {
+            this.jiguangFileName = jiguangFileName;
+            return this;
+        }
+
+        public ImageFilePayload build() {
+            ImageFilePayload imageFilePayload = new ImageFilePayload();
+            imageFilePayload.setImageType(imageType);
+            imageFilePayload.setOppoFileName(oppoFileName);
+            imageFilePayload.setXiaomiFileName(xiaomiFileName);
+            imageFilePayload.setHuaweiFileName(huaweiFileName);
+            imageFilePayload.setFcmFileName(fcmFileName);
+            imageFilePayload.setJiguangFileName(jiguangFileName);
+            return imageFilePayload;
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/cn/jpush/api/image/model/ImageSource.java b/src/main/java/cn/jpush/api/image/model/ImageSource.java
new file mode 100644
index 00000000..5bd847c1
--- /dev/null
+++ b/src/main/java/cn/jpush/api/image/model/ImageSource.java
@@ -0,0 +1,27 @@
+package cn.jpush.api.image.model;
+
+/**
+ * @author fuyx
+ * @version  2020-12-14
+ */
+public enum ImageSource {
+
+    /**
+     * Network Resource
+     */
+    URL("byurls"),
+    /**
+     * File Resource
+     */
+    FILE("byfiles");
+
+    private final String value;
+
+    ImageSource(final String value) {
+        this.value = value;
+    }
+
+    public String value() {
+        return this.value;
+    }
+}
diff --git a/src/main/java/cn/jpush/api/image/model/ImageType.java b/src/main/java/cn/jpush/api/image/model/ImageType.java
new file mode 100644
index 00000000..24b37425
--- /dev/null
+++ b/src/main/java/cn/jpush/api/image/model/ImageType.java
@@ -0,0 +1,27 @@
+package cn.jpush.api.image.model;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author fuyx
+ * @version 2020-12-14
+ */
+public enum ImageType {
+
+    @SerializedName("1")
+    BIG_PICTURE(1),
+    @SerializedName("2")
+    LARGE_ICON(2),
+    @SerializedName("3")
+    SMALL_ICON(3);
+
+    private final int value;
+
+    ImageType(final int value) {
+        this.value = value;
+    }
+
+    public int value() {
+        return this.value;
+    }
+}
diff --git a/src/main/java/cn/jpush/api/image/model/ImageUploadResult.java b/src/main/java/cn/jpush/api/image/model/ImageUploadResult.java
new file mode 100644
index 00000000..2435eb22
--- /dev/null
+++ b/src/main/java/cn/jpush/api/image/model/ImageUploadResult.java
@@ -0,0 +1,31 @@
+package cn.jpush.api.image.model;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+@Data
+public class ImageUploadResult {
+
+    @SerializedName("media_id")
+    private String mediaId;
+    @SerializedName("oppo_image_url")
+    private String oppoImageUrl;
+    @SerializedName("xiaomi_image_url")
+    private String xiaomiImageUrl;
+    @SerializedName("huawei_image_url")
+    private String huaweiImageUrl;
+    @SerializedName("fcm_image_url")
+    private String fcmImageUrl;
+    @SerializedName("jiguang_image_url")
+    private String jiguangImageUrl;
+    @SerializedName("error")
+    private Error error;
+
+    @Data
+    public static class Error {
+        @SerializedName("message")
+        private String message;
+        @SerializedName("code")
+        private int code;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/jpush/api/image/model/ImageUrlPayload.java b/src/main/java/cn/jpush/api/image/model/ImageUrlPayload.java
new file mode 100644
index 00000000..62cd0626
--- /dev/null
+++ b/src/main/java/cn/jpush/api/image/model/ImageUrlPayload.java
@@ -0,0 +1,119 @@
+package cn.jpush.api.image.model;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import lombok.Data;
+
+@Data
+public class ImageUrlPayload {
+    private static final String IMAGE_TYPE = "image_type";
+    private static final String IMAGE_URL = "image_url";
+    private static final String OPPO_IMAGE_URL = "oppo_image_url";
+    private static final String XIAOMI_IMAGE_URL = "xiaomi_image_url";
+    private static final String HUAWEI_IMAGE_URL = "huawei_image_url";
+    private static final String FCM_IMAGE_URL = "fcm_image_url";
+    private static final String JIGUANG_IMAGE_URL = "jiguang_image_url";
+
+    private ImageType imageType;
+    private String imageUrl;
+    private String oppoImageUrl;
+    private String xiaomiImageUrl;
+    private String huaweiImageUrl;
+    private String fcmImageUrl;
+    private String jiguangImageUrl;
+
+    public JsonElement toJSON() {
+        JsonObject json = new JsonObject();
+        if (null != imageType) {
+            json.addProperty(IMAGE_TYPE, imageType.value());
+        }
+        if (null != imageUrl) {
+            json.addProperty(IMAGE_URL, imageUrl);
+        }
+        if (null != oppoImageUrl) {
+            json.addProperty(OPPO_IMAGE_URL, oppoImageUrl);
+        }
+        if (null != xiaomiImageUrl) {
+            json.addProperty(XIAOMI_IMAGE_URL, xiaomiImageUrl);
+        }
+        if (null != huaweiImageUrl) {
+            json.addProperty(HUAWEI_IMAGE_URL, huaweiImageUrl);
+        }
+        if (null != fcmImageUrl) {
+            json.addProperty(FCM_IMAGE_URL, fcmImageUrl);
+        }
+        if (null != jiguangImageUrl) {
+            json.addProperty(JIGUANG_IMAGE_URL, jiguangImageUrl);
+        }
+        return json;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+        private ImageType imageType;
+        private String imageUrl;
+        private String oppoImageUrl;
+        private String xiaomiImageUrl;
+        private String huaweiImageUrl;
+        private String fcmImageUrl;
+        private String jiguangImageUrl;
+
+        private Builder() {
+        }
+
+        public static Builder builder() {
+            return new Builder();
+        }
+
+        public Builder setImageType(ImageType imageType) {
+            this.imageType = imageType;
+            return this;
+        }
+
+        public Builder setImageUrl(String imageUrl) {
+            this.imageUrl = imageUrl;
+            return this;
+        }
+
+        public Builder setOppoImageUrl(String oppoImageUrl) {
+            this.oppoImageUrl = oppoImageUrl;
+            return this;
+        }
+
+        public Builder setXiaomiImageUrl(String xiaomiImageUrl) {
+            this.xiaomiImageUrl = xiaomiImageUrl;
+            return this;
+        }
+
+        public Builder setHuaweiImageUrl(String huaweiImageUrl) {
+            this.huaweiImageUrl = huaweiImageUrl;
+            return this;
+        }
+
+        public Builder setFcmImageUrl(String fcmImageUrl) {
+            this.fcmImageUrl = fcmImageUrl;
+            return this;
+        }
+
+        public Builder setJiguangImageUrl(String jiguangImageUrl) {
+            this.jiguangImageUrl = jiguangImageUrl;
+            return this;
+        }
+
+        public ImageUrlPayload build() {
+            ImageUrlPayload imageUrlPayload = new ImageUrlPayload();
+            imageUrlPayload.setImageType(imageType);
+            imageUrlPayload.setImageUrl(imageUrl);
+            imageUrlPayload.setOppoImageUrl(oppoImageUrl);
+            imageUrlPayload.setXiaomiImageUrl(xiaomiImageUrl);
+            imageUrlPayload.setHuaweiImageUrl(huaweiImageUrl);
+            imageUrlPayload.setFcmImageUrl(fcmImageUrl);
+            imageUrlPayload.setJiguangImageUrl(jiguangImageUrl);
+            return imageUrlPayload;
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/cn/jpush/api/push/model/notification/AndroidNotification.java b/src/main/java/cn/jpush/api/push/model/notification/AndroidNotification.java
index dcc0d197..ab2407cd 100644
--- a/src/main/java/cn/jpush/api/push/model/notification/AndroidNotification.java
+++ b/src/main/java/cn/jpush/api/push/model/notification/AndroidNotification.java
@@ -3,10 +3,7 @@
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonPrimitive;
-import lombok.*;
-import lombok.experimental.Accessors;
 
-import java.util.LinkedHashMap;
 import java.util.Map;
 
 public class AndroidNotification extends PlatformNotification {
@@ -23,6 +20,7 @@ public class AndroidNotification extends PlatformNotification {
     private static final String PRIORITY = "priority";
     private static final String CATEGORY = "category";
     private static final String LARGE_ICON = "large_icon";
+    private static final String SMALL_ICON_URI = "small_icon_uri";
     private static final String INTENT = "intent";
 
     private final String title;
@@ -38,6 +36,7 @@ public class AndroidNotification extends PlatformNotification {
     private int priority;
     private String category;
     private String large_icon;
+    private String small_icon_uri;
     private JsonObject intent;
 
     private AndroidNotification(Object alert,
@@ -51,6 +50,7 @@ private AndroidNotification(Object alert,
                                 int priority,
                                 String category,
                                 String large_icon,
+                                String small_icon_uri,
                                 JsonObject intent,
                                 String channelId,
                                 Map<String, String> extras,
@@ -70,6 +70,7 @@ private AndroidNotification(Object alert,
         this.priority = priority;
         this.category = category;
         this.large_icon = large_icon;
+        this.small_icon_uri = small_icon_uri;
         this.intent = intent;
         this.channelId = channelId;
     }
@@ -142,6 +143,10 @@ public JsonElement toJSON() {
             json.add(LARGE_ICON, new JsonPrimitive(large_icon));
         }
 
+        if (null != small_icon_uri) {
+            json.add(SMALL_ICON_URI, new JsonPrimitive(small_icon_uri));
+        }
+
         if (null != intent) {
             json.add(INTENT, intent);
         }
@@ -164,6 +169,7 @@ public static class Builder extends PlatformNotification.Builder<AndroidNotifica
         private int priority;
         private String category;
         private String large_icon;
+        private String small_icon_uri;
         private JsonObject intent;
         private String channelId;
 
@@ -232,6 +238,11 @@ public Builder setLargeIcon(String largeIcon) {
             return this;
         }
 
+        public Builder setSmallIconUri(String smallIconUri) {
+            this.small_icon_uri = smallIconUri;
+            return this;
+        }
+
         public Builder setIntent(JsonObject intent) {
             if (null == intent) {
                 LOG.warn("Null intent. Throw away it.");
@@ -264,6 +275,7 @@ public AndroidNotification build() {
                     priority,
                     category,
                     large_icon,
+                    small_icon_uri,
                     intent,
                     channelId,
                     extrasBuilder,
diff --git a/src/test/java/cn/jpush/api/image/ImageClientTest.java b/src/test/java/cn/jpush/api/image/ImageClientTest.java
new file mode 100644
index 00000000..849a2322
--- /dev/null
+++ b/src/test/java/cn/jpush/api/image/ImageClientTest.java
@@ -0,0 +1,195 @@
+package cn.jpush.api.image;
+
+import cn.jiguang.common.connection.NativeHttpClient;
+import cn.jiguang.common.resp.APIConnectionException;
+import cn.jiguang.common.resp.APIRequestException;
+import cn.jiguang.common.resp.ResponseWrapper;
+import cn.jpush.api.BaseTest;
+import cn.jpush.api.image.model.ImageFilePayload;
+import cn.jpush.api.image.model.ImageType;
+import cn.jpush.api.image.model.ImageUploadResult;
+import cn.jpush.api.image.model.ImageUrlPayload;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.mockito.internal.util.reflection.FieldSetter;
+
+import java.util.HashMap;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.*;
+
+public class ImageClientTest extends BaseTest {
+
+    @Test
+    public void testUploadImageByUrl() throws NoSuchFieldException, APIConnectionException, APIRequestException {
+        ResponseWrapper mockResponseWrapper = new ResponseWrapper();
+        mockResponseWrapper.responseCode = 200;
+        mockResponseWrapper.responseContent = "{\n" +
+                "    \"media_id\": \"jgmedia-2-14b23451-0001-41ce-89d9-987b465122da\",  \n" +
+                "    \"oppo_image_url\": \"3653918_5f92b5739ae676f5745bcbf4\",   \n" +
+                "    \"xiaomi_image_url\": \"http://f6.market.xiaomi.com/download/MiPass/01fff50f50ba193f94074ec/d1671936d468383f.png\"     \n" +
+                "}";
+
+        NativeHttpClient mockIHttpClient = mock(NativeHttpClient.class);
+        when(mockIHttpClient.sendPost(anyString(), anyString())).thenReturn(mockResponseWrapper);
+
+        ImageClient client = new ImageClient(MASTER_SECRET, APP_KEY);
+        new FieldSetter(client, client.getClass().getDeclaredField("_httpClient")).set(mockIHttpClient);
+        ImageClient spyClient = Mockito.spy(client);
+        ImageUrlPayload payload = ImageUrlPayload.newBuilder()
+                .setImageUrl("xxx")
+                .setImageType(ImageType.BIG_PICTURE)
+                .setFcmImageUrl("xxx")
+                .setHuaweiImageUrl("xxx")
+                .build();
+        ImageUploadResult imageUploadResult = spyClient.uploadImage(payload);
+        verify(mockIHttpClient).sendPost("https://api.jpush.cn/v3/images/byurls", "{\"image_type\":1,\"image_url\":\"xxx\",\"huawei_image_url\":\"xxx\",\"fcm_image_url\":\"xxx\"}");
+
+        assertThat(imageUploadResult.getMediaId(), equalTo("jgmedia-2-14b23451-0001-41ce-89d9-987b465122da"));
+        assertThat(imageUploadResult.getOppoImageUrl(), equalTo("3653918_5f92b5739ae676f5745bcbf4"));
+        assertThat(imageUploadResult.getXiaomiImageUrl(), equalTo("http://f6.market.xiaomi.com/download/MiPass/01fff50f50ba193f94074ec/d1671936d468383f.png"));
+        assertThat(imageUploadResult.getHuaweiImageUrl(), nullValue());
+    }
+
+    @Test
+    public void testUploadImageByFile() throws NoSuchFieldException {
+        String content = "{\n" +
+                "    \"media_id\": \"jgmedia-2-14b23451-0001-41ce-89d9-987b465122da\",  \n" +
+                "    \"oppo_image_url\": \"3653918_5f92b5739ae676f5745bcbf4\",   \n" +
+                "    \"xiaomi_image_url\": \"http://f6.market.xiaomi.com/download/MiPass/01fff50f50ba193f94074ec/d1671936d468383f.png\"     \n" +
+                "}";
+
+        NativeHttpClient mockIHttpClient = mock(NativeHttpClient.class);
+        when(mockIHttpClient.formUploadByPost(anyString(), anyMap(), anyMap(), anyString())).thenReturn(content);
+
+        ImageClient client = new ImageClient(MASTER_SECRET, APP_KEY);
+        new FieldSetter(client, client.getClass().getDeclaredField("_httpClient")).set(mockIHttpClient);
+        ImageClient spyClient = Mockito.spy(client);
+        ImageFilePayload payload = ImageFilePayload.newBuilder()
+                .setImageType(ImageType.LARGE_ICON)
+                .setOppoFileName("oppoXX.jpg")
+                .setXiaomiFileName("dir/xiaomiXX.jpg")
+                .build();
+        ImageUploadResult imageUploadResult = spyClient.uploadImage(payload);
+        HashMap<String, String> textMap = new HashMap<>();
+        textMap.put("image_type", "2");
+        HashMap<String, String> fileMap = new HashMap<>();
+        fileMap.put("oppo_file", "oppoXX.jpg");
+        fileMap.put("xiaomi_file", "dir/xiaomiXX.jpg");
+        verify(mockIHttpClient).formUploadByPost("https://api.jpush.cn/v3/images/byfiles", textMap, fileMap, null);
+
+        assertThat(imageUploadResult.getMediaId(), equalTo("jgmedia-2-14b23451-0001-41ce-89d9-987b465122da"));
+        assertThat(imageUploadResult.getOppoImageUrl(), equalTo("3653918_5f92b5739ae676f5745bcbf4"));
+        assertThat(imageUploadResult.getXiaomiImageUrl(), equalTo("http://f6.market.xiaomi.com/download/MiPass/01fff50f50ba193f94074ec/d1671936d468383f.png"));
+        assertThat(imageUploadResult.getHuaweiImageUrl(), nullValue());
+    }
+
+    @Test
+    public void testModifyImageByUrl() throws NoSuchFieldException, APIConnectionException, APIRequestException {
+        ResponseWrapper mockResponseWrapper = new ResponseWrapper();
+        mockResponseWrapper.responseCode = 200;
+        mockResponseWrapper.responseContent = "{\n" +
+                "    \"media_id\": \"jgmedia-2-14b23451-0001-41ce-89d9-987b465122da\",  \n" +
+                "    \"oppo_image_url\": \"3653918_5f92b5739ae676f5745bcbf4\",   \n" +
+                "    \"xiaomi_image_url\": \"http://f6.market.xiaomi.com/download/MiPass/01fff50f50ba193f94074ec/d1671936d468383f.png\"     \n" +
+                "}";
+        String mediaId = "jgmedia-2-14b23451-0001-41ce-89d9-987b465122da";
+        NativeHttpClient mockIHttpClient = mock(NativeHttpClient.class);
+        when(mockIHttpClient.sendPut(anyString(), anyString())).thenReturn(mockResponseWrapper);
+
+        ImageClient client = new ImageClient(MASTER_SECRET, APP_KEY);
+        new FieldSetter(client, client.getClass().getDeclaredField("_httpClient")).set(mockIHttpClient);
+        ImageClient spyClient = Mockito.spy(client);
+        ImageUrlPayload payload = ImageUrlPayload.newBuilder()
+                .setImageUrl("xxx.jpg")
+                .setFcmImageUrl("xxx.png")
+                .setHuaweiImageUrl("xxx.jpeg")
+                .build();
+        ImageUploadResult imageUploadResult = spyClient.modifyImage(mediaId, payload);
+        verify(mockIHttpClient).sendPut("https://api.jpush.cn/v3/images/byurls/" + mediaId, "{\"image_url\":\"xxx.jpg\",\"huawei_image_url\":\"xxx.jpeg\",\"fcm_image_url\":\"xxx.png\"}");
+
+        assertThat(imageUploadResult.getMediaId(), equalTo("jgmedia-2-14b23451-0001-41ce-89d9-987b465122da"));
+        assertThat(imageUploadResult.getOppoImageUrl(), equalTo("3653918_5f92b5739ae676f5745bcbf4"));
+        assertThat(imageUploadResult.getXiaomiImageUrl(), equalTo("http://f6.market.xiaomi.com/download/MiPass/01fff50f50ba193f94074ec/d1671936d468383f.png"));
+        assertThat(imageUploadResult.getHuaweiImageUrl(), nullValue());
+    }
+
+    @Test
+    public void testModifyImageByFile() throws NoSuchFieldException {
+        String content = "{\n" +
+                "    \"media_id\": \"jgmedia-2-14b23451-0001-41ce-89d9-987b465122da\",  \n" +
+                "    \"oppo_image_url\": \"3653918_5f92b5739ae676f5745bcbf4\",   \n" +
+                "    \"xiaomi_image_url\": \"http://f6.market.xiaomi.com/download/MiPass/01fff50f50ba193f94074ec/d1671936d468383f.png\"     \n" +
+                "}";
+        String mediaId = "jgmedia-2-14b23451-0001-41ce-89d9-987b465122da";
+
+        NativeHttpClient mockIHttpClient = mock(NativeHttpClient.class);
+        when(mockIHttpClient.formUploadByPut(anyString(), anyMap(), anyMap(), anyString())).thenReturn(content);
+
+        ImageClient client = new ImageClient(MASTER_SECRET, APP_KEY);
+        new FieldSetter(client, client.getClass().getDeclaredField("_httpClient")).set(mockIHttpClient);
+        ImageClient spyClient = Mockito.spy(client);
+        ImageFilePayload payload = ImageFilePayload.newBuilder()
+                .setOppoFileName("oppoXX.jpg")
+                .setXiaomiFileName("dir/xiaomiXX.jpg")
+                .build();
+        ImageUploadResult imageUploadResult = spyClient.modifyImage(mediaId, payload);
+        HashMap<String, String> fileMap = new HashMap<>();
+        fileMap.put("oppo_file", "oppoXX.jpg");
+        fileMap.put("xiaomi_file", "dir/xiaomiXX.jpg");
+        verify(mockIHttpClient).formUploadByPut("https://api.jpush.cn/v3/images/byfiles/" + mediaId, null, fileMap, null);
+
+        assertThat(imageUploadResult.getMediaId(), equalTo("jgmedia-2-14b23451-0001-41ce-89d9-987b465122da"));
+        assertThat(imageUploadResult.getOppoImageUrl(), equalTo("3653918_5f92b5739ae676f5745bcbf4"));
+        assertThat(imageUploadResult.getXiaomiImageUrl(), equalTo("http://f6.market.xiaomi.com/download/MiPass/01fff50f50ba193f94074ec/d1671936d468383f.png"));
+        assertThat(imageUploadResult.getHuaweiImageUrl(), nullValue());
+    }
+
+    @Test
+    public void testUploadImageByUrl2() throws APIConnectionException, APIRequestException {
+
+        ImageClient client = new ImageClient(MASTER_SECRET, APP_KEY);
+        ImageUrlPayload payload = ImageUrlPayload.newBuilder()
+                .setImageType(ImageType.LARGE_ICON)
+                .setImageUrl("http://img.aiimg.com/uploads/allimg/151009/280082-151009232P5.jpg")
+                .setOppoImageUrl("http://img.aiimg.com/uploads/allimg/151009/280082-151009232P5.jpg")
+                .setHuaweiImageUrl("http://img.aiimg.com/uploads/allimg/151009/280082-151009232P5.jpg")
+                .build();
+        ImageUploadResult imageUploadResult = client.uploadImage(payload);
+        assertThat(imageUploadResult, notNullValue());
+        String mediaId = imageUploadResult.getMediaId();
+        assertThat(mediaId, notNullValue());
+        ImageUrlPayload payload2 = ImageUrlPayload.newBuilder()
+                .setImageUrl("http://img.aiimg.com/uploads/allimg/151009/280082-151009225435.jpg")
+                .setFcmImageUrl("http://img.aiimg.com/uploads/allimg/151009/280082-151009225435.jpg")
+                .setHuaweiImageUrl("http://img.aiimg.com/uploads/allimg/151009/280082-151009225435.jpg")
+                .build();
+        ImageUploadResult imageUploadResult2 = client.modifyImage(mediaId, payload2);
+        assertThat(imageUploadResult2, notNullValue());
+        assertThat(imageUploadResult2.getHuaweiImageUrl(), notNullValue());
+    }
+
+    @Test
+    public void testUploadImageByFile2() {
+        ImageClient client = new ImageClient(MASTER_SECRET, APP_KEY);
+        ImageFilePayload payload = ImageFilePayload.newBuilder()
+                .setImageType(ImageType.BIG_PICTURE)
+                .setOppoFileName("/Users/yongxing/Downloads/Xnip2020-12-11_14-24-28.jpg")
+                .setXiaomiFileName("/Users/yongxing/Downloads/Xnip2020-12-11_14-24-28.jpg")
+                .build();
+        ImageUploadResult imageUploadResult = client.uploadImage(payload);
+        assertThat(imageUploadResult, notNullValue());
+        assertThat(imageUploadResult.getError(), nullValue());
+        String mediaId = imageUploadResult.getMediaId();
+        assertThat(mediaId, notNullValue());
+        ImageFilePayload payload2 = ImageFilePayload.newBuilder()
+                .setOppoFileName("/Users/yongxing/Downloads/IMG_2778.jpeg")
+                .setXiaomiFileName("/Users/yongxing/Downloads/IMG_2778.jpeg")
+                .build();
+        imageUploadResult = client.modifyImage(mediaId, payload2);
+        assertThat(imageUploadResult, notNullValue());
+        assertThat(imageUploadResult.getOppoImageUrl(), notNullValue());
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/cn/jpush/api/push/remote/NotificationTest.java b/src/test/java/cn/jpush/api/push/remote/NotificationTest.java
index d296b74a..6b73954c 100644
--- a/src/test/java/cn/jpush/api/push/remote/NotificationTest.java
+++ b/src/test/java/cn/jpush/api/push/remote/NotificationTest.java
@@ -2,6 +2,10 @@
 
 import cn.jiguang.common.resp.APIRequestException;
 import cn.jpush.api.SlowTests;
+import cn.jpush.api.image.ImageClient;
+import cn.jpush.api.image.model.ImageType;
+import cn.jpush.api.image.model.ImageUploadResult;
+import cn.jpush.api.image.model.ImageUrlPayload;
 import cn.jpush.api.push.PushResult;
 import cn.jpush.api.push.model.Platform;
 import cn.jpush.api.push.model.PushPayload;
@@ -18,16 +22,16 @@
 
 @Category(SlowTests.class)
 public class NotificationTest extends BaseRemotePushTest {
-    
+
     @Test
     public void sendNotification_alert_json() throws Exception {
         JsonObject json = new JsonObject();
         json.addProperty("key1", "value1");
         json.addProperty("key2", true);
-        
+
         String alert = json.toString();
         System.out.println(alert);
-        
+
         PushPayload payload = PushPayload.newBuilder()
                 .setAudience(Audience.all())
                 .setPlatform(Platform.all())
@@ -39,9 +43,9 @@ public void sendNotification_alert_json() throws Exception {
         PushResult result = _client.sendPush(payload);
         assertTrue(result.isResultOK());
     }
-	
+
     // --------------- Android
-	
+
     @Test
     public void sendNotification_android_title() throws Exception {
         PushPayload payload = PushPayload.newBuilder()
@@ -55,7 +59,7 @@ public void sendNotification_android_title() throws Exception {
         PushResult result = _client.sendPush(payload);
         assertTrue(result.isResultOK());
     }
-    
+
     @Test
     public void sendNotification_android_buildId() throws Exception {
         PushPayload payload = PushPayload.newBuilder()
@@ -70,7 +74,7 @@ public void sendNotification_android_buildId() throws Exception {
         PushResult result = _client.sendPush(payload);
         assertTrue(result.isResultOK());
     }
-    
+
     @Test
     public void sendNotification_android_extras() throws Exception {
         PushPayload payload = PushPayload.newBuilder()
@@ -86,10 +90,43 @@ public void sendNotification_android_extras() throws Exception {
         PushResult result = _client.sendPush(payload);
         assertTrue(result.isResultOK());
     }
-    
-    
+
+    @Test
+    public void sendNotification_android_media_id() throws Exception {
+        ImageClient imageClient = new ImageClient(MASTER_SECRET, APP_KEY);
+        ImageUploadResult imageUploadResult = imageClient.uploadImage(ImageUrlPayload.newBuilder()
+                .setImageType(ImageType.SMALL_ICON)
+                .setImageUrl("http://img.aiimg.com/uploads/allimg/151009/280082-151009225435.jpg")
+                .setXiaomiImageUrl("http://img.aiimg.com/uploads/allimg/151009/280082-151009225435.jpg")
+                .build());
+        String mediaId = imageUploadResult.getMediaId();
+        JsonObject json = new JsonObject();
+        json.addProperty("key1", "value1");
+        json.addProperty("key2", true);
+
+        String alert = json.toString();
+        System.out.println(alert);
+
+        PushPayload payload = PushPayload.newBuilder()
+                .setAudience(Audience.all())
+                .setPlatform(Platform.all())
+                .setNotification(Notification.newBuilder()
+                        .addPlatformNotification(AndroidNotification.newBuilder()
+                                .setAlert(alert)
+                                .setSmallIconUri(mediaId)
+                                .setLargeIcon(mediaId)
+                                .setBigPicPath(mediaId)
+                                .setInbox(mediaId)
+                                .setBigText(mediaId)
+                                .setTitle("title")
+                                .build()).build())
+                .build();
+        PushResult result = _client.sendPush(payload);
+        assertTrue(result.isResultOK());
+    }
+
     // ------------------ ios
-    
+
     @Test
     public void sendNotification_ios_badge() throws Exception {
         PushPayload payload = PushPayload.newBuilder()