+ * Use the {@link Builder} to create and display a dialog.
+ */
+public class AppSettingsDialog implements Parcelable {
+
+ private static final String TAG = "EasyPermissions";
+
+ public static final int DEFAULT_SETTINGS_REQ_CODE = 16061;
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public AppSettingsDialog createFromParcel(Parcel in) {
+ return new AppSettingsDialog(in);
+ }
+
+ @Override
+ public AppSettingsDialog[] newArray(int size) {
+ return new AppSettingsDialog[size];
+ }
+ };
+
+ static final String EXTRA_APP_SETTINGS = "extra_app_settings";
+
+ @StyleRes
+ private final int mThemeResId;
+ private final String mRationale;
+ private final String mTitle;
+ private final String mPositiveButtonText;
+ private final String mNegativeButtonText;
+ private final int mRequestCode;
+ private final int mIntentFlags;
+
+ private Object mActivityOrFragment;
+ private Context mContext;
+
+ private AppSettingsDialog(Parcel in) {
+ mThemeResId = in.readInt();
+ mRationale = in.readString();
+ mTitle = in.readString();
+ mPositiveButtonText = in.readString();
+ mNegativeButtonText = in.readString();
+ mRequestCode = in.readInt();
+ mIntentFlags = in.readInt();
+ }
+
+ private AppSettingsDialog(@NonNull final Object activityOrFragment,
+ @StyleRes int themeResId,
+ @Nullable String rationale,
+ @Nullable String title,
+ @Nullable String positiveButtonText,
+ @Nullable String negativeButtonText,
+ int requestCode,
+ int intentFlags) {
+ setActivityOrFragment(activityOrFragment);
+ mThemeResId = themeResId;
+ mRationale = rationale;
+ mTitle = title;
+ mPositiveButtonText = positiveButtonText;
+ mNegativeButtonText = negativeButtonText;
+ mRequestCode = requestCode;
+ mIntentFlags = intentFlags;
+ }
+
+ static AppSettingsDialog fromIntent(Intent intent, Activity activity) {
+ AppSettingsDialog dialog = intent.getParcelableExtra(AppSettingsDialog.EXTRA_APP_SETTINGS);
+
+ // It's not clear how this could happen, but in the case that it does we should try
+ // to avoid a runtime crash and just use the default dialog.
+ // https://github.com/googlesamples/easypermissions/issues/278
+ if (dialog == null) {
+ Log.e(TAG, "Intent contains null value for EXTRA_APP_SETTINGS: "
+ + "intent=" + intent
+ + ", "
+ + "extras=" + intent.getExtras());
+
+ dialog = new Builder(activity).build();
+ }
+
+ dialog.setActivityOrFragment(activity);
+ return dialog;
+ }
+
+ private void setActivityOrFragment(Object activityOrFragment) {
+ mActivityOrFragment = activityOrFragment;
+
+ if (activityOrFragment instanceof Activity) {
+ mContext = (Activity) activityOrFragment;
+ } else if (activityOrFragment instanceof Fragment) {
+ mContext = ((Fragment) activityOrFragment).getContext();
+ } else {
+ throw new IllegalStateException("Unknown object: " + activityOrFragment);
+ }
+ }
+
+ private void startForResult(Intent intent) {
+ if (mActivityOrFragment instanceof Activity) {
+ ((Activity) mActivityOrFragment).startActivityForResult(intent, mRequestCode);
+ } else if (mActivityOrFragment instanceof Fragment) {
+ ((Fragment) mActivityOrFragment).startActivityForResult(intent, mRequestCode);
+ }
+ }
+
+ /**
+ * Display the built dialog.
+ */
+ public void show() {
+ startForResult(AppSettingsDialogHolderActivity.createShowDialogIntent(mContext, this));
+ }
+
+ /**
+ * Show the dialog. {@link #show()} is a wrapper to ensure backwards compatibility
+ */
+ AlertDialog showDialog(DialogInterface.OnClickListener positiveListener,
+ DialogInterface.OnClickListener negativeListener) {
+ AlertDialog.Builder builder;
+ if (mThemeResId != -1) {
+ builder = new AlertDialog.Builder(mContext, mThemeResId);
+ } else {
+ builder = new AlertDialog.Builder(mContext);
+ }
+ return builder
+ .setCancelable(false)
+ .setTitle(mTitle)
+ .setMessage(mRationale)
+ .setPositiveButton(mPositiveButtonText, positiveListener)
+ .setNegativeButton(mNegativeButtonText, negativeListener)
+ .show();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mThemeResId);
+ dest.writeString(mRationale);
+ dest.writeString(mTitle);
+ dest.writeString(mPositiveButtonText);
+ dest.writeString(mNegativeButtonText);
+ dest.writeInt(mRequestCode);
+ dest.writeInt(mIntentFlags);
+ }
+
+ int getIntentFlags() {
+ return mIntentFlags;
+ }
+
+ /**
+ * Builder for an {@link AppSettingsDialog}.
+ */
+ public static class Builder {
+
+ private final Object mActivityOrFragment;
+ private final Context mContext;
+ @StyleRes
+ private int mThemeResId = -1;
+ private String mRationale;
+ private String mTitle;
+ private String mPositiveButtonText;
+ private String mNegativeButtonText;
+ private int mRequestCode = -1;
+ private boolean mOpenInNewTask = false;
+
+ /**
+ * Create a new Builder for an {@link AppSettingsDialog}.
+ *
+ * @param activity the {@link Activity} in which to display the dialog.
+ */
+ public Builder(@NonNull Activity activity) {
+ mActivityOrFragment = activity;
+ mContext = activity;
+ }
+
+ /**
+ * Create a new Builder for an {@link AppSettingsDialog}.
+ *
+ * @param fragment the {@link Fragment} in which to display the dialog.
+ */
+ public Builder(@NonNull Fragment fragment) {
+ mActivityOrFragment = fragment;
+ mContext = fragment.getContext();
+ }
+
+ /**
+ * Set the dialog theme.
+ */
+ @NonNull
+ public Builder setThemeResId(@StyleRes int themeResId) {
+ mThemeResId = themeResId;
+ return this;
+ }
+
+ /**
+ * Set the title dialog. Default is "Permissions Required".
+ */
+ @NonNull
+ public Builder setTitle(@Nullable String title) {
+ mTitle = title;
+ return this;
+ }
+
+ /**
+ * Set the title dialog. Default is "Permissions Required".
+ */
+ @NonNull
+ public Builder setTitle(@StringRes int title) {
+ mTitle = mContext.getString(title);
+ return this;
+ }
+
+ /**
+ * Set the rationale dialog. Default is
+ * "This app may not work correctly without the requested permissions.
+ * Open the app settings screen to modify app permissions."
+ */
+ @NonNull
+ public Builder setRationale(@Nullable String rationale) {
+ mRationale = rationale;
+ return this;
+ }
+
+ /**
+ * Set the rationale dialog. Default is
+ * "This app may not work correctly without the requested permissions.
+ * Open the app settings screen to modify app permissions."
+ */
+ @NonNull
+ public Builder setRationale(@StringRes int rationale) {
+ mRationale = mContext.getString(rationale);
+ return this;
+ }
+
+ /**
+ * Set the positive button text, default is {@link android.R.string#ok}.
+ */
+ @NonNull
+ public Builder setPositiveButton(@Nullable String text) {
+ mPositiveButtonText = text;
+ return this;
+ }
+
+ /**
+ * Set the positive button text, default is {@link android.R.string#ok}.
+ */
+ @NonNull
+ public Builder setPositiveButton(@StringRes int textId) {
+ mPositiveButtonText = mContext.getString(textId);
+ return this;
+ }
+
+ /**
+ * Set the negative button text, default is {@link android.R.string#cancel}.
+ *
+ * To know if a user cancelled the request, check if your permissions were given with {@link
+ * EasyPermissions#hasPermissions(Context, String...)} in {@see
+ * Activity#onActivityResult(int, int, Intent)}. If you still don't have the right
+ * permissions, then the request was cancelled.
+ */
+ @NonNull
+ public Builder setNegativeButton(@Nullable String text) {
+ mNegativeButtonText = text;
+ return this;
+ }
+
+ /**
+ * Set the negative button text, default is {@link android.R.string#cancel}.
+ */
+ @NonNull
+ public Builder setNegativeButton(@StringRes int textId) {
+ mNegativeButtonText = mContext.getString(textId);
+ return this;
+ }
+
+ /**
+ * Set the request code use when launching the Settings screen for result, can be retrieved
+ * in the calling Activity's {@see Activity#onActivityResult(int, int, Intent)} method.
+ * Default is {@link #DEFAULT_SETTINGS_REQ_CODE}.
+ */
+ @NonNull
+ public Builder setRequestCode(int requestCode) {
+ mRequestCode = requestCode;
+ return this;
+ }
+
+ /**
+ * Set whether the settings screen should be opened in a separate task. This is achieved by
+ * setting {@link Intent#FLAG_ACTIVITY_NEW_TASK#FLAG_ACTIVITY_NEW_TASK} on
+ * the Intent used to open the settings screen.
+ */
+ @NonNull
+ public Builder setOpenInNewTask(boolean openInNewTask) {
+ mOpenInNewTask = openInNewTask;
+ return this;
+ }
+
+ /**
+ * Build the {@link AppSettingsDialog} from the specified options. Generally followed by a
+ * call to {@link AppSettingsDialog#show()}.
+ */
+ @NonNull
+ public AppSettingsDialog build() {
+ mRationale = TextUtils.isEmpty(mRationale) ?
+ mContext.getString(R.string.rationale_ask_again) : mRationale;
+ mTitle = TextUtils.isEmpty(mTitle) ?
+ mContext.getString(R.string.title_settings_dialog) : mTitle;
+ mPositiveButtonText = TextUtils.isEmpty(mPositiveButtonText) ?
+ mContext.getString(android.R.string.ok) : mPositiveButtonText;
+ mNegativeButtonText = TextUtils.isEmpty(mNegativeButtonText) ?
+ mContext.getString(android.R.string.cancel) : mNegativeButtonText;
+ mRequestCode = mRequestCode > 0 ? mRequestCode : DEFAULT_SETTINGS_REQ_CODE;
+
+ int intentFlags = 0;
+ if (mOpenInNewTask) {
+ intentFlags |= Intent.FLAG_ACTIVITY_NEW_TASK;
+ }
+
+ return new AppSettingsDialog(
+ mActivityOrFragment,
+ mThemeResId,
+ mRationale,
+ mTitle,
+ mPositiveButtonText,
+ mNegativeButtonText,
+ mRequestCode,
+ intentFlags);
+ }
+
+ }
+
+}
diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/AppSettingsDialogHolderActivity.java b/app/src/module_easypermission/java/com/yizhuan/erban/AppSettingsDialogHolderActivity.java
new file mode 100644
index 000000000..f55b66aeb
--- /dev/null
+++ b/app/src/module_easypermission/java/com/yizhuan/erban/AppSettingsDialogHolderActivity.java
@@ -0,0 +1,65 @@
+package com.yizhuan.erban;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import androidx.annotation.RestrictTo;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class AppSettingsDialogHolderActivity extends AppCompatActivity implements DialogInterface.OnClickListener {
+ private static final int APP_SETTINGS_RC = 7534;
+
+ private AlertDialog mDialog;
+ private int mIntentFlags;
+
+ public static Intent createShowDialogIntent(Context context, AppSettingsDialog dialog) {
+ Intent intent = new Intent(context, AppSettingsDialogHolderActivity.class);
+ intent.putExtra(AppSettingsDialog.EXTRA_APP_SETTINGS, dialog);
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ AppSettingsDialog appSettingsDialog = AppSettingsDialog.fromIntent(getIntent(), this);
+ mIntentFlags = appSettingsDialog.getIntentFlags();
+ mDialog = appSettingsDialog.showDialog(this, this);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mDialog != null && mDialog.isShowing()) {
+ mDialog.dismiss();
+ }
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == Dialog.BUTTON_POSITIVE) {
+ Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(Uri.fromParts("package", getPackageName(), null));
+ intent.addFlags(mIntentFlags);
+ startActivityForResult(intent, APP_SETTINGS_RC);
+ } else if (which == Dialog.BUTTON_NEGATIVE) {
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ } else {
+ throw new IllegalStateException("Unknown button type: " + which);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ setResult(resultCode, data);
+ finish();
+ }
+}
diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/EasyPermissions.java b/app/src/module_easypermission/java/com/yizhuan/erban/EasyPermissions.java
new file mode 100644
index 000000000..d020ebf20
--- /dev/null
+++ b/app/src/module_easypermission/java/com/yizhuan/erban/EasyPermissions.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.yizhuan.erban;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Size;
+import androidx.core.app.ActivityCompat;
+import androidx.fragment.app.Fragment;
+import androidx.core.content.ContextCompat;
+import android.util.Log;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import com.yizhuan.erban.helper.PermissionHelper;
+
+/**
+ * Utility to request and check System permissions for apps targeting Android M (API >= 23).
+ */
+public class EasyPermissions {
+
+ /**
+ * Callback interface to receive the results of {@code EasyPermissions.requestPermissions()}
+ * calls.
+ */
+ public interface PermissionCallbacks extends ActivityCompat.OnRequestPermissionsResultCallback {
+
+ void onPermissionsGranted(int requestCode, @NonNull List perms);
+
+ void onPermissionsDenied(int requestCode, @NonNull List perms);
+ }
+
+ /**
+ * Callback interface to receive button clicked events of the rationale dialog
+ */
+ public interface RationaleCallbacks {
+ void onRationaleAccepted(int requestCode);
+
+ void onRationaleDenied(int requestCode);
+ }
+
+ private static final String TAG = "EasyPermissions";
+
+ /**
+ * Check if the calling context has a set of permissions.
+ *
+ * @param context the calling context.
+ * @param perms one ore more permissions, such as {@link Manifest.permission#CAMERA}.
+ * @return true if all permissions are already granted, false if at least one permission is not
+ * yet granted.
+ * @see Manifest.permission
+ */
+ public static boolean hasPermissions(@NonNull Context context,
+ @Size(min = 1) @NonNull String... perms) {
+ // Always return true for SDK < M, let the system deal with the permissions
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ Log.w(TAG, "hasPermissions: API version < M, returning true by default");
+
+ // DANGER ZONE!!! Changing this will break the library.
+ return true;
+ }
+
+ // Null context may be passed if we have detected Low API (less than M) so getting
+ // to this point with a null context should not be possible.
+ if (context == null) {
+ throw new IllegalArgumentException("Can't check permissions for null context");
+ }
+
+ for (String perm : perms) {
+ if (ContextCompat.checkSelfPermission(context, perm)
+ != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Request a set of permissions, showing a rationale if the system requests it.
+ *
+ * @param host requesting context.
+ * @param rationale a message explaining why the application needs this set of permissions;
+ * will be displayed if the user rejects the request the first time.
+ * @param requestCode request code to track this request, must be < 256.
+ * @param perms a set of permissions to be requested.
+ * @see Manifest.permission
+ */
+ public static void requestPermissions(
+ @NonNull Activity host, @NonNull String rationale,
+ @IntRange(from = 0, to = 255) int requestCode, @Size(min = 1) @NonNull String... perms) {
+ requestPermissions(
+ new PermissionRequest.Builder(host, requestCode, perms)
+ .setRationale(rationale)
+ .build());
+ }
+
+ /**
+ * Request permissions from a Support Fragment with standard OK/Cancel buttons.
+ *
+ * @see #requestPermissions(Activity, String, int, String...)
+ */
+ public static void requestPermissions(
+ @NonNull Fragment host, @NonNull String rationale,
+ @IntRange(from = 0, to = 255) int requestCode, @Size(min = 1) @NonNull String... perms) {
+ requestPermissions(
+ new PermissionRequest.Builder(host, requestCode, perms)
+ .setRationale(rationale)
+ .build());
+ }
+
+ /**
+ * Request a set of permissions.
+ *
+ * @param request the permission request
+ * @see PermissionRequest
+ */
+ public static void requestPermissions(PermissionRequest request) {
+
+ // Check for permissions before dispatching the request
+ if (hasPermissions(request.getHelper().getContext(), request.getPerms())) {
+ notifyAlreadyHasPermissions(
+ request.getHelper().getHost(), request.getRequestCode(), request.getPerms());
+ return;
+ }
+
+ // Request permissions
+ request.getHelper().requestPermissions(
+ request.getRationale(),
+ request.getPositiveButtonText(),
+ request.getNegativeButtonText(),
+ request.getTheme(),
+ request.getRequestCode(),
+ request.getPerms());
+ }
+
+ /**
+ * Handle the result of a permission request, should be called from the calling {@link
+ * Activity}'s {@link ActivityCompat.OnRequestPermissionsResultCallback#onRequestPermissionsResult(int,
+ * String[], int[])} method.
+ *
+ * If any permissions were granted or denied, the {@code object} will receive the appropriate
+ * callbacks through {@link PermissionCallbacks} and methods annotated with {@link
+ * AfterPermissionGranted} will be run if appropriate.
+ *
+ * @param requestCode requestCode argument to permission result callback.
+ * @param permissions permissions argument to permission result callback.
+ * @param grantResults grantResults argument to permission result callback.
+ * @param receivers an array of objects that have a method annotated with {@link
+ * AfterPermissionGranted} or implement {@link PermissionCallbacks}.
+ */
+ public static void onRequestPermissionsResult(@IntRange(from = 0, to = 255) int requestCode,
+ @NonNull String[] permissions,
+ @NonNull int[] grantResults,
+ @NonNull Object... receivers) {
+ // Make a collection of granted and denied permissions from the request.
+ List granted = new ArrayList<>();
+ List denied = new ArrayList<>();
+ for (int i = 0; i < permissions.length; i++) {
+ String perm = permissions[i];
+ if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
+ granted.add(perm);
+ } else {
+ denied.add(perm);
+ }
+ }
+
+ // iterate through all receivers
+ for (Object object : receivers) {
+ // Report granted permissions, if any.
+ if (!granted.isEmpty()) {
+ if (object instanceof PermissionCallbacks) {
+ ((PermissionCallbacks) object).onPermissionsGranted(requestCode, granted);
+ }
+ }
+
+ // Report denied permissions, if any.
+ if (!denied.isEmpty()) {
+ if (object instanceof PermissionCallbacks) {
+ ((PermissionCallbacks) object).onPermissionsDenied(requestCode, denied);
+ }
+ }
+
+ // If 100% successful, call annotated methods
+ if (!granted.isEmpty() && denied.isEmpty()) {
+ runAnnotatedMethods(object, requestCode);
+ }
+ }
+ }
+
+ /**
+ * Check if at least one permission in the list of denied permissions has been permanently
+ * denied (user clicked "Never ask again").
+ *
+ * Note: Due to a limitation in the information provided by the Android
+ * framework permissions API, this method only works after the permission
+ * has been denied and your app has received the onPermissionsDenied callback.
+ * Otherwise the library cannot distinguish permanent denial from the
+ * "not yet denied" case.
+ *
+ * @param host context requesting permissions.
+ * @param deniedPermissions list of denied permissions, usually from {@link
+ * PermissionCallbacks#onPermissionsDenied(int, List)}
+ * @return {@code true} if at least one permission in the list was permanently denied.
+ */
+ public static boolean somePermissionPermanentlyDenied(@NonNull Activity host,
+ @NonNull List deniedPermissions) {
+ return PermissionHelper.newInstance(host)
+ .somePermissionPermanentlyDenied(deniedPermissions);
+ }
+
+ /**
+ * @see #somePermissionPermanentlyDenied(Activity, List)
+ */
+ public static boolean somePermissionPermanentlyDenied(@NonNull Fragment host,
+ @NonNull List deniedPermissions) {
+ return PermissionHelper.newInstance(host)
+ .somePermissionPermanentlyDenied(deniedPermissions);
+ }
+
+ /**
+ * Check if a permission has been permanently denied (user clicked "Never ask again").
+ *
+ * @param host context requesting permissions.
+ * @param deniedPermission denied permission.
+ * @return {@code true} if the permissions has been permanently denied.
+ */
+ public static boolean permissionPermanentlyDenied(@NonNull Activity host,
+ @NonNull String deniedPermission) {
+ return PermissionHelper.newInstance(host).permissionPermanentlyDenied(deniedPermission);
+ }
+
+ /**
+ * @see #permissionPermanentlyDenied(Activity, String)
+ */
+ public static boolean permissionPermanentlyDenied(@NonNull Fragment host,
+ @NonNull String deniedPermission) {
+ return PermissionHelper.newInstance(host).permissionPermanentlyDenied(deniedPermission);
+ }
+
+ /**
+ * See if some denied permission has been permanently denied.
+ *
+ * @param host requesting context.
+ * @param perms array of permissions.
+ * @return true if the user has previously denied any of the {@code perms} and we should show a
+ * rationale, false otherwise.
+ */
+ public static boolean somePermissionDenied(@NonNull Activity host,
+ @NonNull String... perms) {
+ return PermissionHelper.newInstance(host).somePermissionDenied(perms);
+ }
+
+ /**
+ * @see #somePermissionDenied(Activity, String...)
+ */
+ public static boolean somePermissionDenied(@NonNull Fragment host,
+ @NonNull String... perms) {
+ return PermissionHelper.newInstance(host).somePermissionDenied(perms);
+ }
+
+ /**
+ * Run permission callbacks on an object that requested permissions but already has them by
+ * simulating {@link PackageManager#PERMISSION_GRANTED}.
+ *
+ * @param object the object requesting permissions.
+ * @param requestCode the permission request code.
+ * @param perms a list of permissions requested.
+ */
+ private static void notifyAlreadyHasPermissions(@NonNull Object object,
+ int requestCode,
+ @NonNull String[] perms) {
+ int[] grantResults = new int[perms.length];
+ for (int i = 0; i < perms.length; i++) {
+ grantResults[i] = PackageManager.PERMISSION_GRANTED;
+ }
+
+ onRequestPermissionsResult(requestCode, perms, grantResults, object);
+ }
+
+ /**
+ * Find all methods annotated with {@link AfterPermissionGranted} on a given object with the
+ * correct requestCode argument.
+ *
+ * @param object the object with annotated methods.
+ * @param requestCode the requestCode passed to the annotation.
+ */
+ private static void runAnnotatedMethods(@NonNull Object object, int requestCode) {
+ Class clazz = object.getClass();
+ if (isUsingAndroidAnnotations(object)) {
+ clazz = clazz.getSuperclass();
+ }
+
+ while (clazz != null) {
+ for (Method method : clazz.getDeclaredMethods()) {
+ AfterPermissionGranted ann = method.getAnnotation(AfterPermissionGranted.class);
+ if (ann != null) {
+ // Check for annotated methods with matching request code.
+ if (ann.value() == requestCode) {
+ // Method must be void so that we can invoke it
+ if (method.getParameterTypes().length > 0) {
+ throw new RuntimeException(
+ "Cannot execute method " + method.getName() + " because it is non-void method and/or has input parameters.");
+ }
+
+ try {
+ // Make method accessible if private
+ if (!method.isAccessible()) {
+ method.setAccessible(true);
+ }
+ method.invoke(object);
+ } catch (IllegalAccessException e) {
+ Log.e(TAG, "runDefaultMethod:IllegalAccessException", e);
+ } catch (InvocationTargetException e) {
+ Log.e(TAG, "runDefaultMethod:InvocationTargetException", e);
+ }
+ }
+ }
+ }
+
+ clazz = clazz.getSuperclass();
+ }
+ }
+
+ /**
+ * Determine if the project is using the AndroidAnnotations library.
+ */
+ private static boolean isUsingAndroidAnnotations(@NonNull Object object) {
+ if (!object.getClass().getSimpleName().endsWith("_")) {
+ return false;
+ }
+ try {
+ Class clazz = Class.forName("org.androidannotations.api.view.HasViews");
+ return clazz.isInstance(object);
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+}
diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/PermissionRequest.java b/app/src/module_easypermission/java/com/yizhuan/erban/PermissionRequest.java
new file mode 100644
index 000000000..4d534580a
--- /dev/null
+++ b/app/src/module_easypermission/java/com/yizhuan/erban/PermissionRequest.java
@@ -0,0 +1,260 @@
+package com.yizhuan.erban;
+
+import android.app.Activity;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.Size;
+import androidx.annotation.StringRes;
+import androidx.annotation.StyleRes;
+import androidx.fragment.app.Fragment;
+
+import java.util.Arrays;
+
+import com.yizhuan.erban.helper.PermissionHelper;
+
+/**
+ * An immutable model object that holds all of the parameters associated with a permission request,
+ * such as the permissions, request code, and rationale.
+ *
+ * @see EasyPermissions#requestPermissions(PermissionRequest)
+ * @see Builder
+ */
+public final class PermissionRequest {
+ private final PermissionHelper mHelper;
+ private final String[] mPerms;
+ private final int mRequestCode;
+ private final String mRationale;
+ private final String mPositiveButtonText;
+ private final String mNegativeButtonText;
+ private final int mTheme;
+
+ private PermissionRequest(PermissionHelper helper,
+ String[] perms,
+ int requestCode,
+ String rationale,
+ String positiveButtonText,
+ String negativeButtonText,
+ int theme) {
+ mHelper = helper;
+ mPerms = perms.clone();
+ mRequestCode = requestCode;
+ mRationale = rationale;
+ mPositiveButtonText = positiveButtonText;
+ mNegativeButtonText = negativeButtonText;
+ mTheme = theme;
+ }
+
+ @NonNull
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public PermissionHelper getHelper() {
+ return mHelper;
+ }
+
+ @NonNull
+ public String[] getPerms() {
+ return mPerms.clone();
+ }
+
+ public int getRequestCode() {
+ return mRequestCode;
+ }
+
+ @NonNull
+ public String getRationale() {
+ return mRationale;
+ }
+
+ @NonNull
+ public String getPositiveButtonText() {
+ return mPositiveButtonText;
+ }
+
+ @NonNull
+ public String getNegativeButtonText() {
+ return mNegativeButtonText;
+ }
+
+ @StyleRes
+ public int getTheme() {
+ return mTheme;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PermissionRequest request = (PermissionRequest) o;
+
+ return Arrays.equals(mPerms, request.mPerms) && mRequestCode == request.mRequestCode;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Arrays.hashCode(mPerms);
+ result = 31 * result + mRequestCode;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "PermissionRequest{" +
+ "mHelper=" + mHelper +
+ ", mPerms=" + Arrays.toString(mPerms) +
+ ", mRequestCode=" + mRequestCode +
+ ", mRationale='" + mRationale + '\'' +
+ ", mPositiveButtonText='" + mPositiveButtonText + '\'' +
+ ", mNegativeButtonText='" + mNegativeButtonText + '\'' +
+ ", mTheme=" + mTheme +
+ '}';
+ }
+
+ /**
+ * Builder to build a permission request with variable options.
+ *
+ * @see PermissionRequest
+ */
+ public static final class Builder {
+ private final PermissionHelper mHelper;
+ private final int mRequestCode;
+ private final String[] mPerms;
+
+ private String mRationale;
+ private String mPositiveButtonText;
+ private String mNegativeButtonText;
+ private int mTheme = -1;
+
+ /**
+ * Construct a new permission request builder with a host, request code, and the requested
+ * permissions.
+ *
+ * @param activity the permission request host
+ * @param requestCode request code to track this request; must be < 256
+ * @param perms the set of permissions to be requested
+ */
+ public Builder(@NonNull Activity activity, int requestCode,
+ @NonNull @Size(min = 1) String... perms) {
+ mHelper = PermissionHelper.newInstance(activity);
+ mRequestCode = requestCode;
+ mPerms = perms;
+ }
+
+ /**
+ * @see #Builder(Activity, int, String...)
+ */
+ public Builder(@NonNull Fragment fragment, int requestCode,
+ @NonNull @Size(min = 1) String... perms) {
+ mHelper = PermissionHelper.newInstance(fragment);
+ mRequestCode = requestCode;
+ mPerms = perms;
+ }
+
+ /**
+ * Set the rationale to display to the user if they don't allow your permissions on the
+ * first try. This rationale will be shown as long as the user has denied your permissions
+ * at least once, but has not yet permanently denied your permissions. Should the user
+ * permanently deny your permissions, use the {@link AppSettingsDialog} instead.
+ *
+ * The default rationale text is {@link R.string#rationale_ask}.
+ *
+ * @param rationale the rationale to be displayed to the user should they deny your
+ * permission at least once
+ */
+ @NonNull
+ public Builder setRationale(@Nullable String rationale) {
+ mRationale = rationale;
+ return this;
+ }
+
+ /**
+ * @param resId the string resource to be used as a rationale
+ * @see #setRationale(String)
+ */
+ @NonNull
+ public Builder setRationale(@StringRes int resId) {
+ mRationale = mHelper.getContext().getString(resId);
+ return this;
+ }
+
+ /**
+ * Set the positive button text for the rationale dialog should it be shown.
+ *
+ * The default is {@link android.R.string#ok}
+ */
+ @NonNull
+ public Builder setPositiveButtonText(@Nullable String positiveButtonText) {
+ mPositiveButtonText = positiveButtonText;
+ return this;
+ }
+
+ /**
+ * @see #setPositiveButtonText(String)
+ */
+ @NonNull
+ public Builder setPositiveButtonText(@StringRes int resId) {
+ mPositiveButtonText = mHelper.getContext().getString(resId);
+ return this;
+ }
+
+ /**
+ * Set the negative button text for the rationale dialog should it be shown.
+ *
+ * The default is {@link android.R.string#cancel}
+ */
+ @NonNull
+ public Builder setNegativeButtonText(@Nullable String negativeButtonText) {
+ mNegativeButtonText = negativeButtonText;
+ return this;
+ }
+
+ /**
+ * @see #setNegativeButtonText(String)
+ */
+ @NonNull
+ public Builder setNegativeButtonText(@StringRes int resId) {
+ mNegativeButtonText = mHelper.getContext().getString(resId);
+ return this;
+ }
+
+ /**
+ * Set the theme to be used for the rationale dialog should it be shown.
+ *
+ * @param theme a style resource
+ */
+ @NonNull
+ public Builder setTheme(@StyleRes int theme) {
+ mTheme = theme;
+ return this;
+ }
+
+ /**
+ * Build the permission request.
+ *
+ * @return the permission request
+ * @see EasyPermissions#requestPermissions(PermissionRequest)
+ * @see PermissionRequest
+ */
+ @NonNull
+ public PermissionRequest build() {
+ if (mRationale == null) {
+ mRationale = mHelper.getContext().getString(R.string.rationale_ask);
+ }
+ if (mPositiveButtonText == null) {
+ mPositiveButtonText = mHelper.getContext().getString(android.R.string.ok);
+ }
+ if (mNegativeButtonText == null) {
+ mNegativeButtonText = mHelper.getContext().getString(android.R.string.cancel);
+ }
+
+ return new PermissionRequest(
+ mHelper,
+ mPerms,
+ mRequestCode,
+ mRationale,
+ mPositiveButtonText,
+ mNegativeButtonText,
+ mTheme);
+ }
+ }
+}
diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/RationaleDialogClickListener.java b/app/src/module_easypermission/java/com/yizhuan/erban/RationaleDialogClickListener.java
new file mode 100644
index 000000000..dc1fa5015
--- /dev/null
+++ b/app/src/module_easypermission/java/com/yizhuan/erban/RationaleDialogClickListener.java
@@ -0,0 +1,77 @@
+package com.yizhuan.erban;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import androidx.fragment.app.Fragment;
+
+import java.util.Arrays;
+
+import com.yizhuan.erban.helper.PermissionHelper;
+
+/**
+ * Click listener for either {@link RationaleDialogFragment} or {@link RationaleDialogFragmentCompat}.
+ */
+class RationaleDialogClickListener implements Dialog.OnClickListener {
+
+ private Object mHost;
+ private RationaleDialogConfig mConfig;
+ private EasyPermissions.PermissionCallbacks mCallbacks;
+ private EasyPermissions.RationaleCallbacks mRationaleCallbacks;
+
+ RationaleDialogClickListener(RationaleDialogFragmentCompat compatDialogFragment,
+ RationaleDialogConfig config,
+ EasyPermissions.PermissionCallbacks callbacks,
+ EasyPermissions.RationaleCallbacks rationaleCallbacks) {
+
+ mHost = compatDialogFragment.getParentFragment() != null
+ ? compatDialogFragment.getParentFragment()
+ : compatDialogFragment.getActivity();
+
+ mConfig = config;
+ mCallbacks = callbacks;
+ mRationaleCallbacks = rationaleCallbacks;
+
+ }
+
+ RationaleDialogClickListener(RationaleDialogFragment dialogFragment,
+ RationaleDialogConfig config,
+ EasyPermissions.PermissionCallbacks callbacks,
+ EasyPermissions.RationaleCallbacks dialogCallback) {
+
+ mHost = dialogFragment.getActivity();
+
+ mConfig = config;
+ mCallbacks = callbacks;
+ mRationaleCallbacks = dialogCallback;
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ int requestCode = mConfig.requestCode;
+ if (which == Dialog.BUTTON_POSITIVE) {
+ String[] permissions = mConfig.permissions;
+ if (mRationaleCallbacks != null) {
+ mRationaleCallbacks.onRationaleAccepted(requestCode);
+ }
+ if (mHost instanceof Fragment) {
+ PermissionHelper.newInstance((Fragment) mHost).directRequestPermissions(requestCode, permissions);
+ } else if (mHost instanceof Activity) {
+ PermissionHelper.newInstance((Activity) mHost).directRequestPermissions(requestCode, permissions);
+ } else {
+ throw new RuntimeException("Host must be an Activity or Fragment!");
+ }
+ } else {
+ if (mRationaleCallbacks != null) {
+ mRationaleCallbacks.onRationaleDenied(requestCode);
+ }
+ notifyPermissionDenied();
+ }
+ }
+
+ private void notifyPermissionDenied() {
+ if (mCallbacks != null) {
+ mCallbacks.onPermissionsDenied(mConfig.requestCode, Arrays.asList(mConfig.permissions));
+ }
+ }
+}
diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/RationaleDialogConfig.java b/app/src/module_easypermission/java/com/yizhuan/erban/RationaleDialogConfig.java
new file mode 100644
index 000000000..36c57a3f3
--- /dev/null
+++ b/app/src/module_easypermission/java/com/yizhuan/erban/RationaleDialogConfig.java
@@ -0,0 +1,141 @@
+package com.yizhuan.erban;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.StyleRes;
+import androidx.appcompat.app.AlertDialog;
+
+/**
+ * Configuration for either {@link RationaleDialogFragment} or {@link RationaleDialogFragmentCompat}.
+ */
+class RationaleDialogConfig {
+
+ private static final String KEY_POSITIVE_BUTTON = "positiveButton";
+ private static final String KEY_NEGATIVE_BUTTON = "negativeButton";
+ private static final String KEY_RATIONALE_MESSAGE = "rationaleMsg";
+ private static final String KEY_THEME = "theme";
+ private static final String KEY_REQUEST_CODE = "requestCode";
+ private static final String KEY_PERMISSIONS = "permissions";
+
+ String positiveButton;
+ String negativeButton;
+ int theme;
+ int requestCode;
+ String rationaleMsg;
+ String[] permissions;
+
+ RationaleDialogConfig(@NonNull String positiveButton,
+ @NonNull String negativeButton,
+ @NonNull String rationaleMsg,
+ @StyleRes int theme,
+ int requestCode,
+ @NonNull String[] permissions) {
+
+ this.positiveButton = positiveButton;
+ this.negativeButton = negativeButton;
+ this.rationaleMsg = rationaleMsg;
+ this.theme = theme;
+ this.requestCode = requestCode;
+ this.permissions = permissions;
+ }
+
+ RationaleDialogConfig(Bundle bundle) {
+ positiveButton = bundle.getString(KEY_POSITIVE_BUTTON);
+ negativeButton = bundle.getString(KEY_NEGATIVE_BUTTON);
+ rationaleMsg = bundle.getString(KEY_RATIONALE_MESSAGE);
+ theme = bundle.getInt(KEY_THEME);
+ requestCode = bundle.getInt(KEY_REQUEST_CODE);
+ permissions = bundle.getStringArray(KEY_PERMISSIONS);
+ }
+
+ Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_POSITIVE_BUTTON, positiveButton);
+ bundle.putString(KEY_NEGATIVE_BUTTON, negativeButton);
+ bundle.putString(KEY_RATIONALE_MESSAGE, rationaleMsg);
+ bundle.putInt(KEY_THEME, theme);
+ bundle.putInt(KEY_REQUEST_CODE, requestCode);
+ bundle.putStringArray(KEY_PERMISSIONS, permissions);
+
+ return bundle;
+ }
+
+ Dialog createDialog(Context context, Dialog.OnClickListener listener) {
+ Dialog dialog = new Dialog(context, R.style.Dialog_Transparent);
+ dialog.setCancelable(false);
+ dialog.setCanceledOnTouchOutside(false);
+ View view = LayoutInflater.from(context).inflate(R.layout.layout_permission_rationale_dialog, null);
+ View tv_rationale = view.findViewById(R.id.tv_rationale);
+ if (tv_rationale instanceof TextView) {
+ ((TextView) tv_rationale).setText(rationaleMsg);
+ }
+ View btn_cancel = view.findViewById(R.id.btn_cancel);
+ if (btn_cancel instanceof Button) {
+ ((Button) btn_cancel).setText(negativeButton);
+ btn_cancel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dialog.dismiss();
+ if (listener != null) {
+ listener.onClick(dialog, DialogInterface.BUTTON_NEGATIVE);
+ }
+ }
+ });
+ }
+ View btn_ok = view.findViewById(R.id.btn_ok);
+ if (btn_ok instanceof Button) {
+ ((Button) btn_ok).setText(positiveButton);
+ btn_ok.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dialog.dismiss();
+ if (listener != null) {
+ listener.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
+ }
+ }
+ });
+ }
+ dialog.setContentView(view);
+ dialog.create();
+ return dialog;
+ }
+
+ AlertDialog createSupportDialog(Context context, Dialog.OnClickListener listener) {
+ AlertDialog.Builder builder;
+ if (theme > 0) {
+ builder = new AlertDialog.Builder(context, theme);
+ } else {
+ builder = new AlertDialog.Builder(context);
+ }
+ return builder
+ .setCancelable(false)
+ .setPositiveButton(positiveButton, listener)
+ .setNegativeButton(negativeButton, listener)
+ .setMessage(rationaleMsg)
+ .create();
+ }
+
+ android.app.AlertDialog createFrameworkDialog(Context context, Dialog.OnClickListener listener) {
+ android.app.AlertDialog.Builder builder;
+ if (theme > 0) {
+ builder = new android.app.AlertDialog.Builder(context, theme);
+ } else {
+ builder = new android.app.AlertDialog.Builder(context);
+ }
+ return builder
+ .setCancelable(false)
+ .setPositiveButton(positiveButton, listener)
+ .setNegativeButton(negativeButton, listener)
+ .setMessage(rationaleMsg)
+ .create();
+ }
+
+}
diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/RationaleDialogFragment.java b/app/src/module_easypermission/java/com/yizhuan/erban/RationaleDialogFragment.java
new file mode 100644
index 000000000..eb9d04738
--- /dev/null
+++ b/app/src/module_easypermission/java/com/yizhuan/erban/RationaleDialogFragment.java
@@ -0,0 +1,113 @@
+package com.yizhuan.erban;
+
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.StyleRes;
+
+/**
+ * {@link DialogFragment} to display rationale for permission requests when the request comes from
+ * a Fragment or Activity that can host a Fragment.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class RationaleDialogFragment extends DialogFragment {
+
+ public static final String TAG = "RationaleDialogFragment";
+
+ private EasyPermissions.PermissionCallbacks mPermissionCallbacks;
+ private EasyPermissions.RationaleCallbacks mRationaleCallbacks;
+ private boolean mStateSaved = false;
+
+ public static RationaleDialogFragment newInstance(
+ @NonNull String positiveButton,
+ @NonNull String negativeButton,
+ @NonNull String rationaleMsg,
+ @StyleRes int theme,
+ int requestCode,
+ @NonNull String[] permissions) {
+
+ // Create new Fragment
+ RationaleDialogFragment dialogFragment = new RationaleDialogFragment();
+
+ // Initialize configuration as arguments
+ RationaleDialogConfig config = new RationaleDialogConfig(
+ positiveButton, negativeButton, rationaleMsg, theme, requestCode, permissions);
+ dialogFragment.setArguments(config.toBundle());
+
+ return dialogFragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && getParentFragment() != null) {
+ if (getParentFragment() instanceof EasyPermissions.PermissionCallbacks) {
+ mPermissionCallbacks = (EasyPermissions.PermissionCallbacks) getParentFragment();
+ }
+ if (getParentFragment() instanceof EasyPermissions.RationaleCallbacks){
+ mRationaleCallbacks = (EasyPermissions.RationaleCallbacks) getParentFragment();
+ }
+
+ }
+
+ if (context instanceof EasyPermissions.PermissionCallbacks) {
+ mPermissionCallbacks = (EasyPermissions.PermissionCallbacks) context;
+ }
+
+ if (context instanceof EasyPermissions.RationaleCallbacks) {
+ mRationaleCallbacks = (EasyPermissions.RationaleCallbacks) context;
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ mStateSaved = true;
+ super.onSaveInstanceState(outState);
+ }
+
+ /**
+ * Version of {@link #show(FragmentManager, String)} that no-ops when an IllegalStateException
+ * would otherwise occur.
+ */
+ public void showAllowingStateLoss(FragmentManager manager, String tag) {
+ // API 26 added this convenient method
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ if (manager.isStateSaved()) {
+ return;
+ }
+ }
+
+ if (mStateSaved) {
+ return;
+ }
+
+ show(manager, tag);
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mPermissionCallbacks = null;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ // Rationale dialog should not be cancelable
+ setCancelable(false);
+
+ // Get config from arguments, create click listener
+ RationaleDialogConfig config = new RationaleDialogConfig(getArguments());
+ RationaleDialogClickListener clickListener =
+ new RationaleDialogClickListener(this, config, mPermissionCallbacks, mRationaleCallbacks);
+
+ // Create an AlertDialog
+ return config.createDialog(getActivity(), clickListener);
+ }
+
+}
diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/RationaleDialogFragmentCompat.java b/app/src/module_easypermission/java/com/yizhuan/erban/RationaleDialogFragmentCompat.java
new file mode 100644
index 000000000..e23ddc6bc
--- /dev/null
+++ b/app/src/module_easypermission/java/com/yizhuan/erban/RationaleDialogFragmentCompat.java
@@ -0,0 +1,97 @@
+package com.yizhuan.erban;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.StyleRes;
+import androidx.fragment.app.FragmentManager;
+import androidx.appcompat.app.AppCompatDialogFragment;
+
+/**
+ * {@link AppCompatDialogFragment} to display rationale for permission requests when the request
+ * comes from a Fragment or Activity that can host a Fragment.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class RationaleDialogFragmentCompat extends AppCompatDialogFragment {
+
+ public static final String TAG = "RationaleDialogFragmentCompat";
+
+ private EasyPermissions.PermissionCallbacks mPermissionCallbacks;
+ private EasyPermissions.RationaleCallbacks mRationaleCallbacks;
+
+ public static RationaleDialogFragmentCompat newInstance(
+ @NonNull String rationaleMsg,
+ @NonNull String positiveButton,
+ @NonNull String negativeButton,
+ @StyleRes int theme,
+ int requestCode,
+ @NonNull String[] permissions) {
+
+ // Create new Fragment
+ RationaleDialogFragmentCompat dialogFragment = new RationaleDialogFragmentCompat();
+
+ // Initialize configuration as arguments
+ RationaleDialogConfig config = new RationaleDialogConfig(
+ positiveButton, negativeButton, rationaleMsg, theme, requestCode, permissions);
+ dialogFragment.setArguments(config.toBundle());
+
+ return dialogFragment;
+ }
+
+ /**
+ * Version of {@link #show(FragmentManager, String)} that no-ops when an IllegalStateException
+ * would otherwise occur.
+ */
+ public void showAllowingStateLoss(FragmentManager manager, String tag) {
+ if (manager.isStateSaved()) {
+ return;
+ }
+
+ show(manager, tag);
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (getParentFragment() != null) {
+ if (getParentFragment() instanceof EasyPermissions.PermissionCallbacks) {
+ mPermissionCallbacks = (EasyPermissions.PermissionCallbacks) getParentFragment();
+ }
+ if (getParentFragment() instanceof EasyPermissions.RationaleCallbacks){
+ mRationaleCallbacks = (EasyPermissions.RationaleCallbacks) getParentFragment();
+ }
+ }
+
+ if (context instanceof EasyPermissions.PermissionCallbacks) {
+ mPermissionCallbacks = (EasyPermissions.PermissionCallbacks) context;
+ }
+
+ if (context instanceof EasyPermissions.RationaleCallbacks) {
+ mRationaleCallbacks = (EasyPermissions.RationaleCallbacks) context;
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mPermissionCallbacks = null;
+ mRationaleCallbacks = null;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ // Rationale dialog should not be cancelable
+ setCancelable(false);
+
+ // Get config from arguments, create click listener
+ RationaleDialogConfig config = new RationaleDialogConfig(getArguments());
+ RationaleDialogClickListener clickListener =
+ new RationaleDialogClickListener(this, config, mPermissionCallbacks, mRationaleCallbacks);
+
+ // Create an AlertDialog
+ return config.createDialog(getContext(), clickListener);
+ }
+}
diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/helper/ActivityPermissionHelper.java b/app/src/module_easypermission/java/com/yizhuan/erban/helper/ActivityPermissionHelper.java
new file mode 100644
index 000000000..c7a7b45cf
--- /dev/null
+++ b/app/src/module_easypermission/java/com/yizhuan/erban/helper/ActivityPermissionHelper.java
@@ -0,0 +1,59 @@
+package com.yizhuan.erban.helper;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.annotation.StyleRes;
+import androidx.core.app.ActivityCompat;
+import android.util.Log;
+
+import com.yizhuan.erban.RationaleDialogFragment;
+
+/**
+ * Permissions helper for {@link Activity}.
+ */
+class ActivityPermissionHelper extends PermissionHelper {
+ private static final String TAG = "ActPermissionHelper";
+
+ public ActivityPermissionHelper(Activity host) {
+ super(host);
+ }
+
+ @Override
+ public void directRequestPermissions(int requestCode, @NonNull String... perms) {
+ ActivityCompat.requestPermissions(getHost(), perms, requestCode);
+ }
+
+ @Override
+ public boolean shouldShowRequestPermissionRationale(@NonNull String perm) {
+ return ActivityCompat.shouldShowRequestPermissionRationale(getHost(), perm);
+ }
+
+ @Override
+ public Context getContext() {
+ return getHost();
+ }
+
+ @Override
+ public void showRequestPermissionRationale(@NonNull String rationale,
+ @NonNull String positiveButton,
+ @NonNull String negativeButton,
+ @StyleRes int theme,
+ int requestCode,
+ @NonNull String... perms) {
+ FragmentManager fm = getHost().getFragmentManager();
+
+ // Check if fragment is already showing
+ Fragment fragment = fm.findFragmentByTag(RationaleDialogFragment.TAG);
+ if (fragment instanceof RationaleDialogFragment) {
+ Log.d(TAG, "Found existing fragment, not showing rationale.");
+ return;
+ }
+
+ RationaleDialogFragment
+ .newInstance(positiveButton, negativeButton, rationale, theme, requestCode, perms)
+ .showAllowingStateLoss(fm, RationaleDialogFragment.TAG);
+ }
+}
diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/helper/AppCompatActivityPermissionsHelper.java b/app/src/module_easypermission/java/com/yizhuan/erban/helper/AppCompatActivityPermissionsHelper.java
new file mode 100644
index 000000000..f324042ae
--- /dev/null
+++ b/app/src/module_easypermission/java/com/yizhuan/erban/helper/AppCompatActivityPermissionsHelper.java
@@ -0,0 +1,37 @@
+package com.yizhuan.erban.helper;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
+import androidx.fragment.app.FragmentManager;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * Permissions helper for {@link AppCompatActivity}.
+ */
+class AppCompatActivityPermissionsHelper extends BaseSupportPermissionsHelper {
+
+ public AppCompatActivityPermissionsHelper(AppCompatActivity host) {
+ super(host);
+ }
+
+ @Override
+ public FragmentManager getSupportFragmentManager() {
+ return getHost().getSupportFragmentManager();
+ }
+
+ @Override
+ public void directRequestPermissions(int requestCode, @NonNull String... perms) {
+ ActivityCompat.requestPermissions(getHost(), perms, requestCode);
+ }
+
+ @Override
+ public boolean shouldShowRequestPermissionRationale(@NonNull String perm) {
+ return ActivityCompat.shouldShowRequestPermissionRationale(getHost(), perm);
+ }
+
+ @Override
+ public Context getContext() {
+ return getHost();
+ }
+}
diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/helper/BaseSupportPermissionsHelper.java b/app/src/module_easypermission/java/com/yizhuan/erban/helper/BaseSupportPermissionsHelper.java
new file mode 100644
index 000000000..55ba05168
--- /dev/null
+++ b/app/src/module_easypermission/java/com/yizhuan/erban/helper/BaseSupportPermissionsHelper.java
@@ -0,0 +1,45 @@
+package com.yizhuan.erban.helper;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.StyleRes;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import android.util.Log;
+
+import com.yizhuan.erban.RationaleDialogFragmentCompat;
+
+/**
+ * Implementation of {@link PermissionHelper} for Support Library host classes.
+ */
+public abstract class BaseSupportPermissionsHelper extends PermissionHelper {
+
+ private static final String TAG = "BSPermissionsHelper";
+
+ public BaseSupportPermissionsHelper(@NonNull T host) {
+ super(host);
+ }
+
+ public abstract FragmentManager getSupportFragmentManager();
+
+ @Override
+ public void showRequestPermissionRationale(@NonNull String rationale,
+ @NonNull String positiveButton,
+ @NonNull String negativeButton,
+ @StyleRes int theme,
+ int requestCode,
+ @NonNull String... perms) {
+
+ FragmentManager fm = getSupportFragmentManager();
+
+ // Check if fragment is already showing
+ Fragment fragment = fm.findFragmentByTag(RationaleDialogFragmentCompat.TAG);
+ if (fragment instanceof RationaleDialogFragmentCompat) {
+ Log.d(TAG, "Found existing fragment, not showing rationale.");
+ return;
+ }
+
+ RationaleDialogFragmentCompat
+ .newInstance(rationale, positiveButton, negativeButton, theme, requestCode, perms)
+ .showAllowingStateLoss(fm, RationaleDialogFragmentCompat.TAG);
+ }
+}
diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/helper/LowApiPermissionsHelper.java b/app/src/module_easypermission/java/com/yizhuan/erban/helper/LowApiPermissionsHelper.java
new file mode 100644
index 000000000..0bd60387c
--- /dev/null
+++ b/app/src/module_easypermission/java/com/yizhuan/erban/helper/LowApiPermissionsHelper.java
@@ -0,0 +1,47 @@
+package com.yizhuan.erban.helper;
+
+import android.app.Activity;
+import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.annotation.StyleRes;
+import androidx.fragment.app.Fragment;
+
+/**
+ * Permissions helper for apps built against API < 23, which do not need runtime permissions.
+ */
+class LowApiPermissionsHelper extends PermissionHelper {
+ public LowApiPermissionsHelper(@NonNull T host) {
+ super(host);
+ }
+
+ @Override
+ public void directRequestPermissions(int requestCode, @NonNull String... perms) {
+ throw new IllegalStateException("Should never be requesting permissions on API < 23!");
+ }
+
+ @Override
+ public boolean shouldShowRequestPermissionRationale(@NonNull String perm) {
+ return false;
+ }
+
+ @Override
+ public void showRequestPermissionRationale(@NonNull String rationale,
+ @NonNull String positiveButton,
+ @NonNull String negativeButton,
+ @StyleRes int theme,
+ int requestCode,
+ @NonNull String... perms) {
+ throw new IllegalStateException("Should never be requesting permissions on API < 23!");
+ }
+
+ @Override
+ public Context getContext() {
+ if (getHost() instanceof Activity) {
+ return (Context) getHost();
+ } else if (getHost() instanceof Fragment) {
+ return ((Fragment) getHost()).getContext();
+ } else {
+ throw new IllegalStateException("Unknown host: " + getHost());
+ }
+ }
+}
diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/helper/PermissionHelper.java b/app/src/module_easypermission/java/com/yizhuan/erban/helper/PermissionHelper.java
new file mode 100644
index 000000000..4ff87fd57
--- /dev/null
+++ b/app/src/module_easypermission/java/com/yizhuan/erban/helper/PermissionHelper.java
@@ -0,0 +1,113 @@
+package com.yizhuan.erban.helper;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Build;
+import androidx.annotation.NonNull;
+import androidx.annotation.StyleRes;
+import androidx.fragment.app.Fragment;
+import androidx.appcompat.app.AppCompatActivity;
+
+import java.util.List;
+
+/**
+ * Delegate class to make permission calls based on the 'host' (Fragment, Activity, etc).
+ */
+public abstract class PermissionHelper {
+
+ private T mHost;
+
+ @NonNull
+ public static PermissionHelper extends Activity> newInstance(Activity host) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ return new LowApiPermissionsHelper<>(host);
+ }
+
+ if (host instanceof AppCompatActivity)
+ return new AppCompatActivityPermissionsHelper((AppCompatActivity) host);
+ else {
+ return new ActivityPermissionHelper(host);
+ }
+ }
+
+ @NonNull
+ public static PermissionHelper newInstance(Fragment host) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ return new LowApiPermissionsHelper<>(host);
+ }
+
+ return new SupportFragmentPermissionHelper(host);
+ }
+
+ // ============================================================================
+ // Public concrete methods
+ // ============================================================================
+
+ public PermissionHelper(@NonNull T host) {
+ mHost = host;
+ }
+
+ private boolean shouldShowRationale(@NonNull String... perms) {
+ for (String perm : perms) {
+ if (shouldShowRequestPermissionRationale(perm)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void requestPermissions(@NonNull String rationale,
+ @NonNull String positiveButton,
+ @NonNull String negativeButton,
+ @StyleRes int theme,
+ int requestCode,
+ @NonNull String... perms) {
+ if (shouldShowRationale(perms)) {
+ showRequestPermissionRationale(
+ rationale, positiveButton, negativeButton, theme, requestCode, perms);
+ } else {
+ directRequestPermissions(requestCode, perms);
+ }
+ }
+
+ public boolean somePermissionPermanentlyDenied(@NonNull List perms) {
+ for (String deniedPermission : perms) {
+ if (permissionPermanentlyDenied(deniedPermission)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public boolean permissionPermanentlyDenied(@NonNull String perms) {
+ return !shouldShowRequestPermissionRationale(perms);
+ }
+
+ public boolean somePermissionDenied(@NonNull String... perms) {
+ return shouldShowRationale(perms);
+ }
+
+ @NonNull
+ public T getHost() {
+ return mHost;
+ }
+
+ // ============================================================================
+ // Public abstract methods
+ // ============================================================================
+
+ public abstract void directRequestPermissions(int requestCode, @NonNull String... perms);
+
+ public abstract boolean shouldShowRequestPermissionRationale(@NonNull String perm);
+
+ public abstract void showRequestPermissionRationale(@NonNull String rationale,
+ @NonNull String positiveButton,
+ @NonNull String negativeButton,
+ @StyleRes int theme,
+ int requestCode,
+ @NonNull String... perms);
+
+ public abstract Context getContext();
+
+}
diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/helper/SupportFragmentPermissionHelper.java b/app/src/module_easypermission/java/com/yizhuan/erban/helper/SupportFragmentPermissionHelper.java
new file mode 100644
index 000000000..4afa4ceb5
--- /dev/null
+++ b/app/src/module_easypermission/java/com/yizhuan/erban/helper/SupportFragmentPermissionHelper.java
@@ -0,0 +1,36 @@
+package com.yizhuan.erban.helper;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+/**
+ * Permissions helper for {@link Fragment} from the support library.
+ */
+class SupportFragmentPermissionHelper extends BaseSupportPermissionsHelper {
+
+ public SupportFragmentPermissionHelper(@NonNull Fragment host) {
+ super(host);
+ }
+
+ @Override
+ public FragmentManager getSupportFragmentManager() {
+ return getHost().getChildFragmentManager();
+ }
+
+ @Override
+ public void directRequestPermissions(int requestCode, @NonNull String... perms) {
+ getHost().requestPermissions(perms, requestCode);
+ }
+
+ @Override
+ public boolean shouldShowRequestPermissionRationale(@NonNull String perm) {
+ return getHost().shouldShowRequestPermissionRationale(perm);
+ }
+
+ @Override
+ public Context getContext() {
+ return getHost().getActivity();
+ }
+}
diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/helper/package-info.java b/app/src/module_easypermission/java/com/yizhuan/erban/helper/package-info.java
new file mode 100644
index 000000000..4dad8370f
--- /dev/null
+++ b/app/src/module_easypermission/java/com/yizhuan/erban/helper/package-info.java
@@ -0,0 +1,4 @@
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package com.yizhuan.erban.helper;
+
+import androidx.annotation.RestrictTo;
diff --git a/app/src/module_easypermission/res/layout/layout_permission_rationale_dialog.xml b/app/src/module_easypermission/res/layout/layout_permission_rationale_dialog.xml
new file mode 100644
index 000000000..1ce5520e5
--- /dev/null
+++ b/app/src/module_easypermission/res/layout/layout_permission_rationale_dialog.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/module_easypermission/res/values/colors.xml b/app/src/module_easypermission/res/values/colors.xml
new file mode 100644
index 000000000..eab258e4c
--- /dev/null
+++ b/app/src/module_easypermission/res/values/colors.xml
@@ -0,0 +1,7 @@
+
+
+
+ #ff212121
+ @android:color/black
+ #ff80cbc4
+
diff --git a/app/src/module_easypermission/res/values/strings.xml b/app/src/module_easypermission/res/values/strings.xml
new file mode 100644
index 000000000..614e53396
--- /dev/null
+++ b/app/src/module_easypermission/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+ 如果沒有請求的權限,此應用程式可能無法正常工作。
+ 如果沒有請求的權限,此應用程式可能無法正常工作,打開應用設置頁面以修改應用權限。
+ 所需權限
+
diff --git a/app/src/module_easypermission/res/values/styles.xml b/app/src/module_easypermission/res/values/styles.xml
new file mode 100644
index 000000000..edb0562f9
--- /dev/null
+++ b/app/src/module_easypermission/res/values/styles.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/module_luban/java/com/yizhuan/erban/Checker.java b/app/src/module_luban/java/com/yizhuan/erban/Checker.java
new file mode 100644
index 000000000..badf89a44
--- /dev/null
+++ b/app/src/module_luban/java/com/yizhuan/erban/Checker.java
@@ -0,0 +1,224 @@
+package com.yizhuan.erban;
+
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+enum Checker {
+ SINGLE;
+
+ public static final String TAG = "Luban";
+
+ private static List format = new ArrayList<>();
+ private static final String JPG = ".jpg";
+ private static final String JPEG = ".jpeg";
+ private static final String PNG = ".png";
+ private static final String WEBP = ".webp";
+ private static final String GIF = ".gif";
+
+ private final byte[] JPEG_SIGNATURE = new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF};
+
+ static {
+ format.add(JPG);
+ format.add(JPEG);
+ format.add(PNG);
+ format.add(WEBP);
+ format.add(GIF);
+ }
+
+ /**
+ * Determine if it is JPG.
+ *
+ * @param is image file input stream
+ */
+ boolean isJPG(InputStream is) {
+ return isJPG(toByteArray(is));
+ }
+
+ /**
+ * Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
+ */
+ int getOrientation(InputStream is) {
+ return getOrientation(toByteArray(is));
+ }
+
+ private boolean isJPG(byte[] data) {
+ if (data == null || data.length < 3) {
+ return false;
+ }
+ byte[] signatureB = new byte[]{data[0], data[1], data[2]};
+ return Arrays.equals(JPEG_SIGNATURE, signatureB);
+ }
+
+ private int getOrientation(byte[] jpeg) {
+ if (jpeg == null) {
+ return 0;
+ }
+
+ int offset = 0;
+ int length = 0;
+
+ // ISO/IEC 10918-1:1993(E)
+ while (offset + 3 < jpeg.length && (jpeg[offset++] & 0xFF) == 0xFF) {
+ int marker = jpeg[offset] & 0xFF;
+
+ // Check if the marker is a padding.
+ if (marker == 0xFF) {
+ continue;
+ }
+ offset++;
+
+ // Check if the marker is SOI or TEM.
+ if (marker == 0xD8 || marker == 0x01) {
+ continue;
+ }
+ // Check if the marker is EOI or SOS.
+ if (marker == 0xD9 || marker == 0xDA) {
+ break;
+ }
+
+ // Get the length and check if it is reasonable.
+ length = pack(jpeg, offset, 2, false);
+ if (length < 2 || offset + length > jpeg.length) {
+ Log.e(TAG, "Invalid length");
+ return 0;
+ }
+
+ // Break if the marker is EXIF in APP1.
+ if (marker == 0xE1 && length >= 8
+ && pack(jpeg, offset + 2, 4, false) == 0x45786966
+ && pack(jpeg, offset + 6, 2, false) == 0) {
+ offset += 8;
+ length -= 8;
+ break;
+ }
+
+ // Skip other markers.
+ offset += length;
+ length = 0;
+ }
+
+ // JEITA CP-3451 Exif Version 2.2
+ if (length > 8) {
+ // Identify the byte order.
+ int tag = pack(jpeg, offset, 4, false);
+ if (tag != 0x49492A00 && tag != 0x4D4D002A) {
+ Log.e(TAG, "Invalid byte order");
+ return 0;
+ }
+ boolean littleEndian = (tag == 0x49492A00);
+
+ // Get the offset and check if it is reasonable.
+ int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
+ if (count < 10 || count > length) {
+ Log.e(TAG, "Invalid offset");
+ return 0;
+ }
+ offset += count;
+ length -= count;
+
+ // Get the count and go through all the elements.
+ count = pack(jpeg, offset - 2, 2, littleEndian);
+ while (count-- > 0 && length >= 12) {
+ // Get the tag and check if it is orientation.
+ tag = pack(jpeg, offset, 2, littleEndian);
+ if (tag == 0x0112) {
+ int orientation = pack(jpeg, offset + 8, 2, littleEndian);
+ switch (orientation) {
+ case 1:
+ return 0;
+ case 3:
+ return 180;
+ case 6:
+ return 90;
+ case 8:
+ return 270;
+ }
+ Log.e(TAG, "Unsupported orientation");
+ return 0;
+ }
+ offset += 12;
+ length -= 12;
+ }
+ }
+
+ Log.e(TAG, "Orientation not found");
+ return 0;
+ }
+
+ String extSuffix(InputStreamProvider input) {
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(input.open(), null, options);
+ return options.outMimeType.replace("image/", ".");
+ } catch (Exception e) {
+ return JPG;
+ }
+ }
+
+ boolean needCompress(int leastCompressSize, int maxSize, String path) {
+ //需求扩展:需要额外判断分辨率最大值限制
+ if (maxSize > 0) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(path, options);
+ if (options.outWidth > maxSize || options.outHeight > maxSize) {
+ return true;
+ }
+ }
+ if (leastCompressSize > 0) {
+ File source = new File(path);
+ return source.exists() && source.length() > (leastCompressSize << 10);
+ }
+ return true;
+ }
+
+ private int pack(byte[] bytes, int offset, int length, boolean littleEndian) {
+ int step = 1;
+ if (littleEndian) {
+ offset += length - 1;
+ step = -1;
+ }
+
+ int value = 0;
+ while (length-- > 0) {
+ value = (value << 8) | (bytes[offset] & 0xFF);
+ offset += step;
+ }
+ return value;
+ }
+
+ private byte[] toByteArray(InputStream is) {
+ if (is == null) {
+ return new byte[0];
+ }
+
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+ int read;
+ byte[] data = new byte[4096];
+
+ try {
+ while ((read = is.read(data, 0, data.length)) != -1) {
+ buffer.write(data, 0, read);
+ }
+ } catch (Exception ignored) {
+ return new byte[0];
+ } finally {
+ try {
+ buffer.close();
+ } catch (IOException ignored) {
+ }
+ }
+
+ return buffer.toByteArray();
+ }
+}
diff --git a/app/src/module_luban/java/com/yizhuan/erban/CompressionPredicate.java b/app/src/module_luban/java/com/yizhuan/erban/CompressionPredicate.java
new file mode 100644
index 000000000..01855c4ae
--- /dev/null
+++ b/app/src/module_luban/java/com/yizhuan/erban/CompressionPredicate.java
@@ -0,0 +1,20 @@
+package com.yizhuan.erban;
+
+/**
+ * Created on 2018/1/3 19:43
+ *
+ * @author andy
+ *
+ * Get the input stream through this interface, and obtain the picture using compatible files and FileProvider
+ */
+public interface InputStreamProvider {
+
+ InputStream open() throws IOException;
+
+ String getPath();
+}
diff --git a/app/src/module_luban/java/com/yizhuan/erban/Luban.java b/app/src/module_luban/java/com/yizhuan/erban/Luban.java
new file mode 100644
index 000000000..e140103d0
--- /dev/null
+++ b/app/src/module_luban/java/com/yizhuan/erban/Luban.java
@@ -0,0 +1,397 @@
+package com.yizhuan.erban;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+public class Luban implements Handler.Callback {
+ private static final String TAG = "Luban";
+ private static final String DEFAULT_DISK_CACHE_DIR = "luban_disk_cache";
+
+ private static final int MSG_COMPRESS_SUCCESS = 0;
+ private static final int MSG_COMPRESS_START = 1;
+ private static final int MSG_COMPRESS_ERROR = 2;
+
+ private String mTargetDir;
+ private boolean focusAlpha;
+ private int mLeastCompressSize;
+ private int mMostCompressSize;
+ private int mMaxSize;
+ private OnRenameListener mRenameListener;
+ private OnCompressListener mCompressListener;
+ private CompressionPredicate mCompressionPredicate;
+ private List mStreamProviders;
+
+ private Handler mHandler;
+
+ private Luban(Builder builder) {
+ this.mTargetDir = builder.mTargetDir;
+ this.mRenameListener = builder.mRenameListener;
+ this.mStreamProviders = builder.mStreamProviders;
+ this.mCompressListener = builder.mCompressListener;
+ this.mLeastCompressSize = builder.mLeastCompressSize;
+ this.mCompressionPredicate = builder.mCompressionPredicate;
+ this.mMostCompressSize = builder.mMostCompressSize;
+ this.mMaxSize = builder.mMaxSize;
+ mHandler = new Handler(Looper.getMainLooper(), this);
+ }
+
+ public static Builder with(Context context) {
+ return new Builder(context);
+ }
+
+ /**
+ * Returns a file with a cache image name in the private cache directory.
+ *
+ * @param context A context.
+ */
+ private File getImageCacheFile(Context context, String suffix) {
+ if (TextUtils.isEmpty(mTargetDir)) {
+ mTargetDir = getImageCacheDir(context).getAbsolutePath();
+ }
+
+ String cacheBuilder = mTargetDir + "/" +
+ System.currentTimeMillis() +
+ (int) (Math.random() * 1000) +
+ (TextUtils.isEmpty(suffix) ? ".jpg" : suffix);
+
+ return new File(cacheBuilder);
+ }
+
+ private File getImageCustomFile(Context context, String filename) {
+ if (TextUtils.isEmpty(mTargetDir)) {
+ mTargetDir = getImageCacheDir(context).getAbsolutePath();
+ }
+
+ String cacheBuilder = mTargetDir + "/" + filename;
+
+ return new File(cacheBuilder);
+ }
+
+ /**
+ * Returns a directory with a default name in the private cache directory of the application to
+ * use to store retrieved audio.
+ *
+ * @param context A context.
+ * @see #getImageCacheDir(Context, String)
+ */
+ private File getImageCacheDir(Context context) {
+ return getImageCacheDir(context, DEFAULT_DISK_CACHE_DIR);
+ }
+
+ /**
+ * Returns a directory with the given name in the private cache directory of the application to
+ * use to store retrieved media and thumbnails.
+ *
+ * @param context A context.
+ * @param cacheName The name of the subdirectory in which to store the cache.
+ * @see #getImageCacheDir(Context)
+ */
+ private static File getImageCacheDir(Context context, String cacheName) {
+ File cacheDir = context.getExternalCacheDir();
+ if (cacheDir != null) {
+ File result = new File(cacheDir, cacheName);
+ if (!result.mkdirs() && (!result.exists() || !result.isDirectory())) {
+ // File wasn't able to create a directory, or the result exists but not a directory
+ return null;
+ }
+ return result;
+ }
+ if (Log.isLoggable(TAG, Log.ERROR)) {
+ Log.e(TAG, "default disk cache dir is null");
+ }
+ return null;
+ }
+
+ /**
+ * start asynchronous compress thread
+ */
+ private void launch(final Context context) {
+ if (mStreamProviders == null || mStreamProviders.size() == 0 && mCompressListener != null) {
+ mCompressListener.onError(new NullPointerException("image file cannot be null"));
+ }
+
+ Iterator iterator = mStreamProviders.iterator();
+
+ while (iterator.hasNext()) {
+ final InputStreamProvider path = iterator.next();
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_START));
+
+ File result = compress(context, path);
+
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_SUCCESS, result));
+ } catch (IOException e) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_ERROR, e));
+ }
+ }
+ }).start();
+
+ iterator.remove();
+ }
+ }
+
+ /**
+ * start compress and return the file
+ */
+ private File get(InputStreamProvider input, Context context) throws IOException {
+ return new Engine(input, getImageCacheFile(context, Checker.SINGLE.extSuffix(input)), focusAlpha, mMaxSize, mMostCompressSize).compress();
+ }
+
+ private List get(Context context) throws IOException {
+ List results = new ArrayList<>();
+ Iterator iterator = mStreamProviders.iterator();
+
+ while (iterator.hasNext()) {
+ results.add(compress(context, iterator.next()));
+ iterator.remove();
+ }
+
+ return results;
+ }
+
+ private File compress(Context context, InputStreamProvider path) throws IOException {
+ File result;
+
+ File outFile = getImageCacheFile(context, Checker.SINGLE.extSuffix(path));
+
+ if (mRenameListener != null) {
+ String filename = mRenameListener.rename(path.getPath());
+ outFile = getImageCustomFile(context, filename);
+ }
+
+ if (mCompressionPredicate != null) {
+ if (mCompressionPredicate.apply(path.getPath())
+ && Checker.SINGLE.needCompress(mLeastCompressSize, mMaxSize, path.getPath())) {
+ result = new Engine(path, outFile, focusAlpha, mMaxSize, mMostCompressSize).compress();
+ } else {
+ result = new File(path.getPath());
+ }
+ } else {
+ result = Checker.SINGLE.needCompress(mLeastCompressSize, mMaxSize, path.getPath()) ?
+ new Engine(path, outFile, focusAlpha, mMaxSize, mMostCompressSize).compress() :
+ new File(path.getPath());
+ }
+
+ return result;
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (mCompressListener == null) return false;
+
+ switch (msg.what) {
+ case MSG_COMPRESS_START:
+ mCompressListener.onStart();
+ break;
+ case MSG_COMPRESS_SUCCESS:
+ mCompressListener.onSuccess((File) msg.obj);
+ break;
+ case MSG_COMPRESS_ERROR:
+ mCompressListener.onError((Throwable) msg.obj);
+ break;
+ }
+ return false;
+ }
+
+ public static class Builder {
+ private Context context;
+ private String mTargetDir;
+ private boolean focusAlpha;
+ private int mLeastCompressSize = 100;
+ //默认大小不能超过大小
+ private int mMostCompressSize = 0;
+ //分辨率 宽或高最大值
+ private int mMaxSize = 0;
+ private OnRenameListener mRenameListener;
+ private OnCompressListener mCompressListener;
+ private CompressionPredicate mCompressionPredicate;
+ private List mStreamProviders;
+
+ Builder(Context context) {
+ this.context = context;
+ this.mStreamProviders = new ArrayList<>();
+ }
+
+ private Luban build() {
+ return new Luban(this);
+ }
+
+ public Builder load(InputStreamProvider inputStreamProvider) {
+ mStreamProviders.add(inputStreamProvider);
+ return this;
+ }
+
+ public Builder load(final File file) {
+ mStreamProviders.add(new InputStreamProvider() {
+ @Override
+ public InputStream open() throws IOException {
+ return new FileInputStream(file);
+ }
+
+ @Override
+ public String getPath() {
+ return file.getAbsolutePath();
+ }
+ });
+ return this;
+ }
+
+ public Builder load(final String string) {
+ mStreamProviders.add(new InputStreamProvider() {
+ @Override
+ public InputStream open() throws IOException {
+ return new FileInputStream(string);
+ }
+
+ @Override
+ public String getPath() {
+ return string;
+ }
+ });
+ return this;
+ }
+
+ public Builder load(List list) {
+ for (T src : list) {
+ if (src instanceof String) {
+ load((String) src);
+ } else if (src instanceof File) {
+ load((File) src);
+ } else if (src instanceof Uri) {
+ load((Uri) src);
+ } else {
+ throw new IllegalArgumentException("Incoming data type exception, it must be String, File, Uri or Bitmap");
+ }
+ }
+ return this;
+ }
+
+ public Builder load(final Uri uri) {
+ mStreamProviders.add(new InputStreamProvider() {
+ @Override
+ public InputStream open() throws IOException {
+ return context.getContentResolver().openInputStream(uri);
+ }
+
+ @Override
+ public String getPath() {
+ return uri.getPath();
+ }
+ });
+ return this;
+ }
+
+ public Builder putGear(int gear) {
+ return this;
+ }
+
+ public Builder setRenameListener(OnRenameListener listener) {
+ this.mRenameListener = listener;
+ return this;
+ }
+
+ public Builder setCompressListener(OnCompressListener listener) {
+ this.mCompressListener = listener;
+ return this;
+ }
+
+ public Builder setTargetDir(String targetDir) {
+ this.mTargetDir = targetDir;
+ return this;
+ }
+
+ /**
+ * Do I need to keep the image's alpha channel
+ *
+ * @param focusAlpha
true - to keep alpha channel, the compress speed will be slow.
+ *
false - don't keep alpha channel, it might have a black background.
+ */
+ public Builder setFocusAlpha(boolean focusAlpha) {
+ this.focusAlpha = focusAlpha;
+ return this;
+ }
+
+ /**
+ * do not compress when the origin image file size less than one value
+ *
+ * @param size the value of file size, unit KB, default 100K
+ */
+ public Builder ignoreBy(int size) {
+ this.mLeastCompressSize = size;
+ return this;
+ }
+
+ /**
+ * do not compress when the origin image file size more than one value
+ *
+ * @param size the value of file size, unit KB
+ */
+ public Builder setMostCompressSize(int size) {
+ this.mMostCompressSize = size;
+ return this;
+ }
+
+ public Builder setMaxSize(int size) {
+ this.mMaxSize = size;
+ return this;
+ }
+
+ /**
+ * do compress image when return value was true, otherwise, do not compress the image file
+ *
+ * @param compressionPredicate A predicate callback that returns true or false for the given input path should be compressed.
+ */
+ public Builder filter(CompressionPredicate compressionPredicate) {
+ this.mCompressionPredicate = compressionPredicate;
+ return this;
+ }
+
+
+ /**
+ * begin compress image with asynchronous
+ */
+ public void launch() {
+ build().launch(context);
+ }
+
+ public File get(final String path) throws IOException {
+ return build().get(new InputStreamProvider() {
+ @Override
+ public InputStream open() throws IOException {
+ return new FileInputStream(path);
+ }
+
+ @Override
+ public String getPath() {
+ return path;
+ }
+ }, context);
+ }
+
+ /**
+ * begin compress image with synchronize
+ *
+ * @return the thumb image file list
+ */
+ public List get() throws IOException {
+ return build().get(context);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/module_luban/java/com/yizhuan/erban/OnCompressListener.java b/app/src/module_luban/java/com/yizhuan/erban/OnCompressListener.java
new file mode 100644
index 000000000..9efe69203
--- /dev/null
+++ b/app/src/module_luban/java/com/yizhuan/erban/OnCompressListener.java
@@ -0,0 +1,21 @@
+package com.yizhuan.erban;
+
+import java.io.File;
+
+public interface OnCompressListener {
+
+ /**
+ * Fired when the compression is started, override to handle in your own code
+ */
+ void onStart();
+
+ /**
+ * Fired when a compression returns successfully, override to handle in your own code
+ */
+ void onSuccess(File file);
+
+ /**
+ * Fired when a compression fails to complete, override to handle in your own code
+ */
+ void onError(Throwable e);
+}
diff --git a/app/src/module_luban/java/com/yizhuan/erban/OnRenameListener.java b/app/src/module_luban/java/com/yizhuan/erban/OnRenameListener.java
new file mode 100644
index 000000000..f26b4fe5e
--- /dev/null
+++ b/app/src/module_luban/java/com/yizhuan/erban/OnRenameListener.java
@@ -0,0 +1,22 @@
+package com.yizhuan.erban;
+
+/**
+ * Author: zibin
+ * Datetime: 2018/5/18
+ *
+ * 提供修改压缩图片命名接口
+ *
+ * A functional interface (callback) that used to rename the file after compress.
+ */
+public interface OnRenameListener {
+
+ /**
+ * 压缩前调用该方法用于修改压缩后文件名
+ *
+ * Call before compression begins.
+ *
+ * @param filePath 传入文件路径/ file path
+ * @return 返回重命名后的字符串/ file name
+ */
+ String rename(String filePath);
+}
diff --git a/core/src/main/java/com/yizhuan/xchat_android_core/Constants.java b/core/src/main/java/com/yizhuan/xchat_android_core/Constants.java
index 36b2ce100..64fdb0784 100644
--- a/core/src/main/java/com/yizhuan/xchat_android_core/Constants.java
+++ b/core/src/main/java/com/yizhuan/xchat_android_core/Constants.java
@@ -116,4 +116,11 @@ public class Constants {
public static final String ANDROID = "android";
+ //上传的图片 默认大小不能超过大小 640KB
+ public static final int UPLOAD_IMAGE_MAX_FILE_LENGTH = 640;
+ //上传的图片 默认宽高最大值 2340
+ public static final int UPLOAD_IMAGE_MAX_SIZE = 2340;
+ //上传的gif 默认大小不能超过 1MB
+ public static final int UPLOAD_GIF_MAX_SIZE = 1 << 20;
+
}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/session/actions/PickImageAction.java b/nim_uikit/src/com/netease/nim/uikit/business/session/actions/PickImageAction.java
index ae2a42bb7..41d054028 100644
--- a/nim_uikit/src/com/netease/nim/uikit/business/session/actions/PickImageAction.java
+++ b/nim_uikit/src/com/netease/nim/uikit/business/session/actions/PickImageAction.java
@@ -44,6 +44,21 @@ public abstract class PickImageAction extends BaseAction {
public void onClick() {
int requestCode = makeRequestCode(RequestCode.PICK_IMAGE);
showSelector(getTitleId(), requestCode, multiSelect, tempFile());
+
+// (DialogFragmentUtils.show("photoDialog", this, PhotoDialog::class.java) as? PhotoDialog)
+// ?.setOnResultCallBack(object : PhotoDialog.OnResultCallBack {
+// override fun takePhotoCallBack(path: String?) {
+// path?.also {
+// mGroupPhotoPath = it
+// }
+// }
+//
+// override fun choicePhotoCallBack(paths: List?) {
+// if (paths != null && paths.isNotEmpty()) {
+// mGroupPhotoPath = paths[0]
+// }
+// }
+// })
}
private String tempFile() {