这篇文章主要讲解了“Android时间设置问题怎么解决”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Android时间设置问题怎么解决”吧!
问题现象
最近处理了一个非常有意思的系统bug,修改系统时间,重启后居然没有生效
注意要关闭使用网络提供的时间和使用网络提供的时区这两个开关。
重启后显示的时间日期为
显示的时间既不是我设置的时间,也不是当前时间(当前时间为2023-03-20 15:49),那么显示的这个时间到底是什么时间呢?
为了弄清楚这个问题,我研究了一下Android设置时间的逻辑,研究过程中还发现了一些彩蛋。
源码分析
首先是设置时间的逻辑,源码位于packages/apps/Settings/src/com/android/settings/datetime/DatePreferenceController.java
public class DatePreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, DatePickerDialog.OnDateSetListener { //省略部分代码 private final DatePreferenceHost mHost; @Override public boolean handlePreferenceTreeClick(Preference preference) { //点击日期后处理 if (!TextUtils.equals(preference.getKey(), KEY_DATE)) { return false; } //显示日期选择框 mHost.showDatePicker(); return true; } //省略部分代码}
mHost
是DatePreferenceHost
接口,接口实现在packages/apps/Settings/src/com/android/settings/DateTimeSettings.java
中,因此,showDatePicker()
的逻辑位于该实现类中
@SearchIndexablepublic class DateTimeSettings extends DashboardFragment implements TimePreferenceController.TimePreferenceHost, DatePreferenceController.DatePreferenceHost { //省略部分代码@Override public void showDatePicker() { //显示日期选择对话框 showDialog(DatePreferenceController.DIALOG_DATEPICKER); } //省略部分代码}
showDialog()
定义在父类packages/apps/Settings/src/com/android/settings/SettingsPreferenceFragment.java
中
public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceFragment implements DialogCreatable, HelpResourceProvider, Indexable { protected void showDialog(int dialogId) { if (mDialogFragment != null) { Log.e(TAG, "Old dialog fragment not null!"); } //创建SettingsDialogFragment并进行show mDialogFragment = SettingsDialogFragment.newInstance(this, dialogId); mDialogFragment.show(getChildFragmentManager(), Integer.toString(dialogId)); }}
showDialog()
中就是创建了SettingsDialogFragment
然后显示,SettingsDialogFragment
是SettingsPreferenceFragment
的一个内部类,看一下SettingsDialogFragment
的定义
public static class SettingsDialogFragment extends InstrumentedDialogFragment { private static final String KEY_DIALOG_ID = "key_dialog_id"; private static final String KEY_PARENT_FRAGMENT_ID = "key_parent_fragment_id"; private Fragment mParentFragment; private DialogInterface.OnCancelListener mOnCancelListener; private DialogInterface.OnDismissListener mOnDismissListener; public static SettingsDialogFragment newInstance(DialogCreatable fragment, int dialogId) { if (!(fragment instanceof Fragment)) { throw new IllegalArgumentException("fragment argument must be an instance of " + Fragment.class.getName()); } final SettingsDialogFragment settingsDialogFragment = new SettingsDialogFragment(); settingsDialogFragment.setParentFragment(fragment); settingsDialogFragment.setDialogId(dialogId); return settingsDialogFragment; } @Override public int getMetricsCategory() { if (mParentFragment == null) { return Instrumentable.METRICS_CATEGORY_UNKNOWN; } final int metricsCategory = ((DialogCreatable) mParentFragment).getDialogMetricsCategory(mDialogId); if (metricsCategory <= 0) { throw new IllegalStateException("Dialog must provide a metrics category"); } return metricsCategory; } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (mParentFragment != null) { outState.putInt(KEY_DIALOG_ID, mDialogId); outState.putInt(KEY_PARENT_FRAGMENT_ID, mParentFragment.getId()); } } @Override public void onStart() { super.onStart(); if (mParentFragment != null && mParentFragment instanceof SettingsPreferenceFragment) { ((SettingsPreferenceFragment) mParentFragment).onDialogShowing(); } } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { if (savedInstanceState != null) { mDialogId = savedInstanceState.getInt(KEY_DIALOG_ID, 0); mParentFragment = getParentFragment(); int mParentFragmentId = savedInstanceState.getInt(KEY_PARENT_FRAGMENT_ID, -1); if (mParentFragment == null) { mParentFragment = getFragmentManager().findFragmentById(mParentFragmentId); } if (!(mParentFragment instanceof DialogCreatable)) { throw new IllegalArgumentException( (mParentFragment != null ? mParentFragment.getClass().getName() : mParentFragmentId) + " must implement " + DialogCreatable.class.getName()); } // This dialog fragment could be created from non-SettingsPreferenceFragment if (mParentFragment instanceof SettingsPreferenceFragment) { // restore mDialogFragment in mParentFragment ((SettingsPreferenceFragment) mParentFragment).mDialogFragment = this; } } //通过DialogCreatable接口剥离了dialog的创建 return ((DialogCreatable) mParentFragment).onCreateDialog(mDialogId); } @Override public void onCancel(DialogInterface dialog) { super.onCancel(dialog); if (mOnCancelListener != null) { mOnCancelListener.onCancel(dialog); } } @Override public void onDismiss(DialogInterface dialog) { super.onDismiss(dialog); if (mOnDismissListener != null) { mOnDismissListener.onDismiss(dialog); } } public int getDialogId() { return mDialogId; } @Override public void onDetach() { super.onDetach(); // This dialog fragment could be created from non-SettingsPreferenceFragment if (mParentFragment instanceof SettingsPreferenceFragment) { // in case the dialog is not explicitly removed by removeDialog() if (((SettingsPreferenceFragment) mParentFragment).mDialogFragment == this) { ((SettingsPreferenceFragment) mParentFragment).mDialogFragment = null; } } } private void setParentFragment(DialogCreatable fragment) { mParentFragment = (Fragment) fragment; } private void setDialogId(int dialogId) { mDialogId = dialogId; } }
很标准的自定义DialogFragment
的模板代码,核心代码在onCreateDialog()
方法当中,但此方法通过DialogCreatable
接口剥离了dialog的创建,这里也很好理解,因为不仅有设置日期的Dialog,还有设置时间的Dialog,如果写死的话,那么就需要定义两个DialogFragment
,所以这里它给抽象出来了,DialogCreatable
接口的实现仍然在DateTimeSettings
当中,它的父类SettingsPreferenceFragment
实现了DialogCreatable
@SearchIndexablepublic class DateTimeSettings extends DashboardFragment implements TimePreferenceController.TimePreferenceHost, DatePreferenceController.DatePreferenceHost { //省略部分代码@Override public Dialog onCreateDialog(int id) { //根据选项创建对应的dialog switch (id) { case DatePreferenceController.DIALOG_DATEPICKER: return use(DatePreferenceController.class) .buildDatePicker(getActivity()); case TimePreferenceController.DIALOG_TIMEPICKER: return use(TimePreferenceController.class) .buildTimePicker(getActivity()); default: throw new IllegalArgumentException(); } } //省略部分代码}
根据用户选择的操作(设置日期or设置时间),创建对应的dialog,最终的创建过程由DatePreferenceController
来完成
public class DatePreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, DatePickerDialog.OnDateSetListener { //省略部分代码public DatePickerDialog buildDatePicker(Activity activity) { final Calendar calendar = Calendar.getInstance(); //创建DatePickerDialog final DatePickerDialog d = new DatePickerDialog( activity, this, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)); // The system clock can't represent dates outside this range. calendar.clear(); calendar.set(2007, Calendar.JANUARY, 1); //设置最小时间为2007-01-01 d.getDatePicker().setMinDate(calendar.getTimeInMillis()); calendar.clear(); calendar.set(2037, Calendar.DECEMBER, 31); //设置最大时间为2037-12-31 d.getDatePicker().setMaxDate(calendar.getTimeInMillis()); return d; } //省略部分代码}
这里可以看到,系统限制了可选的日期范围为2007-01-01至2037-12-31,实际操作也确实是这样子的(开发板和小米手机都是),此为彩蛋1。
看一下DatePickerDialog
的定义
public class DatePickerDialog extends AlertDialog implements OnClickListener, OnDateChangedListener { private static final String YEAR = "year"; private static final String MONTH = "month"; private static final String DAY = "day"; @UnsupportedAppUsage private final DatePicker mDatePicker; private OnDateSetListener mDateSetListener; //省略部分代码 private DatePickerDialog(@NonNull Context context, @StyleRes int themeResId, @Nullable OnDateSetListener listener, @Nullable Calendar calendar, int year, int monthOfYear, int dayOfMonth) { super(context, resolveDialogTheme(context, themeResId)); final Context themeContext = getContext(); final LayoutInflater inflater = LayoutInflater.from(themeContext); //初始化Dialog的View final View view = inflater.inflate(R.layout.date_picker_dialog, null); setView(view); setButton(BUTTON_POSITIVE, themeContext.getString(R.string.ok), this); setButton(BUTTON_NEGATIVE, themeContext.getString(R.string.cancel), this); setButtonPanelLayoutHint(LAYOUT_HINT_SIDE); if (calendar != null) { year = calendar.get(Calendar.YEAR); monthOfYear = calendar.get(Calendar.MONTH); dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); } mDatePicker = (DatePicker) view.findViewById(R.id.datePicker); mDatePicker.init(year, monthOfYear, dayOfMonth, this); mDatePicker.setValidationCallback(mValidationCallback); mDateSetListener = listener; } //省略部分代码 public void setOnDateSetListener(@Nullable OnDateSetListener listener) { mDateSetListener = listener; } @Override public void onClick(@NonNull DialogInterface dialog, int which) { switch (which) { case BUTTON_POSITIVE: if (mDateSetListener != null) { // Clearing focus forces the dialog to commit any pending // changes, e.g. typed text in a NumberPicker. mDatePicker.clearFocus(); //设置完成回调 mDateSetListener.onDateSet(mDatePicker, mDatePicker.getYear(), mDatePicker.getMonth(), mDatePicker.getDayOfMonth()); } break; case BUTTON_NEGATIVE: cancel(); break; } } //省略部分代码 public interface OnDateSetListener { void onDateSet(DatePicker view, int year, int month, int dayOfMonth); }}
可以看到也是标准的自定义Dialog,不过它是继承的AlertDialog
,设置完成后通过OnDateSetListener
进行回调,而DatePreferenceController
实现了该接口
public class DatePreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, DatePickerDialog.OnDateSetListener {//省略部分代码 @Override public void onDateSet(DatePicker view, int year, int month, int day) { //设置日期 setDate(year, month, day); //更新UI mHost.updateTimeAndDateDisplay(mContext); } //省略部分代码 @VisibleForTesting void setDate(int year, int month, int day) { Calendar c = Calendar.getInstance(); c.set(Calendar.YEAR, year); c.set(Calendar.MONTH, month); c.set(Calendar.DAY_OF_MONTH, day); //设置日期与定义的最小日期取最大值,也就意味着设置的日期不能小于定义的最小日期 long when = Math.max(c.getTimeInMillis(), DatePreferenceHost.MIN_DATE); if (when / 1000 < Integer.MAX_VALUE) { //设置系统时间 ((AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE)).setTime(when); } }}
可以看到系统定义了一个最小日期DatePreferenceHost.MIN_DATE
,其值为2007-11-05 0:00
public interface UpdateTimeAndDateCallback { // Minimum time is Nov 5, 2007, 0:00. long MIN_DATE = 1194220800000L; void updateTimeAndDateDisplay(Context context);}
最终显示日期会在目标日期和最小日期中取最大值,也就是说设定的日期不能小于最小日期,而上文说到,选择的日期范围为2007-01-01至2037-12-31,因此,如果你设置的日期在2007-01-01至2007-11-05之间,最终都会显示2007-11-05,实际测试也是如此(开发板和小米手机都是),此为彩蛋2。
选择完时间后,最后通过AlarmManagerService
来设置系统内核的时间,此处涉及到跨进程通信,使用的通信方式是AIDL,直接到AlarmManagerService
看看如何设置内核时间的
class AlarmManagerService extends SystemService { //省略部分代码 private final IBinder mService = new IAlarmManager.Stub() { //省略部分代码@Override public boolean setTime(long millis) { //先授权 getContext().enforceCallingOrSelfPermission( "android.permission.SET_TIME", "setTime");//然后设置系统内核时间 return setTimeImpl(millis); } //省略部分代码 } //省略部分代码 boolean setTimeImpl(long millis) { if (!mInjector.isAlarmDriverPresent()) { Slog.w(TAG, "Not setting time since no alarm driver is available."); return false; } synchronized (mLock) { final long currentTimeMillis = mInjector.getCurrentTimeMillis(); //设置系统内核时间 mInjector.setKernelTime(millis); final TimeZone timeZone = TimeZone.getDefault(); final int currentTzOffset = timeZone.getOffset(currentTimeMillis); final int newTzOffset = timeZone.getOffset(millis); if (currentTzOffset != newTzOffset) { Slog.i(TAG, "Timezone offset has changed, updating kernel timezone"); //设置系统内核时区 mInjector.setKernelTimezone(-(newTzOffset / 60000)); } // The native implementation of setKernelTime can return -1 even when the kernel // time was set correctly, so assume setting kernel time was successful and always // return true. return true; } } //省略部分代码 @VisibleForTesting static class Injector { //省略部分代码 void setKernelTime(long millis) { Log.d("jasonwan", "setKernelTime: "+millis); if (mNativeData != 0) { //在native层完成内核时间的设置 AlarmManagerService.setKernelTime(mNativeData, millis); } } //省略部分代码 } //native层完成 private static native int setKernelTime(long nativeData, long millis); private static native int setKernelTimezone(long nativeData, int minuteswest); //省略部分代码}
可以看到最终是在native层完成内核时间的设置,这也理所当然,毕竟java是应用层,触及不到kernel层。
回到最开始的问题,为啥开机之后却不是我们设置的时间呢,这就要看看开机之后系统是怎么设置时间的。同样在AlarmManagerService
里面,因为它是SystemService
的子类,所以会随着开机启动而启动,而Service启动后必定会执行它的生命周期方法,设置时间的逻辑就是在onStart()
生命周期方法里面
class AlarmManagerService extends SystemService { //省略部分代码 @Override public void onStart() { mInjector.init(); synchronized (mLock) { //省略部分代码 // We have to set current TimeZone info to kernel // because kernel doesn't keep this after reboot //设置时区,从SystemProperty中读取 setTimeZoneImpl(SystemProperties.get(TIMEZONE_PROPERTY)); // Ensure that we're booting with a halfway sensible current time. Use the // most recent of Build.TIME, the root file system's timestamp, and the // value of the ro.build.date.utc system property (which is in seconds). //设置时区 //先读取系统编译时间 long utc = 1000L * SystemProperties.getLong("ro.build.date.utc", -1L); //再读取根目录最近的修改的时间 long lastModified = Environment.getRootDirectory().lastModified(); //然后读取系统构建时间,三个时间取最大值 final long systemBuildTime = Long.max( utc, Long.max(lastModified, Build.TIME)); //代码1 Log.d("jasonwan", "onStart: utc="+utc+", lastModified="+lastModified+", BuildTime="+Build.TIME+", currentTimeMillis="+mInjector.getCurrentTimeMillis()); //设置的时间小于最大值,则将最大值设置为系统内核的时间,注意,因为我们刚刚已经设置了内核时间,所以重启后通过System.currentTimeMillis()得到的时间戳为我们设置的时间,此判断意味着,系统编译时间、根目录最近修改时间、系统构建时间、设置的时间,这四者当中取最大值作为重启后的内核时间 if (mInjector.getCurrentTimeMillis() < systemBuildTime) { //这里mInjector.getCurrentTimeMillis()其实就是System.currentTimeMillis() Slog.i(TAG, "Current time only " + mInjector.getCurrentTimeMillis() + ", advancing to build time " + systemBuildTime); mInjector.setKernelTime(systemBuildTime); } //省略部分代码 } //省略部分代码 @VisibleForTesting static class Injector { //省略部分代码 void setKernelTimezone(int minutesWest) { AlarmManagerService.setKernelTimezone(mNativeData, minutesWest); } void setKernelTime(long millis) { //代码2 Log.d("jasonwan", "setKernelTime: "+millis); if (mNativeData != 0) { AlarmManagerService.setKernelTime(mNativeData, millis); } } //省略部分代码 long getElapsedRealtime() { return SystemClock.elapsedRealtime(); } long getCurrentTimeMillis() { return System.currentTimeMillis(); } //省略部分代码 }}
实践验证
根据源码分析得知,系统最终会在系统编译时间、根目录最近修改时间、系统构建时间、设置的时间,这四者当中取最大值作为重启后的内核时间,这里我在代码1和代码2处埋下了log,看看四个时间的值分别是多少,以及最终设置的内核时间是多少,我在设置中手动设置的日期为2022-10-01,重启后的日志如下
四个值分别为:
系统编译时间:1669271830000,格式化后为2022-11-24 14:37:10
根目录最近修改时间:1678865533000,格式化后为2023-03-15 15:32:13
构建时间:1669271830000,同系统编译时间
设置的时间:1664609754998,格式化后为2022-10-01 15:35:54
注意,我们只需要注意日期,不需要关注时分秒,可以看到四个时间当中,最大的为根目录最近修改时间,所以最终显示的日期为2023-03-15,此为彩蛋3。
我在开发板和小米手机上测试的结果相同,说明MIUI保留了这一块的逻辑,但是MIUI也有一个bug,就是明明我关闭了使用网络提供的时间和使用网络提供的时区,它还是给我自动更新了日期和时间,除非开启飞行模式之后才不自动更新。
同时我们还注意到,系统编译时间ro.build.date.utc
跟系统构建时间Build.TIME
是相同的,这很好理解,编译跟构建是一个意思,而且Build.TIME
的取值其实也来自于ro.build.date.utc
public class Build { //省略部分代码 public static final long TIME = getLong("ro.build.date.utc") * 1000; //省略部分代码}
感谢各位的阅读,以上就是“Android时间设置问题怎么解决”的内容了,经过本文的学习后,相信大家对Android时间设置问题怎么解决这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是编程网,小编将为大家推送更多相关知识点的文章,欢迎关注!