Ver Fonte

initial commit

zhipeng há 4 anos atrás
commit
1815eb2a57

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+/build

+ 34 - 0
build.gradle

@@ -0,0 +1,34 @@
+plugins {
+    id 'com.android.library'
+}
+
+android {
+    compileSdkVersion 30
+
+    defaultConfig {
+        minSdkVersion 21
+        targetSdkVersion 30
+        versionCode 100
+        versionName "1.0.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled true
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    implementation 'androidx.recyclerview:recyclerview:1.2.1'
+    compileOnly project(':atmob-ad')
+    compileOnly project(':group-ad')
+}

+ 5 - 0
consumer-rules.pro

@@ -0,0 +1,5 @@
+-keep class com.atmob.task.AppTaskAdapter{*;}
+-keep class com.atmob.task.AppTaskView{
+    public <methods>;
+}
+-keep class com.atmob.task.utils.AppTaskHandler{*;}

+ 996 - 0
proguard-dic.txt

@@ -0,0 +1,996 @@
+ᐝ
+ι
+ʿ
+ᐧ
+ᐨ
+・
+ﹳ
+゙
+ᴵ
+ᵎ
+ᵔ
+ᵢ
+ⁱ
+ﹶ
+ﹺ
+ー
+ᐠ
+ᐣ
+ᐩ
+ᑊ
+ᕀ
+ᵕ
+ᵣ
+יִ
+יּ
+ᐟ
+ᐡ
+ᐪ
+ᒽ
+ᔇ
+ᔈ
+ᗮ
+ᴶ
+ᴸ
+ᵀ
+ᵋ
+ᵗ
+゚
+เ
+Ꭵ
+ᐤ
+ᒡ
+ᒢ
+ᖮ
+ᵌ
+ᵓ
+ᵙ
+ᵛ
+ᵥ
+ﯨ
+ﹴ
+ﹸ
+ﹾ
+৲
+ᐢ
+ᒻ
+ᔅ
+ᔉ
+ᔊ
+ᔋ
+ᕁ
+ᕑ
+ᕽ
+ᘁ
+ᵄ
+ᵞ
+ᵧ
+וּ
+וֹ
+ﹲ
+ﹷ
+ﹻ
+ﹼ
+ﺑ
+ﻧ
+ᑉ
+ᑋ
+ᑦ
+ᒾ
+ᓪ
+ᓫ
+ᔾ
+ᕐ
+ᕝ
+ᵒ
+ᵘ
+ᵤ
+ⁿ
+Ⅰ
+ⅰ
+丶
+ﭔ
+ﭠ
+ﯦ
+ﯩ
+ﯾ
+ﹰ
+ﺗ
+ﻳ
+_
+ニ
+า
+ᐥ
+ᒃ
+ᓒ
+ᕪ
+ᙆ
+ᴊ
+ᴷ
+ᵏ
+ⅼ
+ﭘ
+ﺒ
+ﺛ
+ﺩ
+ﻨ
+ィ
+ɿ
+ง
+ว
+ᐦ
+ᒄ
+ᒼ
+ᓑ
+ᔆ
+ᴖ
+ᴬ
+ᴱ
+ᴲ
+ᴾ
+ᵁ
+ᵃ
+ᵅ
+ᵉ
+ᵊ
+ᵡ
+ᵪ
+ḯ
+Ị
+ị
+ゝ
+ー
+ヽ
+一
+גּ
+זּ
+נּ
+רּ
+ﭕ
+ﭜ
+ﭡ
+ﭤ
+ﯧ
+ﯿ
+ﹹ
+ﹿ
+ﺘ
+ﺫ
+ﻴ
+ſ
+৳
+ฯ
+ๅ
+ᐞ
+ᓐ
+ᓭ
+ᓯ
+ᓱ
+ᓴ
+ᔥ
+ᖦ
+ᴗ
+ᴴ
+ᴿ
+ᵇ
+ᵖ
+ᵟ
+ḷ
+ṙ
+ṛ
+ỉ
+ἰ
+ἱ
+ὶ
+ί
+ῐ
+ῑ
+‿
+⁀
+⁔
+丨
+氵
+灬
+ﭙ
+ﮂ
+ﮄ
+ﹽ
+ﺋ
+ﺜ
+ﻟ
+ノ
+ઽ
+ເ
+ᓰ
+ᓲ
+ᓵ
+ᔿ
+ᕻ
+ᴄ
+ᴐ
+ᴛ
+ᴺ
+ᵈ
+ᵑ
+ᵨ
+Ḯ
+Ἰ
+Ἱ
+Ῐ
+Ῑ
+Ὶ
+Ί
+ℴ
+ⅹ
+ⅽ
+ײַ
+ﬧ
+דּ
+ﭝ
+ﭥ
+ﮆ
+ﹱ
+ﺀ
+ﺪ
+ﺭ
+j
+ュ
+ა
+ი
+Ꮀ
+Ꮮ
+ᒣ
+ᒥ
+ᒧ
+ᒪ
+ᓳ
+ᘄ
+ᴠ
+ᴰ
+ᴻ
+ᵠ
+ᵩ
+ḻ
+ṟ
+ẛ
+Ỉ
+ῒ
+ΐ
+Ⅼ
+ⅴ
+ィ
+ךּ
+כּ
+ﭨ
+ﮢ
+ﺌ
+ﺬ
+ﺯ
+ﻣ
+J
+L
+ァ
+イ
+フ
+ヘ
+ণ
+จ
+แ
+ๆ
+Ꭻ
+Ꮁ
+Ꮣ
+ᒦ
+ᒨ
+ᒫ
+ᖟ
+ᘇ
+ᙇ
+ᴧ
+ᴮ
+ᴳ
+ᴼ
+ᵍ
+ᵐ
+ᵚ
+ᵝ
+ᵦ
+ẋ
+ẍ
+〳
+〵
+ノ
+亅
+亠
+冫
+לּ
+ﮃ
+ﮅ
+ﱠ
+ﱢ
+ﺮ
+ﻠ
+ﻩ
+c
+ゥ
+ェ
+テ
+ナ
+ン
+Ŀ
+ধ
+ร
+ใ
+Ꭲ
+Ꭸ
+Ꮠ
+ᐜ
+ᒩ
+ᓶ
+ᓷ
+ᓸ
+ᓹ
+ᓼ
+ᓽ
+ᔀ
+ᔁ
+ᔄ
+ᔨ
+ᔭ
+ᖕ
+ᘆ
+ᴋ
+ᴹ
+ᴽ
+ḟ
+Ḷ
+ḹ
+ḽ
+ṝ
+ṿ
+ἲ
+ἳ
+ἴ
+ἵ
+ῖ
+ℐ
+〱
+丿
+בּ
+ﭩ
+ﮇ
+ﮊ
+ﮞ
+ﮣ
+ﺰ
+ﻪ
+ッ
+シ
+ソ
+ト
+ユ
+ο
+ऽ
+บ
+ย
+ะ
+າ
+ᐳ
+ᐸ
+ᒉ
+ᒋ
+ᒍ
+ᒐ
+ᓓ
+ᓕ
+ᓗ
+ᓚ
+ᓺ
+ᓻ
+ᓾ
+ᓿ
+ᔂ
+ᔃ
+ᔦ
+ᔩ
+ᔪ
+ᔮ
+ᘤ
+ᚐ
+ᴈ
+ᴏ
+ᴢ
+ᴣ
+ᵂ
+Ḭ
+ḭ
+ṫ
+ṭ
+Ẏ
+ẗ
+Ἲ
+Ἳ
+Ἴ
+Ἵ
+ⅈ
+冖
+הּ
+כֿ
+ﮈ
+ﺓ
+ﻤ
+ﻥ
+f
+i
+t
+v
+ャ
+エ
+コ
+ヒ
+ミ
+リ
+レ
+र
+ঌ
+গ
+ঢ
+ব
+শ
+ঽ
+ก
+კ
+ᐯ
+ᐴ
+ᐹ
+ᒌ
+ᒎ
+ᒑ
+ᒬ
+ᒭ
+ᒮ
+ᒯ
+ᒲ
+ᒳ
+ᒶ
+ᒷ
+ᒺ
+ᓖ
+ᓘ
+ᓛ
+ᔫ
+ᘂ
+ᘢ
+ᚁ
+ᚆ
+ᴒ
+ᴫ
+Ḻ
+Ṫ
+Ỳ
+Ỵ
+ἶ
+ἷ
+ῗ
+ℓ
+Ⅴ
+ゞ
+イ
+忄
+אּ
+ﮋ
+ﺏ
+ﺔ
+ﺣ
+ﻡ
+u
+z
+ォ
+ョ
+ア
+マ
+ラ
+ワ
+ट
+ও
+চ
+দ
+ন
+প
+য
+র
+হ
+ৰ
+ค
+ฅ
+ถ
+ท
+ป
+ผ
+ภ
+ล
+ห
+โ
+ไ
+Ⴡ
+ძ
+ᐵ
+ᑈ
+ᒏ
+ᒰ
+ᒱ
+ᒴ
+ᒵ
+ᒸ
+ᒹ
+ᓙ
+ᔬ
+ᖧ
+ᖨ
+ᖪ
+ᖬ
+ᖽ
+ᖾ
+ᖿ
+ᗁ
+ᘅ
+ᘣ
+ᘦ
+ᘧ
+ᴉ
+ᴘ
+ᴝ
+ᴦ
+ᴩ
+ᴭ
+Ṭ
+ṯ
+ẏ
+ẓ
+ọ
+ỵ
+Ἶ
+Ἷ
+ℷ
+Ⅱ
+ⅱ
+々
+ぃ
+ァ
+ッ
+ヾ
+乀
+宀
+ﬥ
+צּ
+בֿ
+ﭒ
+ﭞ
+ﺕ
+ﺟ
+ﺧ
+ﻋ
+ﻌ
+ﻢ
+F
+I
+l
+n
+r
+s
+ヲ
+ウ
+キ
+ク
+ケ
+ス
+チ
+ハ
+モ
+п
+ऱ
+এ
+খ
+ঘ
+ষ
+ঢ়
+ฑ
+ต
+น
+ม
+อ
+ງ
+ე
+პ
+Ꮧ
+Ꮭ
+ᐱ
+ᓮ
+ᔱ
+ᔲ
+ᔹ
+ᔺ
+ᔽ
+ᕂ
+ᕃ
+ᕄ
+ᕆ
+ᖅ
+ᖩ
+ᖫ
+ᖭ
+ᖸ
+ᖺ
+ᗀ
+ᘥ
+ᵆ
+Ḟ
+Ḹ
+Ḽ
+Ṿ
+Ὑ
+Ῠ
+Ῡ
+Ὺ
+Ύ
+K
+Ⅽ
+Ↄ
+く
+っ
+へ
+ゥ
+ト
+リ
+ヮ
+ヶ
+丫
+乁
+爫
+ﬤ
+טּ
+סּ
+ףּ
+ﭖ
+ﭴ
+ﭸ
+ﮉ
+ﮌ
+ﮐ
+ﱟ
+ﱡ
+ﺙ
+ﻏ
+ﻐ
+ﻛ
+k
+ヌ
+メ

+ 47 - 0
proguard-rules.pro

@@ -0,0 +1,47 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+-packageobfuscationdictionary proguard-dic.txt
+-obfuscationdictionary proguard-dic.txt
+-classobfuscationdictionary proguard-dic.txt
+
+-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
+-optimizationpasses 5
+-allowaccessmodification
+
+-dontusemixedcaseclassnames
+-dontskipnonpubliclibraryclasses
+-verbose
+-dontwarn
+-dontusemixedcaseclassnames
+-dontskipnonpubliclibraryclasses
+-dontskipnonpubliclibraryclassmembers
+-dontpreverify
+-verbose
+-keepattributes *Annotation*,InnerClasses
+-keepattributes Signature
+-keepattributes SourceFile,LineNumberTable
+
+-keep class com.atmob.task.AppTaskAdapter{*;}
+-keep class com.atmob.task.AppTaskView{
+    public <methods>;
+}
+-keep class com.atmob.task.utils.AppTaskHandler{*;}

+ 5 - 0
src/main/AndroidManifest.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.atmob.task">
+
+</manifest>

+ 23 - 0
src/main/java/com/atmob/task/AppTaskAdapter.java

@@ -0,0 +1,23 @@
+package com.atmob.task;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.atmob.task.bean.AppTaskBean;
+
+public abstract class AppTaskAdapter {
+    public abstract View onCreateView(LayoutInflater inflater, ViewGroup parent);
+
+    public abstract View[] getClickViews();
+
+    public abstract void onRenderAppName(AppTaskBean appTaskBean, String appName);
+
+    public abstract void onRenderAppIcon(AppTaskBean appTaskBean, String appIconUrl);
+
+    public abstract void onRenderTaskStatus(AppTaskBean appTaskBean, @AppTaskBean.AppTaskStatus int taskStatus);
+
+    public abstract void onRenderDownloadProgress(long total, long progress);
+
+    public abstract void onDownloadStatusChanged(boolean isDownloading);
+}

+ 256 - 0
src/main/java/com/atmob/task/AppTaskView.java

@@ -0,0 +1,256 @@
+package com.atmob.task;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.atmob.http.BaseHttpObserver;
+import com.atmob.manager.HttpFailManager;
+import com.atmob.request.CommonBaseRequest;
+import com.atmob.task.adapter.AppTaskItemAdapter;
+import com.atmob.task.bean.AppTaskBean;
+import com.atmob.task.bean.AppTaskDataResponse;
+import com.atmob.task.bean.AppTaskUpdateRequest;
+import com.atmob.task.bean.AppTaskUpdateResponse;
+import com.atmob.task.data.NetworkClient;
+import com.atmob.task.utils.AppTaskEvent;
+import com.atmob.task.view.TwinklingRecyclerView;
+import com.atmob.utils.RxSchedulersUtils;
+
+import atmob.io.reactivex.rxjava3.disposables.CompositeDisposable;
+import atmob.io.reactivex.rxjava3.disposables.Disposable;
+
+public class AppTaskView extends TwinklingRecyclerView implements AppTaskItemAdapter.OnItemActionCallback {
+
+    private static final String TAG = AppTaskView.class.getSimpleName();
+
+    private static final int WHAT_CHECK_APP_EXIST = 387;
+
+    private CompositeDisposable compositeDisposable;
+
+    private AppTaskItemAdapter appTaskItemAdapter;
+
+    private OnAppTaskItemClickListener onAppTaskItemClickListener;
+
+    private OnAppTaskRewardRequestListener onAppTaskRewardRequestListener;
+
+    private boolean loadingFlag;
+
+    private Handler handler;
+
+    public AppTaskView(@NonNull Context context) {
+        this(context, null);
+    }
+
+    public AppTaskView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AppTaskView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init();
+    }
+
+    private void init() {
+        handler = new Handler(Looper.getMainLooper(), msg -> {
+            if (msg.what == WHAT_CHECK_APP_EXIST) {
+                appTaskItemAdapter.checkAppExist();
+                return true;
+            }
+            return false;
+        });
+        compositeDisposable = new CompositeDisposable();
+        setLayoutManager(new LinearLayoutManager(getContext()));
+    }
+
+    private void loadData() {
+        if (appTaskItemAdapter == null || loadingFlag) {
+            return;
+        }
+        loadingFlag = true;
+        NetworkClient.api().loadAppTask(new CommonBaseRequest())
+                .compose(RxSchedulersUtils.observableIO2Main())
+                .subscribe(new BaseHttpObserver<AppTaskDataResponse>() {
+
+                    @Override
+                    public void onGotDisposable(Disposable disposable) {
+                        addDisposable(disposable);
+                        AppTaskEvent.report(AppTaskEvent.ATE_LOAD_DATA);
+                    }
+
+                    @Override
+                    public void onSuccess(AppTaskDataResponse data) {
+                        loadingFlag = false;
+                        if (data == null) {
+                            return;
+                        }
+                        AppTaskBean.CDN = data.getUrl();
+                        if (data.getPtasks() != null) {
+                            appTaskItemAdapter.update(data.getPtasks());
+                        }
+                        AppTaskEvent.report(AppTaskEvent.ATE_LOAD_DATA_SUCCESS);
+                    }
+
+                    @Override
+                    public void onFailed(int code, String msg) {
+                        loadingFlag = false;
+                        HttpFailManager.handleHttpFail(code, msg);
+                    }
+                });
+    }
+
+    private void addDisposable(Disposable disposable) {
+        if (compositeDisposable == null) {
+            compositeDisposable = new CompositeDisposable();
+        }
+        compositeDisposable.add(disposable);
+    }
+
+    public void init(@NonNull AppTaskAdapter appTaskAdapter) {
+        appTaskItemAdapter = new AppTaskItemAdapter(appTaskAdapter, compositeDisposable);
+        appTaskItemAdapter.setOnItemActionCallback(this);
+        super.setAdapter(appTaskItemAdapter);
+    }
+
+    /**
+     * @param onAppTaskItemClickListener see{@link OnAppTaskItemClickListener}
+     * @return this
+     */
+    public AppTaskView setOnAppTaskItemClickListener(OnAppTaskItemClickListener onAppTaskItemClickListener) {
+        this.onAppTaskItemClickListener = onAppTaskItemClickListener;
+        return this;
+    }
+
+    /**
+     * @param onAppTaskRewardRequestListener see{@link OnAppTaskRewardRequestListener}
+     * @return this
+     */
+    public AppTaskView setOnAppTaskRewardRequestListener(OnAppTaskRewardRequestListener onAppTaskRewardRequestListener) {
+        this.onAppTaskRewardRequestListener = onAppTaskRewardRequestListener;
+        return this;
+    }
+
+    /**
+     * 触发积分墙列表第一个可触发的积分墙任务
+     */
+    public void triggerFirstTask() {
+        if (appTaskItemAdapter != null) {
+            appTaskItemAdapter.triggerFirstTask();
+        }
+    }
+
+    /**
+     * @return 积分墙任务数量
+     */
+    public int getTaskNum() {
+        if (appTaskItemAdapter != null) {
+            return appTaskItemAdapter.getItemCount();
+        }
+        return 0;
+    }
+
+    /**
+     * 刷新积分墙任务
+     */
+    public void refreshTaskList() {
+        loadData();
+    }
+
+    @Override
+    public void setAdapter(@Nullable RecyclerView.Adapter adapter) {
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        loadData();
+    }
+
+    @Override
+    public void onVisibilityAggregated(boolean isVisible) {
+        super.onVisibilityAggregated(isVisible);
+        onVisibilityAggregatedInternal(isVisible);
+    }
+
+    private void onVisibilityAggregatedInternal(boolean isVisible) {
+        if (isVisible && appTaskItemAdapter != null) {
+            if (handler.hasMessages(WHAT_CHECK_APP_EXIST)) {
+                handler.removeMessages(WHAT_CHECK_APP_EXIST);
+            }
+            handler.sendEmptyMessageDelayed(WHAT_CHECK_APP_EXIST, 1000);
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (compositeDisposable != null) {
+            compositeDisposable.clear();
+        }
+        if (appTaskItemAdapter != null) {
+            appTaskItemAdapter.release();
+        }
+    }
+
+    @Override
+    public void onClick(AppTaskBean appTaskBean) {
+        if (onAppTaskItemClickListener != null) {
+            onAppTaskItemClickListener.onClick(appTaskBean.getTaskStatus());
+        }
+    }
+
+    @Override
+    public void updateStatus(long id, int targetStatus) {
+        NetworkClient.api().updateAppTaskStatus(new AppTaskUpdateRequest(id, targetStatus))
+                .compose(RxSchedulersUtils.observableIO2Main())
+                .subscribe(new BaseHttpObserver<AppTaskUpdateResponse>() {
+
+                    @Override
+                    public void onGotDisposable(Disposable disposable) {
+                        addDisposable(disposable);
+                    }
+
+                    @Override
+                    public void onSuccess(AppTaskUpdateResponse data) {
+                        loadData();
+                        Log.d(TAG, "app task status update success, id ==> " + id + ", target status ==> " + targetStatus);
+                    }
+
+                    @Override
+                    public void onFailed(int code, String msg) {
+                        HttpFailManager.handleHttpFail(code, msg);
+                    }
+                });
+    }
+
+    @Override
+    public void getReward(AppTaskBean appTaskBean) {
+        if (onAppTaskRewardRequestListener != null) {
+            onAppTaskRewardRequestListener.onReward(appTaskBean);
+        }
+    }
+
+    /**
+     * 请求奖励回调
+     */
+    public interface OnAppTaskRewardRequestListener {
+        void onReward(AppTaskBean appTaskBean);
+    }
+
+    /**
+     * 积分墙按钮回调, 常用作事件上报
+     */
+    public interface OnAppTaskItemClickListener {
+        /**
+         * @param appTaskStatus 积分墙任务的当前状态
+         */
+        void onClick(@AppTaskBean.AppTaskStatus int appTaskStatus);
+    }
+}

+ 578 - 0
src/main/java/com/atmob/task/adapter/AppTaskItemAdapter.java

@@ -0,0 +1,578 @@
+package com.atmob.task.adapter;
+
+import android.annotation.SuppressLint;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.annotation.IntegerRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.AsyncListDiffer;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.atmob.task.AppTaskAdapter;
+import com.atmob.task.bean.AdAppInfoData;
+import com.atmob.task.bean.AppTaskBean;
+import com.atmob.task.bean.AppTaskDownloadWrapper;
+import com.atmob.task.data.LocalData;
+import com.atmob.task.utils.AppTaskEvent;
+import com.atmob.task.utils.InstallFaultToleranceUtils;
+import com.atmob.task.utils.RetrofitDownloader;
+import com.atmob.utils.AppInfoUtils;
+import com.atmob.utils.ResUtils;
+import com.atmob.utils.RxSchedulersUtils;
+import com.atmob.utils.Utils;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import atmob.io.reactivex.rxjava3.core.Completable;
+import atmob.io.reactivex.rxjava3.core.CompletableObserver;
+import atmob.io.reactivex.rxjava3.core.Observable;
+import atmob.io.reactivex.rxjava3.core.Observer;
+import atmob.io.reactivex.rxjava3.disposables.CompositeDisposable;
+import atmob.io.reactivex.rxjava3.disposables.Disposable;
+import atmob.io.reactivex.rxjava3.schedulers.Schedulers;
+
+public class AppTaskItemAdapter extends RecyclerView.Adapter<AppTaskItemAdapter.AppTaskItemViewHolder> {
+
+    private static final String TAG = AppTaskItemAdapter.class.getSimpleName();
+
+    private static final String DIR_PATH = Utils.getContext().getExternalCacheDir().getPath();
+
+    /**
+     * 安装成功后, 是否强制打开
+     */
+    private static final boolean FORCE_OPEN = true;
+
+    private final HashMap<Long, String> potentialInstalledList = new HashMap<>();
+
+    private final AsyncListDiffer<AppTaskDownloadWrapper> asyncListDiffer = new AsyncListDiffer<>(this, new DiffCallback());
+
+    private final CompositeDisposable compositeDisposable;
+
+    private OnItemActionCallback onItemActionCallback;
+
+    private AppTaskAdapter appTaskAdapter;
+
+    public AppTaskItemAdapter(AppTaskAdapter appTaskAdapter, CompositeDisposable compositeDisposable) {
+        this.compositeDisposable = compositeDisposable;
+        this.appTaskAdapter = appTaskAdapter;
+    }
+
+    @NonNull
+    @Override
+    public AppTaskItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        if (appTaskAdapter == null) {
+            throw new IllegalStateException("appTaskAdapter can't not be null.");
+        }
+        View view = appTaskAdapter.onCreateView(LayoutInflater.from(parent.getContext()), parent);
+        AppTaskItemViewHolder appTaskItemViewHolder = new AppTaskItemViewHolder(view);
+        bindClickListener();
+        return appTaskItemViewHolder;
+    }
+
+    private void bindClickListener() {
+        if (appTaskAdapter == null) {
+            throw new IllegalStateException("appTaskAdapter can't not be null.");
+        }
+        View[] clickViews = appTaskAdapter.getClickViews();
+        if (clickViews == null || clickViews.length == 0) {
+            return;
+        }
+        View.OnClickListener onClickListener = v -> onAppTaskItemClick((AppTaskDownloadWrapper) v.getTag());
+        for (View clickView : clickViews) {
+            clickView.setOnClickListener(onClickListener);
+        }
+    }
+
+    @SuppressLint("ResourceType")
+    @Override
+    public void onBindViewHolder(@NonNull AppTaskItemViewHolder holder, int position) {
+        if (appTaskAdapter == null) {
+            throw new IllegalStateException("appTaskAdapter can't not be null.");
+        }
+        AppTaskDownloadWrapper appTaskDownloadWrapper = asyncListDiffer.getCurrentList().get(position);
+        // download status
+        appTaskAdapter.onDownloadStatusChanged(appTaskDownloadWrapper.isDownloading());
+        // app name
+        appTaskAdapter.onRenderAppName(appTaskDownloadWrapper, appTaskDownloadWrapper.getAppName());
+        // app icon
+        appTaskAdapter.onRenderAppIcon(appTaskDownloadWrapper, appTaskDownloadWrapper.getAppIcon());
+        // click view
+        setTag2ClickViews(appTaskDownloadWrapper);
+        // task status
+        appTaskAdapter.onRenderTaskStatus(appTaskDownloadWrapper, appTaskDownloadWrapper.getTaskStatus());
+        // download progress
+        appTaskAdapter.onRenderDownloadProgress(appTaskDownloadWrapper.getTotal(), appTaskDownloadWrapper.getProgress());
+    }
+
+    private void setTag2ClickViews(AppTaskDownloadWrapper appTaskDownloadWrapper) {
+        if (appTaskAdapter == null) {
+            throw new IllegalStateException("appTaskAdapter can't not be null.");
+        }
+        View[] clickViews = appTaskAdapter.getClickViews();
+        if (clickViews != null && clickViews.length > 0) {
+            for (View clickView : clickViews) {
+                clickView.setTag(appTaskDownloadWrapper);
+            }
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull AppTaskItemViewHolder holder, int position, @NonNull List<Object> payloads) {
+        if (appTaskAdapter == null) {
+            throw new IllegalStateException("appTaskAdapter can't not be null.");
+        }
+        AppTaskDownloadWrapper appTaskDownloadWrapper = asyncListDiffer.getCurrentList().get(position);
+        setTag2ClickViews(appTaskDownloadWrapper);
+        if (payloads.size() == 0) {
+            onBindViewHolder(holder, position);
+        } else {
+            for (Object payload : payloads) {
+                Bundle bundle = (Bundle) payload;
+                for (String key : bundle.keySet()) {
+                    switch (key) {
+                        case "payload_app_name":
+                            appTaskAdapter.onRenderAppName(appTaskDownloadWrapper, bundle.getString(key));
+                            break;
+                        case "payload_task_status":
+                            appTaskAdapter.onRenderTaskStatus(appTaskDownloadWrapper, bundle.getInt(key));
+                            break;
+                        case "payload_downloading":
+                            appTaskAdapter.onDownloadStatusChanged(bundle.getBoolean(key));
+                            break;
+                        case "payload_progress":
+                        case "payload_total":
+                            if (bundle.get("payload_progress") == null || bundle.get("payload_total") == null) {
+                                break;
+                            }
+                            appTaskAdapter.onRenderDownloadProgress(bundle.getLong("payload_total"), bundle.getLong("payload_progress"));
+                            break;
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        return asyncListDiffer.getCurrentList().size();
+    }
+
+    private void onAppTaskItemClick(AppTaskDownloadWrapper appTaskDownloadWrapper) {
+        if (appTaskDownloadWrapper == null) {
+            return;
+        }
+        switch (appTaskDownloadWrapper.getTaskStatus()) {
+            case AppTaskBean.AppTaskStatus.Initial:
+                Log.d(TAG, "onAppTaskItemClick: toInstall.");
+                toInstall(appTaskDownloadWrapper);
+                break;
+            case AppTaskBean.AppTaskStatus.Installed:
+                toOpen(appTaskDownloadWrapper);
+                Log.d(TAG, "onAppTaskItemClick: toOpen.");
+                break;
+            case AppTaskBean.AppTaskStatus.Opened:
+                if (onItemActionCallback != null) {
+                    onItemActionCallback.getReward(appTaskDownloadWrapper);
+                }
+                Log.d(TAG, "onAppTaskItemClick: getReward.");
+                break;
+            case AppTaskBean.AppTaskStatus.Finish:
+                //ignore
+                break;
+        }
+        if (onItemActionCallback != null) {
+            onItemActionCallback.onClick(appTaskDownloadWrapper);
+        }
+        reportClickEvent(appTaskDownloadWrapper);
+    }
+
+    private void reportClickEvent(AppTaskBean appTaskBean) {
+        switch (appTaskBean.getTaskStatus()) {
+            case AppTaskBean.AppTaskStatus.Finish:
+                break;
+            case AppTaskBean.AppTaskStatus.Initial:
+            case AppTaskBean.AppTaskStatus.Installed:
+                AppTaskEvent.report(AppTaskEvent.ATE_CLICK_ITEM_INITIAL);
+                break;
+            case AppTaskBean.AppTaskStatus.Opened:
+                AppTaskEvent.report(AppTaskEvent.ATE_CLICK_ITEM_REWARD);
+                break;
+        }
+    }
+
+    private void toOpen(@NonNull AppTaskDownloadWrapper appTaskDownloadWrapper) {
+        String appPkgName = appTaskDownloadWrapper.getAppPkgName();
+        if (AppInfoUtils.isExistPackage(Utils.getContext(), appPkgName)) {
+            AppTaskEvent.report(AppTaskEvent.ATE_LAUNCH);
+            AppInfoUtils.launchApp(appPkgName);
+            if (onItemActionCallback != null) {
+                onItemActionCallback.updateStatus(appTaskDownloadWrapper.getId(), AppTaskBean.AppTaskStatus.Opened);
+            }
+            removeFromCheckList(appTaskDownloadWrapper.getId());
+            Log.d(TAG, "toOpen: app exist, open it.");
+        } else {
+            AppTaskEvent.report(AppTaskEvent.ATE_DOWNLOAD);
+            toDownload(appTaskDownloadWrapper);
+            Log.d(TAG, "toOpen: app not exist, try to download.");
+        }
+    }
+
+    private void toInstall(@NonNull AppTaskDownloadWrapper appTaskDownloadWrapper) {
+        // first of all, check local apk file is exists
+        Observable.just(appTaskDownloadWrapper.getAppPkgName())
+                .map(appPackageName -> {
+                    // this step is check apk file that download by sdk is exists
+                    AdAppInfoData adAppInfoData = LocalData.queryAdAppInfoByPackageName(appPackageName);
+                    if (adAppInfoData == null) {
+                        return new File("");
+                    }
+                    String apkPath = adAppInfoData.getApkPath();
+                    File file = new File(apkPath);
+                    file = file.exists() && file.isFile() ? file : new File("");
+                    Log.d(TAG, "toInstall: sdk apk path ==> " + apkPath + ", file available ==> " + (!"".equals(file.getPath())));
+                    return file;
+                })
+                .map(apkFile -> {
+                    // if sdk download file is exist just use it
+                    if ("".equals(apkFile.getPath())) {
+                        // if non, this step is check apk file that download by ourself is exists
+                        File downloadFile = new File(DIR_PATH, appTaskDownloadWrapper.getId() + ".apk");
+                        if (downloadFile.exists() && downloadFile.isFile() && !checkFileOccupy(downloadFile)) {
+                            apkFile = downloadFile;
+                        }
+                        Log.d(TAG, "toInstall: download apk path ==> " + downloadFile.getPath() + ", file available ==> " + (!"".equals(apkFile.getPath())));
+                    }
+                    if ("".equals(apkFile.getPath())) {
+                        throw new Exception("none available apk file");
+                    } else {
+                        return apkFile;
+                    }
+                })
+                .compose(RxSchedulersUtils.observableIO2Main())
+                .subscribe(new Observer<File>() {
+                    @Override
+                    public void onSubscribe(@atmob.io.reactivex.rxjava3.annotations.NonNull Disposable d) {
+                        compositeDisposable.add(d);
+                    }
+
+                    @Override
+                    public void onNext(@atmob.io.reactivex.rxjava3.annotations.NonNull File file) {
+                        // local file exists, install it
+                        InstallFaultToleranceUtils.installApk(Utils.getContext(), file, () -> {
+                            AppTaskEvent.report(AppTaskEvent.ATE_DOWNLOAD);
+                            toDownload(appTaskDownloadWrapper);
+                            Log.d(TAG, "toInstall: apk install failed, try to download");
+                        });
+                        addToCheckList(appTaskDownloadWrapper);
+                    }
+
+                    @Override
+                    public void onError(@atmob.io.reactivex.rxjava3.annotations.NonNull Throwable e) {
+                        // local file not exist, try to download
+                        AppTaskEvent.report(AppTaskEvent.ATE_DOWNLOAD);
+                        toDownload(appTaskDownloadWrapper);
+                        Log.d(TAG, "toInstall: None available apk file, try to download, msg ==> " + e.getMessage());
+                    }
+
+                    @Override
+                    public void onComplete() {
+                        // ignore, this method invoked means local file exists
+                    }
+                });
+    }
+
+    private void toDownload(AppTaskDownloadWrapper appTaskDownloadWrapper) {
+        String fileName = appTaskDownloadWrapper.getId() + ".apk";
+        RetrofitDownloader.download(appTaskDownloadWrapper.getAppLink(), DIR_PATH, fileName, new RetrofitDownloader.ProgressObserver() {
+            @Override
+            public void onStart() {
+                appTaskDownloadWrapper.setDownloading(true);
+                Bundle bundle = new Bundle();
+                bundle.putBoolean("payload_downloading", true);
+                notifyItemChanged(asyncListDiffer.getCurrentList().indexOf(appTaskDownloadWrapper), bundle);
+                Log.d(TAG, "toDownload onStart: download start, task ==> " + appTaskDownloadWrapper);
+            }
+
+            @Override
+            public void onProgress(long total, long progress) {
+                appTaskDownloadWrapper.setDownloading(true);
+                appTaskDownloadWrapper.setTotal(total);
+                appTaskDownloadWrapper.setProgress(progress);
+                Bundle bundle = new Bundle();
+                bundle.putBoolean("payload_downloading", true);
+                bundle.putLong("payload_progress", progress);
+                bundle.putLong("payload_total", total);
+                notifyItemChanged(asyncListDiffer.getCurrentList().indexOf(appTaskDownloadWrapper), bundle);
+            }
+
+            @Override
+            public void onComplete(File file) {
+                appTaskDownloadWrapper.setDownloading(false);
+                InstallFaultToleranceUtils.installApk(Utils.getContext(), file, null);
+                addToCheckList(appTaskDownloadWrapper);
+                Bundle bundle = new Bundle();
+                bundle.putBoolean("payload_downloading", false);
+                notifyItemChanged(asyncListDiffer.getCurrentList().indexOf(appTaskDownloadWrapper), bundle);
+                Log.d(TAG, "toDownload onComplete: download complete, task ==> " + appTaskDownloadWrapper);
+                AppTaskEvent.report(AppTaskEvent.ATE_DOWNLOAD_SUCCESS);
+            }
+
+            @Override
+            public void onError(String message) {
+                appTaskDownloadWrapper.setDownloading(false);
+                Toast.makeText(Utils.getContext(), "下载失败,请重试", Toast.LENGTH_SHORT).show();
+                Bundle bundle = new Bundle();
+                bundle.putBoolean("payload_downloading", false);
+                notifyItemChanged(asyncListDiffer.getCurrentList().indexOf(appTaskDownloadWrapper), bundle);
+                Log.d(TAG, "toDownload onError: download error, msg ==> " + message + ", task ==> " + appTaskDownloadWrapper);
+            }
+        });
+    }
+
+    private boolean checkFileOccupy(File downloadFile) {
+        return !downloadFile.renameTo(downloadFile);
+    }
+
+    public void update(ArrayList<AppTaskBean> appTaskBeans) {
+        ArrayList<AppTaskDownloadWrapper> appTaskDownloadWrappers = new ArrayList<>();
+        Observable.fromIterable(appTaskBeans)
+                .filter(appTaskBean -> appTaskBean.getTaskStatus() != AppTaskBean.AppTaskStatus.Finish)
+                .map(AppTaskDownloadWrapper::new)
+                .compose(RxSchedulersUtils.observableIO2Main())
+                .subscribe(new Observer<AppTaskDownloadWrapper>() {
+                    @Override
+                    public void onSubscribe(@atmob.io.reactivex.rxjava3.annotations.NonNull Disposable d) {
+                        compositeDisposable.add(d);
+                    }
+
+                    @Override
+                    public void onNext(@atmob.io.reactivex.rxjava3.annotations.NonNull AppTaskDownloadWrapper appTaskDownloadWrapper) {
+                        if (RetrofitDownloader.isActiveTaskExist(appTaskDownloadWrapper.getAppLink())) {
+                            toDownload(appTaskDownloadWrapper);
+                        }
+                        appTaskDownloadWrappers.add(appTaskDownloadWrapper);
+                    }
+
+                    @Override
+                    public void onError(@atmob.io.reactivex.rxjava3.annotations.NonNull Throwable e) {
+                        Log.e(TAG, "onError: AppTaskBean transform to AppTaskDownloadWrapper error", e);
+                    }
+
+                    @Override
+                    public void onComplete() {
+                        Log.d(TAG, "onComplete: app task list update success, total length ==> " + appTaskDownloadWrappers.size());
+                        Collections.reverse(appTaskDownloadWrappers);
+                        asyncListDiffer.submitList(appTaskDownloadWrappers);
+                    }
+                });
+    }
+
+    public void checkAppExist() {
+        Log.d(TAG, "app resume, start checkAppExist()");
+        Completable.fromAction(() -> {
+            Iterator<Map.Entry<Long, String>> iterator = potentialInstalledList.entrySet().iterator();
+            while (iterator.hasNext()) {
+                Map.Entry<Long, String> next = iterator.next();
+                Long appTaskId = next.getKey();
+                String appTaskPackageName = next.getValue();
+                if (TextUtils.isEmpty(appTaskPackageName)) {
+                    iterator.remove();
+                    continue;
+                }
+                boolean existPackage = AppInfoUtils.isExistPackage(Utils.getContext(), appTaskPackageName);
+                if (existPackage) {
+                    if (FORCE_OPEN) {
+                        Log.d(TAG, "checkAppExist: app exist, open it. (id ==> " + appTaskId + ", packageName ==> " + appTaskPackageName + ")");
+                        AppTaskEvent.report(AppTaskEvent.ATE_CONFIRM_INSTALLED_LAUNCH);
+                        AppInfoUtils.launchApp(appTaskPackageName);
+                        if (onItemActionCallback != null) {
+                            onItemActionCallback.updateStatus(appTaskId, AppTaskBean.AppTaskStatus.Opened);
+                        }
+                    } else {
+                        Log.d(TAG, "checkAppExist: app exist, just report. (id ==> " + appTaskId + ", packageName ==> " + appTaskPackageName + ")");
+                        if (onItemActionCallback != null) {
+                            onItemActionCallback.updateStatus(appTaskId, AppTaskBean.AppTaskStatus.Installed);
+                        }
+                    }
+                    iterator.remove();
+                } else {
+                    Log.d(TAG, "checkAppExist: app not exist. (id ==> " + appTaskId + ", packageName ==> " + appTaskPackageName + ")");
+                }
+            }
+        })
+                .subscribeOn(Schedulers.io())
+                .observeOn(Schedulers.io())
+                .subscribe(new CompletableObserver() {
+                    @Override
+                    public void onSubscribe(@atmob.io.reactivex.rxjava3.annotations.NonNull Disposable d) {
+                        compositeDisposable.add(d);
+                    }
+
+                    @Override
+                    public void onComplete() {
+
+                    }
+
+                    @Override
+                    public void onError(@atmob.io.reactivex.rxjava3.annotations.NonNull Throwable e) {
+                        e.printStackTrace();
+                    }
+                });
+    }
+
+    private void addToCheckList(@Nullable AppTaskBean appTaskBeanGet) {
+        if (appTaskBeanGet == null) {
+            return;
+        }
+        potentialInstalledList.put(appTaskBeanGet.getId(), appTaskBeanGet.getAppPkgName());
+        Log.d(TAG, "addToCheckList() called with: appTaskBeanGet = [" + appTaskBeanGet + "]");
+    }
+
+    private void removeFromCheckList(long id) {
+        potentialInstalledList.remove(id);
+        Log.d(TAG, "removeFromCheckList() called with: id = [" + id + "]");
+    }
+
+    public void setOnItemActionCallback(OnItemActionCallback onItemActionCallback) {
+        this.onItemActionCallback = onItemActionCallback;
+    }
+
+    public void release() {
+        RetrofitDownloader.clearAllObserver();
+        appTaskAdapter = null;
+        Log.d(TAG, "release: clear all observer.");
+    }
+
+    public void triggerFirstTask() {
+        for (int i = 0; i < asyncListDiffer.getCurrentList().size(); i++) {
+            AppTaskDownloadWrapper appTaskDownloadWrapper = asyncListDiffer.getCurrentList().get(i);
+            if (appTaskDownloadWrapper == null) {
+                continue;
+            }
+            if (appTaskDownloadWrapper.getTaskStatus() != AppTaskBean.AppTaskStatus.Opened && !appTaskDownloadWrapper.isDownloading()) {
+                onAppTaskItemClick(appTaskDownloadWrapper);
+                Log.d(TAG, "triggerFirstTask: task ==> " + appTaskDownloadWrapper);
+                return;
+            }
+        }
+        Log.d(TAG, "triggerFirstTask: not has available task.");
+    }
+
+    /**
+     * 列表内部通过这个类获取item必要组件
+     */
+    public static class AppTaskItemViewHolder extends RecyclerView.ViewHolder {
+
+        public AppTaskItemViewHolder(@NonNull View itemView) {
+            super(itemView);
+        }
+    }
+
+    public static class AppIconConfig {
+
+        /**
+         * 默认图标
+         */
+        @IntegerRes
+        private int placeholderResId;
+
+        /**
+         * 图标的圆角 in dip
+         */
+        private int round;
+
+        @SuppressWarnings("ResourceType")
+        public AppIconConfig() {
+            placeholderResId = ResUtils.getRes("atmob_default_icon", "drawable");
+            round = 0;
+        }
+
+        public AppIconConfig placeholderResId(int placeholderResId) {
+            this.placeholderResId = placeholderResId;
+            return this;
+        }
+
+        public AppIconConfig round(int round) {
+            this.round = round;
+            return this;
+        }
+    }
+
+    /**
+     * 用于构建积分墙item, see{@link AppTaskItemViewHolder}
+     */
+    public interface ViewHolderCreator {
+        AppTaskItemViewHolder onCreateViewHolder(LayoutInflater layoutInflater, ViewGroup parent);
+    }
+
+    private static class DiffCallback extends DiffUtil.ItemCallback<AppTaskDownloadWrapper> {
+
+        @Override
+        public boolean areItemsTheSame(@NonNull AppTaskDownloadWrapper oldItem, @NonNull AppTaskDownloadWrapper newItem) {
+            if (oldItem.getId() == 0) {
+                return oldItem.getAppPkgName().equals(newItem.getAppPkgName());
+            }
+            return oldItem.getId() == newItem.getId();
+        }
+
+        @Override
+        public boolean areContentsTheSame(@NonNull AppTaskDownloadWrapper oldItem, @NonNull AppTaskDownloadWrapper newItem) {
+            if (TextUtils.isEmpty(oldItem.getAppName()) || TextUtils.isEmpty(newItem.getAppName()) || !oldItem.getAppName().equals(newItem.getAppName())) {
+                return false;
+            }
+            if (oldItem.getTaskStatus() != newItem.getTaskStatus()) {
+                return false;
+            }
+            if (oldItem.isDownloading() != newItem.isDownloading()) {
+                return false;
+            }
+            return oldItem.getProgress() == newItem.getProgress() && oldItem.getTotal() == newItem.getTotal();
+        }
+
+        @Nullable
+        @org.jetbrains.annotations.Nullable
+        @Override
+        public Object getChangePayload(@NonNull AppTaskDownloadWrapper oldItem, @NonNull AppTaskDownloadWrapper newItem) {
+            Bundle bundle = new Bundle();
+            if (TextUtils.isEmpty(oldItem.getAppName()) || TextUtils.isEmpty(newItem.getAppName()) || !oldItem.getAppName().equals(newItem.getAppName())) {
+                bundle.putString("payload_app_name", newItem.getAppName());
+            }
+            if (oldItem.getTaskStatus() != newItem.getTaskStatus()) {
+                bundle.putInt("payload_task_status", newItem.getTaskStatus());
+            }
+            if (oldItem.isDownloading() != newItem.isDownloading()) {
+                bundle.putBoolean("payload_downloading", newItem.isDownloading());
+            }
+            if (oldItem.getProgress() != newItem.getProgress() || oldItem.getTotal() != newItem.getTotal()) {
+                bundle.putLong("payload_progress", newItem.getProgress());
+                bundle.putLong("payload_total", newItem.getTotal());
+            }
+            if (bundle.isEmpty()) {
+                return null;
+            }
+            return bundle;
+        }
+    }
+
+    public interface OnItemActionCallback {
+        void onClick(AppTaskBean appTaskBean);
+
+        void updateStatus(long id, int targetStatus);
+
+        void getReward(AppTaskBean appTaskBean);
+    }
+}

+ 46 - 0
src/main/java/com/atmob/task/bean/AdAppInfoData.java

@@ -0,0 +1,46 @@
+package com.atmob.task.bean;
+
+import androidx.annotation.NonNull;
+
+public class AdAppInfoData {
+
+    @NonNull
+    private String packageName;
+
+    private String apkPath;
+
+    private String appName;
+
+    public AdAppInfoData() {
+    }
+
+    public AdAppInfoData(@NonNull String packageName, String apkPath, String appName) {
+        this.packageName = packageName;
+        this.apkPath = apkPath;
+        this.appName = appName;
+    }
+
+    public String getPackageName() {
+        return packageName;
+    }
+
+    public void setPackageName(String packageName) {
+        this.packageName = packageName;
+    }
+
+    public String getApkPath() {
+        return apkPath;
+    }
+
+    public void setApkPath(String apkPath) {
+        this.apkPath = apkPath;
+    }
+
+    public String getAppName() {
+        return appName;
+    }
+
+    public void setAppName(String appName) {
+        this.appName = appName;
+    }
+}

+ 110 - 0
src/main/java/com/atmob/task/bean/AppTaskBean.java

@@ -0,0 +1,110 @@
+package com.atmob.task.bean;
+
+import android.text.TextUtils;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+
+public class AppTaskBean {
+    @IntDef({AppTaskStatus.Initial, AppTaskStatus.Installed, AppTaskStatus.Opened, AppTaskStatus.Finish})
+    public @interface AppTaskStatus {
+        int Initial = 1;
+        int Installed = 4;
+        int Opened = 5;
+        int Finish = 3;
+    }
+
+    public static String CDN;
+    private long id;
+    private String appIcon;
+    private String appPkgName;
+    private String appLink;
+    private String appName;
+    private int taskStatus;
+    private float rewardRatio;
+
+    public long getId() {
+        return id;
+    }
+
+    public void setId(long id) {
+        this.id = id;
+    }
+
+    public String getAppIcon() {
+        if (!TextUtils.isEmpty(appIcon) && appIcon.startsWith("http")) {
+            return appIcon;
+        }
+        return CDN + appIcon;
+    }
+
+    public void setAppIcon(String appIcon) {
+        this.appIcon = appIcon;
+    }
+
+    public String getAppPkgName() {
+        return appPkgName;
+    }
+
+    public void setAppPkgName(String appPkgName) {
+        this.appPkgName = appPkgName;
+    }
+
+    public String getAppLink() {
+        return appLink;
+    }
+
+    public void setAppLink(String appLink) {
+        this.appLink = appLink;
+    }
+
+    public String getAppName() {
+        return appName;
+    }
+
+    public void setAppName(String appName) {
+        this.appName = appName;
+    }
+
+    @AppTaskStatus
+    public int getTaskStatus() {
+        return taskStatus;
+    }
+
+    public void setTaskStatus(int taskStatus) {
+        this.taskStatus = taskStatus;
+    }
+
+    public float getRewardRatio() {
+        return rewardRatio;
+    }
+
+    public void setRewardRatio(float rewardRatio) {
+        this.rewardRatio = rewardRatio;
+    }
+
+    @Override
+    public boolean equals(@Nullable @org.jetbrains.annotations.Nullable Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        AppTaskBean appTaskBean = obj instanceof AppTaskBean ? ((AppTaskBean) obj) : null;
+        if (appTaskBean == null) {
+            return false;
+        }
+        if (appTaskBean.getId() == 0 || this.getId() == 0) {
+            return appTaskBean.getAppPkgName().equals(this.getAppPkgName());
+        }
+        return appTaskBean.getId() == this.getId();
+    }
+
+    @Override
+    public String toString() {
+        return "AppTaskBean{" +
+                "id=" + id +
+                ", appPkgName='" + appPkgName + '\'' +
+                ", appName='" + appName + '\'' +
+                ", taskStatus=" + taskStatus +
+                '}';
+    }
+}

+ 46 - 0
src/main/java/com/atmob/task/bean/AppTaskControlResponse.java

@@ -0,0 +1,46 @@
+package com.atmob.task.bean;
+
+public class AppTaskControlResponse {
+
+    private Info pp;
+
+    public Info getPp() {
+        return pp;
+    }
+
+    public void setPp(Info pp) {
+        this.pp = pp;
+    }
+
+    public static class Info {
+        private boolean pop;
+
+        private int popIndex;
+
+        private int popInterval;
+
+        public boolean isPop() {
+            return pop;
+        }
+
+        public void setPop(boolean pop) {
+            this.pop = pop;
+        }
+
+        public int getPopIndex() {
+            return popIndex;
+        }
+
+        public void setPopIndex(int popIndex) {
+            this.popIndex = popIndex;
+        }
+
+        public int getPopInterval() {
+            return popInterval;
+        }
+
+        public void setPopInterval(int popInterval) {
+            this.popInterval = popInterval;
+        }
+    }
+}

+ 25 - 0
src/main/java/com/atmob/task/bean/AppTaskDataResponse.java

@@ -0,0 +1,25 @@
+package com.atmob.task.bean;
+
+import java.util.ArrayList;
+
+public class AppTaskDataResponse {
+    private String url;
+
+    private ArrayList<AppTaskBean> ptasks;
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public ArrayList<AppTaskBean> getPtasks() {
+        return ptasks;
+    }
+
+    public void setPtasks(ArrayList<AppTaskBean> ptasks) {
+        this.ptasks = ptasks;
+    }
+}

+ 46 - 0
src/main/java/com/atmob/task/bean/AppTaskDownloadWrapper.java

@@ -0,0 +1,46 @@
+package com.atmob.task.bean;
+
+public class AppTaskDownloadWrapper extends AppTaskBean {
+    private boolean isDownloading;
+
+    private long progress;
+
+    private long total;
+
+    public AppTaskDownloadWrapper(AppTaskBean appTaskBean) {
+        if (appTaskBean == null) {
+            return;
+        }
+        setTaskStatus(appTaskBean.getTaskStatus());
+        setAppName(appTaskBean.getAppName());
+        setAppPkgName(appTaskBean.getAppPkgName());
+        setAppIcon(appTaskBean.getAppIcon());
+        setAppLink(appTaskBean.getAppLink());
+        setId(appTaskBean.getId());
+        setRewardRatio(appTaskBean.getRewardRatio());
+    }
+
+    public boolean isDownloading() {
+        return isDownloading;
+    }
+
+    public void setDownloading(boolean downloading) {
+        isDownloading = downloading;
+    }
+
+    public long getProgress() {
+        return progress;
+    }
+
+    public void setProgress(long progress) {
+        this.progress = progress;
+    }
+
+    public long getTotal() {
+        return total;
+    }
+
+    public void setTotal(long total) {
+        this.total = total;
+    }
+}

+ 40 - 0
src/main/java/com/atmob/task/bean/AppTaskUpdateRequest.java

@@ -0,0 +1,40 @@
+package com.atmob.task.bean;
+
+import com.atmob.request.CommonBaseRequest;
+
+public class AppTaskUpdateRequest extends CommonBaseRequest {
+    private long id;
+
+    private int taskStatus;
+
+    private int taskType = 0;
+
+    public AppTaskUpdateRequest(long id, int taskStatus) {
+        this.id = id;
+        this.taskStatus = taskStatus;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public void setId(long id) {
+        this.id = id;
+    }
+
+    public int getTaskStatus() {
+        return taskStatus;
+    }
+
+    public void setTaskStatus(int taskStatus) {
+        this.taskStatus = taskStatus;
+    }
+
+    public int getTaskType() {
+        return taskType;
+    }
+
+    public void setTaskType(int taskType) {
+        this.taskType = taskType;
+    }
+}

+ 13 - 0
src/main/java/com/atmob/task/bean/AppTaskUpdateResponse.java

@@ -0,0 +1,13 @@
+package com.atmob.task.bean;
+
+public class AppTaskUpdateResponse {
+    private String rdn;
+
+    public String getRdn() {
+        return rdn;
+    }
+
+    public void setRdn(String rdn) {
+        this.rdn = rdn;
+    }
+}

+ 31 - 0
src/main/java/com/atmob/task/data/Api.java

@@ -0,0 +1,31 @@
+package com.atmob.task.data;
+
+import com.atmob.request.CommonBaseRequest;
+import com.atmob.request.EventRequest;
+import com.atmob.response.BaseResponse;
+import com.atmob.task.bean.AppTaskControlResponse;
+import com.atmob.task.bean.AppTaskDataResponse;
+import com.atmob.task.bean.AppTaskUpdateRequest;
+import com.atmob.task.bean.AppTaskUpdateResponse;
+
+import atmob.io.reactivex.rxjava3.core.Observable;
+import atmob.retrofit2.http.Body;
+import atmob.retrofit2.http.POST;
+
+public interface Api {
+    //埋点事件上报
+    @POST("/v8ds/v1/app/event/report")
+    Observable<BaseResponse<Object>> eventReport(@Body EventRequest request);
+
+    //积分墙数据
+    @POST("/jfq/v2/welfare/index")
+    Observable<BaseResponse<AppTaskDataResponse>> loadAppTask(@Body CommonBaseRequest request);
+
+    //积分墙任务状态更新
+    @POST("/jfq/v1/adresource/task/update")
+    Observable<BaseResponse<AppTaskUpdateResponse>> updateAppTaskStatus(@Body AppTaskUpdateRequest request);
+
+    //是否有积分墙任务
+    @POST("/v8ds/v1/skin/pop")
+    Observable<BaseResponse<AppTaskControlResponse>> appTaskControlInfo(@Body CommonBaseRequest request);
+}

+ 15 - 0
src/main/java/com/atmob/task/data/LocalData.java

@@ -0,0 +1,15 @@
+package com.atmob.task.data;
+
+import com.atmob.task.bean.AdAppInfoData;
+import com.atmob.utils.MMKVUtils;
+import com.google.gson.Gson;
+
+public class LocalData {
+
+    private static final Gson gson = new Gson();
+
+    public static AdAppInfoData queryAdAppInfoByPackageName(String packageName) {
+        String key = String.format("adAppInfoData_%s", packageName);
+        return new Gson().fromJson(MMKVUtils.getString(key, ""), AdAppInfoData.class);
+    }
+}

+ 9 - 0
src/main/java/com/atmob/task/data/NetworkClient.java

@@ -0,0 +1,9 @@
+package com.atmob.task.data;
+
+import com.atmob.http.RetrofitClient;
+
+public class NetworkClient {
+    public static Api api() {
+        return RetrofitClient.getInstance().create(Api.class);
+    }
+}

+ 69 - 0
src/main/java/com/atmob/task/utils/AppTaskEvent.java

@@ -0,0 +1,69 @@
+package com.atmob.task.utils;
+
+import androidx.annotation.IntDef;
+
+import com.atmob.http.BaseHttpObserver;
+import com.atmob.request.EventRequest;
+import com.atmob.task.data.NetworkClient;
+import com.atmob.utils.AdLog;
+import com.atmob.utils.RxSchedulersUtils;
+
+import atmob.io.reactivex.rxjava3.disposables.Disposable;
+
+public class AppTaskEvent {
+
+    //JFQ-加载积分墙列表	1090200
+    public static final int ATE_LOAD_DATA = 1090200;
+    //JFQ-加载积分墙列表-成功	1090201
+    public static final int ATE_LOAD_DATA_SUCCESS = 1090201;
+    //JFQ-点击立即体验	1090202
+    public static final int ATE_CLICK_ITEM_INITIAL = 1090202;
+    //JFQ-开始下载应用(未下载)	1090203
+    public static final int ATE_DOWNLOAD = 1090203;
+    //JFQ-下载应用成功	1090204
+    public static final int ATE_DOWNLOAD_SUCCESS = 1090204;
+    //JFQ-打开安装流程(已下载未安装)	1090205
+    public static final int ATE_INSTALL = 1090205;
+    //JFQ-打开应用(已安装)	1090206
+    public static final int ATE_LAUNCH = 1090206;
+    //JFQ-二次打开应用(安装成功)	1090207
+    public static final int ATE_CONFIRM_INSTALLED_LAUNCH = 1090207;
+    //JFQ-立即领取	1090208
+    public static final int ATE_CLICK_ITEM_REWARD = 1090208;
+
+    @IntDef({
+            ATE_LOAD_DATA,
+            ATE_LOAD_DATA_SUCCESS,
+            ATE_CLICK_ITEM_INITIAL,
+            ATE_DOWNLOAD,
+            ATE_DOWNLOAD_SUCCESS,
+            ATE_INSTALL,
+            ATE_LAUNCH,
+            ATE_CONFIRM_INSTALLED_LAUNCH,
+            ATE_CLICK_ITEM_REWARD,
+    })
+    public @interface ID {
+    }
+
+    public static void report(@ID int eventId) {
+        NetworkClient.api().eventReport(new EventRequest(String.valueOf(eventId)))
+                .compose(RxSchedulersUtils.observableIOOnly())
+                .subscribe(new BaseHttpObserver<Object>() {
+
+                    @Override
+                    public void onGotDisposable(Disposable disposable) {
+
+                    }
+
+                    @Override
+                    public void onSuccess(Object data) {
+                        AdLog.d("AppTaskEvent", "report success, eventId ==> " + eventId);
+                    }
+
+                    @Override
+                    public void onFailed(int code, String msg) {
+                        AdLog.d("AppTaskEvent", "report failed, eventId ==> " + eventId);
+                    }
+                });
+    }
+}

+ 62 - 0
src/main/java/com/atmob/task/utils/AppTaskHandler.java

@@ -0,0 +1,62 @@
+package com.atmob.task.utils;
+
+import android.util.Log;
+
+import com.atmob.data.AdInjection;
+import com.atmob.http.BaseHttpObserver;
+import com.atmob.request.CommonBaseRequest;
+import com.atmob.response.AppTaskControlResponse;
+import com.atmob.utils.RxSchedulersUtils;
+
+import atmob.io.reactivex.rxjava3.disposables.CompositeDisposable;
+import atmob.io.reactivex.rxjava3.disposables.Disposable;
+
+/**
+ * 提供一些关于积分墙的静态方法
+ */
+public class AppTaskHandler {
+
+    private static final String TAG = AppTaskHandler.class.getSimpleName();
+
+    /**
+     * 异步查询是否有积分墙任务
+     *
+     * @param onTaskResultListener 查询结果的回调
+     * @return 防止出现内存泄漏, 外部自行管理disposable, 这里偷懒了为了使用封装的 {@link BaseHttpObserver}, 应该返回Disposable最优 - -
+     */
+    public static CompositeDisposable hasTask(OnTaskResultListener onTaskResultListener) {
+        CompositeDisposable compositeDisposable = new CompositeDisposable();
+        if (onTaskResultListener == null) {
+            return compositeDisposable;
+        }
+        AdInjection.provideRepository().appTaskControlInfo(new CommonBaseRequest())
+                .compose(RxSchedulersUtils.observableIO2Main())
+                .subscribe(new BaseHttpObserver<AppTaskControlResponse>() {
+
+                    @Override
+                    public void onGotDisposable(Disposable disposable) {
+                        compositeDisposable.add(disposable);
+                    }
+
+                    @Override
+                    public void onSuccess(AppTaskControlResponse data) {
+                        if (data == null) {
+                            onTaskResultListener.onResult(false);
+                            return;
+                        }
+                        AppTaskControlResponse.Info pp = data.getPp();
+                        onTaskResultListener.onResult(pp.isPop());
+                    }
+
+                    @Override
+                    public void onFailed(int code, String msg) {
+                        Log.e(TAG, "onFailed: appTaskControlInfo api failed, code ==> " + code + ", msg ==> " + msg);
+                    }
+                });
+        return compositeDisposable;
+    }
+
+    public interface OnTaskResultListener {
+        void onResult(boolean hasTask);
+    }
+}

+ 68 - 0
src/main/java/com/atmob/task/utils/InstallFaultToleranceUtils.java

@@ -0,0 +1,68 @@
+package com.atmob.task.utils;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+
+import androidx.core.content.FileProvider;
+
+import com.atmob.utils.MMKVUtils;
+
+import java.io.File;
+
+/**
+ * a simple fault tolerance, aim to handle installation fault cause by apk broken
+ * <p>
+ * just record installation times of apk file in same path and same file name
+ * <p>
+ * if that times more than a special number, then delete the file and notify to download
+ */
+public class InstallFaultToleranceUtils {
+    private static final String MMKV_PREFIX = "IFTU_";
+
+    private static final int MAX_ALLOW_INSTALL_TIMES = 2;
+
+    public static void installApk(Context context, File apkFile, OnFileDeleteCallback onFileDeleteCallback) {
+        String apkFilePath = apkFile.getPath();
+        int installTimes = MMKVUtils.getInt(MMKV_PREFIX + apkFilePath, 0);
+        if (installTimes >= MAX_ALLOW_INSTALL_TIMES) {
+            apkFile.delete();
+            if (onFileDeleteCallback != null) {
+                onFileDeleteCallback.call();
+            }
+            MMKVUtils.remove(MMKV_PREFIX + apkFilePath);
+        } else {
+            if (onFileDeleteCallback != null) {
+                AppTaskEvent.report(AppTaskEvent.ATE_INSTALL);
+            }
+            toInstall(context, apkFile);
+            MMKVUtils.putInt(MMKV_PREFIX + apkFilePath, ++installTimes);
+        }
+    }
+
+    private static void toInstall(Context context, File apkFile) {
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        try {
+            String[] command = {"chmod", "777", apkFile.getAbsolutePath()};
+            ProcessBuilder builder = new ProcessBuilder(command);
+            builder.start();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            Uri contentUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", apkFile);
+            intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
+        } else {
+            intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        }
+        context.startActivity(intent);
+    }
+
+    public interface OnFileDeleteCallback {
+        void call();
+    }
+}

+ 309 - 0
src/main/java/com/atmob/task/utils/RetrofitDownloader.java

@@ -0,0 +1,309 @@
+package com.atmob.task.utils;
+
+import android.text.TextUtils;
+
+import androidx.annotation.IntDef;
+
+import com.atmob.utils.RxSchedulersUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import atmob.io.reactivex.rxjava3.annotations.NonNull;
+import atmob.io.reactivex.rxjava3.core.Observable;
+import atmob.io.reactivex.rxjava3.core.ObservableEmitter;
+import atmob.io.reactivex.rxjava3.core.ObservableOnSubscribe;
+import atmob.io.reactivex.rxjava3.core.ObservableSource;
+import atmob.io.reactivex.rxjava3.core.Observer;
+import atmob.io.reactivex.rxjava3.disposables.CompositeDisposable;
+import atmob.io.reactivex.rxjava3.disposables.Disposable;
+import atmob.io.reactivex.rxjava3.functions.Function;
+import atmob.okhttp3.OkHttpClient;
+import atmob.okhttp3.ResponseBody;
+import atmob.retrofit2.Retrofit;
+import atmob.retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory;
+import atmob.retrofit2.http.GET;
+import atmob.retrofit2.http.Streaming;
+import atmob.retrofit2.http.Url;
+
+/**
+ * 下载器, 目前只用于积分墙, 页面退出不会暂停下载, 只在最后一次刷新积分墙列表的页面上显示进度
+ */
+public class RetrofitDownloader {
+
+    private static Retrofit retrofit;
+
+    private static CompositeDisposable compositeDisposable;
+
+    private static final HashMap<String, DownloadTask> downloadTasks = new HashMap<>();
+
+    private synchronized static Retrofit getRetrofit() {
+        if (retrofit == null) {
+            buildNetWork();
+        }
+        return retrofit;
+    }
+
+    public static void download(String url, String dirPath, String fileName, ProgressObserver progressObserver) {
+        if (!checkDownloadParam(url, dirPath, fileName)) {
+            progressObserver.onError("illegal params.");
+            return;
+        }
+
+        DownloadTask downloadTask = downloadTasks.get(url);
+        if (downloadTask != null) {
+            boolean continueDownload = false;
+            switch (downloadTask.taskStatus) {
+                case DownloadStatus.Initial:
+                case DownloadStatus.Progressing:
+                    progressObserver.onStart();
+                    downloadTask.observer = progressObserver;
+                    break;
+                case DownloadStatus.Done:
+                    File file = new File(downloadTask.targetPath);
+                    if (file.exists()) {
+                        progressObserver.onComplete(file);
+                    } else {
+                        continueDownload = true;
+                    }
+                    break;
+                case DownloadStatus.Error:
+                    continueDownload = true;
+                    break;
+            }
+            if (!continueDownload) {
+                return;
+            }
+        }
+
+        String targetFilePath = new File(dirPath, fileName).getPath();
+        DownloadTask newTask = new DownloadTask();
+        newTask.observer = progressObserver;
+        newTask.taskStatus = DownloadStatus.Initial;
+        newTask.targetPath = targetFilePath;
+        downloadTasks.put(url, newTask);
+
+        getRetrofit().create(ApiService.class)
+                .download(url)
+                .compose(RxSchedulersUtils.observableIOOnly())
+                .flatMap((Function<ResponseBody, ObservableSource<DownloadProgress>>) responseBody ->
+                        Observable.create((ObservableOnSubscribe<DownloadProgress>) emitter -> doSave(targetFilePath, emitter, responseBody)))
+                .compose(RxSchedulersUtils.observableIO2Main())
+                .subscribe(new Observer<DownloadProgress>() {
+                    @Override
+                    public void onSubscribe(@NonNull Disposable d) {
+                        addSubscribe(d);
+                        if (newTask.observer != null) {
+                            newTask.observer.onStart();
+                        }
+                    }
+
+                    @Override
+                    public void onNext(@NonNull DownloadProgress downloadProgress) {
+                        if (newTask.observer != null) {
+                            newTask.observer.onProgress(downloadProgress.total, downloadProgress.progress);
+                        }
+                        newTask.taskStatus = DownloadStatus.Progressing;
+                    }
+
+                    @Override
+                    public void onError(@NonNull Throwable e) {
+                        if (newTask.observer != null) {
+                            newTask.observer.onError(e.getMessage());
+                        }
+                        newTask.taskStatus = DownloadStatus.Error;
+                    }
+
+                    @Override
+                    public void onComplete() {
+                        if (newTask.observer != null) {
+                            newTask.observer.onComplete(new File(targetFilePath));
+                        }
+                        newTask.taskStatus = DownloadStatus.Done;
+                    }
+                });
+    }
+
+    public static boolean isActiveTaskExist(String url) {
+        DownloadTask downloadTask = downloadTasks.get(url);
+        if (downloadTask == null) {
+            return false;
+        }
+        return downloadTask.taskStatus == DownloadStatus.Progressing;
+    }
+
+    public static void clearObserver(String url) {
+        DownloadTask downloadTask = downloadTasks.get(url);
+        if (downloadTask != null) {
+            downloadTask.observer = null;
+        }
+    }
+
+    private static boolean checkDownloadParam(String url, String dirPath, String fileName) {
+        return !(TextUtils.isEmpty(url) || TextUtils.isEmpty(dirPath) || TextUtils.isEmpty(fileName));
+    }
+
+    private static void doSave(String filePath, ObservableEmitter<DownloadProgress> emitter, ResponseBody responseBody) {
+        File file = new File(filePath);
+        if (!checkPath(file, emitter)) {
+            return;
+        }
+        long total = responseBody.contentLength();
+        long progress = 0;
+        int len;
+        InputStream inputStream = responseBody.byteStream();
+        byte[] bytes = new byte[1024 * 2];
+        FileOutputStream fileOutputStream = null;
+        try {
+            fileOutputStream = new FileOutputStream(file);
+            while ((len = inputStream.read(bytes)) != -1) {
+                fileOutputStream.write(bytes, 0, len);
+                progress += len;
+                if (emitter != null && !emitter.isDisposed()) {
+                    emitter.onNext(new DownloadProgress(total, progress));
+                }
+            }
+            fileOutputStream.flush();
+        } catch (IOException e) {
+            e.printStackTrace();
+            if (emitter != null && !emitter.isDisposed()) {
+                emitter.onError(e);
+            }
+        } finally {
+            try {
+                if (fileOutputStream != null) {
+                    fileOutputStream.close();
+                }
+                inputStream.close();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            checkDownloadFinish(file, total, emitter);
+        }
+    }
+
+    private static void checkDownloadFinish(File file, long total, ObservableEmitter<DownloadProgress> emitter) {
+        if (file.exists() && file.isFile() && file.length() == total) {
+            if (emitter != null && !emitter.isDisposed()) {
+                emitter.onComplete();
+            }
+        } else {
+            boolean exists = file.exists();
+            boolean delete = false;
+            if (exists) {
+                delete = file.delete();
+            }
+            if (emitter != null && !emitter.isDisposed()) {
+                emitter.onError(new Exception("download finish but the file is not complete, file exists ==> " + exists + ", delete ==> " + delete));
+            }
+        }
+    }
+
+    private static boolean checkPath(File file, ObservableEmitter<DownloadProgress> emitter) {
+        if (file.exists()) {
+            boolean delete = file.delete();
+            if (!delete) {
+                if (emitter != null && !emitter.isDisposed()) {
+                    emitter.onError(new Exception("file is exists, delete failed"));
+                }
+                return false;
+            }
+        }
+        try {
+            boolean newFile = file.createNewFile();
+            if (!newFile) {
+                if (emitter != null && !emitter.isDisposed()) {
+                    emitter.onError(new Exception("create file failed"));
+                }
+            }
+            return newFile;
+        } catch (IOException e) {
+            if (emitter != null && !emitter.isDisposed()) {
+                emitter.onError(e);
+            }
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    private static void buildNetWork() {
+        OkHttpClient okHttpClient = new OkHttpClient.Builder()
+                .connectTimeout(20, TimeUnit.SECONDS)
+                .build();
+
+        retrofit = new Retrofit.Builder()
+                .client(okHttpClient)
+                .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
+                .baseUrl("http://www.baidu.com")
+                .build();
+    }
+
+    private static void addSubscribe(Disposable disposable) {
+        if (compositeDisposable == null) {
+            compositeDisposable = new CompositeDisposable();
+        }
+        compositeDisposable.add(disposable);
+    }
+
+    public static void cancelAll() {
+        if (compositeDisposable != null) {
+            compositeDisposable.clear();
+        }
+        clearAllObserver();
+        downloadTasks.clear();
+    }
+
+    public static void clearAllObserver() {
+        for (Map.Entry<String, DownloadTask> next : downloadTasks.entrySet()) {
+            String url = next.getKey();
+            clearObserver(url);
+        }
+    }
+
+    private interface ApiService {
+        @Streaming
+        @GET
+        Observable<ResponseBody> download(@Url String url);
+    }
+
+    public interface ProgressObserver {
+        void onStart();
+
+        void onProgress(long total, long progress);
+
+        void onComplete(File file);
+
+        void onError(String message);
+    }
+
+    public static class DownloadProgress {
+        private final long total;
+
+        private final long progress;
+
+        public DownloadProgress(long total, long progress) {
+            this.total = total;
+            this.progress = progress;
+        }
+    }
+
+    public static class DownloadTask {
+        @DownloadStatus
+        private int taskStatus = DownloadStatus.Initial;
+        private ProgressObserver observer;
+        private String targetPath;
+    }
+
+    @IntDef({DownloadStatus.Initial, DownloadStatus.Progressing, DownloadStatus.Error, DownloadStatus.Done})
+    public @interface DownloadStatus {
+        int Initial = 1;
+        int Progressing = 2;
+        int Error = 3;
+        int Done = 4;
+    }
+}

+ 93 - 0
src/main/java/com/atmob/task/view/TwinklingRecyclerView.java

@@ -0,0 +1,93 @@
+package com.atmob.task.view;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class TwinklingRecyclerView extends RecyclerView {
+
+    private Paint paint;
+
+    private boolean twinkling;
+    private ValueAnimator valueAnimator;
+
+    public TwinklingRecyclerView(@NonNull Context context) {
+        this(context, null);
+    }
+
+    public TwinklingRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public TwinklingRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init();
+    }
+
+    private void init() {
+        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        paint.setColor(Color.parseColor("#ccff7c7c"));
+        addItemDecoration(new ItemDecoration() {
+            @Override
+            public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
+                super.onDrawOver(c, parent, state);
+                if (!twinkling) {
+                    return;
+                }
+                for (int i = 0; i < parent.getChildCount(); i++) {
+                    View childAt = parent.getChildAt(i);
+                    int left = childAt.getLeft();
+                    int top = childAt.getTop();
+                    int right = childAt.getRight();
+                    int bottom = childAt.getBottom();
+                    c.drawRoundRect(left, top, right, bottom, 10, 10, paint);
+                }
+            }
+        });
+    }
+
+    public void twinkling() {
+        if (valueAnimator == null) {
+            valueAnimator = new ValueAnimator();
+            valueAnimator.setFloatValues(0, 1);
+            valueAnimator.setRepeatCount(2);
+            valueAnimator.setRepeatMode(ValueAnimator.RESTART);
+            valueAnimator.setDuration(500);
+            valueAnimator.addUpdateListener(animation -> {
+                float value = (float) animation.getAnimatedValue();
+                twinkling = value > 0.5;
+                invalidate();
+            });
+            valueAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    super.onAnimationEnd(animation);
+                    twinkling = false;
+                    invalidate();
+                }
+            });
+        }
+        if (valueAnimator.isRunning()) {
+            valueAnimator.cancel();
+        }
+        valueAnimator.start();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (valueAnimator != null) {
+            valueAnimator.cancel();
+        }
+    }
+}