From 139a2237fa0c074e6bfc08a03f33e2a904491c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BD=98=E5=AD=9D=E5=B3=B0?= <2557830190@qq.com> Date: Sat, 11 Nov 2023 00:00:16 +0800 Subject: [PATCH] 3 --- src/calendar/src/main/AndroidManifest.xml | 4 + .../src/main/java/com/ldf/calendar/Const.java | 11 + .../src/main/java/com/ldf/calendar/Utils.java | 305 ++++++++++++ .../calendar/behavior/MonthPagerBehavior.java | 92 ++++ .../behavior/RecyclerViewBehavior.java | 164 +++++++ .../ldf/calendar/component/CalendarAttr.java | 62 +++ .../calendar/component/CalendarRenderer.java | 318 +++++++++++++ .../component/CalendarViewAdapter.java | 334 ++++++++++++++ .../com/ldf/calendar/component/State.java | 11 + .../com/ldf/calendar/interf/IDayRenderer.java | 19 + .../interf/OnAdapterSelectListener.java | 11 + .../calendar/interf/OnSelectDateListener.java | 13 + .../com/ldf/calendar/model/CalendarDate.java | 151 ++++++ .../java/com/ldf/calendar/view/Calendar.java | 152 ++++++ .../main/java/com/ldf/calendar/view/Day.java | 82 ++++ .../java/com/ldf/calendar/view/DayView.java | 85 ++++ .../com/ldf/calendar/view/MonthPager.java | 173 +++++++ .../main/java/com/ldf/calendar/view/Week.java | 36 ++ src/calendar/src/readme | 9 + .../src/main/AndroidManifest.xml | 87 ++++ .../shihoo/daemon/AbsServiceConnection.java | 32 ++ .../com/shihoo/daemon/AbsWorkService.java | 206 +++++++++ .../java/com/shihoo/daemon/DaemonEnv.java | 93 ++++ .../java/com/shihoo/daemon/IntentWrapper.java | 433 ++++++++++++++++++ .../shihoo/daemon/JobSchedulerService.java | 43 ++ .../com/shihoo/daemon/PlayMusicService.java | 114 +++++ .../com/shihoo/daemon/WakeUpReceiver.java | 31 ++ .../com/shihoo/daemon/WatchDogService.java | 287 ++++++++++++ .../shihoo/daemon/WatchProcessPrefHelper.java | 37 ++ .../daemon/singlepixel/ScreenManager.java | 73 +++ .../singlepixel/ScreenReceiverUtil.java | 83 ++++ .../singlepixel/SinglePixelActivity.java | 45 ++ .../com/shihoo/daemon/sync/Authenticator.java | 78 ++++ .../daemon/sync/AuthenticatorService.java | 28 ++ .../com/shihoo/daemon/sync/StubProvider.java | 50 ++ .../com/shihoo/daemon/sync/SyncAdapter.java | 34 ++ .../com/shihoo/daemon/sync/SyncService.java | 30 ++ .../src/main/res/raw/no_notice.mp3 | Bin 0 -> 439659 bytes .../src/main/res/values/strings.xml | 3 + .../src/main/res/values/styles.xml | 16 + 40 files changed, 3835 insertions(+) create mode 100644 src/calendar/src/main/AndroidManifest.xml create mode 100644 src/calendar/src/main/java/com/ldf/calendar/Const.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/Utils.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/behavior/MonthPagerBehavior.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/behavior/RecyclerViewBehavior.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/component/CalendarAttr.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/component/CalendarRenderer.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/component/CalendarViewAdapter.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/component/State.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/interf/IDayRenderer.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/interf/OnAdapterSelectListener.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/interf/OnSelectDateListener.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/model/CalendarDate.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/view/Calendar.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/view/Day.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/view/DayView.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/view/MonthPager.java create mode 100644 src/calendar/src/main/java/com/ldf/calendar/view/Week.java create mode 100644 src/calendar/src/readme create mode 100644 src/daemonlibrary/src/main/AndroidManifest.xml create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/AbsServiceConnection.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/AbsWorkService.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/DaemonEnv.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/IntentWrapper.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/JobSchedulerService.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/PlayMusicService.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/WakeUpReceiver.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/WatchDogService.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/WatchProcessPrefHelper.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/singlepixel/ScreenManager.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/singlepixel/ScreenReceiverUtil.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/singlepixel/SinglePixelActivity.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/Authenticator.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/AuthenticatorService.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/StubProvider.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/SyncAdapter.java create mode 100644 src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/SyncService.java create mode 100644 src/daemonlibrary/src/main/res/raw/no_notice.mp3 create mode 100644 src/daemonlibrary/src/main/res/values/strings.xml create mode 100644 src/daemonlibrary/src/main/res/values/styles.xml diff --git a/src/calendar/src/main/AndroidManifest.xml b/src/calendar/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d7a305c --- /dev/null +++ b/src/calendar/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/calendar/src/main/java/com/ldf/calendar/Const.java b/src/calendar/src/main/java/com/ldf/calendar/Const.java new file mode 100644 index 0000000..ec71dd5 --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/Const.java @@ -0,0 +1,11 @@ +package com.ldf.calendar; + +// 这个类定义了一些常量,用于日历视图的布局和设定 +public class Const { + // 表示日历视图的总列数 + public final static int TOTAL_COL = 7; + // 表示日历视图的总行数 + public final static int TOTAL_ROW = 6; + // 表示当前的页面索引,用于日历视图的显示 + public final static int CURRENT_PAGER_INDEX = 1000; +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/Utils.java b/src/calendar/src/main/java/com/ldf/calendar/Utils.java new file mode 100644 index 0000000..8d66f31 --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/Utils.java @@ -0,0 +1,305 @@ +package com.ldf.calendar; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.support.design.widget.CoordinatorLayout; +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.Scroller; + +import com.ldf.calendar.component.CalendarAttr; +import com.ldf.calendar.model.CalendarDate; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; + +public final class Utils { + + private static HashMap markData = new HashMap<>(); + private static int top; + private static boolean customScrollToBottom = false; + + private Utils() { + } + + /** + * 得到某一个月的具体天数 + * + * @param year 参数月所在年 + * @param month 参数月 + * @return int 参数月所包含的天数 + */ + public static int getMonthDays(int year, int month) { + if (month > 12) { + month = 1; + year += 1; + } else if (month < 1) { + month = 12; + year -= 1; + } + int[] monthDays = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + int days = 0; + // 闰年2月29天 + if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) { + monthDays[1] = 29; + } + try { + days = monthDays[month - 1]; + } catch (Exception e) { + e.getStackTrace(); + } + return days; + } + // 获取年、月、日 + public static int getYear() { + return Calendar.getInstance().get(Calendar.YEAR); + } + + public static int getMonth() { + return Calendar.getInstance().get(Calendar.MONTH) + 1; + } + + public static int getDay() { + return Calendar.getInstance().get(Calendar.DAY_OF_MONTH); + } + + /** + * 得到当前月第一天在其周的位置 + * + * @param year 当前年 + * @param month 当前月 + * @param type 周排列方式 0代表周一作为本周的第一天, 2代表周日作为本周的第一天 + * @return int 本月第一天在其周的位置 + */ + public static int getFirstDayWeekPosition(int year, int month, CalendarAttr.WeekArrayType type) { + Calendar cal = Calendar.getInstance(); + cal.setTime(getDateFromString(year, month)); + int week_index = cal.get(Calendar.DAY_OF_WEEK) - 1; + if (type == CalendarAttr.WeekArrayType.Sunday) { + return week_index; + } else { + week_index = cal.get(Calendar.DAY_OF_WEEK) + 5; + if (week_index >= 7) { + week_index -= 7; + } + } + return week_index; + } + + /** + * 将yyyy-MM-dd类型的字符串转化为对应的Date对象 + * + * @param year 当前年 + * @param month 当前月 + * @return Date 对应的Date对象 + */ + @SuppressLint("SimpleDateFormat") + public static Date getDateFromString(int year, int month) { + String dateString = year + "-" + (month > 9 ? month : ("0" + month)) + "-01"; + Date date = new Date(); + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + date = sdf.parse(dateString); + } catch (ParseException e) { + System.out.println(e.getMessage()); + } + return date; + } + + /** + * 计算参数日期月与当前月相差的月份数 + * + * @param year 参数日期所在年 + * @param month 参数日期所在月 + * @param currentDate 当前月 + * @return int offset 相差月份数 + */ + public static int calculateMonthOffset(int year, int month, CalendarDate currentDate) { + int currentYear = currentDate.getYear(); + int currentMonth = currentDate.getMonth(); + int offset = (year - currentYear) * 12 + (month - currentMonth); + return offset; + } + + /** + * 删除方法, 这里只会删除某个文件夹下的文件,如果传入的directory是个文件,将不做处理 + * + * @param context 上下文 + * @param dpi dp为单位的尺寸 + * @return int 转化而来的对应像素 + */ + public static int dpi2px(Context context, float dpi) { + return (int) (context.getResources().getDisplayMetrics().density * dpi + 0.5f); + } + + /** + * 得到标记日期数据,可以通过该数据得到标记日期的信息,开发者可自定义格式 + * 目前HashMap的组成仅仅是为了DEMO效果 + * + * @return HashMap 标记日期数据 + */ + public static HashMap loadMarkData() { + return markData; + } + + /** + * 设置标记日期数据 + * + * @param data 标记日期数据 + * @return void + */ + public static void setMarkData(HashMap data) { + markData = data; + } + + /** + * 计算偏移距离 + * + * @param offset 偏移值 + * @param min 最小偏移值 + * @param max 最大偏移值 + * @return int offset + */ + private static int calcOffset(int offset, int min, int max) { + if (offset > max) { + return max; + } else if (offset < min) { + return min; + } else { + return offset; + } + } + + /** + * 删除方法, 这里只会删除某个文件夹下的文件,如果传入的directory是个文件,将不做处理 + * + * @param child 需要移动的View + * @param dy 实际偏移量 + * @param minOffset 最小偏移量 + * @param maxOffset 最大偏移量 + * @return void + */ + public static int scroll(View child, int dy, int minOffset, int maxOffset) { + final int initOffset = child.getTop(); + int offset = calcOffset(initOffset - dy, minOffset, maxOffset) - initOffset; + child.offsetTopAndBottom(offset); + return -offset; + } + + /** + * 得到TouchSlop + * + * @param context 上下文 + * @return int touchSlop的具体值 + */ + public static int getTouchSlop(Context context) { + return ViewConfiguration.get(context).getScaledTouchSlop(); + } + + /** + * 得到种子日期所在周的周日 + * + * @param seedDate 种子日期 + * @return CalendarDate 所在周周日 + */ + public static CalendarDate getSunday(CalendarDate seedDate) {// TODO: 16/12/12 得到一个CustomDate对象 + Calendar c = Calendar.getInstance(); + String dateString = seedDate.toString(); + Date date = new Date(); + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-M-d"); + date = sdf.parse(dateString); + } catch (ParseException e) { + System.out.println(e.getMessage()); + } + c.setTime(date); + if (c.get(Calendar.DAY_OF_WEEK) != Calendar.SUNDAY) { + c.add(Calendar.DAY_OF_MONTH, 7 - c.get(Calendar.DAY_OF_WEEK) + 1); + } + return new CalendarDate(c.get(Calendar.YEAR), + c.get(Calendar.MONTH) + 1, + c.get(Calendar.DAY_OF_MONTH)); + } + + /** + * 得到种子日期所在周的周六 + * + * @param seedDate 种子日期 + * @return CalendarDate 所在周周六 + */ + public static CalendarDate getSaturday(CalendarDate seedDate) {// TODO: 16/12/12 得到一个CustomDate对象 + Calendar c = Calendar.getInstance(); + String dateString = seedDate.toString(); + Date date = null; + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-M-d"); + date = sdf.parse(dateString); + } catch (ParseException e) { + System.out.println(e.getMessage()); + } + c.setTime(date); + c.add(Calendar.DAY_OF_MONTH, 7 - c.get(Calendar.DAY_OF_WEEK)); + return new CalendarDate(c.get(Calendar.YEAR), + c.get(Calendar.MONTH) + 1, + c.get(Calendar.DAY_OF_MONTH)); + } + + /** + * 判断上一次滑动改变周月日历是向下滑还是向上滑 向下滑表示切换为月日历模式 向上滑表示切换为周日历模式 + * + * @return boolean 是否是在向下滑动。(true: 已经收缩; false: 已经打开) + */ + public static boolean isScrollToBottom() { + return customScrollToBottom; + } + + /** + * 设置上一次滑动改变周月日历是向下滑还是向上滑 向下滑表示切换为月日历模式 向上滑表示切换为周日历模式 + * + * @return void + */ + public static void setScrollToBottom(boolean customScrollToBottom) { + Utils.customScrollToBottom = customScrollToBottom; + } + + /** + * 通过scrollTo方法完成协调布局的滑动,其中主要使用了ViewCompat.postOnAnimation + * + * @param parent 协调布局parent + * @param child 协调布局协调滑动的child + * @param y 滑动目标位置y轴数值 + * @param duration 滑动执行时间 + * @return void + */ + public static void scrollTo(final CoordinatorLayout parent, final RecyclerView child, final int y, int duration) { + final Scroller scroller = new Scroller(parent.getContext()); + scroller.startScroll(0, top, 0, y - top, duration); //设置scroller的滚动偏移量 + ViewCompat.postOnAnimation(child, new Runnable() { + @Override + public void run() { + //返回值为boolean,true说明滚动尚未完成,false说明滚动已经完成。 + // 这是一个很重要的方法,通常放在View.computeScroll()中,用来判断是否滚动是否结束。 + if (scroller.computeScrollOffset()) { + int delta = scroller.getCurrY() - child.getTop(); + child.offsetTopAndBottom(delta); + saveTop(child.getTop()); + parent.dispatchDependentViewsChanged(child); + ViewCompat.postOnAnimation(child, this); + } + } + }); + } + + public static void saveTop(int y) { + top = y; + } + + public static int loadTop() { + return top; + } +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/behavior/MonthPagerBehavior.java b/src/calendar/src/main/java/com/ldf/calendar/behavior/MonthPagerBehavior.java new file mode 100644 index 0000000..8e9780f --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/behavior/MonthPagerBehavior.java @@ -0,0 +1,92 @@ +package com.ldf.calendar.behavior; + +import android.support.design.widget.CoordinatorLayout; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.View; + +import com.ldf.calendar.Utils; +import com.ldf.calendar.component.CalendarViewAdapter; +import com.ldf.calendar.view.MonthPager; + + +public class MonthPagerBehavior extends CoordinatorLayout.Behavior { + private int top = 0;// 初始顶部偏移量 + private int touchSlop = 1;// 触发滑动的最小距离 + private int offsetY = 0;// Y轴偏移量 + private int dependentViewTop = -1;// 依赖视图的顶部位置 + + @Override + public boolean layoutDependsOn(CoordinatorLayout parent, MonthPager child, View dependency) { + // 确定MonthPager是否依赖于RecyclerView + return dependency instanceof RecyclerView; + } + + @Override + public boolean onLayoutChild(CoordinatorLayout parent, MonthPager child, int layoutDirection) { + parent.onLayoutChild(child, layoutDirection); + // 设置MonthPager的顶部偏移量 + child.offsetTopAndBottom(top); + return true; + } + + @Override + public boolean onDependentViewChanged(CoordinatorLayout parent, MonthPager child, View dependency) { + CalendarViewAdapter calendarViewAdapter = (CalendarViewAdapter) child.getAdapter(); + // 当依赖视图(这里是RecyclerView)发生变化时的响应逻辑 + if (dependentViewTop != -1) { + // 计算垂直方向上的变化量 + int dy = dependency.getTop() - dependentViewTop; + int top = child.getTop(); + if (dy > touchSlop) { + // 根据滑动距离切换到月视图 + calendarViewAdapter.switchToMonth(); + } else if (dy < -touchSlop) { + // 根据滑动距离切换到周视图 + calendarViewAdapter.switchToWeek(child.getRowIndex()); + } + + if (dy > -top) { + dy = -top; + } + + if (dy < -top - child.getTopMovableDistance()) { + dy = -top - child.getTopMovableDistance(); + } + // 设置MonthPager的垂直偏移量 + child.offsetTopAndBottom(dy); + Log.e("ldf", "onDependentViewChanged = " + dy); + + } + // 更新依赖视图的顶部位置 + dependentViewTop = dependency.getTop(); + // 更新MonthPager的顶部位置 + top = child.getTop(); + + if (offsetY > child.getCellHeight()) { + calendarViewAdapter.switchToMonth(); + } + if (offsetY < -child.getCellHeight()) { + calendarViewAdapter.switchToWeek(child.getRowIndex()); + } + + if (dependentViewTop > child.getCellHeight() - 24 + && dependentViewTop < child.getCellHeight() + 24 + && top > -touchSlop - child.getTopMovableDistance() + && top < touchSlop - child.getTopMovableDistance()) { + Utils.setScrollToBottom(true); + calendarViewAdapter.switchToWeek(child.getRowIndex()); + offsetY = 0; + } + if (dependentViewTop > child.getViewHeight() - 24 + && dependentViewTop < child.getViewHeight() + 24 + && top < touchSlop + && top > -touchSlop) { + Utils.setScrollToBottom(false); + calendarViewAdapter.switchToMonth(); + offsetY = 0; + } + + return true; + } +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/behavior/RecyclerViewBehavior.java b/src/calendar/src/main/java/com/ldf/calendar/behavior/RecyclerViewBehavior.java new file mode 100644 index 0000000..63c2cce --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/behavior/RecyclerViewBehavior.java @@ -0,0 +1,164 @@ +package com.ldf.calendar.behavior; + +import android.content.Context; +import android.support.design.widget.CoordinatorLayout; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewPager; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import com.ldf.calendar.Utils; +import com.ldf.calendar.view.MonthPager; + +public class RecyclerViewBehavior extends CoordinatorLayout.Behavior { + boolean hidingTop = false;// 用于标识是否正在隐藏顶部的日历 + boolean showingTop = false;// 用于标识是否正在展示顶部的日历 + private int initOffset = -1;// 初始偏移量,默认值为-1 + private int minOffset = -1;// 最小偏移量,默认值为-1 + private Context context;// 上下文对象 + private boolean initiated = false;// 标识是否已经初始化 + + // 构造函数 + public RecyclerViewBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + this.context = context; + } + + @Override + // 在布局子视图时调用 + public boolean onLayoutChild(CoordinatorLayout parent, RecyclerView child, int layoutDirection) { + parent.onLayoutChild(child, layoutDirection); + // 获取MonthPager实例 + MonthPager monthPager = getMonthPager(parent); + // 初始化最小偏移量和初始偏移量 + initMinOffsetAndInitOffset(parent, child, monthPager); + return true; + } + + // 初始化最小偏移量和初始偏移量 + private void initMinOffsetAndInitOffset(CoordinatorLayout parent, + RecyclerView child, + MonthPager monthPager) { + if (monthPager.getBottom() > 0 && initOffset == -1) { + initOffset = monthPager.getViewHeight(); + // 保存初始偏移量 + saveTop(initOffset); + } + if (!initiated) { + initOffset = monthPager.getViewHeight(); + // 保存初始偏移量 + saveTop(initOffset); + initiated = true; + } + child.offsetTopAndBottom(Utils.loadTop()); + minOffset = getMonthPager(parent).getCellHeight(); + } + + @Override + // 当开始嵌套滚动时调用 + public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, RecyclerView child, + View directTargetChild, View target, int nestedScrollAxes) { + Log.e("ldf", "onStartNestedScroll"); + + MonthPager monthPager = (MonthPager) coordinatorLayout.getChildAt(0); + // 设置MonthPager不可滚动 + monthPager.setScrollable(false); + // 判断是否为垂直滚动 + boolean isVertical = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; + + return isVertical; + } + + @Override + // 在嵌套滚动前调用 + public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, RecyclerView child, + View target, int dx, int dy, int[] consumed) { + Log.e("ldf", "onNestedPreScroll"); + super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed); + child.setVerticalScrollBarEnabled(true); + + MonthPager monthPager = (MonthPager) coordinatorLayout.getChildAt(0); + if (monthPager.getPageScrollState() != ViewPager.SCROLL_STATE_IDLE) { + consumed[1] = dy; + Log.w("ldf", "onNestedPreScroll: MonthPager dragging"); + // 弹出加载月份数据的提示 + Toast.makeText(context, "loading month data", Toast.LENGTH_SHORT).show(); + return; + } + + // 上滑,正在隐藏顶部的日历 + hidingTop = dy > 0 && child.getTop() <= initOffset + && child.getTop() > getMonthPager(coordinatorLayout).getCellHeight(); + // 下滑,正在展示顶部的日历 + showingTop = dy < 0 && !ViewCompat.canScrollVertically(target, -1); + + if (hidingTop || showingTop) { + // 执行滚动操作 + consumed[1] = Utils.scroll(child, dy, + getMonthPager(coordinatorLayout).getCellHeight(), + getMonthPager(coordinatorLayout).getViewHeight()); + // 保存当前位置偏移量 + saveTop(child.getTop()); + } + } + + @Override + // 停止嵌套滚动后调用 + public void onStopNestedScroll(final CoordinatorLayout parent, final RecyclerView child, View target) { + Log.e("ldf", "onStopNestedScroll"); + super.onStopNestedScroll(parent, child, target); + MonthPager monthPager = (MonthPager) parent.getChildAt(0); + // 设置MonthPager可滚动 + monthPager.setScrollable(true); + if (!Utils.isScrollToBottom()) { + if (initOffset - Utils.loadTop() > Utils.getTouchSlop(context) && hidingTop) { + // 执行滚动到指定位置的动画 + Utils.scrollTo(parent, child, getMonthPager(parent).getCellHeight(), 500); + } else { + // 执行滚动到指定位置的动画 + Utils.scrollTo(parent, child, getMonthPager(parent).getViewHeight(), 150); + } + } else { //同上 + if (Utils.loadTop() - minOffset > Utils.getTouchSlop(context) && showingTop) { + Utils.scrollTo(parent, child, getMonthPager(parent).getViewHeight(), 500); + } else { + Utils.scrollTo(parent, child, getMonthPager(parent).getCellHeight(), 150); + } + } + } + + @Override + // 处理嵌套滚动的快速滑动事件 + public boolean onNestedFling(CoordinatorLayout coordinatorLayout, RecyclerView child, View target, float velocityX, float velocityY, boolean consumed) { + Log.d("ldf", "onNestedFling: velocityY: " + velocityY); + return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed); + } + + @Override + // 在嵌套滚动的快速滑动前调用 + public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, RecyclerView child, View target, float velocityX, float velocityY) { + // 日历隐藏和展示过程,不允许RecyclerView进行fling + return hidingTop || showingTop; + } + + // 获取MonthPager实例 + private MonthPager getMonthPager(CoordinatorLayout coordinatorLayout) { + return (MonthPager) coordinatorLayout.getChildAt(0); + } + + // 保存当前位置偏移量 + private void saveTop(int top) { + // 调用工具类保存当前位置偏移量 + Utils.saveTop(top); + if (Utils.loadTop() == initOffset) { + // 设置滚动到底部为false + Utils.setScrollToBottom(false); + } else if (Utils.loadTop() == minOffset) { + // 设置滚动到底部为true + Utils.setScrollToBottom(true); + } + } +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/component/CalendarAttr.java b/src/calendar/src/main/java/com/ldf/calendar/component/CalendarAttr.java new file mode 100644 index 0000000..09e5e14 --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/component/CalendarAttr.java @@ -0,0 +1,62 @@ +package com.ldf.calendar.component; + +/** + * 日历属性类 + */ + +public class CalendarAttr { + + // 周起始类型 + private WeekArrayType weekArrayType; + + // 日历类型 + private CalendarType calendarType; + + // 单元格高度 + private int cellHeight; + + // 单元格宽度 + private int cellWidth; + + public WeekArrayType getWeekArrayType() { + return weekArrayType; + } + + public void setWeekArrayType(WeekArrayType weekArrayType) { + this.weekArrayType = weekArrayType; + } + + public CalendarType getCalendarType() { + return calendarType; + } + + public void setCalendarType(CalendarType calendarType) { + this.calendarType = calendarType; + } + + public int getCellHeight() { + return cellHeight; + } + + public void setCellHeight(int cellHeight) { + this.cellHeight = cellHeight; + } + + public int getCellWidth() { + return cellWidth; + } + + public void setCellWidth(int cellWidth) { + this.cellWidth = cellWidth; + } + + // 周起始类型枚举 + public enum WeekArrayType { + Sunday, Monday + } + + // 日历类型枚举 + public enum CalendarType { + WEEK, MONTH + } +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/component/CalendarRenderer.java b/src/calendar/src/main/java/com/ldf/calendar/component/CalendarRenderer.java new file mode 100644 index 0000000..3eadf7f --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/component/CalendarRenderer.java @@ -0,0 +1,318 @@ +package com.ldf.calendar.component; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.Log; + +import com.ldf.calendar.Const; +import com.ldf.calendar.Utils; +import com.ldf.calendar.interf.IDayRenderer; +import com.ldf.calendar.interf.OnSelectDateListener; +import com.ldf.calendar.model.CalendarDate; +import com.ldf.calendar.view.Calendar; +import com.ldf.calendar.view.Day; +import com.ldf.calendar.view.Week; + +public class CalendarRenderer { + // 用于存储每行的日期数据,数组长度为总行数 + private Week[] weeks = new Week[Const.TOTAL_ROW]; + private Calendar calendar;// 日历对象 + private CalendarAttr attr;// 日历属性 + private IDayRenderer dayRenderer;// 日期渲染器接口 + private Context context;// 上下文 + private OnSelectDateListener onSelectDateListener;// 单元格点击回调事件 + private CalendarDate seedDate; //种子日期 + private CalendarDate selectedDate; //被选中的日期 + private int selectedRowIndex = 0; + + public CalendarRenderer(Calendar calendar, CalendarAttr attr, Context context) { + this.calendar = calendar; + this.attr = attr; + this.context = context; + } + + // 绘制日历 + public void draw(Canvas canvas) { + for (int row = 0; row < Const.TOTAL_ROW; row++) { + if (weeks[row] != null) { + for (int col = 0; col < Const.TOTAL_COL; col++) { + if (weeks[row].days[col] != null) { + // 调用日期渲染器接口绘制日期 + dayRenderer.drawDay(canvas, weeks[row].days[col]); + } + } + } + } + } + + // 处理日期点击事件 + public void onClickDate(int col, int row) { + if (col >= Const.TOTAL_COL || row >= Const.TOTAL_ROW) + return; + if (weeks[row] != null) { + if (attr.getCalendarType() == CalendarAttr.CalendarType.MONTH) { + if (weeks[row].days[col].getState() == State.CURRENT_MONTH) { + // 如果点击的日期是当月日期,则设置为选中状态,并触发选中日期回调事件 + weeks[row].days[col].setState(State.SELECT); + selectedDate = weeks[row].days[col].getDate(); + CalendarViewAdapter.saveSelectedDate(selectedDate); + onSelectDateListener.onSelectDate(selectedDate); + seedDate = selectedDate; + } else if (weeks[row].days[col].getState() == State.PAST_MONTH) { + // 如果点击的日期是上个月的日期,则保存选中的日期并触发选中其他月份日期回调事件 + selectedDate = weeks[row].days[col].getDate(); + CalendarViewAdapter.saveSelectedDate(selectedDate); + onSelectDateListener.onSelectOtherMonth(-1); + onSelectDateListener.onSelectDate(selectedDate); + } else if (weeks[row].days[col].getState() == State.NEXT_MONTH) { + // 如果点击的日期是下个月的日期,则保存选中的日期并触发选中其他月份日期回调事件 + selectedDate = weeks[row].days[col].getDate(); + CalendarViewAdapter.saveSelectedDate(selectedDate); + onSelectDateListener.onSelectOtherMonth(1); + onSelectDateListener.onSelectDate(selectedDate); + } + } else { + // 如果日历类型不是月视图,则直接设置点击的日期为选中状态,并触发选中日期回调事件 + weeks[row].days[col].setState(State.SELECT); + selectedDate = weeks[row].days[col].getDate(); + CalendarViewAdapter.saveSelectedDate(selectedDate); + onSelectDateListener.onSelectDate(selectedDate); + seedDate = selectedDate; + } + } + } + + public void updateWeek(int rowIndex) { + CalendarDate currentWeekLastDay; + if (attr.getWeekArrayType() == CalendarAttr.WeekArrayType.Sunday) { + // 如果一周的起始是星期天,则当前周的最后一天是星期六 + currentWeekLastDay = Utils.getSaturday(seedDate); + } else { + // 如果一周的起始是星期一,则当前周的最后一天是星期天 + currentWeekLastDay = Utils.getSunday(seedDate); + } + int day = currentWeekLastDay.day; + for (int i = Const.TOTAL_COL - 1; i >= 0; i--) { + // 逐个填充当前周的日期数据 + CalendarDate date = currentWeekLastDay.modifyDay(day); + if (weeks[rowIndex] == null) { + weeks[rowIndex] = new Week(rowIndex); + } + if (weeks[rowIndex].days[i] != null) { + // 如果日期数据已存在,则更新状态和日期 + if (date.equals(CalendarViewAdapter.loadSelectedDate())) { + weeks[rowIndex].days[i].setState(State.SELECT); + weeks[rowIndex].days[i].setDate(date); + } else { + weeks[rowIndex].days[i].setState(State.CURRENT_MONTH); + weeks[rowIndex].days[i].setDate(date); + } + } else { + // 如果日期数据不存在,则创建新的日期对象 + if (date.equals(CalendarViewAdapter.loadSelectedDate())) { + weeks[rowIndex].days[i] = new Day(State.SELECT, date, rowIndex, i); + } else { + weeks[rowIndex].days[i] = new Day(State.CURRENT_MONTH, date, rowIndex, i); + } + } + day--; + } + } + + private void instantiateMonth() { + // 上个月的天数 + int lastMonthDays = Utils.getMonthDays(seedDate.year, seedDate.month - 1); + // 当前月的天数 + int currentMonthDays = Utils.getMonthDays(seedDate.year, seedDate.month); + int firstDayPosition = Utils.getFirstDayWeekPosition( + seedDate.year, + seedDate.month, + attr.getWeekArrayType()); + Log.e("ldf", "firstDayPosition = " + firstDayPosition); + + int day = 0; + for (int row = 0; row < Const.TOTAL_ROW; row++) { + day = fillWeek(lastMonthDays, currentMonthDays, firstDayPosition, day, row); + } + } + + private int fillWeek(int lastMonthDays, + int currentMonthDays, + int firstDayWeek, + int day, + int row) { + for (int col = 0; col < Const.TOTAL_COL; col++) { + int position = col + row * Const.TOTAL_COL;// 单元格位置 + if (position >= firstDayWeek && position < firstDayWeek + currentMonthDays) { + day++; + fillCurrentMonthDate(day, row, col); + } else if (position < firstDayWeek) { + instantiateLastMonth(lastMonthDays, firstDayWeek, row, col, position); + } else if (position >= firstDayWeek + currentMonthDays) { + instantiateNextMonth(currentMonthDays, firstDayWeek, row, col, position); + } + } + return day; + } + + private void fillCurrentMonthDate(int day, int row, int col) { + // 填充当前月的日期数据 + CalendarDate date = seedDate.modifyDay(day); + if (weeks[row] == null) { + weeks[row] = new Week(row); + } + if (weeks[row].days[col] != null) { + if (date.equals(CalendarViewAdapter.loadSelectedDate())) { + weeks[row].days[col].setDate(date); + weeks[row].days[col].setState(State.SELECT); + } else { + weeks[row].days[col].setDate(date); + weeks[row].days[col].setState(State.CURRENT_MONTH); + } + } else { + if (date.equals(CalendarViewAdapter.loadSelectedDate())) { + weeks[row].days[col] = new Day(State.SELECT, date, row, col); + } else { + weeks[row].days[col] = new Day(State.CURRENT_MONTH, date, row, col); + } + } + if (date.equals(seedDate)) { + selectedRowIndex = row; + } + } + + private void instantiateNextMonth(int currentMonthDays, + int firstDayWeek, + int row, + int col, + int position) { + // 创建下一个月的日期对象 + CalendarDate date = new CalendarDate( + seedDate.year, + seedDate.month + 1, + position - firstDayWeek - currentMonthDays + 1); + // 如果当前行的周对象为空,则创建新的周对象 + if (weeks[row] == null) { + weeks[row] = new Week(row); + } + // 如果当前行的日对象不为空,则设置日期和状态; + if (weeks[row].days[col] != null) { + weeks[row].days[col].setDate(date); + weeks[row].days[col].setState(State.NEXT_MONTH); + } + //否则创建新的日对象并设置日期和状态 + else { + weeks[row].days[col] = new Day(State.NEXT_MONTH, date, row, col); + } + } + + // 为当前日期视图创建上一个月的日期对象并设置相关状态,同上 + private void instantiateLastMonth(int lastMonthDays, int firstDayWeek, int row, int col, int position) { + CalendarDate date = new CalendarDate( + seedDate.year, + seedDate.month - 1, + lastMonthDays - (firstDayWeek - position - 1)); + if (weeks[row] == null) { + weeks[row] = new Week(row); + } + if (weeks[row].days[col] != null) { + weeks[row].days[col].setDate(date); + weeks[row].days[col].setState(State.PAST_MONTH); + } else { + weeks[row].days[col] = new Day(State.PAST_MONTH, date, row, col); + } + } + + // 设置要显示的日期 + public void showDate(CalendarDate seedDate) { + if (seedDate != null) { + this.seedDate = seedDate; + } else { + this.seedDate = new CalendarDate(); + } + update(); + } + + // 更新日期视图 + public void update() { + // 实例化月份并使日历失效以便重新绘制 + instantiateMonth(); + calendar.invalidate(); + } + + // 获取当前日期 + public CalendarDate getSeedDate() { + return this.seedDate; + } + + // 取消选中日期的状态 + public void cancelSelectState() { + // 遍历所有日期,将选中状态的日期重置为当前月状态并重置选中行索引 + for (int i = 0; i < Const.TOTAL_ROW; i++) { + if (weeks[i] != null) { + for (int j = 0; j < Const.TOTAL_COL; j++) { + if (weeks[i].days[j].getState() == State.SELECT) { + weeks[i].days[j].setState(State.CURRENT_MONTH); + resetSelectedRowIndex(); + break; + } + } + } + } + } + + // 重置选中行索引 + public void resetSelectedRowIndex() { + selectedRowIndex = 0; + } + + // 获取选中行索引 + public int getSelectedRowIndex() { + return selectedRowIndex; + } + + // 设置选中行索引 + public void setSelectedRowIndex(int selectedRowIndex) { + this.selectedRowIndex = selectedRowIndex; + } + + // 获取日历对象 + public Calendar getCalendar() { + return calendar; + } + + // 设置日历对象 + public void setCalendar(Calendar calendar) { + this.calendar = calendar; + } + + // 获取日期属性 + public CalendarAttr getAttr() { + return attr; + } + + // 设置日期属性 + public void setAttr(CalendarAttr attr) { + this.attr = attr; + } + + // 获取上下文对象 + public Context getContext() { + return context; + } + + // 设置上下文对象 + public void setContext(Context context) { + this.context = context; + } + + // 设置日期选择监听器 + public void setOnSelectDateListener(OnSelectDateListener onSelectDateListener) { + this.onSelectDateListener = onSelectDateListener; + } + + // 设置日期渲染器 + public void setDayRenderer(IDayRenderer dayRenderer) { + this.dayRenderer = dayRenderer; + } +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/component/CalendarViewAdapter.java b/src/calendar/src/main/java/com/ldf/calendar/component/CalendarViewAdapter.java new file mode 100644 index 0000000..7084e6b --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/component/CalendarViewAdapter.java @@ -0,0 +1,334 @@ +package com.ldf.calendar.component; + +import android.content.Context; +import android.support.v4.view.PagerAdapter; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import com.ldf.calendar.Utils; +import com.ldf.calendar.interf.IDayRenderer; +import com.ldf.calendar.interf.OnAdapterSelectListener; +import com.ldf.calendar.interf.OnSelectDateListener; +import com.ldf.calendar.model.CalendarDate; +import com.ldf.calendar.view.Calendar; +import com.ldf.calendar.view.MonthPager; + +import java.util.ArrayList; +import java.util.HashMap; + +public class CalendarViewAdapter extends PagerAdapter { + // 保存选中的日期 + private static CalendarDate date = new CalendarDate(); + private ArrayList calendars = new ArrayList<>();// 存储日历实例的列表 + private int currentPosition;// 当前页面位置 + // 日历类型,默认为月视图 + private CalendarAttr.CalendarType calendarType = CalendarAttr.CalendarType.MONTH; + private int rowCount = 0;// 行数 + private CalendarDate seedDate;// 种子日期 + // 日历类型改变监听器 + private OnCalendarTypeChanged onCalendarTypeChangedListener; + // 周排列方式,默认周一作为一周的第一天 + private CalendarAttr.WeekArrayType weekArrayType = CalendarAttr.WeekArrayType.Monday; + + public CalendarViewAdapter(Context context, + OnSelectDateListener onSelectDateListener, + CalendarAttr.CalendarType calendarType, + CalendarAttr.WeekArrayType weekArrayType, + IDayRenderer dayView) { + super(); + this.calendarType = calendarType;// 设置日历类型 + this.weekArrayType = weekArrayType;// 设置周排列方式 + init(context, onSelectDateListener);// 初始化日历 + setCustomDayRenderer(dayView);// 设置自定义日期渲染器 + } + + public static void saveSelectedDate(CalendarDate calendarDate) { + // 保存选中的日期 + date = calendarDate; + } + + public static CalendarDate loadSelectedDate() { + // 加载选中的日期 + return date; + } + + private void init(Context context, OnSelectDateListener onSelectDateListener) { + // 初始化选中的日期为当前日期 + saveSelectedDate(new CalendarDate()); + //初始化的种子日期为今天 + seedDate = new CalendarDate(); + // 创建三个日历实例 + for (int i = 0; i < 3; i++) { + CalendarAttr calendarAttr = new CalendarAttr(); + calendarAttr.setCalendarType(CalendarAttr.CalendarType.MONTH); + calendarAttr.setWeekArrayType(weekArrayType); + Calendar calendar = new Calendar(context, onSelectDateListener, calendarAttr); + calendar.setOnAdapterSelectListener(new OnAdapterSelectListener() { + @Override + public void cancelSelectState() { + // 取消其他选中状态 + cancelOtherSelectState(); + } + + @Override + public void updateSelectState() { + // 更新当前日历 + invalidateCurrentCalendar(); + } + }); + calendars.add(calendar);// 将日历实例添加到列表中 + } + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + Log.e("ldf", "setPrimaryItem"); + super.setPrimaryItem(container, position, object); + this.currentPosition = position;// 设置当前页面位置 + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + Log.e("ldf", "instantiateItem"); + if (position < 2) { + return null; + } + // 获取对应位置的日历实例 + Calendar calendar = calendars.get(position % calendars.size()); + if (calendarType == CalendarAttr.CalendarType.MONTH) { + // 计算当前日期 + CalendarDate current = seedDate.modifyMonth(position - MonthPager.CURRENT_DAY_INDEX); + current.setDay(1);//每月的种子日期都是1号 + calendar.showDate(current); + } else { + CalendarDate current = seedDate.modifyWeek(position - MonthPager.CURRENT_DAY_INDEX); + if (weekArrayType == CalendarAttr.WeekArrayType.Sunday) { + calendar.showDate(Utils.getSaturday(current)); + } else { + calendar.showDate(Utils.getSunday(current)); + } + calendar.updateWeek(rowCount);// 更新周视图 + } + if (container.getChildCount() == calendars.size()) { + container.removeView(calendars.get(position % 3));// 移除视图 + } + if (container.getChildCount() < calendars.size()) { + container.addView(calendar, 0); + } else { + container.addView(calendar, position % 3);// 添加视图 + } + return calendar; + } + + @Override + public int getCount() { + // 返回PagerAdapter所管理的视图总数,这里返回整型最大值,用于支持无限滑动 + return Integer.MAX_VALUE; + } + + @Override + public boolean isViewFromObject(View view, Object object) { + // 判断instantiateItem()方法返回的对象是否与当前视图相关联 + return view == object; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + // 从容器中移除指定位置的视图 + container.removeView(container); + } + + public ArrayList getPagers() { + // 获取所有的日历实例列表 + return calendars; + } + + public void cancelOtherSelectState() { + // 取消所有日历实例的选中状态 + for (int i = 0; i < calendars.size(); i++) { + Calendar calendar = calendars.get(i); + calendar.cancelSelectState(); + } + } + + public void invalidateCurrentCalendar() { + // 更新当前日历实例的状态 + for (int i = 0; i < calendars.size(); i++) { + Calendar calendar = calendars.get(i); + calendar.update(); + if (calendar.getCalendarType() == CalendarAttr.CalendarType.WEEK) { + calendar.updateWeek(rowCount); + } + } + } + + public void setMarkData(HashMap markData) { + // 设置标记数据 + Utils.setMarkData(markData); + } + + public void switchToMonth() { + // 切换到月视图 + if (calendars != null && calendars.size() > 0 && calendarType != CalendarAttr.CalendarType.MONTH) { + onCalendarTypeChangedListener.onCalendarTypeChanged(CalendarAttr.CalendarType.MONTH); + calendarType = CalendarAttr.CalendarType.MONTH; + MonthPager.CURRENT_DAY_INDEX = currentPosition; + Calendar v = calendars.get(currentPosition % 3);//0 + seedDate = v.getSeedDate(); + + Calendar v1 = calendars.get(currentPosition % 3);//0 + v1.switchCalendarType(CalendarAttr.CalendarType.MONTH); + v1.showDate(seedDate); + + Calendar v2 = calendars.get((currentPosition - 1) % 3);//2 + v2.switchCalendarType(CalendarAttr.CalendarType.MONTH); + CalendarDate last = seedDate.modifyMonth(-1); + last.setDay(1); + v2.showDate(last); + + Calendar v3 = calendars.get((currentPosition + 1) % 3);//1 + v3.switchCalendarType(CalendarAttr.CalendarType.MONTH); + CalendarDate next = seedDate.modifyMonth(1); + next.setDay(1); + v3.showDate(next); + } + } + + public void switchToWeek(int rowIndex) { + // 切换到周视图,并指定行索引 + rowCount = rowIndex; + if (calendars != null && calendars.size() > 0 && calendarType != CalendarAttr.CalendarType.WEEK) { + onCalendarTypeChangedListener.onCalendarTypeChanged(CalendarAttr.CalendarType.WEEK); + calendarType = CalendarAttr.CalendarType.WEEK; + MonthPager.CURRENT_DAY_INDEX = currentPosition; + Calendar v = calendars.get(currentPosition % 3); + seedDate = v.getSeedDate(); + + rowCount = v.getSelectedRowIndex(); + + Calendar v1 = calendars.get(currentPosition % 3); + v1.switchCalendarType(CalendarAttr.CalendarType.WEEK); + v1.showDate(seedDate); + v1.updateWeek(rowIndex); + + Calendar v2 = calendars.get((currentPosition - 1) % 3); + v2.switchCalendarType(CalendarAttr.CalendarType.WEEK); + CalendarDate last = seedDate.modifyWeek(-1); + if (weekArrayType == CalendarAttr.WeekArrayType.Sunday) { + v2.showDate(Utils.getSaturday(last)); + } else { + v2.showDate(Utils.getSunday(last)); + }//每周的种子日期为这一周的最后一天 + v2.updateWeek(rowIndex); + + Calendar v3 = calendars.get((currentPosition + 1) % 3); + v3.switchCalendarType(CalendarAttr.CalendarType.WEEK); + CalendarDate next = seedDate.modifyWeek(1); + if (weekArrayType == CalendarAttr.WeekArrayType.Sunday) { + v3.showDate(Utils.getSaturday(next)); + } else { + v3.showDate(Utils.getSunday(next)); + }//每周的种子日期为这一周的最后一天 + v3.updateWeek(rowIndex); + } + } + + public void notifyMonthDataChanged(CalendarDate date) { + // 通知月数据发生改变,并刷新日历 + seedDate = date; + refreshCalendar(); + } + + public void notifyDataChanged(CalendarDate date) { + // 通知数据发生改变,并保存选中的日期,然后刷新日历 + seedDate = date; + saveSelectedDate(date); + refreshCalendar(); + } + + public void notifyDataChanged() { + // 通知数据发生改变,并刷新日历 + refreshCalendar(); + } + + private void refreshCalendar() { + // 刷新日历的显示 + if (calendarType == CalendarAttr.CalendarType.WEEK) { + MonthPager.CURRENT_DAY_INDEX = currentPosition; + Calendar v1 = calendars.get(currentPosition % 3); + v1.showDate(seedDate); + v1.updateWeek(rowCount); + + Calendar v2 = calendars.get((currentPosition - 1) % 3); + CalendarDate last = seedDate.modifyWeek(-1); + if (weekArrayType == CalendarAttr.WeekArrayType.Sunday) { + v2.showDate(Utils.getSaturday(last)); + } else { + v2.showDate(Utils.getSunday(last)); + } + v2.updateWeek(rowCount); + + Calendar v3 = calendars.get((currentPosition + 1) % 3); + CalendarDate next = seedDate.modifyWeek(1); + if (weekArrayType == CalendarAttr.WeekArrayType.Sunday) { + v3.showDate(Utils.getSaturday(next)); + } else { + v3.showDate(Utils.getSunday(next)); + }//每周的种子日期为这一周的最后一天 + v3.updateWeek(rowCount); + } else { + MonthPager.CURRENT_DAY_INDEX = currentPosition; + + Calendar v1 = calendars.get(currentPosition % 3);//0 + v1.showDate(seedDate); + + Calendar v2 = calendars.get((currentPosition - 1) % 3);//2 + CalendarDate last = seedDate.modifyMonth(-1); + last.setDay(1); + v2.showDate(last); + + Calendar v3 = calendars.get((currentPosition + 1) % 3);//1 + CalendarDate next = seedDate.modifyMonth(1); + next.setDay(1); + v3.showDate(next); + } + } + + public CalendarAttr.CalendarType getCalendarType() { + // 获取当前日历类型 + return calendarType; + } + + /** + * 为每一个Calendar实例设置renderer对象 + * + * @return void + */ + public void setCustomDayRenderer(IDayRenderer dayRenderer) { + // 为每个日历实例设置自定义渲染器对象 + Calendar c0 = calendars.get(0); + c0.setDayRenderer(dayRenderer); + + Calendar c1 = calendars.get(1); + c1.setDayRenderer(dayRenderer.copy()); + + Calendar c2 = calendars.get(2); + c2.setDayRenderer(dayRenderer.copy()); + } + + public void setOnCalendarTypeChangedListener(OnCalendarTypeChanged onCalendarTypeChangedListener) { + // 设置日历类型改变监听器 + this.onCalendarTypeChangedListener = onCalendarTypeChangedListener; + } + + public CalendarAttr.WeekArrayType getWeekArrayType() { + // 获取一周的起始日期类型(星期日或星期一) + return weekArrayType; + } + + public interface OnCalendarTypeChanged { + // 定义日历类型改变的回调接口 + void onCalendarTypeChanged(CalendarAttr.CalendarType type); + } +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/component/State.java b/src/calendar/src/main/java/com/ldf/calendar/component/State.java new file mode 100644 index 0000000..43e3de4 --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/component/State.java @@ -0,0 +1,11 @@ +package com.ldf.calendar.component; + +/** + * 日历状态枚举 + */ +public enum State { + CURRENT_MONTH, // 当前月 + PAST_MONTH, // 过去的月份 + NEXT_MONTH, // 下一个月 + SELECT // 选中状态 +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/interf/IDayRenderer.java b/src/calendar/src/main/java/com/ldf/calendar/interf/IDayRenderer.java new file mode 100644 index 0000000..ac740b7 --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/interf/IDayRenderer.java @@ -0,0 +1,19 @@ +package com.ldf.calendar.interf; + +import android.graphics.Canvas; + +import com.ldf.calendar.view.Day; + +/** + * 日历每日单元渲染接口 + */ +public interface IDayRenderer { + //刷新内容 + void refreshContent(); + + //绘制单个日期 + void drawDay(Canvas canvas, Day day); + + //复制日历渲染器 + IDayRenderer copy(); +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/interf/OnAdapterSelectListener.java b/src/calendar/src/main/java/com/ldf/calendar/interf/OnAdapterSelectListener.java new file mode 100644 index 0000000..f5c1ac4 --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/interf/OnAdapterSelectListener.java @@ -0,0 +1,11 @@ +package com.ldf.calendar.interf; + +// 定义了一个接口 OnAdapterSelectListener +public interface OnAdapterSelectListener { + + // 取消选择状态的方法声明 + void cancelSelectState(); + + // 更新选择状态的方法声明 + void updateSelectState(); +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/interf/OnSelectDateListener.java b/src/calendar/src/main/java/com/ldf/calendar/interf/OnSelectDateListener.java new file mode 100644 index 0000000..56c4a79 --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/interf/OnSelectDateListener.java @@ -0,0 +1,13 @@ +package com.ldf.calendar.interf; + +import com.ldf.calendar.model.CalendarDate; + +// 定义了一个接口 OnSelectDateListener +public interface OnSelectDateListener { + + // 选中日期的方法声明,接收一个 CalendarDate 对象作为参数 + void onSelectDate(CalendarDate date); + + // 选中其它月份日期的方法声明,接收表示月份偏移量的整数作为参数 + void onSelectOtherMonth(int offset); +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/model/CalendarDate.java b/src/calendar/src/main/java/com/ldf/calendar/model/CalendarDate.java new file mode 100644 index 0000000..77a82f3 --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/model/CalendarDate.java @@ -0,0 +1,151 @@ +package com.ldf.calendar.model; + +import android.util.Log; + +import com.ldf.calendar.Utils; + +import java.io.Serializable; +import java.util.Calendar; + +public class CalendarDate implements Serializable { + private static final long serialVersionUID = 1L; + public int year; + public int month; //1~12 + public int day; + // 构造方法,用指定的年、月、日来初始化对象 + public CalendarDate(int year, int month, int day) { + if (month > 12) { + month = 1; + year++; + } else if (month < 1) { + month = 12; + year--; + } + this.year = year; + this.month = month; + this.day = day; + } + // 无参构造方法,默认使用当前系统日期来初始化对象 + public CalendarDate() { + this.year = Utils.getYear(); + this.month = Utils.getMonth(); + this.day = Utils.getDay(); + } + + /** + * 通过修改当前Date对象的天数返回一个修改后的Date + * + * @return CalendarDate 修改后的日期 + */ + public CalendarDate modifyDay(int day) { + int lastMonthDays = Utils.getMonthDays(this.year, this.month - 1); + int currentMonthDays = Utils.getMonthDays(this.year, this.month); + + CalendarDate modifyDate; + if (day > currentMonthDays) { + modifyDate = new CalendarDate(this.year, this.month, this.day); + Log.e("ldf", "移动天数过大"); + } else if (day > 0) { + modifyDate = new CalendarDate(this.year, this.month, day); + } else if (day > 0 - lastMonthDays) { + modifyDate = new CalendarDate(this.year, this.month - 1, lastMonthDays + day); + } else { + modifyDate = new CalendarDate(this.year, this.month, this.day); + Log.e("ldf", "移动天数过大"); + } + return modifyDate; + } + + /** + * 通过修改当前Date对象的所在周返回一个修改后的Date + * + * @return CalendarDate 修改后的日期 + */ + public CalendarDate modifyWeek(int offset) { + CalendarDate result = new CalendarDate(); + Calendar c = Calendar.getInstance(); + c.set(Calendar.YEAR, year); + c.set(Calendar.MONTH, month - 1); + c.set(Calendar.DAY_OF_MONTH, day); + c.add(Calendar.DATE, offset * 7); + result.setYear(c.get(Calendar.YEAR)); + result.setMonth(c.get(Calendar.MONTH) + 1); + result.setDay(c.get(Calendar.DATE)); + return result; + } + + /** + * 通过修改当前Date对象的所在月返回一个修改后的Date + * + * @return CalendarDate 修改后的日期 + */ + public CalendarDate modifyMonth(int offset) { + CalendarDate result = new CalendarDate(); + int addToMonth = this.month + offset; + if (offset > 0) { + if (addToMonth > 12) { + result.setYear(this.year + (addToMonth - 1) / 12); + result.setMonth(addToMonth % 12 == 0 ? 12 : addToMonth % 12); + } else { + result.setYear(this.year); + result.setMonth(addToMonth); + } + } else { + if (addToMonth == 0) { + result.setYear(this.year - 1); + result.setMonth(12); + } else if (addToMonth < 0) { + result.setYear(this.year + addToMonth / 12 - 1); + int month = 12 - Math.abs(addToMonth) % 12; + result.setMonth(month == 0 ? 12 : month); + } else { + result.setYear(this.year); + result.setMonth(addToMonth == 0 ? 12 : addToMonth); + } + } + return result; + } + + @Override + public String toString() { + return year + "-" + month + "-" + day; + } + // 下面是获取、设置年、月、日 + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } + + public int getMonth() { + return month; + } + + public void setMonth(int month) { + this.month = month; + } + + public int getDay() { + return day; + } + + public void setDay(int day) { + this.day = day; + } + // 比较两个日期是否相等 + public boolean equals(CalendarDate date) { + if (date == null) { + return false; + } + return this.getYear() == date.getYear() + && this.getMonth() == date.getMonth() + && this.getDay() == date.getDay(); + } + // 克隆当前日期对象 + public CalendarDate cloneSelf() { + return new CalendarDate(year, month, day); + } + +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/view/Calendar.java b/src/calendar/src/main/java/com/ldf/calendar/view/Calendar.java new file mode 100644 index 0000000..0eaa696 --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/view/Calendar.java @@ -0,0 +1,152 @@ +package com.ldf.calendar.view; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.view.MotionEvent; +import android.view.View; + +import com.ldf.calendar.Const; +import com.ldf.calendar.Utils; +import com.ldf.calendar.component.CalendarAttr; +import com.ldf.calendar.component.CalendarRenderer; +import com.ldf.calendar.interf.IDayRenderer; +import com.ldf.calendar.interf.OnAdapterSelectListener; +import com.ldf.calendar.interf.OnSelectDateListener; +import com.ldf.calendar.model.CalendarDate; + +@SuppressLint("ViewConstructor") +public class Calendar extends View { + /** + * 日历列数 + */ + private CalendarAttr.CalendarType calendarType;// 日历类型 + private int cellHeight; // 单元格高度 + private int cellWidth; // 单元格宽度 + + private OnSelectDateListener onSelectDateListener; // 单元格点击回调事件 + private Context context;// 上下文 + private CalendarAttr calendarAttr;// 日历属性 + private CalendarRenderer renderer;// 日历渲染器 + + private OnAdapterSelectListener onAdapterSelectListener;// 适配器选择监听器 + private float touchSlop; // 触摸阈值 + private float posX = 0;// 点击位置X坐标 + private float posY = 0;// 点击位置Y坐标 + // 构造函数 + public Calendar(Context context, + OnSelectDateListener onSelectDateListener, + CalendarAttr attr) { + super(context); + this.onSelectDateListener = onSelectDateListener; + calendarAttr = attr; + init(context); + } + // 初始化方法 + private void init(Context context) { + this.context = context; + touchSlop = Utils.getTouchSlop(context); // 获取触摸阈值 + initAttrAndRenderer(); + } + // 初始化属性和渲染器 + private void initAttrAndRenderer() { + renderer = new CalendarRenderer(this, calendarAttr, context); + renderer.setOnSelectDateListener(onSelectDateListener); + } + // 绘制方法 + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + renderer.draw(canvas); + } + // 大小改变时调用 + @Override + protected void onSizeChanged(int w, int h, int oldW, int oldH) { + super.onSizeChanged(w, h, oldW, oldH); + cellHeight = h / Const.TOTAL_ROW;// 计算单元格高度 + cellWidth = w / Const.TOTAL_COL;// 计算单元格宽度 + calendarAttr.setCellHeight(cellHeight); + calendarAttr.setCellWidth(cellWidth); + renderer.setAttr(calendarAttr); + } + + /* + * 触摸事件为了确定点击的位置日期 + */ + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + posX = event.getX(); + posY = event.getY(); + break; + case MotionEvent.ACTION_UP: + float disX = event.getX() - posX; + float disY = event.getY() - posY; + if (Math.abs(disX) < touchSlop && Math.abs(disY) < touchSlop) { + int col = (int) (posX / cellWidth);// 计算列索引 + int row = (int) (posY / cellHeight);// 计算行索引 + onAdapterSelectListener.cancelSelectState(); + renderer.onClickDate(col, row);// 调用渲染器处理点击事件 + onAdapterSelectListener.updateSelectState(); + invalidate(); + } + break; + } + return true; + } + // 获取日历类型 + public CalendarAttr.CalendarType getCalendarType() { + return calendarAttr.getCalendarType(); + } + // 切换日历类型 + public void switchCalendarType(CalendarAttr.CalendarType calendarType) { + calendarAttr.setCalendarType(calendarType); + renderer.setAttr(calendarAttr); + } + // 获取单元格高度 + public int getCellHeight() { + return cellHeight; + } + // 重置选中的行索引 + public void resetSelectedRowIndex() { + renderer.resetSelectedRowIndex(); + } + // 获取选中的行索引 + public int getSelectedRowIndex() { + return renderer.getSelectedRowIndex(); + } + // 设置选中的行索引 + public void setSelectedRowIndex(int selectedRowIndex) { + renderer.setSelectedRowIndex(selectedRowIndex); + } + // 设置适配器选择监听器 + public void setOnAdapterSelectListener(OnAdapterSelectListener onAdapterSelectListener) { + this.onAdapterSelectListener = onAdapterSelectListener; + } + // 显示指定日期 + public void showDate(CalendarDate current) { + renderer.showDate(current); + } + // 更新星期数 + public void updateWeek(int rowCount) { + renderer.updateWeek(rowCount); + invalidate(); + } + // 更新日历 + public void update() { + renderer.update(); + } + // 取消选中状态 + public void cancelSelectState() { + renderer.cancelSelectState(); + } + // 获取种子日期 + public CalendarDate getSeedDate() { + return renderer.getSeedDate(); + } + // 设置日期渲染器 + public void setDayRenderer(IDayRenderer dayRenderer) { + renderer.setDayRenderer(dayRenderer); + } +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/view/Day.java b/src/calendar/src/main/java/com/ldf/calendar/view/Day.java new file mode 100644 index 0000000..d43db0c --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/view/Day.java @@ -0,0 +1,82 @@ +package com.ldf.calendar.view; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.ldf.calendar.component.State; +import com.ldf.calendar.model.CalendarDate; + +/** + * 表示日历中的某一天的状态和位置信息的类 + */ + +public class Day implements Parcelable { + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public Day createFromParcel(Parcel source) { + return new Day(source); + } + + @Override + public Day[] newArray(int size) { + return new Day[size]; + } + }; + private State state;// 日期的状态 + private CalendarDate date;// 日期信息 + private int posRow;// 在日历中的行位置 + private int posCol;// 在日历中的列位置 + // 构造方法 + public Day(State state, CalendarDate date, int posRow, int posCol) { + this.state = state; + this.date = date; + this.posRow = posRow; + this.posCol = posCol; + } + // 从Parcel对象中读取数据以恢复Day对象的状态的构造方法 + protected Day(Parcel in) { + int tmpState = in.readInt(); + this.state = tmpState == -1 ? null : State.values()[tmpState]; + this.date = (CalendarDate) in.readSerializable(); + this.posRow = in.readInt(); + this.posCol = in.readInt(); + } + //获取和设置日期的状态、日期的信息,行列位置 + public State getState() { + return state; + } + public void setState(State state) { + this.state = state; + } + public CalendarDate getDate() { + return date; + } + public void setDate(CalendarDate date) { + this.date = date; + } + public int getPosRow() { + return posRow; + } + public void setPosRow(int posRow) { + this.posRow = posRow; + } + public int getPosCol() { + return posCol; + } + public void setPosCol(int posCol) { + this.posCol = posCol; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(this.state == null ? -1 : this.state.ordinal()); + dest.writeSerializable(this.date); + dest.writeInt(this.posRow); + dest.writeInt(this.posCol); + } +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/view/DayView.java b/src/calendar/src/main/java/com/ldf/calendar/view/DayView.java new file mode 100644 index 0000000..9a39cde --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/view/DayView.java @@ -0,0 +1,85 @@ +package com.ldf.calendar.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.RelativeLayout; + +import com.ldf.calendar.interf.IDayRenderer; + +/** + * 自定义的日历视图,用于显示日历中的某一天的视图 + */ +public abstract class DayView extends RelativeLayout implements IDayRenderer { + + protected Day day; + protected Context context; + protected int layoutResource; + + /** + * 构造器 传入资源文件创建DayView + * + * @param layoutResource 资源文件 + * @param context 上下文 + */ + public DayView(Context context, int layoutResource) { + super(context); + setupLayoutResource(layoutResource); + this.context = context; + this.layoutResource = layoutResource; + } + + /** + * 为自定义的DayView设置资源文件 + * + * @param layoutResource 资源文件 + * @return CalendarDate 修改后的日期 + */ + private void setupLayoutResource(int layoutResource) { + View inflated = LayoutInflater.from(getContext()).inflate(layoutResource, this); + inflated.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + inflated.layout(0, 0, getMeasuredWidth(), getMeasuredHeight()); + } + /** + * 刷新 DayView 的内容 + */ + @Override + public void refreshContent() { + measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + layout(0, 0, getMeasuredWidth(), getMeasuredHeight()); + } + /** + * 绘制特定日期的视图 + * + * @param canvas 画布 + * @param day 特定日期 + */ + @Override + public void drawDay(Canvas canvas, Day day) { + this.day = day; + refreshContent(); + int saveId = canvas.save(); + canvas.translate(getTranslateX(canvas, day), + day.getPosRow() * getMeasuredHeight()); + draw(canvas); + canvas.restoreToCount(saveId); + } + /** + * 计算视图在画布中的横向偏移量 + * + * @param canvas 画布 + * @param day 特定日期 + * @return 视图横向偏移量 + */ + private int getTranslateX(Canvas canvas, Day day) { + int dx; + int canvasWidth = canvas.getWidth() / 7; + int viewWidth = getMeasuredWidth(); + int moveX = (canvasWidth - viewWidth) / 2; + dx = day.getPosCol() * canvasWidth + moveX; + return dx; + } +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/view/MonthPager.java b/src/calendar/src/main/java/com/ldf/calendar/view/MonthPager.java new file mode 100644 index 0000000..62ac697 --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/view/MonthPager.java @@ -0,0 +1,173 @@ +package com.ldf.calendar.view; + +import android.content.Context; +import android.support.design.widget.CoordinatorLayout; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; + +import com.ldf.calendar.behavior.MonthPagerBehavior; +import com.ldf.calendar.component.CalendarViewAdapter; + +@CoordinatorLayout.DefaultBehavior(MonthPagerBehavior.class) +public class MonthPager extends ViewPager { + public static int CURRENT_DAY_INDEX = 1000; + + private int currentPosition = CURRENT_DAY_INDEX;// 当前页面位置 + private int cellHeight;// 单元格高度 + private int viewHeight;// 视图高度 + private int rowIndex = 6;// 行索引,默认为6行 + + private OnPageChangeListener monthPageChangeListener;// 月份切换监听器 + private boolean pageChangeByGesture = false;// 是否通过手势切换页面 + private boolean hasPageChangeListener = false;// 是否有自定义的页面改变监听器 + private boolean scrollable = true;// 是否可以滚动 + private int pageScrollState = ViewPager.SCROLL_STATE_IDLE;// 页面滚动状态,默认为静止状态 + // 构造函数 + public MonthPager(Context context) { + this(context, null); + } + // 带属性的构造函数 + public MonthPager(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + // 初始化方法 + private void init() { + // 创建内部的 onPageChangeListener + ViewPager.OnPageChangeListener viewPageChangeListener = new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + if (monthPageChangeListener != null) { + monthPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageSelected(int position) { + // 更新当前位置 + currentPosition = position; + if (pageChangeByGesture) { + if (monthPageChangeListener != null) { + monthPageChangeListener.onPageSelected(position); + } + pageChangeByGesture = false; + } + } + + @Override + public void onPageScrollStateChanged(int state) { + // 更新页面滚动状态 + pageScrollState = state; + if (monthPageChangeListener != null) { + monthPageChangeListener.onPageScrollStateChanged(state); + } + pageChangeByGesture = true; + } + }; + // 添加内部监听器 + addOnPageChangeListener(viewPageChangeListener); + // 更新监听器状态为已添加 + hasPageChangeListener = true; + } + + @Override + public void addOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + if (hasPageChangeListener) { + // 只能使用自定义的监听器 + Log.e("ldf", "MonthPager Just Can Use Own OnPageChangeListener"); + } else { + super.addOnPageChangeListener(listener); + } + } + // 添加自定义的监听器 + public void addOnPageChangeListener(OnPageChangeListener listener) { + // 设置监听器 + this.monthPageChangeListener = listener; + // 提示只能使用自定义的监听器 + Log.e("ldf", "MonthPager Just Can Use Own OnPageChangeListener"); + } + // 设置是否可滚动 + public void setScrollable(boolean scrollable) { + this.scrollable = scrollable; + } + // 触摸事件处理 + @Override + public boolean onTouchEvent(MotionEvent me) { + if (!scrollable) + return false;// 如果不可滚动,则返回false + else + return super.onTouchEvent(me); // 否则调用父类的方法 + } + // 拦截触摸事件处理 + @Override + public boolean onInterceptTouchEvent(MotionEvent me) { + if (!scrollable) + return false;// 同上 + else + return super.onInterceptTouchEvent(me); + } + // 切换到其他月份 + public void selectOtherMonth(int offset) { + // 设置当前页面位置 + setCurrentItem(currentPosition + offset); + // 获取日历视图适配器 + CalendarViewAdapter calendarViewAdapter = (CalendarViewAdapter) getAdapter(); + // 通知数据已更改 + calendarViewAdapter.notifyDataChanged(CalendarViewAdapter.loadSelectedDate()); + } + // 设置当前页面位置 + public int getPageScrollState() { + return pageScrollState; + } + // 获取顶部可移动距离 + public int getTopMovableDistance() { + // 获取日历视图适配器 + CalendarViewAdapter calendarViewAdapter = (CalendarViewAdapter) getAdapter(); + if (calendarViewAdapter == null) { + return cellHeight; + } + // 获取选定行索引 + rowIndex = calendarViewAdapter.getPagers().get(currentPosition % 3).getSelectedRowIndex(); + return cellHeight * rowIndex; + } + // 获取单元格高度 + public int getCellHeight() { + return cellHeight; + } + // 获取视图高度 + public int getViewHeight() { + return viewHeight; + } + public void setViewHeight(int viewHeight) { + cellHeight = viewHeight / 6; + this.viewHeight = viewHeight; + } + // 获取当前位置 + public int getCurrentPosition() { + return currentPosition; + } + public void setCurrentPosition(int currentPosition) { + this.currentPosition = currentPosition; + } + // 获取行索引 + public int getRowIndex() { + CalendarViewAdapter calendarViewAdapter = (CalendarViewAdapter) getAdapter(); + rowIndex = calendarViewAdapter.getPagers().get(currentPosition % 3).getSelectedRowIndex(); + Log.e("ldf", "getRowIndex = " + rowIndex); + return rowIndex; + } + public void setRowIndex(int rowIndex) { + this.rowIndex = rowIndex; + } + // 自定义的页面改变监听器接口 + public interface OnPageChangeListener { + // 页面滚动回调 + void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); + // 页面选中回调 + void onPageSelected(int position); + // 页面滚动状态改变回调 + void onPageScrollStateChanged(int state); + } +} \ No newline at end of file diff --git a/src/calendar/src/main/java/com/ldf/calendar/view/Week.java b/src/calendar/src/main/java/com/ldf/calendar/view/Week.java new file mode 100644 index 0000000..82288ad --- /dev/null +++ b/src/calendar/src/main/java/com/ldf/calendar/view/Week.java @@ -0,0 +1,36 @@ +package com.ldf.calendar.view; + +import com.ldf.calendar.Const; + +// 表示日历中的一周 +public class Week { + // 当前周所在的行数 + public int row; + // 用于存储一周中的每一天的Day对象,长度为总列数 + public Day[] days = new Day[Const.TOTAL_COL]; + + // 构造函数,初始化周的行数 + public Week(int row) { + this.row = row; + } + + // 获取当前周所在的行数 + public int getRow() { + return row; + } + + // 设置当前周所在的行数 + public void setRow(int row) { + this.row = row; + } + + // 获取一周中的每一天的Day对象数组 + public Day[] getDays() { + return days; + } + + // 设置一周中的每一天的Day对象数组 + public void setDays(Day[] days) { + this.days = days; + } +} \ No newline at end of file diff --git a/src/calendar/src/readme b/src/calendar/src/readme new file mode 100644 index 0000000..baf70df --- /dev/null +++ b/src/calendar/src/readme @@ -0,0 +1,9 @@ +1. 左右滑动的view左边有时候也会显示今天的view,划过去之后才会变 - fixed +2. 月周切换问题 +3. 收缩后的周日历滑动问题 +4. 下拉时下面的view的滑动问题,能否做成先下拉下面的view, 拉不动时才处理上面的日历的view --fixed +5. 回到今天的问题(跳转到指定日期) - fixed +6. 滑动的下一页的选中问题 -- fixed +7. 自定义日历View的Wrap_content模式 --not fixed full +8. 日历和RecycleView的依赖关系是什么时候建立的 +9. 不同分辨率的显示问题 \ No newline at end of file diff --git a/src/daemonlibrary/src/main/AndroidManifest.xml b/src/daemonlibrary/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d90ec11 --- /dev/null +++ b/src/daemonlibrary/src/main/AndroidManifest.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/AbsServiceConnection.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/AbsServiceConnection.java new file mode 100644 index 0000000..96edbb7 --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/AbsServiceConnection.java @@ -0,0 +1,32 @@ +package com.shihoo.daemon; + +import android.content.ComponentName; +import android.content.ServiceConnection; +import android.os.IBinder; + +// 抽象类,用于定义服务连接的行为 +abstract class AbsServiceConnection implements ServiceConnection { + + // 当前绑定的状态 + boolean mConnectedState = false; + + // 当服务成功连接时调用 + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mConnectedState = true; + } + // 当服务断开连接时调用 + @Override + public void onServiceDisconnected(ComponentName name) { + mConnectedState = false; + onDisconnected(name); + } + + // 当绑定的服务死掉时调用,此时视为服务断开连接 + @Override + public void onBindingDied(ComponentName name) { + onServiceDisconnected(name); + } + // 子类必须实现的方法,用于处理服务断开连接的逻辑 + public abstract void onDisconnected(ComponentName name); +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/AbsWorkService.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/AbsWorkService.java new file mode 100644 index 0000000..874a95d --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/AbsWorkService.java @@ -0,0 +1,206 @@ +package com.shihoo.daemon; + +import android.app.Notification; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Handler; +import android.os.IBinder; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +public abstract class AbsWorkService extends Service { + + // 常量定义 + protected static final int HASH_CODE = 1; + private StopBroadcastReceiver stopBroadcastReceiver; + + // 服务连接 + private AbsServiceConnection mConnection = new AbsServiceConnection() { + @Override + public void onDisconnected(ComponentName name) { + // 当连接断开时,判断是否需要停止服务,并重新启动 WatchDogService + Boolean shouldStopService = shouldStopService(null, 0, 0); + DaemonEnv.startServiceMayBind(AbsWorkService.this, WatchDogService.class, mConnection, shouldStopService); + } + }; + + // 创建时调用 + @Override + public void onCreate() { + super.onCreate(); + Log.d("wsh-daemon", "AbsWorkService onCreate 启动。。。。"); + + // 注册广播接收器 + startRegisterReceiver(); + + // 在特定的Android版本下利用漏洞启动前台服务而不显示通知 + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { + startForeground(HASH_CODE, new Notification()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + // 在API Level 18及以上的Android系统中,启动前台服务而不显示通知 + Boolean shouldStopService = shouldStopService(null, 0, 0); + DaemonEnv.startServiceSafely(AbsWorkService.this, WorkNotificationService.class, shouldStopService); + } + } + + // 设置 WatchDogService 组件的启用状态 + getPackageManager().setComponentEnabledSetting(new ComponentName(getPackageName(), WatchDogService.class.getName()), + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); + } + + // 接收启动命令时调用 + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return onStart(intent, flags, startId); + } + + // 绑定服务时调用 + @NonNull + @Override + public IBinder onBind(Intent intent) { + return onBindService(intent, null); + } + + // 即将销毁时调用 + protected void onEnd(Intent rootIntent) { + onServiceKilled(rootIntent); + // 不同的进程,所有的静态和单例都会失效 + Boolean shouldStopService = shouldStopService(null, 0, 0); + if (shouldStopService){ + return; + } + Log.d("wsh-daemon", "onEnd ---- 搞事 + onDestroy :" + shouldStopService); + DaemonEnv.startServiceSafely(AbsWorkService.this, WatchDogService.class, false); + } + + // 用户移除应用的任务时调用 + @Override + public void onTaskRemoved(Intent rootIntent) { + Log.d("wsh-daemon", "onEnd ---- 搞事 + onTaskRemoved :"); + onEnd(rootIntent); + } + + // 销毁时调用 + @Override + public void onDestroy() { + Log.d("wsh-daemon", "onEnd ---- 搞事 + onDestroy :"); + onEnd(null); + startUnRegisterReceiver(); + } + + // 判断是否需要停止服务 + public abstract Boolean shouldStopService(@Nullable Intent intent, int flags, int startId); + + // 开始任务 + public abstract void startWork(Intent intent, int flags, int startId); + + // 停止任务 + public abstract void stopWork(@Nullable Intent intent, int flags, int startId); + + // 判断任务是否在运行 + public abstract Boolean isWorkRunning(Intent intent, int flags, int startId); + + // 绑定服务时调用 + @NonNull + public abstract IBinder onBindService(Intent intent, Void alwaysNull); + + // 任务被杀死时调用 + public abstract void onServiceKilled(Intent rootIntent); + + // 在任务启动时调用 + protected int onStart(Intent intent, int flags, int startId) { + //启动守护服务,运行在:watch子进程中 + Boolean shouldStopService = shouldStopService(null, 0, 0); + DaemonEnv.startServiceMayBind(AbsWorkService.this, WatchDogService.class, mConnection, shouldStopService); + if (shouldStopService) { + // 如果需要停止服务,则不做任何操作 + } else { + startService(intent, flags, startId); + } + return START_STICKY; + } + + // 开始服务 + void startService(Intent intent, int flags, int startId) { + //若还没有取消订阅,说明任务仍在运行,为防止重复启动,直接 return + Boolean workRunning = isWorkRunning(intent, flags, startId); + if (workRunning != null && workRunning){ + return; + } + //业务逻辑 + startWork(intent, flags, startId); + } + + // 停止服务 + private void stopService(Intent intent, int flags, int startId) { + //取消对任务的订阅 + startUnRegisterReceiver(); + + // 给实现者处理业务逻辑 + stopWork(intent, flags, startId); + + if (mConnection.mConnectedState) { + unbindService(mConnection); + } + exit(); + } + + // 退出应用 + private void exit() { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + stopSelf(); + System.exit(0); + } + }, 3000); + } + + // 用于在特定版本的Android系统中启动前台服务而不显示通知 + public static class WorkNotificationService extends Service { + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + startForeground(AbsWorkService.HASH_CODE, new Notification()); + stopSelf(); + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + } + + // 注册广播接收器 + private void startRegisterReceiver(){ + if (stopBroadcastReceiver == null){ + stopBroadcastReceiver = new StopBroadcastReceiver(); + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(DaemonEnv.ACTION_CANCEL_JOB_ALARM_SUB); + registerReceiver(stopBroadcastReceiver,intentFilter); + } + } + + // 注销广播接收器 + private void startUnRegisterReceiver(){ + if (stopBroadcastReceiver != null){ + unregisterReceiver(stopBroadcastReceiver); + stopBroadcastReceiver = null; + } + } + + // 接收停止任务的广播 + class StopBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + stopService(null, 0, 0); + } + } +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/DaemonEnv.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/DaemonEnv.java new file mode 100644 index 0000000..b47e289 --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/DaemonEnv.java @@ -0,0 +1,93 @@ +package com.shihoo.daemon; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.support.annotation.NonNull; +import android.util.Log; + +/** + * DaemonEnv 类用于管理守护进程的环境设置和服务操作。 + */ +public final class DaemonEnv { + + //取消定时任务的广播动作 + static final String ACTION_CANCEL_JOB_ALARM_SUB = "com.shihoo.CANCEL_JOB_ALARM_SUB"; + //默认的唤醒间隔时间为 2 分钟 + static final int DEFAULT_WAKE_UP_INTERVAL = 2 * 60 * 1000; + //最小的唤醒间隔时间为 1 分钟 + static final int MINIMAL_WAKE_UP_INTERVAL = 60 * 1000; + // 多进程时,尽量少用静态、单例 此处不得已 + public static Class mWorkServiceClass; + + /** + * 启动并绑定服务,如果需要停止,则直接返回。 + * + * @param context 上下文环境 + * @param serviceClass 要启动的服务类 + * @param connection 服务连接 + * @param isNeedStop 是否需要停止服务 + */ + static void startServiceMayBind(@NonNull final Context context, + @NonNull final Class serviceClass, + @NonNull AbsServiceConnection connection, + boolean isNeedStop) { + if (isNeedStop) { + return; + } + // 判断当前绑定的状态 + if (!connection.mConnectedState) { + Log.d("wsh-daemon", "启动并绑定服务:" + serviceClass.getSimpleName()); + final Intent intent = new Intent(context, serviceClass); + startServiceSafely(context, serviceClass, false); + context.bindService(intent, connection, Context.BIND_AUTO_CREATE); + } + } + + /** + * 安全启动服务,如果需要停止,则直接返回。 + * + * @param context 上下文环境 + * @param serviceClass 要启动的服务类 + * @param isNeedStop 是否需要停止服务 + */ + public static void startServiceSafely(Context context, Class serviceClass, boolean isNeedStop) { + if (isNeedStop) { + return; + } + Log.d("wsh-daemon", "安全启动服务: " + serviceClass.getSimpleName()); + try { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { + context.startForegroundService(new Intent(context, serviceClass)); + } else { + context.startService((new Intent(context, serviceClass))); + } + } catch (Exception ignored) { + // 发生异常时不做处理 + } + } + + /** + * 获取实际的唤醒间隔时间,最小为 1 分钟。 + * + * @param sWakeUpInterval 唤醒时间间隔 + * @return 实际的唤醒间隔时间 + */ + static int getWakeUpInterval(int sWakeUpInterval) { + return Math.max(sWakeUpInterval, MINIMAL_WAKE_UP_INTERVAL); + } + + /** + * 停止所有服务,发送停止广播通知所有进程终止。 + * + * @param context 上下文环境 + */ + public static void stopAllServices(Context context) { + if (context != null) { + Log.d("wsh-daemon", "发送停止广播"); + // 以广播的形式通知所有进程终止 + context.sendBroadcast(new Intent(ACTION_CANCEL_JOB_ALARM_SUB)); + } + } +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/IntentWrapper.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/IntentWrapper.java new file mode 100644 index 0000000..0591191 --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/IntentWrapper.java @@ -0,0 +1,433 @@ +package com.shihoo.daemon; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Build; +import android.os.PowerManager; +import android.provider.Settings; +import android.support.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + +public class IntentWrapper { + + //Android 7.0+ Doze 模式 + protected static final int DOZE = 98; + //华为 自启管理 + protected static final int HUAWEI = 99; + //华为 锁屏清理 + protected static final int HUAWEI_GOD = 100; + //小米 自启动管理 + protected static final int XIAOMI = 101; + //小米 神隐模式 + protected static final int XIAOMI_GOD = 102; + //三星 5.0/5.1 自启动应用程序管理 + protected static final int SAMSUNG_L = 103; + //魅族 自启动管理 + protected static final int MEIZU = 104; + //魅族 待机耗电管理 + protected static final int MEIZU_GOD = 105; + //Oppo 自启动管理 + protected static final int OPPO = 106; + //三星 6.0+ 未监视的应用程序管理 + protected static final int SAMSUNG_M = 107; + //Oppo 自启动管理(旧版本系统) + protected static final int OPPO_OLD = 108; + //Vivo 后台高耗电 + protected static final int VIVO_GOD = 109; + //金立 应用自启 + protected static final int GIONEE = 110; + //乐视 自启动管理 + protected static final int LETV = 111; + //乐视 应用保护 + protected static final int LETV_GOD = 112; + //酷派 自启动管理 + protected static final int COOLPAD = 113; + //联想 后台管理 + protected static final int LENOVO = 114; + //联想 后台耗电优化 + protected static final int LENOVO_GOD = 115; + //中兴 自启管理 + protected static final int ZTE = 116; + //中兴 锁屏加速受保护应用 + protected static final int ZTE_GOD = 117; + + + public static List getIntentWrapperList(Context context) { + List sIntentWrapperList = new ArrayList<>(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + boolean ignoringBatteryOptimizations = pm.isIgnoringBatteryOptimizations(context.getPackageName()); + if (!ignoringBatteryOptimizations) { + Intent dozeIntent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + dozeIntent.setData(Uri.parse("package:" + context.getPackageName())); + sIntentWrapperList.add(new IntentWrapper(dozeIntent, DOZE)); + } + } + + //华为 自启管理 + Intent huaweiIntent = new Intent(); + huaweiIntent.setAction("huawei.intent.action.HSM_BOOTAPP_MANAGER"); + sIntentWrapperList.add(new IntentWrapper(huaweiIntent, HUAWEI)); + + //华为 锁屏清理 + Intent huaweiGodIntent = new Intent(); + huaweiGodIntent.setComponent(new ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.optimize.process.ProtectActivity")); + sIntentWrapperList.add(new IntentWrapper(huaweiGodIntent, HUAWEI_GOD)); + + //小米 自启动管理 + Intent xiaomiIntent = new Intent(); + xiaomiIntent.setAction("miui.intent.action.OP_AUTO_START"); + xiaomiIntent.addCategory(Intent.CATEGORY_DEFAULT); + sIntentWrapperList.add(new IntentWrapper(xiaomiIntent, XIAOMI)); + + //小米 神隐模式 + Intent xiaomiGodIntent = new Intent(); + xiaomiGodIntent.setComponent(new ComponentName("com.miui.powerkeeper", "com.miui.powerkeeper.ui.HiddenAppsConfigActivity")); + xiaomiGodIntent.putExtra("package_name", context.getPackageName()); + xiaomiGodIntent.putExtra("package_label", getApplicationName(context)); + sIntentWrapperList.add(new IntentWrapper(xiaomiGodIntent, XIAOMI_GOD)); + + //三星 5.0/5.1 自启动应用程序管理 + Intent samsungLIntent = context.getPackageManager().getLaunchIntentForPackage("com.samsung.android.sm"); + if (samsungLIntent != null) sIntentWrapperList.add(new IntentWrapper(samsungLIntent, SAMSUNG_L)); + + //三星 6.0+ 未监视的应用程序管理 + Intent samsungMIntent = new Intent(); + samsungMIntent.setComponent(new ComponentName("com.samsung.android.sm_cn", "com.samsung.android.sm.ui.battery.BatteryActivity")); + sIntentWrapperList.add(new IntentWrapper(samsungMIntent, SAMSUNG_M)); + + //魅族 自启动管理 + Intent meizuIntent = new Intent("com.meizu.safe.security.SHOW_APPSEC"); + meizuIntent.addCategory(Intent.CATEGORY_DEFAULT); + meizuIntent.putExtra("packageName", context.getPackageName()); + sIntentWrapperList.add(new IntentWrapper(meizuIntent, MEIZU)); + + //魅族 待机耗电管理 + Intent meizuGodIntent = new Intent(); + meizuGodIntent.setComponent(new ComponentName("com.meizu.safe", "com.meizu.safe.powerui.PowerAppPermissionActivity")); + sIntentWrapperList.add(new IntentWrapper(meizuGodIntent, MEIZU_GOD)); + + //Oppo 自启动管理 + Intent oppoIntent = new Intent(); + oppoIntent.setComponent(new ComponentName("com.coloros.safecenter", "com.coloros.safecenter.permission.startup.StartupAppListActivity")); + sIntentWrapperList.add(new IntentWrapper(oppoIntent, OPPO)); + + //Oppo 自启动管理(旧版本系统) + Intent oppoOldIntent = new Intent(); + oppoOldIntent.setComponent(new ComponentName("com.color.safecenter", "com.color.safecenter.permission.startup.StartupAppListActivity")); + sIntentWrapperList.add(new IntentWrapper(oppoOldIntent, OPPO_OLD)); + + //Vivo 后台高耗电 + Intent vivoGodIntent = new Intent(); + vivoGodIntent.setComponent(new ComponentName("com.vivo.abe", "com.vivo.applicationbehaviorengine.ui.ExcessivePowerManagerActivity")); + sIntentWrapperList.add(new IntentWrapper(vivoGodIntent, VIVO_GOD)); + + //金立 应用自启 + Intent gioneeIntent = new Intent(); + gioneeIntent.setComponent(new ComponentName("com.gionee.softmanager", "com.gionee.softmanager.MainActivity")); + sIntentWrapperList.add(new IntentWrapper(gioneeIntent, GIONEE)); + + //乐视 自启动管理 + Intent letvIntent = new Intent(); + letvIntent.setComponent(new ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.AutobootManageActivity")); + sIntentWrapperList.add(new IntentWrapper(letvIntent, LETV)); + + //乐视 应用保护 + Intent letvGodIntent = new Intent(); + letvGodIntent.setComponent(new ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.BackgroundAppManageActivity")); + sIntentWrapperList.add(new IntentWrapper(letvGodIntent, LETV_GOD)); + + //酷派 自启动管理 + Intent coolpadIntent = new Intent(); + coolpadIntent.setComponent(new ComponentName("com.yulong.android.security", "com.yulong.android.seccenter.tabbarmain")); + sIntentWrapperList.add(new IntentWrapper(coolpadIntent, COOLPAD)); + + //联想 后台管理 + Intent lenovoIntent = new Intent(); + lenovoIntent.setComponent(new ComponentName("com.lenovo.security", "com.lenovo.security.purebackground.PureBackgroundActivity")); + sIntentWrapperList.add(new IntentWrapper(lenovoIntent, LENOVO)); + + //联想 后台耗电优化 + Intent lenovoGodIntent = new Intent(); + lenovoGodIntent.setComponent(new ComponentName("com.lenovo.powersetting", "com.lenovo.powersetting.ui.Settings$HighPowerApplicationsActivity")); + sIntentWrapperList.add(new IntentWrapper(lenovoGodIntent, LENOVO_GOD)); + + //中兴 自启管理 + Intent zteIntent = new Intent(); + zteIntent.setComponent(new ComponentName("com.zte.heartyservice", "com.zte.heartyservice.autorun.AppAutoRunManager")); + sIntentWrapperList.add(new IntentWrapper(zteIntent, ZTE)); + + //中兴 锁屏加速受保护应用 + Intent zteGodIntent = new Intent(); + zteGodIntent.setComponent(new ComponentName("com.zte.heartyservice", "com.zte.heartyservice.setting.ClearAppSettingsActivity")); + sIntentWrapperList.add(new IntentWrapper(zteGodIntent, ZTE_GOD)); +// } + return sIntentWrapperList; + } + + + public static String getApplicationName(Context context) { + String sApplicationName = ""; + PackageManager pm; + ApplicationInfo ai; + try { + pm = context.getPackageManager(); + ai = pm.getApplicationInfo(context.getPackageName(), 0); + sApplicationName = pm.getApplicationLabel(ai).toString(); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + sApplicationName = context.getPackageName(); + } + return sApplicationName; + } + + /** + * 处理白名单. + * @return 弹过框的 IntentWrapper. + */ + @NonNull + public static List whiteListMatters(final Activity a, String reason) { + List showed = new ArrayList<>(); + if (reason == null) reason = "核心服务的持续运行"; + List intentWrapperList = getIntentWrapperList(a); + for (final IntentWrapper iw : intentWrapperList) { + //如果本机上没有能处理这个Intent的Activity,说明不是对应的机型,直接忽略进入下一次循环。 + if (!iw.doesActivityExists(a)) continue; + switch (iw.type) { + case DOZE: + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + PowerManager pm = (PowerManager) a.getSystemService(Context.POWER_SERVICE); + if (pm.isIgnoringBatteryOptimizations(a.getPackageName())) break; + new AlertDialog.Builder(a) + .setCancelable(false) + .setTitle("需要忽略 " + getApplicationName(a) + " 的电池优化") + .setMessage(reason + "需要 " + getApplicationName(a) + " 加入到电池优化的忽略名单。\n\n" + + "请点击『确定』,在弹出的『忽略电池优化』对话框中,选择『是』。") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int w) {iw.startActivitySafely(a);} + }) + .show(); + showed.add(iw); + } + break; + case HUAWEI: + new AlertDialog.Builder(a) + .setCancelable(false) + .setTitle("需要允许 " + getApplicationName(a) + " 自动启动") + .setMessage(reason + "需要允许 " + getApplicationName(a) + " 的自动启动。\n\n" + + "请点击『确定』,在弹出的『自启管理』中,将 " + getApplicationName(a) + " 对应的开关打开。") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int w) {iw.startActivitySafely(a);} + }) + .show(); + showed.add(iw); + break; + case ZTE_GOD: + case HUAWEI_GOD: + new AlertDialog.Builder(a) + .setCancelable(false) + .setTitle(getApplicationName(a) + " 需要加入锁屏清理白名单") + .setMessage(reason + "需要 " + getApplicationName(a) + " 加入到锁屏清理白名单。\n\n" + + "请点击『确定』,在弹出的『锁屏清理』列表中,将 " + getApplicationName(a) + " 对应的开关打开。") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int w) {iw.startActivitySafely(a);} + }) + .show(); + showed.add(iw); + break; + case XIAOMI_GOD: + new AlertDialog.Builder(a) + .setCancelable(false) + .setTitle("需要关闭 " + getApplicationName(a) + " 的神隐模式") + .setMessage(reason + "需要关闭 " + getApplicationName(a) + " 的神隐模式。\n\n" + + "请点击『确定』,在弹出的 " + getApplicationName(a) + " 神隐模式设置中,选择『无限制』,然后选择『允许定位』。") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int w) {iw.startActivitySafely(a);} + }) + .show(); + showed.add(iw); + break; + case SAMSUNG_L: + new AlertDialog.Builder(a) + .setCancelable(false) + .setTitle("需要允许 " + getApplicationName(a) + " 的自启动") + .setMessage(reason + "需要 " + getApplicationName(a) + " 在屏幕关闭时继续运行。\n\n" + + "请点击『确定』,在弹出的『智能管理器』中,点击『内存』,选择『自启动应用程序』选项卡,将 " + getApplicationName(a) + " 对应的开关打开。") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int w) {iw.startActivitySafely(a);} + }) + .show(); + showed.add(iw); + break; + case SAMSUNG_M: + new AlertDialog.Builder(a) + .setCancelable(false) + .setTitle("需要允许 " + getApplicationName(a) + " 的自启动") + .setMessage(reason + "需要 " + getApplicationName(a) + " 在屏幕关闭时继续运行。\n\n" + + "请点击『确定』,在弹出的『电池』页面中,点击『未监视的应用程序』->『添加应用程序』,勾选 " + getApplicationName(a) + ",然后点击『完成』。") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int w) {iw.startActivitySafely(a);} + }) + .show(); + showed.add(iw); + break; + case MEIZU: + new AlertDialog.Builder(a) + .setCancelable(false) + .setTitle("需要允许 " + getApplicationName(a) + " 保持后台运行") + .setMessage(reason + "需要允许 " + getApplicationName(a) + " 保持后台运行。\n\n" + + "请点击『确定』,在弹出的应用信息界面中,将『后台管理』选项更改为『保持后台运行』。") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int w) {iw.startActivitySafely(a);} + }) + .show(); + showed.add(iw); + break; + case MEIZU_GOD: + new AlertDialog.Builder(a) + .setCancelable(false) + .setTitle(getApplicationName(a) + " 需要在待机时保持运行") + .setMessage(reason + "需要 " + getApplicationName(a) + " 在待机时保持运行。\n\n" + + "请点击『确定』,在弹出的『待机耗电管理』中,将 " + getApplicationName(a) + " 对应的开关打开。") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int w) {iw.startActivitySafely(a);} + }) + .show(); + showed.add(iw); + break; + case ZTE: + case LETV: + case XIAOMI: + case OPPO: + case OPPO_OLD: + new AlertDialog.Builder(a) + .setCancelable(false) + .setTitle("需要允许 " + getApplicationName(a) + " 的自启动") + .setMessage(reason + "需要 " + getApplicationName(a) + " 加入到自启动白名单。\n\n" + + "请点击『确定』,在弹出的『自启动管理』中,将 " + getApplicationName(a) + " 对应的开关打开。") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int w) {iw.startActivitySafely(a);} + }) + .show(); + showed.add(iw); + break; + case COOLPAD: + new AlertDialog.Builder(a) + .setCancelable(false) + .setTitle("需要允许 " + getApplicationName(a) + " 的自启动") + .setMessage(reason + "需要允许 " + getApplicationName(a) + " 的自启动。\n\n" + + "请点击『确定』,在弹出的『酷管家』中,找到『软件管理』->『自启动管理』,取消勾选 " + getApplicationName(a) + ",将 " + getApplicationName(a) + " 的状态改为『已允许』。") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int w) {iw.startActivitySafely(a);} + }) + .show(); + showed.add(iw); + break; + case VIVO_GOD: + new AlertDialog.Builder(a) + .setCancelable(false) + .setTitle("需要允许 " + getApplicationName(a) + " 的后台运行") + .setMessage(reason + "需要允许 " + getApplicationName(a) + " 在后台高耗电时运行。\n\n" + + "请点击『确定』,在弹出的『后台高耗电』中,将 " + getApplicationName(a) + " 对应的开关打开。") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int w) {iw.startActivitySafely(a);} + }) + .show(); + showed.add(iw); + break; + case GIONEE: + new AlertDialog.Builder(a) + .setCancelable(false) + .setTitle(getApplicationName(a) + " 需要加入应用自启和绿色后台白名单") + .setMessage(reason + "需要允许 " + getApplicationName(a) + " 的自启动和后台运行。\n\n" + + "请点击『确定』,在弹出的『系统管家』中,分别找到『应用管理』->『应用自启』和『绿色后台』->『清理白名单』,将 " + getApplicationName(a) + " 添加到白名单。") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int w) {iw.startActivitySafely(a);} + }) + .show(); + showed.add(iw); + break; + case LETV_GOD: + new AlertDialog.Builder(a) + .setCancelable(false) + .setTitle("需要禁止 " + getApplicationName(a) + " 被自动清理") + .setMessage(reason + "需要禁止 " + getApplicationName(a) + " 被自动清理。\n\n" + + "请点击『确定』,在弹出的『应用保护』中,将 " + getApplicationName(a) + " 对应的开关关闭。") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int w) {iw.startActivitySafely(a);} + }) + .show(); + showed.add(iw); + break; + case LENOVO: + new AlertDialog.Builder(a) + .setCancelable(false) + .setTitle("需要允许 " + getApplicationName(a) + " 的后台运行") + .setMessage(reason + "需要允许 " + getApplicationName(a) + " 的后台自启、后台 GPS 和后台运行。\n\n" + + "请点击『确定』,在弹出的『后台管理』中,分别找到『后台自启』、『后台 GPS』和『后台运行』,将 " + getApplicationName(a) + " 对应的开关打开。") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int w) {iw.startActivitySafely(a);} + }) + .show(); + showed.add(iw); + break; + case LENOVO_GOD: + new AlertDialog.Builder(a) + .setCancelable(false) + .setTitle("需要关闭 " + getApplicationName(a) + " 的后台耗电优化") + .setMessage(reason + "需要关闭 " + getApplicationName(a) + " 的后台耗电优化。\n\n" + + "请点击『确定』,在弹出的『后台耗电优化』中,将 " + getApplicationName(a) + " 对应的开关关闭。") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int w) {iw.startActivitySafely(a);} + }) + .show(); + showed.add(iw); + break; + } + } + return showed; + } + + /** + * 防止华为机型未加入白名单时按返回键回到桌面再锁屏后几秒钟进程被杀 + */ + public static void onBackPressed(Activity a) { + Intent launcherIntent = new Intent(Intent.ACTION_MAIN); + launcherIntent.addCategory(Intent.CATEGORY_HOME); + a.startActivity(launcherIntent); + } + + protected Intent intent; + protected int type; + + protected IntentWrapper(Intent intent, int type) { + this.intent = intent; + this.type = type; + } + + /** + * 判断本机上是否有能处理当前Intent的Activity + */ + protected boolean doesActivityExists(Context context) { + PackageManager pm = context.getPackageManager(); + List list = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + return list != null && list.size() > 0; + } + + /** + * 安全地启动一个Activity + */ + protected void startActivitySafely(Activity activityContext) { + try { activityContext.startActivity(intent); } catch (Exception e) { e.printStackTrace(); } + } +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/JobSchedulerService.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/JobSchedulerService.java new file mode 100644 index 0000000..158e60a --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/JobSchedulerService.java @@ -0,0 +1,43 @@ +package com.shihoo.daemon; + +import android.annotation.TargetApi; +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.content.Intent; +import android.os.Build; +import android.util.Log; + +/** + * JobSchedulerService 类用于处理作业调度的服务。 + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class JobSchedulerService extends JobService { + + /** + * 当作业启动时回调该方法。在此方法中启动 WatchDogService 服务。 + * + * @param params 作业参数 + * @return 布尔值,表示作业是否已经完成 + */ + @Override + public boolean onStartJob(JobParameters params) { + Log.d("wsh-daemon", "JobSchedulerService onStartJob 启动。。。。"); + // 启动 WatchDogService 服务 + DaemonEnv.startServiceSafely(JobSchedulerService.this, + WatchDogService.class, + !WatchProcessPrefHelper.getIsStartDaemon(JobSchedulerService.this)); + return false; + } + + /** + * 当作业停止时回调该方法。 + * + * @param params 作业参数 + * @return 布尔值,表示作业是否需要重新安排 + */ + @Override + public boolean onStopJob(JobParameters params) { + Log.d("wsh-daemon", "JobSchedulerService onStopJob 停止。。。。"); + return false; + } +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/PlayMusicService.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/PlayMusicService.java new file mode 100644 index 0000000..fb57b10 --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/PlayMusicService.java @@ -0,0 +1,114 @@ +package com.shihoo.daemon; + +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.MediaPlayer; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +/** + * Created by shihoo ON 2018/12/13. + * Email shihu.wang@bodyplus.cc 451082005@qq.com + * + * 后台播放无声音乐 + */ +public class PlayMusicService extends Service { + + private boolean mNeedStop = false; //控制是否播放音频 + private MediaPlayer mMediaPlayer; + private StopBroadcastReceiver stopBroadcastReceiver; + +// private IBinder mIBinder; + + @Override + public IBinder onBind(Intent intent) { +// return mIBinder; + return null; + } + + @Override + public void onCreate() { + super.onCreate(); +// mIBinder = new Messenger(new Handler()).getBinder(); + + startRegisterReceiver(); + mMediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.no_notice); + mMediaPlayer.setLooping(true); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + startPlayMusic(); + return START_STICKY; + } + + private void startPlayMusic(){ + if (mMediaPlayer!=null && !mMediaPlayer.isPlaying() && !mNeedStop) { + new Thread(new Runnable() { + @Override + public void run() { + Log.d("wsh-daemon", "开始后台播放音乐"); + mMediaPlayer.start(); + } + }).start(); + } + } + + private void stopPlayMusic() { + if (mMediaPlayer != null) { + Log.d("wsh-daemon", "关闭后台播放音乐"); + mMediaPlayer.stop(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + stopPlayMusic(); + Log.d("wsh-daemon", "----> stopPlayMusic ,停止服务"); + // 重启自己 + if (!mNeedStop) { + Log.d("wsh-daemon", "----> PlayMusic ,重启服务"); + Intent intent = new Intent(getApplicationContext(), PlayMusicService.class); + if (Build.VERSION.SDK_INT>Build.VERSION_CODES.O)startForegroundService(intent); + else startService(intent); + } + } + + private void startRegisterReceiver(){ + if (stopBroadcastReceiver == null){ + stopBroadcastReceiver = new StopBroadcastReceiver(); + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(DaemonEnv.ACTION_CANCEL_JOB_ALARM_SUB); + registerReceiver(stopBroadcastReceiver,intentFilter); + } + } + + private void startUnRegisterReceiver(){ + if (stopBroadcastReceiver != null){ + unregisterReceiver(stopBroadcastReceiver); + stopBroadcastReceiver = null; + } + } + + /** + * 停止自己 + */ + private void stopService(){ + mNeedStop = true; + startUnRegisterReceiver(); + stopSelf(); + } + + class StopBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + stopService(); + } + } +} diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/WakeUpReceiver.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/WakeUpReceiver.java new file mode 100644 index 0000000..8a2cdee --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/WakeUpReceiver.java @@ -0,0 +1,31 @@ +package com.shihoo.daemon; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * WakeUpReceiver 是用于接收系统唤醒广播的接收器类。 + */ +public class WakeUpReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + // 当接收到系统唤醒广播时,启动 WatchDogService 服务 + DaemonEnv.startServiceSafely(context, WatchDogService.class, + !WatchProcessPrefHelper.getIsStartDaemon(context)); + } + + /** + * WakeUpAutoStartReceiver 用于在应用启动时检查是否需要启动守护服务的接收器类。 + */ + public static class WakeUpAutoStartReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + // 在应用启动时检查是否需要启动守护服务,并启动 WatchDogService 服务 + DaemonEnv.startServiceSafely(context, WatchDogService.class, + !WatchProcessPrefHelper.getIsStartDaemon(context)); + } + } +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/WatchDogService.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/WatchDogService.java new file mode 100644 index 0000000..193b421 --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/WatchDogService.java @@ -0,0 +1,287 @@ +package com.shihoo.daemon; + +import android.app.AlarmManager; +import android.app.Notification; +import android.app.PendingIntent; +import android.app.Service; +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Handler; +import android.os.IBinder; +import android.os.Messenger; +import android.util.Log; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; + +import com.shihoo.daemon.singlepixel.ScreenManager; +import com.shihoo.daemon.singlepixel.ScreenReceiverUtil; + + +// 后台守护服务 +public class WatchDogService extends Service { + + // 通知的 ID + protected static final int HASH_CODE = 2; + + // 用于处理定时任务的 Disposable 对象 + protected static Disposable mDisposable; + // 用于启动定时任务的 PendingIntent 对象 + protected static PendingIntent mPendingIntent; + + // 停止广播接收器 + private StopBroadcastReceiver stopBroadcastReceiver; + + // 是否应该停止自身服务 + private boolean IsShouldStopSelf; + + // 服务连接对象 + private AbsServiceConnection mConnection = new AbsServiceConnection() { + @Override + public void onDisconnected(ComponentName name) { + // 如果不应该停止自身服务,则重新绑定工作服务 + if (!IsShouldStopSelf) { + startBindWorkServices(); + } + } + }; + + // 启动或绑定工作服务 + private void startBindWorkServices(){ + if (DaemonEnv.mWorkServiceClass != null) { + DaemonEnv.startServiceMayBind(WatchDogService.this, DaemonEnv.mWorkServiceClass, mConnection, IsShouldStopSelf); + } + } + + // 在服务启动时进行初始化操作 + protected final int onStart(Intent intent, int flags, int startId) { + // 如果 Disposable 对象已存在且未被处理,则返回 START_STICKY + if (mDisposable != null && !mDisposable.isDisposed()) { + return START_STICKY; + } + + // 根据不同的 Android 版本执行不同的操作 + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { + startForeground(HASH_CODE, new Notification()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) + DaemonEnv.startServiceSafely(WatchDogService.this, WatchDogNotificationService.class, IsShouldStopSelf); + } + + //定时检查 AbsWorkService 是否在运行,如果不在运行就把它拉起来 + //Android 5.0+ 使用 JobScheduler,效果比 AlarmManager 好 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + JobInfo.Builder builder = new JobInfo.Builder(HASH_CODE, + new ComponentName(WatchDogService.this, JobSchedulerService.class)); + builder.setPeriodic(DaemonEnv.getWakeUpInterval(DaemonEnv.MINIMAL_WAKE_UP_INTERVAL)); + //Android 7.0+ 增加了一项针对 JobScheduler 的新限制,最小间隔只能是下面设定的数字 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + builder.setPeriodic(JobInfo.getMinPeriodMillis(), JobInfo.getMinFlexMillis()); + } + builder.setPersisted(true); + JobScheduler scheduler = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE); + scheduler.schedule(builder.build()); + } else { + //Android 4.4- 使用 AlarmManager + AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE); + Intent i = new Intent(WatchDogService.this, DaemonEnv.mWorkServiceClass); + mPendingIntent = PendingIntent.getService(WatchDogService.this, HASH_CODE, i, PendingIntent.FLAG_UPDATE_CURRENT); + am.setRepeating(AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + DaemonEnv.getWakeUpInterval(DaemonEnv.MINIMAL_WAKE_UP_INTERVAL), + DaemonEnv.getWakeUpInterval(DaemonEnv.MINIMAL_WAKE_UP_INTERVAL), mPendingIntent); + } + + //使用定时 Observable,避免 Android 定制系统 JobScheduler / AlarmManager 唤醒间隔不稳定的情况 + mDisposable = Observable + .interval(DaemonEnv.getWakeUpInterval(DaemonEnv.MINIMAL_WAKE_UP_INTERVAL), TimeUnit.MILLISECONDS) + .subscribe(new Consumer() { + @Override + public void accept(Long aLong) throws Exception { + startBindWorkServices(); + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + throwable.printStackTrace(); + } + }); + startBindWorkServices(); + return START_STICKY; + } + + @Override + public final int onStartCommand(Intent intent, int flags, int startId) { + return onStart(intent, flags, startId); + } + + @Override + public final IBinder onBind(Intent intent) { + return new Messenger(new Handler()).getBinder(); + } + + private void onEnd(Intent rootIntent) { + if (IsShouldStopSelf){ + return; + } + Log.d("wsh-daemon", "onEnd ---- 搞事 + IsShouldStopSelf :" + IsShouldStopSelf); + DaemonEnv.startServiceSafely(WatchDogService.this, + DaemonEnv.mWorkServiceClass + ,false); + DaemonEnv.startServiceSafely(WatchDogService.this, + WatchDogService.class + ,false); + } + + /** + * 最近任务列表中划掉卡片时回调 + */ + @Override + public void onTaskRemoved(Intent rootIntent) { + Log.d("wsh-daemon", "onEnd ---- 搞事 + onTaskRemoved :" + IsShouldStopSelf); + onEnd(rootIntent); + } + + /** + * 设置-正在运行中停止服务时回调 + */ + @Override + public void onDestroy() { + Log.d("wsh-daemon", "onEnd ---- 搞事 + onDestroy :" + IsShouldStopSelf); + onEnd(null); + startUnRegisterReceiver(); + } + + @Override + public void onCreate() { + super.onCreate(); + startRegisterReceiver(); + createScreenListener(); + } + + /** + * 停止运行本服务,本进程 + */ + public void stopService(){ + IsShouldStopSelf = true; + startUnRegisterReceiver(); + cancelJobAlarmSub(); + if (mConnection.mConnectedState) { + unbindService(mConnection); + } + exit(); + } + /** + * 退出应用程序 + */ + private void exit(){ + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + stopSelf(); + System.exit(0); + } + },3000); + } + + // 注册广播接收器 + private void startRegisterReceiver(){ + if (stopBroadcastReceiver == null){ + stopBroadcastReceiver = new StopBroadcastReceiver(); + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(DaemonEnv.ACTION_CANCEL_JOB_ALARM_SUB); + registerReceiver(stopBroadcastReceiver,intentFilter); + } + } + // 注销广播接收器 + private void startUnRegisterReceiver(){ + if (stopBroadcastReceiver != null){ + unregisterReceiver(stopBroadcastReceiver); + stopBroadcastReceiver = null; + } + } + + /** + * 用于在不需要服务运行的时候取消 Job / Alarm / Subscription. + * + * 因 WatchDogService 运行在 :watch 子进程, 请勿在主进程中直接调用此方法. + * 而是向 WakeUpReceiver 发送一个 Action 为 WakeUpReceiver.ACTION_CANCEL_JOB_ALARM_SUB 的广播. + */ + public void cancelJobAlarmSub() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + JobScheduler scheduler = (JobScheduler) WatchDogService.this.getSystemService(JOB_SCHEDULER_SERVICE); + scheduler.cancel(HASH_CODE); + } else { + AlarmManager am = (AlarmManager) WatchDogService.this.getSystemService(ALARM_SERVICE); + if (mPendingIntent != null) { + am.cancel(mPendingIntent); + } + } + if (mDisposable !=null && !mDisposable.isDisposed()){ + mDisposable.dispose(); + } + } + /** + * 利用漏洞在 API Level 18 及以上的 Android 系统中,启动前台服务而不显示通知 + * 运行在:watch子进程中 + */ + public static class WatchDogNotificationService extends Service { + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + startForeground(WatchDogService.HASH_CODE, new Notification()); + stopSelf(); + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + } + + // 创建屏幕监听器 + private ScreenReceiverUtil mScreenListener; + private ScreenManager mScreenManager; + + private void createScreenListener(){ + // 注册锁屏广播监听器 + mScreenListener = new ScreenReceiverUtil(this); + mScreenManager = ScreenManager.getInstance(this); + mScreenListener.setScreenReceiverListener(mScreenListenerer); + } + + // 屏幕监听器的监听方法 + private ScreenReceiverUtil.SreenStateListener mScreenListenerer = new ScreenReceiverUtil.SreenStateListener() { + @Override + public void onSreenOn() { // 亮屏 + } + + @Override + public void onSreenOff() { //锁屏 + mScreenManager.startActivity(); + Log.d("wsh-daemon", "打开了1像素Activity"); + } + + @Override + public void onUserPresent() { // 解锁 + mScreenManager.finishActivity(); + Log.d("wsh-daemon", "关闭了1像素Activity"); + } + }; + + // 停止服务的广播接收器 + class StopBroadcastReceiver extends BroadcastReceiver{ + @Override + public void onReceive(Context context, Intent intent) { + stopService(); + } + } +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/WatchProcessPrefHelper.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/WatchProcessPrefHelper.java new file mode 100644 index 0000000..fe9c499 --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/WatchProcessPrefHelper.java @@ -0,0 +1,37 @@ +package com.shihoo.daemon; + +import android.content.Context; + +/** + * WatchProcessPrefHelper 是用于管理守护进程设置的辅助类。 + */ +public class WatchProcessPrefHelper { + // SharedPreferences 文件名 + private static final String SHARED_UTILS = "watch_process"; + // 是否启动守护进程的键名 + private static final String KEY_IS_START_DAEMON = "is_start_sport"; + + /** + * 设置是否启动守护进程 + * @param context 上下文对象 + * @param isStartDaemon 是否启动守护进程 + */ + public static void setIsStartSDaemon(Context context, boolean isStartDaemon){ + // 使用 SharedPreferences 存储是否启动守护进程的设置 + context.getSharedPreferences(SHARED_UTILS, Context.MODE_MULTI_PROCESS) + .edit() + .putBoolean(KEY_IS_START_DAEMON, isStartDaemon) + .apply(); + } + + /** + * 获取是否启动守护进程的设置 + * @param context 上下文对象 + * @return 返回是否启动守护进程的设置,默认为 false + */ + public static boolean getIsStartDaemon(Context context){ + // 从 SharedPreferences 中获取是否启动守护进程的设置,默认为 false + return context.getSharedPreferences(SHARED_UTILS, Context.MODE_MULTI_PROCESS) + .getBoolean(KEY_IS_START_DAEMON, false); + } +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/singlepixel/ScreenManager.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/singlepixel/ScreenManager.java new file mode 100644 index 0000000..80415fc --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/singlepixel/ScreenManager.java @@ -0,0 +1,73 @@ +package com.shihoo.daemon.singlepixel; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; + +import java.lang.ref.WeakReference; + +/** + * ScreenManager是一个用于管理SinglePixelActivity的帮助类。 + */ +public class ScreenManager { + private static final String TAG = ScreenManager.class.getSimpleName(); + private static ScreenManager sInstance; // 单例实例 + private Context mContext; // 上下文 + private WeakReference mActivity; // 弱引用的Activity实例 + + /** + * 构造函数,私有化,外部无法直接实例化该类 + * + * @param mContext 上下文 + */ + private ScreenManager(Context mContext) { + this.mContext = mContext; + } + + /** + * 获取ScreenManager的实例,采用单例模式 + * + * @param context 上下文 + * @return ScreenManager的实例 + */ + public static ScreenManager getInstance(Context context) { + if (sInstance == null) { + sInstance = new ScreenManager(context); + } + return sInstance; + } + + /** + * 设置SinglePixelActivity的引用 + * + * @param activity SinglePixelActivity实例 + */ + public void setSingleActivity(Activity activity) { + mActivity = new WeakReference<>(activity); // 使用弱引用保存Activity实例 + } + + /** + * 启动SinglePixelActivity + */ + public void startActivity() { + Intent intent = new Intent(mContext, SinglePixelActivity.class); + // 设置启动标志为新任务 + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 启动SinglePixelActivity + mContext.startActivity(intent); + } + + /** + * 结束SinglePixelActivity + */ + public void finishActivity() { + if (mActivity != null) { + // 获取Activity实例 + Activity activity = mActivity.get(); + if (activity != null) { + // 结束Activity + activity.finish(); + } + } + } +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/singlepixel/ScreenReceiverUtil.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/singlepixel/ScreenReceiverUtil.java new file mode 100644 index 0000000..6c66aa0 --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/singlepixel/ScreenReceiverUtil.java @@ -0,0 +1,83 @@ +package com.shihoo.daemon.singlepixel; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +/** + * ScreenReceiverUtil是一个用于监听屏幕状态变化的帮助类。 + */ +public class ScreenReceiverUtil { + private Context mContext; // 上下文 + private SreenBroadcastReceiver mScreenReceiver; // 广播接收器 + private SreenStateListener mStateReceiverListener; // 屏幕状态监听器 + + /** + * 构造函数 + * + * @param mContext 上下文 + */ + public ScreenReceiverUtil(Context mContext) { + this.mContext = mContext; + } + + /** + * 设置屏幕状态监听器 + * + * @param mStateReceiverListener 屏幕状态监听器 + */ + public void setScreenReceiverListener(SreenStateListener mStateReceiverListener) { + this.mStateReceiverListener = mStateReceiverListener; + // 动态启动广播接收器 + this.mScreenReceiver = new SreenBroadcastReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_USER_PRESENT); + // 注册广播接收器 + mContext.registerReceiver(mScreenReceiver, filter); + } + + /** + * 停止屏幕状态监听 + */ + public void stopScreenReceiverListener() { + // 取消注册广播接收器 + mContext.unregisterReceiver(mScreenReceiver); + } + + /** + * 屏幕状态监听回调接口 + */ + public interface SreenStateListener { + void onSreenOn(); // 屏幕点亮时回调 + + void onSreenOff(); // 屏幕关闭时回调 + + void onUserPresent(); // 用户解锁时回调 + } + + /** + * 广播接收器,用于接收屏幕相关广播 + */ + public class SreenBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (mStateReceiverListener == null) { + return; + } + if (Intent.ACTION_SCREEN_ON.equals(action)) { + // 开屏 + mStateReceiverListener.onSreenOn(); + } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { + // 锁屏 + mStateReceiverListener.onSreenOff(); + } else if (Intent.ACTION_USER_PRESENT.equals(action)) { + // 解锁 + mStateReceiverListener.onUserPresent(); + } + } + } +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/singlepixel/SinglePixelActivity.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/singlepixel/SinglePixelActivity.java new file mode 100644 index 0000000..154fc22 --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/singlepixel/SinglePixelActivity.java @@ -0,0 +1,45 @@ +package com.shihoo.daemon.singlepixel; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.util.Log; +import android.view.Gravity; +import android.view.Window; +import android.view.WindowManager; + +import com.shihoo.daemon.WatchDogService; + +/** + * SinglePixelActivity是一个单像素的Activity,用于保持前台服务存活。 + */ +public class SinglePixelActivity extends Activity { + + private static final String TAG = SinglePixelActivity.class.getSimpleName(); + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 设置窗口位置和大小 + Window mWindow = getWindow(); + mWindow.setGravity(Gravity.LEFT | Gravity.TOP); + WindowManager.LayoutParams attrParams = mWindow.getAttributes(); + attrParams.x = 0; + attrParams.y = 0; + attrParams.height = 1; + attrParams.width = 1; + mWindow.setAttributes(attrParams); + // 将SinglePixelActivity设置给ScreenManager + ScreenManager.getInstance(this).setSingleActivity(this); + } + + @Override + protected void onDestroy() { + Log.d("wsh-daemon", "1像素Activity --- onDestroy"); + // 在Activity销毁时启动WatchDogService,用于保持进程存活 + Intent intentAlive = new Intent(this, WatchDogService.class); + startService(intentAlive); + super.onDestroy(); + } +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/Authenticator.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/Authenticator.java new file mode 100644 index 0000000..3839fcd --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/Authenticator.java @@ -0,0 +1,78 @@ +package com.shihoo.daemon.sync; + +import android.accounts.AbstractAccountAuthenticator; +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.AccountManager; +import android.accounts.NetworkErrorException; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +public class Authenticator extends AbstractAccountAuthenticator { + + final Context context; + + public Authenticator(Context context) { + super(context); + this.context = context; + } + + // 添加账户 + @Override + public Bundle addAccount(AccountAuthenticatorResponse response, + String accountType, String authTokenType, + String[] requiredFeatures, Bundle options) + throws NetworkErrorException { + Intent intent = new Intent(); + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, + response); + Bundle bundle = new Bundle(); + bundle.putParcelable(AccountManager.KEY_INTENT, intent); + return bundle; + } + + // 确认凭据 + @Override + public Bundle confirmCredentials(AccountAuthenticatorResponse response, + Account account, Bundle options) throws NetworkErrorException { + return null; + } + + // 编辑属性 + @Override + public Bundle editProperties(AccountAuthenticatorResponse response, + String accountType) { + return null; + } + + // 获取访问令牌 + @Override + public Bundle getAuthToken(AccountAuthenticatorResponse response, + Account account, String authTokenType, Bundle options) + throws NetworkErrorException { + return null; + } + + // 获取访问令牌标签 + @Override + public String getAuthTokenLabel(String authTokenType) { + return null; + } + + // 是否具有指定特性 + @Override + public Bundle hasFeatures(AccountAuthenticatorResponse response, + Account account, String[] features) + throws NetworkErrorException { + return null; + } + + // 更新凭据 + @Override + public Bundle updateCredentials(AccountAuthenticatorResponse response, + Account account, String authTokenType, Bundle options) + throws NetworkErrorException { + return null; + } +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/AuthenticatorService.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/AuthenticatorService.java new file mode 100644 index 0000000..faa0f8d --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/AuthenticatorService.java @@ -0,0 +1,28 @@ +package com.shihoo.daemon.sync; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +public class AuthenticatorService extends Service { + + private Authenticator mAuthenticator; + + public AuthenticatorService() { + } + + // 当服务被创建时调用 + @Override + public void onCreate() { + super.onCreate(); + // 创建一个新的 Authenticator 实例 + mAuthenticator = new Authenticator(this); + } + + // 绑定服务时调用,返回一个 IBinder + @Override + public IBinder onBind(Intent intent) { + // 返回 Authenticator 的 IBinder,以便其他组件可以与其进行交互 + return mAuthenticator.getIBinder(); + } +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/StubProvider.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/StubProvider.java new file mode 100644 index 0000000..3d49f27 --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/StubProvider.java @@ -0,0 +1,50 @@ +package com.shihoo.daemon.sync; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.support.annotation.Nullable; + +public class StubProvider extends ContentProvider { + + // 当内容提供者被创建时调用 + @Override + public boolean onCreate() { + // 这里返回了false,表示内容提供者未能成功初始化 + return false; + } + + // 查询指定 URI 的数据 + @Nullable + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + return null; + } + + // 获取指定 URI 的数据类型 + @Nullable + @Override + public String getType(Uri uri) { + return null; + } + + // 插入数据到指定的 URI + @Nullable + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + // 根据指定条件删除数据 + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } + + // 根据指定条件更新数据 + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return 0; + } +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/SyncAdapter.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/SyncAdapter.java new file mode 100644 index 0000000..5f994ae --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/SyncAdapter.java @@ -0,0 +1,34 @@ +package com.shihoo.daemon.sync; + +import android.accounts.Account; +import android.content.AbstractThreadedSyncAdapter; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.SyncResult; +import android.os.Bundle; + +import com.shihoo.daemon.DaemonEnv; +import com.shihoo.daemon.WatchDogService; +import com.shihoo.daemon.WatchProcessPrefHelper; + +public class SyncAdapter extends AbstractThreadedSyncAdapter { + + // 上下文对象 + private Context mContext; + + // 构造函数 + public SyncAdapter(Context context, boolean autoInitialize) { + super(context, autoInitialize); + this.mContext = context; + } + + // 执行数据同步操作 + @Override + public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { + + // 在此方法中执行数据同步操作 + // 这里调用了 DaemonEnv.startServiceSafely 方法启动 WatchDogService 服务 + // 如果 WatchProcessPrefHelper.getIsStartDaemon(mContext) 返回 false,则启动该服务 + DaemonEnv.startServiceSafely(mContext, WatchDogService.class, !WatchProcessPrefHelper.getIsStartDaemon(mContext)); + } +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/SyncService.java b/src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/SyncService.java new file mode 100644 index 0000000..6e05411 --- /dev/null +++ b/src/daemonlibrary/src/main/java/com/shihoo/daemon/sync/SyncService.java @@ -0,0 +1,30 @@ +package com.shihoo.daemon.sync; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +public class SyncService extends Service { + // 静态的同步适配器对象 + private static SyncAdapter sSyncAdapter = null; + // 同步锁对象 + private static final Object sSyncAdapterLock = new Object(); + + public SyncService() { + } + + @Override + public void onCreate() { + super.onCreate(); + synchronized (sSyncAdapterLock) { + // 在服务创建时,实例化同步适配器对象并进行同步操作 + sSyncAdapter = new SyncAdapter(getApplicationContext(), true); + } + } + + @Override + public IBinder onBind(Intent intent) { + // 返回同步适配器的 Binder 对象 + return sSyncAdapter.getSyncAdapterBinder(); + } +} \ No newline at end of file diff --git a/src/daemonlibrary/src/main/res/raw/no_notice.mp3 b/src/daemonlibrary/src/main/res/raw/no_notice.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..83ba74185264a714224e0e0e6f120d8fbbd65aa0 GIT binary patch literal 439659 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 z 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 + deamon + diff --git a/src/daemonlibrary/src/main/res/values/styles.xml b/src/daemonlibrary/src/main/res/values/styles.xml new file mode 100644 index 0000000..8fb2a24 --- /dev/null +++ b/src/daemonlibrary/src/main/res/values/styles.xml @@ -0,0 +1,16 @@ + + + +