diff --git a/app/build.gradle b/app/build.gradle index 44583fbdd..5e4eaea91 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -108,6 +108,8 @@ android { 'src/module_treasure_box/java', 'src/module_community/java', 'src/module_album/java', + 'src/module_easypermission/java', + 'src/module_luban/java', ] @@ -128,6 +130,7 @@ android { 'src/module_treasure_box/res', 'src/module_community/res', 'src/module_album/res', + 'src/module_easypermission/res', ] diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e77ec1818..f9d07e82e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -995,6 +995,11 @@ + \ No newline at end of file diff --git a/app/src/main/java/com/yizhuan/erban/MainActivity.java b/app/src/main/java/com/yizhuan/erban/MainActivity.java index 5e0de8a01..903b36548 100644 --- a/app/src/main/java/com/yizhuan/erban/MainActivity.java +++ b/app/src/main/java/com/yizhuan/erban/MainActivity.java @@ -40,7 +40,7 @@ import com.orhanobut.logger.Logger; import com.tencent.bugly.crashreport.CrashReport; import com.tongdaxing.erban.upgrade.AppUpgradeHelper; import com.trello.rxlifecycle3.android.ActivityEvent; -import com.yizhuan.erban.application.ActivityStackManager; +import com.yizhuan.erban.application.XChatApplication; import com.yizhuan.erban.avroom.activity.AVRoomActivity; import com.yizhuan.erban.base.BaseMvpActivity; import com.yizhuan.erban.common.widget.CircleImageView; @@ -800,8 +800,8 @@ public class MainActivity extends BaseMvpActivity @Subscribe(threadMode = ThreadMode.MAIN) public void onImPushMsgPmLimitTimeEvent(ImPushMsgPmLimitTimeEvent event) { //先判断Avroom存不存在,存在的话则不处理,avroom会处理 - WeakReference reference = ActivityStackManager.getInstance().getAvRoomActWeakRef(); - if (reference != null && reference.get() != null) { + Activity reference = XChatApplication.gStack.getAvRoomActivity(); + if (reference != null) { return; } getLimitEnterRoomHelper().handleThisContext(this, event.getData(), true, this::handlePmExitRoom); diff --git a/app/src/main/java/com/yizhuan/erban/application/ActivityLifeManager.java b/app/src/main/java/com/yizhuan/erban/application/ActivityLifeManager.java deleted file mode 100644 index b11b7c9f8..000000000 --- a/app/src/main/java/com/yizhuan/erban/application/ActivityLifeManager.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.yizhuan.erban.application; - -import android.app.Activity; -import android.app.Application; -import android.os.Bundle; - -import com.adjust.sdk.Adjust; -import com.yizhuan.erban.location.LocationManager; -import com.yizhuan.xchat_android_core.auth.AuthModel; - -/** - * activity的管理 - * create by lvzebiao @2019/3/13 - */ -public class ActivityLifeManager implements Application.ActivityLifecycleCallbacks { - - private int count = 0; - - @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) { - ActivityStackManager.getInstance().addActivity(activity); - } - - @Override - public void onActivityStarted(Activity activity) { - if (count == 0) { - //应用回到前台了 - //这里进行暗号的判断 - //PwdCodeMgr.get().handlePopPwdCodeWindow(activity); - if (AuthModel.get().isImLogin()) { - //签到弹窗 -// SignDialogTimeManager.checkSignDialog(activity, true); - LocationManager.uploadLocation(activity); - } - } - count++; - } - - @Override - public void onActivityStopped(Activity activity) { - count--; - if (count < 0) { - count = 0; - } - if (count == 0) { - //应用隐藏到后台了 - } - } - - @Override - public void onActivityResumed(Activity activity) { - ActivityStackManager.getInstance().setTopUpgradeActivity(activity); - Adjust.onResume(); - } - - @Override - public void onActivityPaused(Activity activity) { - Adjust.onPause(); - } - - - @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) { - - } - - @Override - public void onActivityDestroyed(Activity activity) { - ActivityStackManager.getInstance().removeActivity(activity); - } - - -} diff --git a/app/src/main/java/com/yizhuan/erban/application/ActivityStackManager.java b/app/src/main/java/com/yizhuan/erban/application/ActivityStackManager.java deleted file mode 100644 index e9f03856b..000000000 --- a/app/src/main/java/com/yizhuan/erban/application/ActivityStackManager.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.yizhuan.erban.application; - -import android.app.Activity; - -import com.yizhuan.erban.avroom.activity.AVRoomActivity; -import com.yizhuan.erban.utils.ActWhiteListMrg; - -import java.lang.ref.WeakReference; - -import lombok.Getter; - - -/** - * create by lvzebiao @2019/8/8 - */ -public class ActivityStackManager { - - /** - * 用于保持最顶的Activity实例,排除中转的activity - * 比如{@link com.yizhuan.erban.MiddleActivity} - */ - @Getter - private WeakReference topUpgradeWeakRef; - - @Getter - private WeakReference avRoomActWeakRef; - - public void setTopUpgradeActivity(Activity activity) { - if (!ActWhiteListMrg.isTempActivity(activity)) { - topUpgradeWeakRef = new WeakReference<>(activity); - } - } - - public void addActivity(Activity activity){ - if (activity instanceof AVRoomActivity) { - avRoomActWeakRef = new WeakReference<>(activity); - } - } - - public void removeActivity(Activity activity){ - if (activity instanceof AVRoomActivity) { - avRoomActWeakRef = null; - } - } - - private static final class Helper { - public static final ActivityStackManager INSTANCE = new ActivityStackManager(); - } - - public static ActivityStackManager getInstance() { - return Helper.INSTANCE; - } - -} diff --git a/app/src/main/java/com/yizhuan/erban/application/GlobalHandleManager.java b/app/src/main/java/com/yizhuan/erban/application/GlobalHandleManager.java index 4934cbaab..571d057a1 100644 --- a/app/src/main/java/com/yizhuan/erban/application/GlobalHandleManager.java +++ b/app/src/main/java/com/yizhuan/erban/application/GlobalHandleManager.java @@ -52,12 +52,7 @@ public class GlobalHandleManager { } public Activity getActivity() { - WeakReference weakReference = ActivityStackManager.getInstance() - .getTopUpgradeWeakRef(); - if (weakReference == null || weakReference.get() == null) { - return null; - } - return weakReference.get(); + return XChatApplication.gStack.getTopActivity(); } @Subscribe(threadMode = ThreadMode.MAIN) diff --git a/app/src/main/java/com/yizhuan/erban/application/XChatApplication.java b/app/src/main/java/com/yizhuan/erban/application/XChatApplication.java index f4895463c..b156b8ae7 100644 --- a/app/src/main/java/com/yizhuan/erban/application/XChatApplication.java +++ b/app/src/main/java/com/yizhuan/erban/application/XChatApplication.java @@ -3,7 +3,6 @@ package com.yizhuan.erban.application; import static com.yizhuan.xchat_android_core.im.custom.bean.CustomAttachment.CUSTOM_MSG_HEADER_TYPE_OPEN_ROOM_NOTI; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.app.Activity; import android.app.Application; import android.content.Context; @@ -50,6 +49,8 @@ import com.yizhuan.erban.AgentActivity; import com.yizhuan.erban.BuildConfig; import com.yizhuan.erban.NimMiddleActivity; import com.yizhuan.erban.R; +import com.yizhuan.erban.common.app.ActivityStack; +import com.yizhuan.erban.common.util.AppLifeCycleHelper; import com.yizhuan.erban.module_hall.HallDataManager; import com.yizhuan.erban.other.activity.SplashActivity; import com.yizhuan.erban.radish.wallet.RadishWalletManager; @@ -167,7 +168,8 @@ public class XChatApplication extends Application { }; private static XChatApplication instance; //生命周期监听 - private static ActivityLifeManager lifeManager; + private static AppLifeCycleHelper lifeCycleHelper; + public static ActivityStack gStack = new ActivityStack(); private static boolean isInitOtherSDK = false; //static 代码段可以防止内存泄露 @@ -256,10 +258,10 @@ public class XChatApplication extends Application { } CrashReport.initCrashReport(context, BuildConfig.DEBUG ? XChatConstants.BUGLY_KEY_DEBUG : XChatConstants.BUGLY_KEY_RELEASE, BuildConfig.DEBUG); //生命周期监听 - if (lifeManager == null) { - lifeManager = new ActivityLifeManager(); + if (lifeCycleHelper == null) { + lifeCycleHelper = new AppLifeCycleHelper(); } - registerActivityLifecycleCallback(lifeManager); + registerActivityLifecycleCallback(lifeCycleHelper); initLinkedMe(); MobSDK.init(context); MobSDK.submitPolicyGrantResult(true); @@ -280,6 +282,10 @@ public class XChatApplication extends Application { } + public static void registerActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks) { + ((Application) BasicConfig.INSTANCE.getAppContext().getApplicationContext()).registerActivityLifecycleCallbacks(callbacks); + } + /** * 初始化linkedMe */ @@ -315,12 +321,6 @@ public class XChatApplication extends Application { BasicConfig.INSTANCE.setImageDir(Constants.IMAGE_CACHE_DIR); } - - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - public static void registerActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks) { - ((Application) BasicConfig.INSTANCE.getAppContext().getApplicationContext()).registerActivityLifecycleCallbacks(callbacks); - } - /** * 云信配置 * @@ -554,9 +554,9 @@ public class XChatApplication extends Application { public void onTerminate() { super.onTerminate(); CrashReport.closeBugly(); - if (lifeManager != null) { - unregisterActivityLifecycleCallbacks(lifeManager); - } GlobalHandleManager.get().unInit(); + if (lifeCycleHelper != null) { + unregisterActivityLifecycleCallbacks(lifeCycleHelper); + } } } diff --git a/app/src/main/java/com/yizhuan/erban/common/app/ActivityStack.java b/app/src/main/java/com/yizhuan/erban/common/app/ActivityStack.java new file mode 100644 index 000000000..21fcae9dc --- /dev/null +++ b/app/src/main/java/com/yizhuan/erban/common/app/ActivityStack.java @@ -0,0 +1,161 @@ +package com.yizhuan.erban.common.app; + +import android.app.Activity; +import android.content.Context; + +import com.yizhuan.erban.application.XChatApplication; +import com.yizhuan.erban.avroom.activity.AVRoomActivity; +import com.yizhuan.erban.utils.ActWhiteListMrg; +import com.yizhuan.xchat_android_core.utils.Logger; + +import java.lang.ref.WeakReference; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Activity栈管理类 + */ +public class ActivityStack { + private static final String TAG = "ActivityStack"; + private final CopyOnWriteArrayList> mActivity = new CopyOnWriteArrayList<>(); + private WeakReference mTopActivity; + private WeakReference mAvRoomActivity; + + public int getActivityNum() { + return mActivity.size(); + } + + public void finishAllActivity() { + for (WeakReference activity : mActivity) { + if (!isActivityValid(activity)) { + continue; + } + activity.get().finish(); + } + mActivity.clear(); + } + + public boolean isActivityExist(Class clazz) { + return getActivity(clazz) != null; + } + + public Activity getActivity(Class clazz) { + for (WeakReference activity : mActivity) { + if (!isActivityValid(activity) || !activity.get().getClass().equals(clazz)) { + continue; + } + return activity.get(); + } + return null; + } + + public Context getTopContext() { + Context context = null; + if (isActivityValid(mTopActivity)) { + context = mTopActivity.get(); + } else { + if (mActivity.size() > 0) { + context = mActivity.get(mActivity.size() - 1).get(); + } + if (context == null) { + context = XChatApplication.gContext; + } + } + return context; + } + + public Activity getTopActivity() { + return mTopActivity == null ? null : mTopActivity.get(); + } + + public Activity getAvRoomActivity() { + return mAvRoomActivity == null ? null : mAvRoomActivity.get(); + } + + public Activity getActivityUnderTop() { + if (getActivityNum() < 2) { + return null; + } + return mActivity.get(mActivity.size() - 2).get(); + } + + public void onActivityCreated(Activity activity) { + if (activity == null) { + return; + } + Activity existActivity = getActivity(activity.getClass()); + if (existActivity != null) { + Logger.error(TAG, "已存在多个相同的实例:" + existActivity.getClass()); + existActivity.finish();//如果onActivityCreated回调时返回栈里面已存在当前Activity的实例,则先关掉之前的Activity,避免创建过多实例消耗太多内存 + } + for (WeakReference ac : mActivity) { + Activity realActivity = ac.get(); + if (realActivity == null || realActivity != activity) { + continue; + } + mActivity.remove(ac); + break; + } + mActivity.add(new WeakReference<>(activity)); + if (!ActWhiteListMrg.isTempActivity(activity)) { + if (mTopActivity == null) { + mTopActivity = new WeakReference<>(activity); + } else if (mTopActivity.get() != activity) { + mTopActivity.clear(); + mTopActivity = new WeakReference<>(activity); + } + } + if (activity instanceof AVRoomActivity) { + if (mAvRoomActivity == null) { + mAvRoomActivity = new WeakReference<>(activity); + } else if (mAvRoomActivity.get() != activity) { + mAvRoomActivity.clear(); + mAvRoomActivity = new WeakReference<>(activity); + } + } + } + + public void onActivityResumed(Activity activity) { + if (activity == null) { + return; + } + if (!ActWhiteListMrg.isTempActivity(activity)) { + if (mTopActivity == null) { + mTopActivity = new WeakReference<>(activity); + } else if (mTopActivity.get() != activity) { + mTopActivity.clear(); + mTopActivity = new WeakReference<>(activity); + } + } + if (activity instanceof AVRoomActivity) { + if (mAvRoomActivity == null) { + mAvRoomActivity = new WeakReference<>(activity); + } else if (mAvRoomActivity.get() != activity) { + mAvRoomActivity.clear(); + mAvRoomActivity = new WeakReference<>(activity); + } + } + } + + public void onActivityDestroyed(Activity activity) { + if (activity == null) { + return; + } + for (WeakReference ac : mActivity) { + Activity realActivity = ac.get(); + if (realActivity == null || realActivity != activity) { + continue; + } + mActivity.remove(ac); + break; + } + if (activity instanceof AVRoomActivity) { + mAvRoomActivity.clear(); + mAvRoomActivity = null; + } + } + + private boolean isActivityValid(WeakReference activityWeakReference) { + return activityWeakReference != null && activityWeakReference.get() != null && !activityWeakReference.get().isFinishing() && !activityWeakReference.get().isDestroyed(); + } + +} diff --git a/app/src/main/java/com/yizhuan/erban/common/dialog/PhotoDialog.kt b/app/src/main/java/com/yizhuan/erban/common/dialog/PhotoDialog.kt new file mode 100644 index 000000000..ea5c66ec4 --- /dev/null +++ b/app/src/main/java/com/yizhuan/erban/common/dialog/PhotoDialog.kt @@ -0,0 +1,279 @@ +package com.yizhuan.erban.common.dialog + +import android.Manifest +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.Settings +import android.text.TextUtils +import android.view.Gravity +import android.view.Window +import android.view.WindowManager +import com.hjq.toast.ToastUtils +import kotlinx.coroutines.Job +import com.yizhuan.erban.EasyPermissions +import com.yizhuan.erban.R +import com.yizhuan.erban.application.XChatApplication +import com.yizhuan.erban.base.BaseDialogFragment +import com.yizhuan.erban.common.photo.PhotoProvider +import com.yizhuan.erban.common.util.PhotoCompressCallback +import com.yizhuan.erban.common.util.PhotoCompressUtil +import com.yizhuan.erban.common.util.PhotosCompressCallback +import com.yizhuan.erban.databinding.PhotoDialogBinding +import com.yizhuan.erban.ui.setting.ModifyPwdActivity +import com.yizhuan.erban.ui.widget.dialog.CommonTipDialog +import com.yizhuan.xchat_android_library.utils.ResUtil + +/** + * 该对话框的功能提供拍摄和选择图片 + */ +class PhotoDialog : BaseDialogFragment(), EasyPermissions.PermissionCallbacks { + private var mOnResultCallBack: OnResultCallBack? = null + private var mJob: Job? = null + + companion object { + private const val PERMISSION_CODE_CAMERA = 100 + private const val REQUEST_CODE_CAMERA = 101 + private const val PERMISSION_CODE_STORAGE_1 = 200 + private const val REQUEST_CODE_STORAGE_1 = 201 + private const val PERMISSION_CODE_STORAGE_2 = 202 + private const val REQUEST_CODE_STORAGE_2 = 203 + private const val REQUEST_CODE_OPEN_PHOTO_PROVIDER = 103 + private const val REQUEST_CODE_OPEN_CAMERA_PROVIDER = 104 + } + + override fun initBefore(savedInstanceState: Bundle?) { + super.initBefore(savedInstanceState) + dialog?.requestWindowFeature(Window.FEATURE_NO_TITLE) + } + + override fun setListener() { + binding?.tvTakePhoto?.setOnClickListener { + checkCameraPermission() + } + binding?.tvChoicePicture?.setOnClickListener { + checkStoragePermission1() + } + } + + private fun initDialog() { + dialog?.window?.also { + it.decorView.setPadding(0, 0, 0, 0) + it.attributes = it.attributes.apply { + gravity = Gravity.BOTTOM + width = WindowManager.LayoutParams.MATCH_PARENT + } + it.setBackgroundDrawableResource(R.drawable.photo_dialog_bg) + } + } + + override fun onStart() { + super.onStart() + initDialog() + } + + private fun checkStoragePermission1() { + if (!EasyPermissions.hasPermissions( + XChatApplication.gContext, Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ) + ) { + EasyPermissions.requestPermissions( + this, + getString(R.string.permission_storage_rationale), + PERMISSION_CODE_STORAGE_1, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ) + } else { + PhotoProvider.photoProvider(this, resultCode = REQUEST_CODE_OPEN_PHOTO_PROVIDER) + } + } + + private fun checkCameraPermission() { + if (!EasyPermissions.hasPermissions(XChatApplication.gContext, Manifest.permission.CAMERA)) { + EasyPermissions.requestPermissions( + this, + getString(R.string.permission_camera_rationale), + PERMISSION_CODE_CAMERA, + Manifest.permission.CAMERA + ) + } else { + checkStoragePermission2() + } + } + + private fun checkStoragePermission2() { + if (!EasyPermissions.hasPermissions( + XChatApplication.gContext, Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ) + ) { + EasyPermissions.requestPermissions( + this, + getString(R.string.permission_storage_rationale), + PERMISSION_CODE_STORAGE_2, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ) + } else { + PhotoProvider.photoCamera(this, REQUEST_CODE_OPEN_CAMERA_PROVIDER) + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) + } + + override fun onPermissionsGranted(requestCode: Int, perms: MutableList) { + if (requestCode == PERMISSION_CODE_CAMERA) { + checkCameraPermission() + } else if (requestCode == PERMISSION_CODE_STORAGE_1) { + checkStoragePermission1() + } else if (requestCode == PERMISSION_CODE_STORAGE_2) { + checkStoragePermission2() + } + } + + override fun onPermissionsDenied(requestCode: Int, perms: MutableList) { + if (requestCode == PERMISSION_CODE_STORAGE_1 || requestCode == PERMISSION_CODE_STORAGE_2 || requestCode == PERMISSION_CODE_CAMERA) { + val requestTip: String = + if (requestCode == PERMISSION_CODE_STORAGE_1 || requestCode == PERMISSION_CODE_STORAGE_2) { + getString(R.string.permission_storage_denied) + } else { + getString(R.string.permission_camera_denied) + } + val mPrivacyDialog = CommonTipDialog(context) + mPrivacyDialog.setTipMsg(requestTip) + mPrivacyDialog.setOkText(getString(R.string.room_perform_go_update)) + mPrivacyDialog.setOnActionListener( + object : CommonTipDialog.OnActionListener { + override fun onOk() { + //同意跳到应用详情页面 + val packageUri = + Uri.parse("package:${activity?.packageName}") + val intent = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + packageUri + ) + if (requestCode == PERMISSION_CODE_STORAGE_1) { + startActivityForResult( + intent, REQUEST_CODE_STORAGE_1 + ) + } else if (requestCode == PERMISSION_CODE_STORAGE_2) { + startActivityForResult( + intent, + REQUEST_CODE_STORAGE_2 + ) + } else { + startActivityForResult( + intent, REQUEST_CODE_CAMERA + ) + } + } + + override fun onCancel() { + super.onCancel() + //取消跳到应用详情页面 + if (requestCode == PERMISSION_CODE_STORAGE_1 || requestCode == PERMISSION_CODE_STORAGE_2) { + ToastUtils.show(getString(R.string.permission_storage_refused)) + } else { + ToastUtils.show(getString(R.string.permission_camera_refused)) + } + } + } + ) + mPrivacyDialog.show() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CODE_STORAGE_1) { + checkStoragePermission1() + } else if (requestCode == REQUEST_CODE_STORAGE_2) { + checkStoragePermission2() + } else if (requestCode == REQUEST_CODE_CAMERA) { + checkCameraPermission() + } else + if (resultCode == RESULT_OK) { + when (requestCode) { + REQUEST_CODE_OPEN_CAMERA_PROVIDER -> { + if (mOnResultCallBack == null || data == null) return + PhotoProvider.getResultPathListAsync(data) { paths -> + val list = paths?.toMutableList() ?: ArrayList() + val path = list[0] + if (!TextUtils.isEmpty(path)) { + mJob?.cancel() + mJob = PhotoCompressUtil.compress( + XChatApplication.gContext, + path, + PhotoCompressUtil.getCompressCachePath(), + object : PhotoCompressCallback { + override fun onSuccess(compressedImg: String) { + mOnResultCallBack?.takePhotoCallBack(compressedImg) + } + + override fun onFail(e: Throwable) { + mOnResultCallBack?.takePhotoCallBack(path) + } + }) + + } else { + mOnResultCallBack?.takePhotoCallBack(path) + } + } + } + REQUEST_CODE_OPEN_PHOTO_PROVIDER -> { + if (mOnResultCallBack == null || data == null) return + PhotoProvider.getResultPathListAsync(data) { list -> + val paths = list?.toMutableList() ?: ArrayList() + if (paths.isEmpty()) { + mOnResultCallBack?.choicePhotoCallBack(paths) + } else { + mJob?.cancel() + mJob = PhotoCompressUtil.compress( + XChatApplication.gContext, + paths, + PhotoCompressUtil.getCompressCachePath(), + object : PhotosCompressCallback { + override fun onSuccess(compressedImgList: ArrayList) { + mOnResultCallBack?.choicePhotoCallBack(compressedImgList) + } + + override fun onFail(e: Throwable) { + mOnResultCallBack?.choicePhotoCallBack(paths) + } + + }) + } + } + } + } + dismissAllowingStateLoss() + } + } + + fun setOnResultCallBack(onResultCallBack: OnResultCallBack) { + mOnResultCallBack = onResultCallBack + } + + override fun onWillDestroy() { + mJob?.cancel() + super.onWillDestroy() + } + + interface OnResultCallBack { + + fun takePhotoCallBack(path: String?) + + fun choicePhotoCallBack(paths: List?) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/yizhuan/erban/common/photo/PhotoProvider.kt b/app/src/main/java/com/yizhuan/erban/common/photo/PhotoProvider.kt index c29603dbf..7cd4be99b 100644 --- a/app/src/main/java/com/yizhuan/erban/common/photo/PhotoProvider.kt +++ b/app/src/main/java/com/yizhuan/erban/common/photo/PhotoProvider.kt @@ -195,7 +195,9 @@ object PhotoProvider { * 清除复制缓存 */ fun clearCache() { - Logger.debug(TAG, "clearCache => mLastSelectTime: ${TimeUtils.getDateTimeString(mLastSelectTime, TIME_FORMAT)}") + Logger.debug( + TAG, "clearCache => mLastSelectTime: ${TimeUtils.getDateTimeString( + mLastSelectTime, TIME_FORMAT)}") FileHelper.removeAllFile(getInternalPath() + File.separator) } diff --git a/app/src/main/java/com/yizhuan/erban/common/util/AppLifeCycleHelper.java b/app/src/main/java/com/yizhuan/erban/common/util/AppLifeCycleHelper.java new file mode 100644 index 000000000..0d512727f --- /dev/null +++ b/app/src/main/java/com/yizhuan/erban/common/util/AppLifeCycleHelper.java @@ -0,0 +1,90 @@ +package com.yizhuan.erban.common.util; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.adjust.sdk.Adjust; +import com.yizhuan.erban.application.XChatApplication; +import com.yizhuan.erban.location.LocationManager; +import com.yizhuan.xchat_android_core.auth.AuthModel; + +/** + * Activity生命周期工具类 + */ +public class AppLifeCycleHelper implements Application.ActivityLifecycleCallbacks{ + private static boolean sBackground = false; + private static int sActivityReferences = 0; + private static boolean sIsActivityChangingConfigurations = false; + private static OnAppVisibleChange sOnAppVisibleChange = new OnAppVisibleChange(); + + @Override + public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) { + XChatApplication.gStack.onActivityCreated(activity); + } + + @Override + public void onActivityStarted(@NonNull Activity activity) { + if (++AppLifeCycleHelper.sActivityReferences == 1 && !AppLifeCycleHelper.sIsActivityChangingConfigurations) { + AppLifeCycleHelper.onForeground(); + + //应用回到前台了 + //这里进行暗号的判断 + //PwdCodeMgr.get().handlePopPwdCodeWindow(activity); + if (AuthModel.get().isImLogin()) { + //签到弹窗 +// SignDialogTimeManager.checkSignDialog(activity, true); + LocationManager.uploadLocation(activity); + } + } + } + + @Override + public void onActivityResumed(@NonNull Activity activity) { + XChatApplication.gStack.onActivityResumed(activity); + Adjust.onResume(); + } + + @Override + public void onActivityPaused(@NonNull Activity activity) { + Adjust.onPause(); + } + + @Override + public void onActivityStopped(@NonNull Activity activity) { + AppLifeCycleHelper.sIsActivityChangingConfigurations = activity.isChangingConfigurations(); + if (--AppLifeCycleHelper.sActivityReferences == 0 && !AppLifeCycleHelper.sIsActivityChangingConfigurations) { + AppLifeCycleHelper.onBackground(); + } + } + + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle bundle) { + + } + + @Override + public void onActivityDestroyed(@NonNull Activity activity) { + XChatApplication.gStack.onActivityDestroyed(activity); + } + + private static void onForeground() { + sBackground = false; + CoreUtils.send(sOnAppVisibleChange); + } + + private static void onBackground() { + sBackground = true; + CoreUtils.send(sOnAppVisibleChange); + } + + public static boolean isBackground() { + return sBackground; + } + + public static class OnAppVisibleChange { + } +} + diff --git a/app/src/main/java/com/yizhuan/erban/common/util/CoreUtils.java b/app/src/main/java/com/yizhuan/erban/common/util/CoreUtils.java new file mode 100644 index 000000000..1d920f62b --- /dev/null +++ b/app/src/main/java/com/yizhuan/erban/common/util/CoreUtils.java @@ -0,0 +1,61 @@ +package com.yizhuan.erban.common.util; + +import com.yizhuan.xchat_android_core.utils.Logger; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.EventBusException; + +public class CoreUtils { + private static final String TAG = "CoreUtils"; + private static EventBus sEventBus; + + public static void send(T moduleCallback) { + if (moduleCallback == null) { + Logger.warn(TAG, "moduleCallback == null"); + return; + } + try { + CoreUtils.getEventBus().post(moduleCallback); + } catch (Exception e) { + Logger.error(TAG, "EventBus exception", e); + } + } + + public static void register(T receiver) { + try { + if (!CoreUtils.getEventBus().isRegistered(receiver)) { + CoreUtils.getEventBus().register(receiver); + } + } catch (EventBusException e) { + //ignore + } catch (Exception e) { + Logger.error(TAG, "register error", e); + } + } + + public static void unregister(T receiver) { + try { + if (CoreUtils.getEventBus().isRegistered(receiver)) { + CoreUtils.getEventBus().unregister(receiver); + } + } catch (EventBusException e) { + //ignore + } catch (Exception e) { + Logger.error(TAG, "unregister error", e); + } + } + + private static EventBus getEventBus() { + if (sEventBus == null) { + synchronized (CoreUtils.class) { + if (sEventBus == null) { + sEventBus = EventBus.builder().throwSubscriberException(false) + .logSubscriberExceptions(false).logNoSubscriberMessages(false) + .build(); + } + } + } + return sEventBus; + } + +} diff --git a/app/src/main/java/com/yizhuan/erban/common/util/DialogFragmentUtils.java b/app/src/main/java/com/yizhuan/erban/common/util/DialogFragmentUtils.java new file mode 100644 index 000000000..5d6fba5c1 --- /dev/null +++ b/app/src/main/java/com/yizhuan/erban/common/util/DialogFragmentUtils.java @@ -0,0 +1,224 @@ +package com.yizhuan.erban.common.util; + +import android.app.Activity; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import com.yizhuan.erban.base.BaseActivity; +import com.yizhuan.erban.base.BaseDialogFragment; +import com.yizhuan.xchat_android_core.utils.Logger; + +import java.util.List; + + +/** + * User: wukai + * Date: 2017/04/06 + * Description: + */ +public class DialogFragmentUtils { + + private static final String TAG = "DialogFragmentUtils"; + + public static final String KEY_DIALOG_TAG = "dialog_tag"; + private static int mFragmentIndex = 0; + + public static String getFragmentTag(String tag, boolean appendIndexTag) { + if (appendIndexTag) { + return String.format("%s_%d", tag, mFragmentIndex++); + } + return tag; + } + + public static String getTag(Bundle bundle) { + return bundle.getString(KEY_DIALOG_TAG); + } + + public static boolean isShowing(String fragmentTag, Activity activity) { + if (activity == null) { + Logger.debug(TAG, "check isShowing activity is null return"); + return false; + } + if (activity.isFinishing()) { + Logger.debug(TAG, "check isShowing activity isFinishing return"); + return false; + } + + if (activity.isDestroyed()) { + Logger.debug(TAG, "check isShowing activity is isDestroyed"); + return false; + } + + if (!(activity instanceof BaseActivity)) { + Logger.error(TAG, "check isShowing activity fail, activity is not BaseActivity"); + return false; + } + + FragmentManager fragmentManager = ((BaseActivity) activity).getSupportFragmentManager(); + + Fragment fragmentByTag = fragmentManager.findFragmentByTag(fragmentTag); + if (fragmentByTag == null) { + return false; + } + + if (fragmentByTag instanceof DialogFragment) { + DialogFragment dialogFragment = (DialogFragment) fragmentByTag; + return dialogFragment.getDialog() != null && dialogFragment.getDialog().isShowing(); + } else return fragmentByTag.isVisible(); + + } + + public static BaseDialogFragment show(String fragmentTag, Activity activity, Class clazz, Bundle bundle, boolean appendIndexTag) { + try { + BaseDialogFragment baseDialogFragment = clazz.newInstance(); + return show(fragmentTag, activity, baseDialogFragment, bundle, appendIndexTag); + } catch (Exception e1) { + Logger.error(TAG, String.valueOf(e1)); + } + return null; + } + + public static BaseDialogFragment show(String fragmentTag, Activity activity, BaseDialogFragment baseDialogFragment, Bundle bundle, boolean appendIndexTag) { + if (TextUtils.isEmpty(fragmentTag)) { + Logger.error(TAG, "show dialog failure, fragmentTag = null"); + return null; + } + + if (!ActivityHelper.isCanUse(activity)) { + Logger.error(TAG, "show dialog failure, activity is not can use"); + return null; + } + + if (!(activity instanceof BaseActivity)) { + Logger.error(TAG, "show dialog activity fail, activity is not BaseActivity"); + return null; + } + + FragmentManager fragmentManager = ((BaseActivity) activity).getSupportFragmentManager(); + + if (bundle == null) { + bundle = new Bundle(); + } + + String newFragmentTag = DialogFragmentUtils.getFragmentTag(fragmentTag, appendIndexTag); + Logger.debug(TAG, "showDialog: " + newFragmentTag); + + Fragment fragment = fragmentManager.findFragmentByTag(newFragmentTag); + if (fragment == null) { + bundle.putString(KEY_DIALOG_TAG, newFragmentTag); + baseDialogFragment.setArguments(bundle); + } else { + if (ActivityHelper.isCanUse(activity)) { + baseDialogFragment = ((BaseDialogFragment) fragment); + baseDialogFragment.dismissAllowingStateLoss(); + } + } + + if (ActivityHelper.isCanUse(activity) && baseDialogFragment != null) { + try { + doShow(fragmentManager, baseDialogFragment, newFragmentTag); + } catch (Exception e) { + Logger.error(TAG, String.valueOf(e)); + } + } else { + return null; + } + return baseDialogFragment; + } + + + public static void doShow(final FragmentManager fragmentManager, final BaseDialogFragment dialogFragment, final String tag) { + PauseWorkerHandler.instance().post(new Runnable() { + @Override + public void run() { + if (!fragmentManager.isStateSaved()) { // 保证只有在activity存在的时候弹出dialog,防止崩溃 + // Fix 快速显示同一个弹窗报 Fragment already added: 异常 + if (isInvokeShow(dialogFragment)) { + Logger.error(TAG, "Already has fragment=" + dialogFragment.getTag()); + return; + } + dialogFragment.show(fragmentManager, tag); + } + } + }); + } + + public static BaseDialogFragment show(String fragmentTag, Activity activity, Class clazz, Bundle bundle) { + return show(fragmentTag, activity, clazz, bundle, true); + } + + public static BaseDialogFragment show(String fragmentTag, Activity activity, Class clazz) { + return show(fragmentTag, activity, clazz, null, true); + } + + public static void dismiss(String fragmentTag, Activity activity) { + if (TextUtils.isEmpty(fragmentTag)) { + Logger.error(TAG, "dismiss dialog failure, fragmentTag = null"); + return; + } + if (activity == null) { + Logger.error(TAG, "dismiss %s dialog failure, Activity = null, tag=" + fragmentTag); + return; + } + if (activity.isFinishing()) { + Logger.error(TAG, "dismiss %s dialog failure, Activity is finish, tag=" + fragmentTag); + return; + } + if (activity.isDestroyed()) { + Logger.error(TAG, "dismiss %s dialog failure, Activity is null, tag=" + fragmentTag); + return; + } + if (!(activity instanceof BaseActivity)) { + Logger.error(TAG, "dismiss %s dialog failure, Activity is not BaseActivity, tag=" + fragmentTag); + return; + } + FragmentManager manager = ((BaseActivity) activity).getSupportFragmentManager(); + Utils.executePendingTransactionsSafely(fragmentTag, manager); + Fragment fragment = manager.findFragmentByTag(fragmentTag); + if (fragment == null) { + Logger.error(TAG, "dismiss %s dialog failure, Fragment = null, tag="+fragmentTag); + tryCleanDialogFragment(fragmentTag, manager); + } else { + Logger.debug(TAG, "closeDialog=" + fragmentTag); + ((DialogFragment) fragment).dismissAllowingStateLoss(); + } + } + + public static void tryCleanDialogFragment(String fragmentTag, FragmentManager fragmentManager) { + List fragmentList = fragmentManager.getFragments(); + int sizeChildFrgList = fragmentList.size(); + for (int i = sizeChildFrgList - 1; i >= 0; i--) { + Fragment brotherFragment = fragmentList.get(i); + if (brotherFragment == null) { + continue; + } + Bundle arguments = brotherFragment.getArguments(); + if (arguments == null) { + continue; + } + String findTag = getTag(arguments); + if (!TextUtils.isEmpty(findTag) && + brotherFragment instanceof DialogFragment && findTag.equals(fragmentTag)) { + Logger.debug(TAG, "closeDialog " + fragmentTag + "by tryCleanDialogFragment"); + ((DialogFragment) brotherFragment).dismissAllowingStateLoss(); + break; + } + } + } + + private static boolean isInvokeShow(DialogFragment dialogFragment) { + Object value = ReflectionUtils.getFieldValue(dialogFragment, "mShownByMe"); + if (value == null) { + Logger.error(TAG, "isInvokeShow value is null"); + return false; + } + boolean isShown = (boolean) value; + Logger.debug(TAG, "isInvokeShow isShown=" + isShown); + return isShown; + } + +} diff --git a/app/src/main/java/com/yizhuan/erban/common/util/PauseWorkerHandler.java b/app/src/main/java/com/yizhuan/erban/common/util/PauseWorkerHandler.java new file mode 100644 index 000000000..2777d81bf --- /dev/null +++ b/app/src/main/java/com/yizhuan/erban/common/util/PauseWorkerHandler.java @@ -0,0 +1,84 @@ +package com.yizhuan.erban.common.util; + +import android.os.Handler; +import android.os.Looper; +import com.yizhuan.xchat_android_core.utils.Logger; + +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.HashMap; +import java.util.Map; +import java.util.Vector; + +/** + * Created by logwee on 2019/4/9. + */ +public class PauseWorkerHandler { + + private static PauseWorkerHandler sInstance = new PauseWorkerHandler(); + + private static final String TAG = "PauseWorkerHandler"; + public static final int CHECK_VISIBLE_INTERVAL = 500; + + private final Vector mRunnableQueueBuffer = new Vector<>(); + private final Map mSingleRunnableQueueBuffer = new HashMap<>(); + private boolean mPaused = false; + private Handler mHandler = new Handler(Looper.getMainLooper()); + + public static PauseWorkerHandler instance() { + return sInstance; + } + + private PauseWorkerHandler() { + CoreUtils.register(this); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onAppVisibleChangeEvent(AppLifeCycleHelper.OnAppVisibleChange event) { + mPaused = AppLifeCycleHelper.isBackground(); + mHandler.removeCallbacks(mCheckAppVisible); + mHandler.postDelayed(mCheckAppVisible, CHECK_VISIBLE_INTERVAL); + } + + private Runnable mCheckAppVisible = new Runnable() { + @Override + public void run() { + boolean isBackground = AppLifeCycleHelper.isBackground(); + Logger.info(TAG, "onAppVisibleChangeEvent isBackground="+isBackground); + if (isBackground) { + pause(); + } else { + resume(); + } + } + }; + + public void post(Runnable runnable) { + if (mPaused) { + mRunnableQueueBuffer.add(runnable); + } else { + runnable.run(); + } + } + + public void resume() { + mPaused = false; + int size = mRunnableQueueBuffer.size(); + for (int i = 0; i < size; i++) { + final Runnable runnable = mRunnableQueueBuffer.elementAt(i); + runnable.run(); + } + mRunnableQueueBuffer.clear(); + + for (Map.Entry stringRunnableEntry : mSingleRunnableQueueBuffer.entrySet()) { + stringRunnableEntry.getValue().run(); + } + mSingleRunnableQueueBuffer.clear(); + } + + public void pause() { + mPaused = true; + } + +} diff --git a/app/src/main/java/com/yizhuan/erban/common/util/PhotoCompressUtil.kt b/app/src/main/java/com/yizhuan/erban/common/util/PhotoCompressUtil.kt new file mode 100644 index 000000000..2e7425460 --- /dev/null +++ b/app/src/main/java/com/yizhuan/erban/common/util/PhotoCompressUtil.kt @@ -0,0 +1,181 @@ +package com.yizhuan.erban.common.util + +import android.content.Context +import android.os.Environment +import android.text.TextUtils +import android.util.Log +import com.yizhuan.erban.Luban +import com.yizhuan.erban.application.XChatApplication +import com.yizhuan.erban.common.file.FileHelper +import com.yizhuan.xchat_android_core.Constants +import kotlinx.coroutines.* +import java.io.File + +object PhotoCompressUtil { + private const val TAG = "PhotoCompressUtil" + private const val TAG_IMAGE_COMPRESS = "default" + private val photoExtensions = arrayOf("jpg", "png", "jpeg", "bmp", "webp") + + private fun checkIsPhoto(path: String?): Boolean { + if (TextUtils.isEmpty(path)) { + return false + } + for (extension in photoExtensions) { + if (path!!.endsWith(extension, true)) { + return true + } + } + return false + } + + @JvmOverloads + @JvmStatic + fun compress(context: Context, imgList: MutableList, outPath: String?, callback: PhotosCompressCallback?, leastCompressSize: Int = 200, focusAlpha: Boolean = false, maxSize: Int = Constants.UPLOAD_IMAGE_MAX_SIZE, mostCompressSize: Int = Constants.UPLOAD_IMAGE_MAX_FILE_LENGTH): Job? { + val notImgMap = HashMap() + val imgs = ArrayList() + imgList.forEachIndexed { index, s -> + if (checkIsPhoto(s)) { + imgs.add(s) + } else { + notImgMap[index] = s + } + } + if (notImgMap.size == imgList.size) { + //纯非图片 + (imgList as? ArrayList)?.let { callback?.onSuccess(it) } + return null + } else { + return CoroutineScope(Dispatchers.Main + SupervisorJob()).launch { + val deferred = async(Dispatchers.IO) { + var list: MutableList? = null + try { + list = Luban.with(context) + .load(imgs) + .ignoreBy(leastCompressSize) + .setTargetDir(outPath) + .setFocusAlpha(focusAlpha) + .setMaxSize(maxSize) + .setMostCompressSize(mostCompressSize) + .get() + } catch (e: Exception) { + Log.e(TAG, "compress error: $e") + } + + list + } + val compressedFileList = deferred.await() + if (!isActive) return@launch + if (compressedFileList.isNullOrEmpty()) { + callback?.onFail(Throwable("compress fail")) + return@launch + } + val compressedList = compressedFileList.map { it.path } as? ArrayList + + try { + if (notImgMap.isNotEmpty()) { + notImgMap.forEach { + compressedList?.add(it.key, it.value) + } + } + compressedList?.let { callback?.onSuccess(it) } + } catch (e: Exception) { + callback?.onFail(Throwable("compress fail")) + } + } + } + + } + + @JvmOverloads + @JvmStatic + fun compress(context: Context, imgPath: String, outPath: String?, callback: PhotoCompressCallback?, leastCompressSize: Int = 200, focusAlpha: Boolean = false, maxSize: Int = Constants.UPLOAD_IMAGE_MAX_SIZE, mostCompressSize: Int = Constants.UPLOAD_IMAGE_MAX_FILE_LENGTH):Job? { + if (!checkIsPhoto(imgPath)) { + callback?.onSuccess(imgPath) + return null + } else { + return CoroutineScope(Dispatchers.Main + SupervisorJob()).launch { + val deferred = async(Dispatchers.IO) { + var list: MutableList? = null + try { + list = Luban.with(context) + .load(imgPath) + .ignoreBy(leastCompressSize) + .setTargetDir(outPath) + .setFocusAlpha(focusAlpha) + .setMaxSize(maxSize) + .setMostCompressSize(mostCompressSize) + .get() + } catch (e: Exception) { + Log.e(TAG, "compress error: $e") + } + + list + } + + val compressedFileList = deferred.await() + if (!isActive) return@launch + if (compressedFileList.isNullOrEmpty()) { + callback?.onFail(Throwable("compress fail")) + } else { + callback?.onSuccess(compressedFileList[0].path) + } + } + } + } + + /** + * 同步线程-压缩 + */ + @JvmOverloads + @JvmStatic + fun synCompress(imgPath: String, outPath: String?, leastCompressSize: Int = 200, focusAlpha: Boolean = false, maxSize: Int = Constants.UPLOAD_IMAGE_MAX_SIZE, mostCompressSize: Int = Constants.UPLOAD_IMAGE_MAX_FILE_LENGTH): String? { + return if (!checkIsPhoto(imgPath)) { + imgPath + } else { + var compressedFileList: MutableList? = null + try { + compressedFileList = Luban.with(XChatApplication.gContext) + .load(imgPath) + .ignoreBy(leastCompressSize) + .setTargetDir(outPath) + .setFocusAlpha(focusAlpha) + .setMaxSize(maxSize) + .setMostCompressSize(mostCompressSize) + .get() + } catch (e: Exception) { + Log.e(TAG, "compress error: $e") + } + if (compressedFileList.isNullOrEmpty()) { + "" + } else { + compressedFileList[0].path + } + } + } + + @JvmStatic + fun getCompressCachePath(tag: String = TAG_IMAGE_COMPRESS): String { + val path = getCompressLocationPath(tag) + FileHelper.ensureDirExists(path) + return path + } + + @JvmStatic + fun clearCompressCache(tag: String = TAG_IMAGE_COMPRESS) { + FileHelper.removeAllFile(getCompressLocationPath(tag)) + } + + private fun getCompressLocationPath(tag: String): String { + return "${FileHelper.getRootFilesDir(Environment.DIRECTORY_PICTURES).absolutePath}/compress/${tag}/" + } +} + +interface PhotosCompressCallback { + fun onSuccess(compressedImgList: ArrayList) + fun onFail(e: Throwable) +} + +interface PhotoCompressCallback { + fun onSuccess(compressedImg: String) + fun onFail(e: Throwable) +} \ No newline at end of file diff --git a/app/src/main/java/com/yizhuan/erban/common/util/ReflectionUtils.java b/app/src/main/java/com/yizhuan/erban/common/util/ReflectionUtils.java new file mode 100644 index 000000000..9b2cac899 --- /dev/null +++ b/app/src/main/java/com/yizhuan/erban/common/util/ReflectionUtils.java @@ -0,0 +1,42 @@ +package com.yizhuan.erban.common.util; + +import com.yizhuan.xchat_android_core.utils.Logger; + +import java.lang.reflect.Field; + +public class ReflectionUtils { + private static final String TAG = "ReflectionUtils"; + + public static Field getDeclaredField(Object object, String fieldName) { + Class clazz = object.getClass(); + while (clazz != Object.class) { + try { + return clazz.getDeclaredField(fieldName); + } catch (Exception e) { + clazz = clazz.getSuperclass(); + } + } + return null; + } + + public static void setFieldValue(Object object, String fieldName, Object value) { + Field field = ReflectionUtils.getDeclaredField(object, fieldName); + try { + field.setAccessible(true); + field.set(object, value); + } catch (Exception e) { + Logger.error(TAG, "ReflectionUtils setFieldValue", e); + } + } + + public static Object getFieldValue(Object object, String fieldName) { + Field field = ReflectionUtils.getDeclaredField(object, fieldName); + try { + field.setAccessible(true); + return field.get(object); + } catch (Exception e) { + Logger.error(TAG, "ReflectionUtils getFieldValue", e); + return null; + } + } +} diff --git a/app/src/main/java/com/yizhuan/erban/common/util/Utils.java b/app/src/main/java/com/yizhuan/erban/common/util/Utils.java index 9d5e0520f..684a0778f 100644 --- a/app/src/main/java/com/yizhuan/erban/common/util/Utils.java +++ b/app/src/main/java/com/yizhuan/erban/common/util/Utils.java @@ -7,6 +7,11 @@ import android.util.DisplayMetrics; import android.view.Display; import android.view.WindowManager; +import androidx.fragment.app.FragmentManager; + +import com.yizhuan.xchat_android_core.utils.Logger; + +import java.lang.reflect.Field; import java.util.List; /** @@ -153,4 +158,23 @@ public class Utils { return (int) px2dip(context, pxHeight); } + public static void executePendingTransactionsSafely(String TAG, FragmentManager fragmentManager) { + if (fragmentManager == null) { + Logger.error(TAG, "executePendingTransactionsSafely fragmentManager == null"); + return; + } + try { + fragmentManager.executePendingTransactions(); + } catch (Exception e) { + Logger.error(TAG, String.valueOf(e)); + try { + Field mExecutingActions = fragmentManager.getClass().getDeclaredField("mExecutingActions"); + mExecutingActions.setAccessible(true); + mExecutingActions.set(fragmentManager, false); + } catch (Exception e1) { + Logger.error(TAG, "set field value fail", e1); + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/yizhuan/erban/common/widget/CircleImageSpan.java b/app/src/main/java/com/yizhuan/erban/common/widget/CircleImageSpan.java index ea35f0189..546de471c 100644 --- a/app/src/main/java/com/yizhuan/erban/common/widget/CircleImageSpan.java +++ b/app/src/main/java/com/yizhuan/erban/common/widget/CircleImageSpan.java @@ -15,8 +15,8 @@ import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import com.netease.nim.uikit.support.glide.GlideApp; -import com.yizhuan.erban.ui.gift.widget.GlideCircleTransform; import com.yizhuan.erban.common.util.Utils; +import com.yizhuan.erban.ui.gift.widget.GlideCircleTransform; import com.yizhuan.xchat_android_library.utils.config.BasicConfig; public class CircleImageSpan extends ImageSpan { diff --git a/app/src/main/java/com/yizhuan/erban/common/widget/CustomImageSpan.java b/app/src/main/java/com/yizhuan/erban/common/widget/CustomImageSpan.java index 6622f2732..e3ad67e6d 100644 --- a/app/src/main/java/com/yizhuan/erban/common/widget/CustomImageSpan.java +++ b/app/src/main/java/com/yizhuan/erban/common/widget/CustomImageSpan.java @@ -19,8 +19,8 @@ import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import com.netease.nim.uikit.support.glide.GlideApp; -import com.yizhuan.erban.ui.widget.magicindicator.buildins.UIUtil; import com.yizhuan.erban.common.util.Utils; +import com.yizhuan.erban.ui.widget.magicindicator.buildins.UIUtil; import com.yizhuan.xchat_android_library.utils.SizeUtils; import com.yizhuan.xchat_android_library.utils.config.BasicConfig; diff --git a/app/src/main/java/com/yizhuan/erban/common/widget/dialog/CommonPopupDialog.java b/app/src/main/java/com/yizhuan/erban/common/widget/dialog/CommonPopupDialog.java index 89fa4344c..214f0df2f 100644 --- a/app/src/main/java/com/yizhuan/erban/common/widget/dialog/CommonPopupDialog.java +++ b/app/src/main/java/com/yizhuan/erban/common/widget/dialog/CommonPopupDialog.java @@ -16,8 +16,8 @@ import android.widget.TextView; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetDialog; import com.yizhuan.erban.R; -import com.yizhuan.erban.ui.widget.ButtonItem; import com.yizhuan.erban.common.util.Utils; +import com.yizhuan.erban.ui.widget.ButtonItem; import java.util.List; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c91661825..0ecdb19b9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4960,5 +4960,12 @@ 鉆石收入 所屬廳 送礼物 + 去設置 + 請您再次考慮一下授予PEKO存儲空間權限,否則PEKO將無法正常加載您本地的文件內容。 + 請您再次考慮一下授予PEKO攝像頭權限,否則PEKO將無法取得您拍攝的內容。 + 為了能正常加載您本地的文件內容,請前往應用權限設置界面打開存儲空間權限。 + 為了能取得您拍攝的內容,請前往應用權限設置界面打開攝像頭權限。 + 您已拒絕提供存儲空間權限,PEKO將無法正常加載您本地的文件內容。 + 您已拒絕提供攝像頭權限,PEKO將無法取得您拍攝的內容。 \ No newline at end of file diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/AfterPermissionGranted.java b/app/src/module_easypermission/java/com/yizhuan/erban/AfterPermissionGranted.java new file mode 100644 index 000000000..e83b4a00a --- /dev/null +++ b/app/src/module_easypermission/java/com/yizhuan/erban/AfterPermissionGranted.java @@ -0,0 +1,29 @@ +/* + * 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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface AfterPermissionGranted { + + int value(); + +} diff --git a/app/src/module_easypermission/java/com/yizhuan/erban/AppSettingsDialog.java b/app/src/module_easypermission/java/com/yizhuan/erban/AppSettingsDialog.java new file mode 100644 index 000000000..144892e68 --- /dev/null +++ b/app/src/module_easypermission/java/com/yizhuan/erban/AppSettingsDialog.java @@ -0,0 +1,356 @@ +package com.yizhuan.erban; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.StringRes; +import androidx.annotation.StyleRes; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + +/** + * Dialog to prompt the user to go to the app's settings screen and enable permissions. If the user + * clicks 'OK' on the dialog, they are sent to the settings screen. The result is returned to the + * Activity via {@see Activity#onActivityResult(int, int, Intent)}. + *

+ * 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 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 @@ + + + + + + + +