Android时间设置的3个小彩蛋分享

问题现象

最近处理了一个非常有意思的系统bug,修改系统时间,重启后居然没有生效

-1

注意要关闭使用网络提供的时间和使用网络提供的时区这两个开关。

重启后显示的时间日期为

-2

显示的时间既不是我设置的时间,也不是当前时间(当前时间为2023-03-20 15:49),那么显示的这个时间到底是什么时间呢?

为了弄清楚这个问题,我研究了一下android设置时间的逻辑,研究过程中还发现了一些彩蛋。

源码分析

首先是设置时间的逻辑,源码位于packages/apps/Settings/src/com/android/settings/datetime/DatePreferenceController.Java

  1. public class DatePreferenceController extends AbstractPreferenceController
  2.          implements PreferenceControllerMixin, DatePickerDialog.OnDateSetListener {
  3.      //省略部分代码
  4.      private final DatePreferenceHost mHost;
  5.      @Override
  6.      public boolean handlePreferenceTreeClick(Preference preference) {
  7.          //点击日期后处理
  8.          if (!TextUtils.equals(preference.getKey(), KEY_DATE)) {
  9.              return false;
  10.          }
  11.          //显示日期选择框
  12.          mHost.showDatePicker();
  13.          return true;
  14.      }
  15.      //省略部分代码
  16. }

mHostDatePreferenceHost接口,接口实现在packages/apps/Settings/src/com/android/settings/DateTimeSettings.java中,因此,showDatePicker()的逻辑位于该实现类中

  1. @SearchIndexable
  2. public class DateTimeSettings extends DashboardFragment implements
  3.          TimePreferenceController.TimePreferenceHost, DatePreferenceController.DatePreferenceHost {
  4.      //省略部分代码
  5.      @Override
  6.      public void showDatePicker() {
  7.          //显示日期选择对话框
  8.          showDialog(DatePreferenceController.DIALOG_DATEPICKER);
  9.      }
  10.      //省略部分代码
  11. }

showDialog()定义在父类packages/apps/Settings/src/com/android/settings/SettingsPreferenceFragment.java

  1. public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceFragment
  2.          implements DialogCreatable, HelpResourceProvider, Indexable {
  3.      protected void showDialog(int dialogId) {
  4.          if (mDialogFragment != null) {
  5.              Log.e(TAG, “Old dialog fragment not null!”);
  6.          }
  7.          //创建SettingsDialogFragment并进行show
  8.          mDialogFragment = SettingsDialogFragment.newInstance(this, dialogId);
  9.          mDialogFragment.show(getChildFragmentManager(), Integer.toString(dialogId));
  10.      }
  11. }

showDialog()中就是创建了SettingsDialogFragment然后显示,SettingsDialogFragmentSettingsPreferenceFragment的一个内部类,看一下SettingsDialogFragment的定义

  1.      public static class SettingsDialogFragment extends InstrumentedDialogFragment {
  2.          private static final String KEY_DIALOG_ID = “key_dialog_id”;
  3.          private static final String KEY_PARENT_FRAGMENT_ID = “key_parent_fragment_id”;
  4.  
  5.          private Fragment mParentFragment;
  6.  
  7.          private DialogInterface.OnCancelListener mOnCancelListener;
  8.          private DialogInterface.OnDismissListener mOnDismissListener;
  9.  
  10.          public static SettingsDialogFragment newInstance(DialogCreatable fragment, int dialogId) {
  11.              if (!(fragment instanceof Fragment)) {
  12.                  throw new IllegalArgumentException(“fragment argument must be an instance of “
  13.                      + Fragment.class.getName());
  14.              }
  15.  
  16.              final SettingsDialogFragment settingsDialogFragment = new SettingsDialogFragment();
  17.              settingsDialogFragment.setParentFragment(fragment);
  18.              settingsDialogFragment.setDialogId(dialogId);
  19.  
  20.              return settingsDialogFragment;
  21.          }
  22.  
  23.          @Override
  24.          public int getMetricsCategory() {
  25.              if (mParentFragment == null) {
  26.                  return Instrumentable.METRICS_CATEGORY_UNKNOWN;
  27.              }
  28.              final int metricsCategory =
  29.                      ((DialogCreatable) mParentFragment).getDialogMetricsCategory(mDialogId);
  30.              if (metricsCategory <= 0) {
  31.                  throw new IllegalStateException(“Dialog must provide a metrics category”);
  32.              }
  33.              return metricsCategory;
  34.          }
  35.  
  36.          @Override
  37.          public void onSaveInstanceState(Bundle outState) {
  38.              super.onSaveInstanceState(outState);
  39.              if (mParentFragment != null) {
  40.                  outState.putInt(KEY_DIALOG_ID, mDialogId);
  41.                  outState.putInt(KEY_PARENT_FRAGMENT_ID, mParentFragment.getId());
  42.              }
  43.          }
  44.  
  45.          @Override
  46.          public void onStart() {
  47.              super.onStart();
  48.  
  49.              if (mParentFragment != null && mParentFragment instanceof SettingsPreferenceFragment) {
  50.                  ((SettingsPreferenceFragment) mParentFragment).onDialogShowing();
  51.              }
  52.          }
  53.  
  54.          @Override
  55.          public Dialog onCreateDialog(Bundle savedInstanceState) {
  56.              if (savedInstanceState != null) {
  57.                  mDialogId = savedInstanceState.getInt(KEY_DIALOG_ID, 0);
  58.                  mParentFragment = getParentFragment();
  59.                  int mParentFragmentId = savedInstanceState.getInt(KEY_PARENT_FRAGMENT_ID, 1);
  60.                  if (mParentFragment == null) {
  61.                      mParentFragment = getFragmentManager().findFragmentById(mParentFragmentId);
  62.                  }
  63.                  if (!(mParentFragment instanceof DialogCreatable)) {
  64.                      throw new IllegalArgumentException(
  65.                      (mParentFragment != null
  66.                      ? mParentFragment.getClass().getName()
  67.                      : mParentFragmentId)
  68.                      + ” must implement “
  69.                      + DialogCreatable.class.getName());
  70.                  }
  71.                  // This dialog fragment could be created from non-SettingsPreferenceFragment
  72.                  if (mParentFragment instanceof SettingsPreferenceFragment) {
  73.                      // restore mDialogFragment in mParentFragment
  74.                      ((SettingsPreferenceFragment) mParentFragment).mDialogFragment = this;
  75.                  }
  76.              }
  77.              //通过DialogCreatable接口剥离了dialog的创建
  78.              return ((DialogCreatable) mParentFragment).onCreateDialog(mDialogId);
  79.          }
  80.  
  81.          @Override
  82.          public void onCancel(DialogInterface dialog) {
  83.              super.onCancel(dialog);
  84.              if (mOnCancelListener != null) {
  85.                  mOnCancelListener.onCancel(dialog);
  86.              }
  87.          }
  88.  
  89.          @Override
  90.          public void onDismiss(DialogInterface dialog) {
  91.              super.onDismiss(dialog);
  92.              if (mOnDismissListener != null) {
  93.                  mOnDismissListener.onDismiss(dialog);
  94.              }
  95.          }
  96.  
  97.          public int getDialogId() {
  98.              return mDialogId;
  99.          }
  100.  
  101.          @Override
  102.          public void onDetach() {
  103.              super.onDetach();
  104.  
  105.              // This dialog fragment could be created from non-SettingsPreferenceFragment
  106.              if (mParentFragment instanceof SettingsPreferenceFragment) {
  107.                  // in case the dialog is not explicitly removed by removeDialog()
  108.                  if (((SettingsPreferenceFragment) mParentFragment).mDialogFragment == this) {
  109.                      ((SettingsPreferenceFragment) mParentFragment).mDialogFragment = null;
  110.                  }
  111.              }
  112.          }
  113.  
  114.          private void setParentFragment(DialogCreatable fragment) {
  115.              mParentFragment = (Fragment) fragment;
  116.          }
  117.  
  118.          private void setDialogId(int dialogId) {
  119.              mDialogId = dialogId;
  120.          }
  121.      }

很标准的自定义DialogFragment的模板代码,核心代码在onCreateDialog()方法当中,但此方法通过DialogCreatable接口剥离了dialog的创建,这里也很好理解,因为不仅有设置日期的Dialog,还有设置时间的Dialog,如果写死的话,那么就需要定义两个DialogFragment,所以这里它给抽象出来了,DialogCreatable接口的实现仍然在DateTimeSettings当中,它的父类SettingsPreferenceFragment实现了DialogCreatable

  1. @SearchIndexable
  2. public class DateTimeSettings extends DashboardFragment implements
  3.          TimePreferenceController.TimePreferenceHost, DatePreferenceController.DatePreferenceHost {
  4.      //省略部分代码
  5.      @Override
  6.      public Dialog onCreateDialog(int id) {
  7.          //根据选项创建对应的dialog
  8.          switch (id) {
  9.              case DatePreferenceController.DIALOG_DATEPICKER:
  10.          return use(DatePreferenceController.class)
  11.                      .buildDatePicker(getActivity());
  12.              case TimePreferenceController.DIALOG_TIMEPICKER:
  13.                  return use(TimePreferenceController.class)
  14.                      .buildTimePicker(getActivity());
  15.              default:
  16.                  throw new IllegalArgumentException();
  17.          }
  18.      }
  19.      //省略部分代码
  20. }

根据用户选择的操作(设置日期or设置时间),创建对应的dialog,最终的创建过程由DatePreferenceController来完成

  1. public class DatePreferenceController extends AbstractPreferenceController
  2.          implements PreferenceControllerMixin, DatePickerDialog.OnDateSetListener {
  3.      //省略部分代码
  4.      public DatePickerDialog buildDatePicker(Activity activity) {
  5.          final Calendar calendar = Calendar.getInstance();
  6.          //创建DatePickerDialog
  7.          final DatePickerDialog d = new DatePickerDialog(
  8.                  activity,
  9.                  this,
  10.                  calendar.get(Calendar.YEAR),
  11.                  calendar.get(Calendar.MONTH),
  12.                  calendar.get(Calendar.DAY_OF_MONTH));
  13.          // The system clock can’t represent dates outside this range.
  14.          calendar.clear();
  15.          calendar.set(2007, Calendar.JANUARY, 1);
  16.          //设置最小时间为2007-01-01
  17.          d.getDatePicker().setMinDate(calendar.getTimeInMillis());
  18.          calendar.clear();
  19.          calendar.set(2037, Calendar.DECEMBER, 31);
  20.          //设置最大时间为2037-12-31
  21.          d.getDatePicker().setMaxDate(calendar.getTimeInMillis());
  22.          return d;
  23.      }
  24.      //省略部分代码
  25. }

这里可以看到,系统限制了可选的日期范围为2007-01-01至2037-12-31,实际操作也确实是这样子的(开发板和小米手机都是),此为彩蛋1。

-3

看一下DatePickerDialog的定义

  1. public class DatePickerDialog extends AlertDialog implements OnClickListener,
  2.          OnDateChangedListener {
  3.      private static final String YEAR = “year”;
  4.      private static final String MONTH = “month”;
  5.      private static final String DAY = “day”;
  6.  
  7.      @UnsupportedAppUsage
  8.      private final DatePicker mDatePicker;
  9.  
  10.      private OnDateSetListener mDateSetListener;
  11.  
  12.      //省略部分代码
  13.  
  14.      private DatePickerDialog(@NonNull Context context, @StyleRes int themeResId,
  15.              @Nullable OnDateSetListener listener, @Nullable Calendar calendar, int year,
  16.              int monthOfYear, int dayOfMonth) {
  17.          super(context, resolveDialogTheme(context, themeResId));
  18.  
  19.          final Context themeContext = getContext();
  20.          final LayoutInflater inflater = LayoutInflater.from(themeContext);
  21.          //初始化Dialog的View
  22.          final View view = inflater.inflate(R.layout.date_picker_dialog, null);
  23.          setView(view);
  24.  
  25.          setButton(BUTTON_POSITIVE, themeContext.getString(R.string.ok), this);
  26.          setButton(BUTTON_NEGATIVE, themeContext.getString(R.string.cancel), this);
  27.          setButtonPanelLayoutHint(LAYOUT_HINT_SIDE);
  28.  
  29.          if (calendar != null) {
  30.              year = calendar.get(Calendar.YEAR);
  31.              monthOfYear = calendar.get(Calendar.MONTH);
  32.              dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
  33.          }
  34.  
  35.          mDatePicker = (DatePicker) view.findViewById(R.id.datePicker);
  36.          mDatePicker.init(year, monthOfYear, dayOfMonth, this);
  37.          mDatePicker.setValidationCallback(mValidationCallback);
  38.  
  39.          mDateSetListener = listener;
  40.      }
  41.  
  42.      //省略部分代码
  43.  
  44.      /**
  45.          * Sets the listener to call when the user sets the date.
  46.          *
  47.          * @param listener the listener to call when the user sets the date
  48.          */
  49.      public void setOnDateSetListener(@Nullable OnDateSetListener listener) {
  50.          mDateSetListener = listener;
  51.      }
  52.  
  53.      @Override
  54.      public void onClick(@NonNull DialogInterface dialog, int which) {
  55.          switch (which) {
  56.              case BUTTON_POSITIVE:
  57.                  if (mDateSetListener != null) {
  58.                      // Clearing focus forces the dialog to commit any pending
  59.                      // changes, e.g. typed text in a NumberPicker.
  60.                      mDatePicker.clearFocus();
  61.                      //设置完成回调
  62.                      mDateSetListener.onDateSet(mDatePicker, mDatePicker.getYear(),
  63.                      mDatePicker.getMonth(), mDatePicker.getDayOfMonth());
  64.                  }
  65.                  break;
  66.              case BUTTON_NEGATIVE:
  67.                  cancel();
  68.                  break;
  69.          }
  70.      }
  71.  
  72.      //省略部分代码
  73.  
  74.      /**
  75.          * The listener used to indicate the user has finished selecting a date.
  76.          */
  77.      public interface OnDateSetListener {
  78.          /**
  79.              * @param view the picker associated with the dialog
  80.              * @param year the selected year
  81.              * @param month the selected month (0-11 for compatibility with
  82.              * {@link Calendar#MONTH})
  83.              * @param dayOfMonth the selected day of the month (1-31, depending on
  84.              * month)
  85.              */
  86.          void onDateSet(DatePicker view, int year, int month, int dayOfMonth);
  87.      }
  88. }

可以看到也是标准的自定义Dialog,不过它是继承的AlertDialog,设置完成后通过OnDateSetListener进行回调,而DatePreferenceController实现了该接口

  1. public class DatePreferenceController extends AbstractPreferenceController
  2.          implements PreferenceControllerMixin, DatePickerDialog.OnDateSetListener {
  3.      //省略部分代码
  4.      @Override
  5.      public void onDateSet(DatePicker view, int year, int month, int day) {
  6.          //设置日期
  7.          setDate(year, month, day);
  8.          //更新UI
  9.          mHost.updateTimeAndDateDisplay(mContext);
  10.      }
  11.      //省略部分代码
  12.      @VisibleForTesting
  13.      void setDate(int year, int month, int day) {
  14.          Calendar c = Calendar.getInstance();
  15.  
  16.          c.set(Calendar.YEAR, year);
  17.          c.set(Calendar.MONTH, month);
  18.          c.set(Calendar.DAY_OF_MONTH, day);
  19.          //设置日期与定义的最小日期取最大值,也就意味着设置的日期不能小于定义的最小日期
  20.          long when = Math.max(c.getTimeInMillis(), DatePreferenceHost.MIN_DATE);
  21.  
  22.          if (when / 1000 < Integer.MAX_VALUE) {
  23.              //设置系统时间
  24.              ((AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE)).setTime(when);
  25.          }
  26.      }
  27. }

可以看到系统定义了一个最小日期DatePreferenceHost.MIN_DATE,其值为2007-11-05 0:00

  1. public interface UpdateTimeAndDateCallback {
  2.      // Minimum time is Nov 5, 2007, 0:00.
  3.      long MIN_DATE = 1194220800000L;
  4.  
  5.      void updateTimeAndDateDisplay(Context context);
  6. }

最终显示日期会在目标日期和最小日期中取最大值,也就是说设定的日期不能小于最小日期,而上文说到,选择的日期范围为2007-01-01至2037-12-31,因此,如果你设置的日期在2007-01-01至2007-11-05之间,最终都会显示2007-11-05,实际测试也是如此(开发板和小米手机都是),此为彩蛋2。

-4

选择完时间后,最后通过AlarmManagerService来设置系统内核的时间,此处涉及到跨进程通信,使用的通信方式是AIDL,直接到AlarmManagerService看看如何设置内核时间的

  1. class AlarmManagerService extends SystemService {
  2.      //省略部分代码
  3.      /**
  4.          * Public-facing binder interface
  5.          */
  6.      private final IBinder mService = new IAlarmManager.Stub() {
  7.          //省略部分代码
  8.          @Override
  9.          public boolean setTime(long millis) {
  10.              //先授权
  11.              getContext().enforceCallingOrSelfPermission(
  12.                      “android.permission.SET_TIME”,
  13.                      “setTime”);
  14.              //然后设置系统内核时间
  15.              return setTimeImpl(millis);
  16.          }
  17.          //省略部分代码
  18.      }
  19.      //省略部分代码
  20.      boolean setTimeImpl(long millis) {
  21.          if (!mInjector.isAlarmDriverPresent()) {
  22.              Slog.w(TAG, “Not setting time since no alarm driver is available.”);
  23.              return false;
  24.          }
  25.  
  26.          synchronized (mLock) {
  27.              final long currentTimeMillis = mInjector.getCurrentTimeMillis();
  28.              //设置系统内核时间
  29.              mInjector.setKernelTime(millis);
  30.              final TimeZone timeZone = TimeZone.getDefault();
  31.              final int currentTzOffset = timeZone.getOffset(currentTimeMillis);
  32.              final int newTzOffset = timeZone.getOffset(millis);
  33.              if (currentTzOffset != newTzOffset) {
  34.                  Slog.i(TAG, “Timezone offset has changed, updating kernel timezone”);
  35.                  //设置系统内核时区
  36.                  mInjector.setKernelTimezone(-(newTzOffset / 60000));
  37.              }
  38.              // The native implementation of setKernelTime can return -1 even when the kernel
  39.              // time was set correctly, so assume setting kernel time was successful and always
  40.              // return true.
  41.              return true;
  42.          }
  43.      }
  44.      //省略部分代码
  45.      @VisibleForTesting
  46.      static class Injector {
  47.      //省略部分代码
  48.          void setKernelTime(long millis) {
  49.              Log.d(“jasonwan”, “setKernelTime: “+millis);
  50.              if (mNativeData != 0) {
  51.                  //在native层完成内核时间的设置
  52.                  AlarmManagerService.setKernelTime(mNativeData, millis);
  53.              }
  54.          }
  55.          //省略部分代码
  56.      }
  57.      //native层完成
  58.      private static native int setKernelTime(long nativeData, long millis);
  59.      private static native int setKernelTimezone(long nativeData, int minuteswest);
  60.      //省略部分代码
  61. }

可以看到最终是在native层完成内核时间的设置,这也理所当然,毕竟java是应用层,触及不到kernel层。

回到最开始的问题,为啥开机之后却不是我们设置的时间呢,这就要看看开机之后系统是怎么设置时间的。同样在AlarmManagerService里面,因为它是SystemService的子类,所以会随着开机启动而启动,而Service启动后必定会执行它的生命周期方法,设置时间的逻辑就是在onStart()生命周期方法里面

  1. class AlarmManagerService extends SystemService {
  2.      //省略部分代码
  3.      @Override
  4.      public void onStart() {
  5.          mInjector.init();
  6.  
  7.          synchronized (mLock) {
  8.              //省略部分代码
  9.  
  10.              // We have to set current TimeZone info to kernel
  11.              // because kernel doesn’t keep this after reboot
  12.              //设置时区,从SystemProperty中读取
  13.              setTimeZoneImpl(SystemProperties.get(TIMEZONE_PROPERTY));
  14.  
  15.              // Ensure that we’re booting with a halfway sensible current time. Use the
  16.              // most recent of Build.TIME, the root file system’s timestamp, and the
  17.              // value of the ro.build.date.utc system property (which is in seconds).
  18.              //设置时区
  19.              //先读取系统编译时间
  20.              long utc = 1000L * SystemProperties.getLong(“ro.build.date.utc”, 1L);
  21.              //再读取根目录最近的修改的时间
  22.              long lastModified = Environment.getRootDirectory().lastModified();
  23.              //然后读取系统构建时间,三个时间取最大值
  24.              final long systemBuildTime = Long.max(
  25.                      utc,
  26.                      Long.max(lastModified, Build.TIME));
  27.              //代码1
  28.              Log.d(“jasonwan”, “onStart: utc=”+utc+“, lastModified=”+lastModified+“, BuildTime=”+Build.TIME+“, currentTimeMillis=”+mInjector.getCurrentTimeMillis());
  29.              //设置的时间小于最大值,则将最大值设置为系统内核的时间,注意,因为我们刚刚已经设置了内核时间,所以重启后通过System.currentTimeMillis()得到的时间戳为我们设置的时间,此判断意味着,系统编译时间、根目录最近修改时间、系统构建时间、设置的时间,这四者当中取最大值作为重启后的内核时间
  30.              if (mInjector.getCurrentTimeMillis() < systemBuildTime) {
  31.                  //这里mInjector.getCurrentTimeMillis()其实就是System.currentTimeMillis()
  32.                  Slog.i(TAG, “Current time only “ + mInjector.getCurrentTimeMillis()
  33.                      + “, advancing to build time “ + systemBuildTime);
  34.                  mInjector.setKernelTime(systemBuildTime);
  35.              }
  36.              //省略部分代码
  37.  
  38.      }
  39.      //省略部分代码
  40.      @VisibleForTesting
  41.      static class Injector {
  42.          //省略部分代码
  43.          void setKernelTimezone(int minutesWest) {
  44.              AlarmManagerService.setKernelTimezone(mNativeData, minutesWest);
  45.          }
  46.  
  47.          void setKernelTime(long millis) {
  48.              //代码2
  49.              Log.d(“jasonwan”, “setKernelTime: “+millis);
  50.              if (mNativeData != 0) {
  51.                  AlarmManagerService.setKernelTime(mNativeData, millis);
  52.              }
  53.          }
  54.  
  55.          //省略部分代码
  56.          long getElapsedRealtime() {
  57.              return SystemClock.elapsedRealtime();
  58.          }
  59.  
  60.          long getCurrentTimeMillis() {
  61.              return System.currentTimeMillis();
  62.          }
  63.          //省略部分代码
  64.      }
  65. }

实践验证

根据源码分析得知,系统最终会在系统编译时间、根目录最近修改时间、系统构建时间、设置的时间,这四者当中取最大值作为重启后的内核时间,这里我在代码1和代码2处埋下了log,看看四个时间的值分别是多少,以及最终设置的内核时间是多少,我在设置中手动设置的日期为2022-10-01,重启后的日志如下

-5

四个值分别为:

  • 系统编译时间: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。

-6

我在开发板和小米手机上测试的结果相同,说明MIUI保留了这一块的逻辑,但是MIUI也有一个bug,就是明明我关闭了使用网络提供的时间和使用网络提供的时区,它还是给我自动更新了日期和时间,除非开启飞行模式之后才不自动更新。

同时我们还注意到,系统编译时间ro.build.date.utc跟系统构建时间Build.TIME是相同的,这很好理解,编译跟构建是一个意思,而且Build.TIME的取值其实也来自于ro.build.date.utc

  1. /**
  2.      * Information about the current build, extracted from system properties.
  3.      */
  4. public class Build {
  5.      //省略部分代码
  6.      /** The time at which the build was produced, given in milliseconds since the Unix epoch. */
  7.      public static final long TIME = getLong(“ro.build.date.utc”) * 1000;
  8.      //省略部分代码
  9. }

我也搞不懂Google为什么要设计两个概念,搞得我一开始还去研究这两个概念的区别,结果没区别,数据源是一样的,尴尬。

结论

设置系统时间必须大于系统编译时间和根目录最近修改时间才会生效。

最后我在想,MIUI是不是可以在这一块优化一下,直接设置里面告诉用户我能设置的时间区域岂不是更人性化,毕竟细节决定成败。

到此这篇关于Android时间设置的3个小彩蛋的文章就介绍到这了,更多相关Android时间设置彩蛋内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

标签

发表评论