Browse Source

配置项目

zk 1 year ago
parent
commit
704f1574e5

+ 5 - 1
app/build.gradle

@@ -93,6 +93,7 @@ dependencies {
     implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
 
     //Atmob SDK
+    implementation 'extra.common:base:1.0.0-SNAPSHOT'
     implementation "extra.pack:channel:1.1.0-SNAPSHOT"
     implementation "extra.common:core:2.0.3-SNAPSHOT" //base utils
     implementation "extra.common:network:1.1.0-SNAPSHOT"
@@ -101,7 +102,10 @@ dependencies {
     implementation("plus.pay:pay:1.1.0-SNAPSHOT") {
         exclude group: 'third.pay', module: 'ali'
     }
-    implementation "extra.common:oaid:1.1.0-SNAPSHOT" //oaid
+    api 'com.alipay.sdk:alipaysdk-android:15.8.16@aar'
+
+    //oaid
+    implementation "extra.common:oaid:1.1.0-SNAPSHOT"
 
     //AppCompat
     implementation "androidx.appcompat:appcompat:$rootProject.appcompat_version"

+ 7 - 1
app/src/main/AndroidManifest.xml

@@ -3,6 +3,9 @@
     xmlns:tools="http://schemas.android.com/tools"
     package="com.datarecovery.my.master">
 
+    <uses-permission android:name="android.permission.INTERNET" />
+
+
     <application
         android:allowBackup="true"
         android:dataExtractionRules="@xml/data_extraction_rules"
@@ -13,6 +16,9 @@
         android:supportsRtl="true"
         tools:replace="android:allowBackup"
         android:theme="@style/Theme.DataRecover"
-        tools:targetApi="31" />
+        tools:targetApi="31">
+
+
+    </application>
 
 </manifest>

+ 42 - 0
app/src/main/java/com/datarecovery/my/master/App.java

@@ -0,0 +1,42 @@
+package com.datarecovery.my.master;
+
+import com.atmob.app.lib.base.BaseApplication;
+import com.atmob.user.AtmobUser;
+import com.datarecovery.my.master.data.consts.Constants;
+
+public class App extends BaseApplication {
+    @Override
+    protected boolean isDebug() {
+        return BuildConfig.DEBUG;
+    }
+
+    @Override
+    protected String defaultChannel() {
+        return Constants.App_DefaultChannel;
+    }
+
+    @Override
+    protected int defaultAppId() {
+        return Constants.App_DefaultAppId;
+    }
+
+    @Override
+    protected int defaultTgPlatformId() {
+        return Constants.App_DefaultTgPlatformId;
+    }
+
+    @Override
+    protected int complianceStrategy() {
+        return AtmobUser.CHINA;
+    }
+
+    @Override
+    protected void initCommon(boolean isMainProcess) {
+
+    }
+
+    @Override
+    public void initAfterGrant(boolean isMainProcess) {
+
+    }
+}

+ 7 - 0
app/src/main/java/com/datarecovery/my/master/data/api/AtmobApi.java

@@ -0,0 +1,7 @@
+package com.datarecovery.my.master.data.api;
+
+
+public interface AtmobApi {
+
+
+}

+ 22 - 0
app/src/main/java/com/datarecovery/my/master/data/consts/Constants.java

@@ -0,0 +1,22 @@
+package com.datarecovery.my.master.data.consts;
+
+import com.datarecovery.my.master.BuildConfig;
+
+public class Constants {
+
+
+    private static final String Atmob_Server_Base_URL_LOCAL = "http://192.168.10.68:56389";
+    public static final String Atmob_Server_Base_URL_REMOTE = BuildConfig.HOST;
+    public static final String Atmob_Server_Base_URL = BuildConfig.isLocalNetwork ? Atmob_Server_Base_URL_LOCAL : Atmob_Server_Base_URL_REMOTE;
+
+
+    public static final String App_DefaultChannel = "Android";
+    public static final int App_DefaultAppId = 0;
+    public static final int App_DefaultTgPlatformId = 0;
+
+    public static final String PRIVACY_POLICY = "https://cdn.v8dashen.com/manyue/static/zrdwzs-my-privacy.html";
+
+    public static final String USER_AGREEMENT = "https://cdn.v8dashen.com/manyue/static/zrdwzs-my-clause.html";
+
+
+}

+ 24 - 0
app/src/main/java/com/datarecovery/my/master/di/GsonModule.java

@@ -0,0 +1,24 @@
+package com.datarecovery.my.master.di;
+
+import com.google.gson.Gson;
+
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+import dagger.hilt.InstallIn;
+import dagger.hilt.components.SingletonComponent;
+
+/**
+ * 提供全局单例Gson对象, 充分利用其内部缓存机制, 提高序列化效率
+ */
+@InstallIn(SingletonComponent.class)
+@Module
+public class GsonModule {
+
+    @Provides
+    @Singleton
+    public static Gson provideGson() {
+        return new Gson();
+    }
+}

+ 38 - 0
app/src/main/java/com/datarecovery/my/master/di/NetworkModule.java

@@ -0,0 +1,38 @@
+package com.datarecovery.my.master.di;
+
+import com.atmob.network.okhttp.AtmobOkHttpClient;
+import com.datarecovery.my.master.BuildConfig;
+import com.datarecovery.my.master.data.api.AtmobApi;
+import com.datarecovery.my.master.data.consts.Constants;
+import com.google.gson.Gson;
+
+import javax.inject.Singleton;
+
+import atmob.okhttp3.OkHttpClient;
+import atmob.retrofit2.Retrofit;
+import atmob.retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory;
+import atmob.retrofit2.converter.gson.GsonConverterFactory;
+import dagger.Module;
+import dagger.Provides;
+import dagger.hilt.InstallIn;
+import dagger.hilt.components.SingletonComponent;
+
+@Module
+@InstallIn(SingletonComponent.class)
+public class NetworkModule {
+    private static final String ATMOB_TAG = "AtmobApi";
+
+    @Singleton
+    @Provides
+    public static AtmobApi provideAtmobApi(Gson gson) {
+        OkHttpClient okHttpClient = AtmobOkHttpClient.newInstance(ATMOB_TAG, BuildConfig.DEBUG);
+        return new Retrofit.Builder()
+                .client(okHttpClient)
+                .addConverterFactory(GsonConverterFactory.create(gson))
+                .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
+                .baseUrl(Constants.Atmob_Server_Base_URL)
+                .build()
+                .create(AtmobApi.class);
+    }
+
+}

+ 281 - 0
app/src/main/java/com/datarecovery/my/master/utils/bindingadapters/ImageViewBindingAdapter.java

@@ -0,0 +1,281 @@
+package com.datarecovery.my.master.utils.bindingadapters;
+
+import android.content.res.ColorStateList;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.widget.ImageView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.Nullable;
+import androidx.databinding.BindingAdapter;
+import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
+
+import com.atmob.common.text.TextUtil;
+import com.atmob.common.ui.SizeUtil;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestBuilder;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.bumptech.glide.load.engine.GlideException;
+import com.bumptech.glide.load.resource.bitmap.CenterCrop;
+import com.bumptech.glide.load.resource.bitmap.CircleCrop;
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
+import com.bumptech.glide.load.resource.gif.GifDrawable;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.RequestOptions;
+import com.bumptech.glide.request.target.Target;
+import com.bumptech.glide.signature.ObjectKey;
+
+public class ImageViewBindingAdapter {
+
+
+    @BindingAdapter("imageRes")
+    public static void setImageResource(ImageView imageView, @DrawableRes int imageRes) {
+        imageView.setImageResource(imageRes);
+    }
+
+    @BindingAdapter("imageDraw")
+    public static void setImageResource(ImageView imageView, Drawable drawable) {
+        imageView.setImageDrawable(drawable);
+    }
+
+    @BindingAdapter({"imageRes", "radius"})
+    public static void setImageResource(ImageView imageView, @DrawableRes int imageRes, int radius) {
+        if (imageRes == 0) {
+            imageView.setImageResource(0);
+            return;
+        }
+        int radiusPx = (int) SizeUtil.dp2px(radius);
+        Glide.with(imageView)
+                .load(imageRes)
+                .signature(new ObjectKey(System.currentTimeMillis()))
+                .diskCacheStrategy(DiskCacheStrategy.ALL)
+                .apply(new RequestOptions().transform(new RoundedCorners(radiusPx)))
+                .into(imageView);
+    }
+
+    @BindingAdapter(value = {"imageUrl", "imageThumbUrl"}, requireAll = false)
+    public static void setImageUrl(ImageView imageView, String imageUrl, String imageThumbUrl) {
+        imageUrl = TextUtils.isEmpty(imageUrl) ? imageThumbUrl : imageUrl;
+        if (!TextUtil.isUrl(imageUrl)) {
+            return;
+        }
+        RequestBuilder<Drawable> requestBuilder = Glide.with(imageView)
+                .load(imageUrl);
+        if (!TextUtils.isEmpty(imageThumbUrl)) {
+            requestBuilder = requestBuilder.thumbnail(Glide.with(imageView).load(imageThumbUrl));
+        }
+        requestBuilder.into(imageView);
+    }
+
+    @BindingAdapter(value = {"imageUrl", "radius"})
+    public static void setImageUrl(ImageView imageView, String imageUrl, int radius) {
+        if (!TextUtil.isUrl(imageUrl)) {
+            return;
+        }
+        int radiusPx = (int) SizeUtil.dp2px(radius);
+        RequestOptions options = new RequestOptions();
+        if (imageView.getScaleType() == ImageView.ScaleType.CENTER_CROP) {
+            options = options.transform(new CenterCrop(), new RoundedCorners(radiusPx));
+        } else {
+            options = options.transform(new RoundedCorners(radiusPx));
+        }
+        Glide.with(imageView)
+                .load(imageUrl)
+                .apply(options)
+                .diskCacheStrategy(DiskCacheStrategy.ALL)
+                .into(imageView);
+    }
+
+    @BindingAdapter(value = {"imageUrl", "placeholder", "error", "radius"})
+    public static void setImageUrl(ImageView imageView, String imageUrl, Drawable placeholder, Drawable error, int radius) {
+        int radiusPx = (int) SizeUtil.dp2px(radius);
+        RequestOptions options = new RequestOptions();
+        if (imageView.getScaleType() == ImageView.ScaleType.CENTER_CROP) {
+            options = options.transform(new CenterCrop(), new RoundedCorners(radiusPx));
+        } else {
+            options = options.transform(new RoundedCorners(radiusPx));
+        }
+        Glide.with(imageView)
+                .load(imageUrl)
+                .apply(options)
+                .placeholder(placeholder)
+                .diskCacheStrategy(DiskCacheStrategy.ALL)
+                .thumbnail(Glide.with(imageView)
+                        .load(error)
+                        .apply(options))
+                .into(imageView);
+    }
+
+    @BindingAdapter(value = {"imageUrl", "placeholder", "error"})
+    public static void setImageUrl(ImageView imageView, String imageUrl, Drawable placeholder, Drawable error) {
+        Glide.with(imageView)
+                .load(imageUrl)
+                .placeholder(placeholder)
+                .diskCacheStrategy(DiskCacheStrategy.ALL)
+                .error(error)
+                .into(imageView);
+    }
+
+    @BindingAdapter(value = {"android:src", "radius"})
+    public static void setImageUrl(ImageView imageView, Drawable drawable, int radius) {
+        if (drawable == null) {
+            imageView.setImageDrawable(null);
+            return;
+        }
+        int radiusPx = (int) SizeUtil.dp2px(radius);
+        RequestOptions options = new RequestOptions();
+        if (imageView.getScaleType() == ImageView.ScaleType.CENTER_CROP) {
+            options = options.transform(new CenterCrop(), new RoundedCorners(radiusPx));
+        } else {
+            options = options.transform(new RoundedCorners(radiusPx));
+        }
+        Glide.with(imageView)
+                .load(drawable)
+                .diskCacheStrategy(DiskCacheStrategy.ALL)
+                .apply(options)
+                .into(imageView);
+    }
+
+    @BindingAdapter(value = {"imageUrl", "circle"})
+    public static void setImageUrl(ImageView imageView, String imageUrl, boolean circle) {
+        if (!TextUtil.isUrl(imageUrl)) {
+            return;
+        }
+        if (circle) {
+            Glide.with(imageView)
+                    .load(imageUrl)
+                    .diskCacheStrategy(DiskCacheStrategy.ALL)
+                    .apply(new RequestOptions().transform(new CircleCrop()))
+                    .into(imageView);
+        } else {
+            setImageUrl(imageView, imageUrl, null);
+        }
+    }
+
+    @BindingAdapter(value = {"imageUri", "circle"})
+    public static void setImageUri(ImageView imageView, String imageUri, boolean circle) {
+        if (TextUtils.isEmpty(imageUri)) {
+            return;
+        }
+        if (circle) {
+            Glide.with(imageView)
+                    .load(imageUri)
+                    .diskCacheStrategy(DiskCacheStrategy.ALL)
+                    .apply(new RequestOptions().transform(new CircleCrop()))
+                    .into(imageView);
+        } else {
+            setImageUri(imageView, imageUri);
+        }
+    }
+
+    @BindingAdapter(value = {"imageUri"})
+    public static void setImageUri(ImageView imageView, String uri) {
+        if (TextUtils.isEmpty(uri)) {
+            return;
+        }
+        setImageUri(imageView, Uri.parse(uri));
+    }
+
+    @BindingAdapter(value = {"imageUri", "radius"})
+    public static void setImageUri(ImageView imageView, String uri, int radius) {
+        if (TextUtils.isEmpty(uri)) {
+            return;
+        }
+        setImageUri(imageView, Uri.parse(uri), radius);
+    }
+
+    @BindingAdapter(value = {"imageUri"})
+    public static void setImageUri(ImageView imageView, Uri uri) {
+        if (uri == null) {
+            return;
+        }
+        Glide.with(imageView)
+                .load(uri)
+                .into(imageView);
+    }
+
+    @BindingAdapter(value = {"imageUri", "radius"})
+    public static void setImageUri(ImageView imageView, Uri uri, int radius) {
+        if (uri == null) {
+            return;
+        }
+        int radiusPx = (int) SizeUtil.dp2px(radius);
+        RequestOptions options = new RequestOptions();
+        if (imageView.getScaleType() == ImageView.ScaleType.CENTER_CROP) {
+            options = options.transform(new CenterCrop(), new RoundedCorners(radiusPx));
+        } else {
+            options = options.transform(new RoundedCorners(radiusPx));
+        }
+        Glide.with(imageView)
+                .load(uri)
+                .diskCacheStrategy(DiskCacheStrategy.ALL)
+                .apply(options)
+                .into(imageView);
+    }
+
+    @BindingAdapter(value = {"imageUri", "placeholder", "radius"})
+    public static void setImageUri(ImageView imageView, Uri uri, Drawable placeholder, int radius) {
+        if (uri == null) {
+            return;
+        }
+        int radiusPx = (int) SizeUtil.dp2px(radius);
+        RequestOptions options = new RequestOptions();
+        if (imageView.getScaleType() == ImageView.ScaleType.CENTER_CROP) {
+            options = options.transform(new CenterCrop(), new RoundedCorners(radiusPx));
+        } else {
+            options = options.transform(new RoundedCorners(radiusPx));
+        }
+        Glide.with(imageView)
+                .load(uri)
+                .diskCacheStrategy(DiskCacheStrategy.ALL)
+                .apply(options)
+                .placeholder(placeholder)
+                .into(imageView);
+    }
+
+    @BindingAdapter(value = {"imageUrl", "gifInterval"})
+    public static void setGifUrl(ImageView imageView, String imageUrl, long interval) {
+        Glide.with(imageView)
+                .load(imageUrl)
+                .listener(new RequestListener<Drawable>() {
+                    @Override
+                    public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
+                        return false;
+                    }
+
+                    @Override
+                    public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
+                        GifDrawable gifDrawable = resource instanceof GifDrawable ? ((GifDrawable) resource) : null;
+                        if (gifDrawable == null) {
+                            return false;
+                        }
+                        gifDrawable.setLoopCount(1);
+                        gifDrawable.registerAnimationCallback(new Animatable2Compat.AnimationCallback() {
+                            @Override
+                            public void onAnimationStart(Drawable drawable) {
+                                super.onAnimationStart(drawable);
+                            }
+
+                            @Override
+                            public void onAnimationEnd(Drawable drawable) {
+                                super.onAnimationEnd(drawable);
+                                imageView.postDelayed(gifDrawable::start, interval);
+                            }
+                        });
+                        return false;
+                    }
+                })
+                .into(imageView);
+    }
+
+    /**
+     * {@link ImageView#getDrawable()}不为空时才生效
+     */
+    @BindingAdapter(value = {"tint"})
+    public static void setTint(ImageView imageView, @ColorInt int tint) {
+        imageView.setImageTintList(ColorStateList.valueOf(tint));
+    }
+}

+ 75 - 0
app/src/main/java/com/datarecovery/my/master/utils/bindingadapters/TextViewBindingAdapter.java

@@ -0,0 +1,75 @@
+package com.datarecovery.my.master.utils.bindingadapters;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.widget.TextView;
+
+import androidx.annotation.ColorRes;
+import androidx.core.content.res.ResourcesCompat;
+import androidx.databinding.BindingAdapter;
+
+
+public class TextViewBindingAdapter {
+
+
+    @BindingAdapter({"assetFontFile"})
+    public static void setAssetFontFile(TextView textView, String assetFontFile) {
+        Context context = textView.getContext();
+        AssetManager assets = context.getAssets();
+        Typeface typeface = Typeface.createFromAsset(assets, assetFontFile);
+        textView.setTypeface(typeface);
+    }
+
+    @BindingAdapter({"textColorRes"})
+    public static void setTextColor(TextView textView, @ColorRes int colorRes) {
+        Resources resources = textView.getResources();
+        try {
+            int color = ResourcesCompat.getColor(resources, colorRes, null);
+            textView.setTextColor(color);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    @BindingAdapter({"textColorResource"})
+    public static void setTextColorResource(TextView textView, int colorRes) {
+        try {
+            textView.setTextColor(colorRes);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    @BindingAdapter(value = {"drawableEnd", "drawableStart", "drawableTop", "drawableBoattrs.xmlttom"}, requireAll = false)
+    public static void setDrawableEnd(
+            TextView textView,
+            Drawable drawableEnd,
+            Drawable drawableStart,
+            Drawable drawableTop,
+            Drawable drawableBottom
+    ) {
+        textView.setCompoundDrawablesWithIntrinsicBounds(drawableStart, drawableTop, drawableEnd, drawableBottom);
+    }
+
+    @BindingAdapter(value = {"isBold"})
+    public static void setBold(TextView textView, boolean isBold) {
+        if (isBold) {
+            textView.setTypeface(null, Typeface.BOLD);
+        } else {
+            textView.setTypeface(null, Typeface.NORMAL);
+        }
+    }
+
+    @BindingAdapter(value = {"deleteLine"})
+    public static void setDeleteLine(TextView textView, boolean isDeleteLine) {
+        if (isDeleteLine) {
+            textView.getPaint().setFlags(Paint.STRIKE_THRU_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
+        } else {
+            textView.setPaintFlags(textView.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
+        }
+    }
+}

+ 155 - 0
app/src/main/java/com/datarecovery/my/master/utils/bindingadapters/ViewBindingAdapter.java

@@ -0,0 +1,155 @@
+package com.datarecovery.my.master.utils.bindingadapters;
+
+import android.content.res.ColorStateList;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.TouchDelegate;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.databinding.BindingAdapter;
+
+import com.atmob.common.logging.AtmobLog;
+import com.atmob.common.ui.SizeUtil;
+
+import java.util.concurrent.TimeUnit;
+
+import atmob.reactivex.rxjava3.disposables.Disposable;
+import atmob.rxjava.utils.RxJavaUtil;
+
+
+public class ViewBindingAdapter {
+    private static final long ClickThrottleWindowDuration = 600; // In milliseconds
+
+    @BindingAdapter(value = {"android:onClickListener"})
+    public static void setClickListener(View view, View.OnClickListener clickListener) {
+        view.setClickable(true);
+        Disposable ignore = RxJavaUtil.click(view)
+                .throttleFirst(ClickThrottleWindowDuration, TimeUnit.MILLISECONDS)
+                .subscribe(unused -> {
+                    if (clickListener != null) {
+                        clickListener.onClick(view);
+                    }
+                }, Throwable::printStackTrace);
+    }
+
+    @BindingAdapter(value = {"android:onClick"})
+    public static void setOnClick(View view, View.OnClickListener clickListener) {
+        view.setClickable(true);
+        Disposable ignore = RxJavaUtil.click(view)
+                .throttleFirst(ClickThrottleWindowDuration, TimeUnit.MILLISECONDS)
+                .subscribe(unused -> {
+                    if (clickListener != null) {
+                        clickListener.onClick(view);
+                    }
+                }, Throwable::printStackTrace);
+    }
+
+    @BindingAdapter(value = {"android:onLongClickListener", "android:longClickable"}, requireAll = false)
+    public static void setOnLongClickListener(View view, View.OnLongClickListener clickListener,
+                                              boolean clickable) {
+        view.setLongClickable(clickable);
+        Disposable ignore = RxJavaUtil.longClick(view)
+                .throttleFirst(ClickThrottleWindowDuration, TimeUnit.MILLISECONDS)
+                .subscribe(unused -> {
+                    if (clickListener != null) {
+                        clickListener.onLongClick(view);
+                    }
+                }, Throwable::printStackTrace);
+    }
+
+    @BindingAdapter(value = {"android:onLongClick", "android:longClickable"}, requireAll = false)
+    public static void setOnLongClick(View view, View.OnLongClickListener clickListener,
+                                      boolean clickable) {
+        view.setLongClickable(clickable);
+        Disposable ignore = RxJavaUtil.longClick(view)
+                .throttleFirst(ClickThrottleWindowDuration, TimeUnit.MILLISECONDS)
+                .subscribe(unused -> {
+                    if (clickListener != null) {
+                        clickListener.onLongClick(view);
+                    }
+                }, Throwable::printStackTrace);
+    }
+
+    @BindingAdapter({"isInvisible"})
+    public static void setIsInvisible(View view, boolean isInvisible) {
+        view.setVisibility(isInvisible ? View.INVISIBLE : View.VISIBLE);
+    }
+
+    @BindingAdapter({"isGone"})
+    public static void setIsGone(View view, boolean isGone) {
+        view.setVisibility(isGone ? View.GONE : View.VISIBLE);
+    }
+
+    @BindingAdapter({"backgroundRes"})
+    public static void setBackgroundRes(View view, @DrawableRes int backgroundRes) {
+        view.setBackgroundResource(backgroundRes);
+    }
+
+    @BindingAdapter({"backgroundDraw"})
+    public static void setBackgroundDrawable(View view, Drawable drawable) {
+        view.setBackground(drawable);
+    }
+
+    /**
+     * {@link View#getBackground()}不为空时才生效
+     */
+    @BindingAdapter({"backgroundTint"})
+    public static void setBackgroundTint(View view, @ColorInt int backgroundTint) {
+        view.setBackgroundTintList(ColorStateList.valueOf(backgroundTint));
+    }
+
+    @BindingAdapter("android:layout_width")
+    public static void setLayoutWidth(View view, float width) {
+        ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+        layoutParams.width = (int) width;
+        view.setLayoutParams(layoutParams);
+    }
+
+    @BindingAdapter("android:layout_height")
+    public static void setLayoutHeight(View view, float height) {
+        ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+        layoutParams.height = (int) height;
+        view.setLayoutParams(layoutParams);
+    }
+
+    @BindingAdapter("expandTouchSize")
+    public static void touchDelegate(View view, int expandTouchSize) {
+        ViewGroup parent = view.getParent() instanceof ViewGroup ? ((ViewGroup) view.getParent()) : null;
+        if (parent == null) {
+            return;
+        }
+        TouchDelegate touchDelegate = parent.getTouchDelegate();
+        if (touchDelegate != null) {
+            AtmobLog.w("View", "touchDelegate is not null");
+        }
+        parent.post(() -> {
+            float px = SizeUtil.dp2px(expandTouchSize);
+            Rect rect = new Rect();
+            view.getHitRect(rect);
+            rect.top -= px;
+            rect.bottom += px;
+            rect.left -= px;
+            rect.right += px;
+            parent.setTouchDelegate(new TouchDelegate(rect, view));
+        });
+    }
+
+    @BindingAdapter(value = {"scaleX", "scaleY", "pivotXPercent", "pivotYPercent"})
+    public static void setScale(View view, final float scaleX, final float scaleY, final float pivotXPercent, final float pivotYPercent) {
+        view.post(() -> {
+            view.setPivotX(Math.max(pivotXPercent, 0) * view.getWidth());
+            view.setPivotY(Math.max(pivotYPercent, 0) * view.getHeight());
+            view.animate()
+                    .scaleX(scaleX < 0f ? 1f : scaleX)
+                    .scaleY(scaleY < 0f ? 1f : scaleY)
+                    .setDuration(150)
+                    .setInterpolator(new DecelerateInterpolator())
+                    .start();
+        });
+    }
+
+}

+ 58 - 0
app/src/main/java/com/datarecovery/my/master/utils/livedata/SingleLiveEvent.java

@@ -0,0 +1,58 @@
+package com.datarecovery.my.master.utils.livedata;
+
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A lifecycle-aware observable that sends only new updates after subscription, used for events like
+ * navigation and Snackbar messages.
+ * <p>
+ * This avoids a common problem with events: on configuration change (like rotation) an update
+ * can be emitted if the observer is active. This LiveData only calls the observable if there's an
+ * explicit call to setValue() or call().
+ * <p>
+ * Note that only one observer is going to be notified of changes.
+ * <p>
+ */
+public class SingleLiveEvent<T> extends MutableLiveData<T> {
+
+    private static final String TAG = "SingleLiveEvent";
+
+    private final AtomicBoolean mPending = new AtomicBoolean(false);
+
+    @MainThread
+    public void observe(@NonNull LifecycleOwner owner, @NonNull final Observer<? super T> observer) {
+        if (hasActiveObservers()) {
+            Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
+        }
+
+        // Observe the internal MutableLiveData
+        super.observe(owner, t -> {
+            if (mPending.compareAndSet(true, false)) {
+                observer.onChanged(t);
+            }
+        });
+    }
+
+    @MainThread
+    public void setValue(@Nullable T t) {
+        mPending.set(true);
+        super.setValue(t);
+    }
+
+    /**
+     * Used for cases where T is Void, to make calls cleaner.
+     */
+    @MainThread
+    public void call() {
+        setValue(null);
+    }
+}