通过代码实现时间、时区的相关设置。在公司的一个android设备中,经常会出现时间不准,比如重启后时间变成1970年,只要设备连上网,会自动同步时间为正确的时间,但是这个同步有时候也没能同步成功,所以需要我们可以自行设置系统时间,或者同步我们自己服务器的时间,因为有些登录操作要求设备的时间和服务器的时间相差不能超过5分钟,一旦超过5分钟则不给登录。

界面如下:

android 设置window flag_设置系统时间


布局代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical"
    android:gravity="center">

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:id="@+id/timeText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            tools:text="2023-08-07 14:44:30"/>

        <CheckBox
            android:id="@+id/autoDateAndTimeCheckBox"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="自动确定日期和时间" />

        <CheckBox
            android:id="@+id/autoTimeZoneTimeCheckBox"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="自动确定时区" />

        <CheckBox
            android:id="@+id/use24HourCheckBox"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="使用24小时格式" />

        <Button
            android:id="@+id/setDateButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="设置日期"/>

        <Button
            android:id="@+id/setTimeButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="设置时间"/>

        <Button
            android:id="@+id/setTimeZoneButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="设置时区"/>

        <Button
            android:id="@+id/setUseNetTimeButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="网络时间"/>

    </androidx.appcompat.widget.LinearLayoutCompat>

</androidx.appcompat.widget.LinearLayoutCompat>

代码实现如下:

清单文件如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:sharedUserId="android.uid.system"
    android:sharedUserMaxSdkVersion="32"
    tools:targetApi="tiramisu">

    <uses-permission android:name="android.permission.SET_TIME" tools:ignore="ProtectedPermissions" />
    <uses-permission android:name="android.permission.SET_TIME_ZONE" tools:ignore="ProtectedPermissions" />
    <uses-permission android:name="android.permission.INTERNET"/>

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.SetTimeDemo"
        tools:targetApi="31"
        android:name=".App">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

这里主要的设置是android:sharedUserId="android.uid.system"和设置权限,另外打包时需要使用系统签名文件进行打包,这样才有权限修改时间。

代码实现如下:

import android.app.AlarmManager
import android.content.Context
import android.os.SystemClock
import android.provider.Settings
import android.text.format.DateFormat
import androidx.annotation.RequiresPermission
import java.util.*
import kotlin.concurrent.thread

class TimeUtil {

    companion object {
        /** 判断系统的时间是否自动获取的 */
        fun isDateTimeAuto(context: Context): Boolean {
            return Settings.Global.getInt(context.contentResolver, Settings.Global.AUTO_TIME, 1) == 1
        }

        /** 判断系统的时区是否是自动获取的 */
        fun isTimeZoneAuto(context: Context): Boolean {
            return Settings.Global.getInt(context.contentResolver, Settings.Global.AUTO_TIME_ZONE, 1) == 1
        }

        /** 系统时间是否是使用24小时制 */
        fun is24HourFormat(context: Context): Boolean {
            return DateFormat.is24HourFormat(context)
            // 下面的方式有问题,比如第一次获取时,如果没有设置过,则会使用默认值24,但其实可能当前是使用的12小时制的,好像一般的手机默认都是使用12小时制的
            // return Settings.System.getInt(context.contentResolver, Settings.System.TIME_12_24, 24) == 24
        }

        /** 设置系统的时间是否需要自动获取 */
        fun setDateTimeAuto(context: Context, autoEnabled: Boolean) {
            Settings.Global.putInt(context.contentResolver, Settings.Global.AUTO_TIME, if (autoEnabled) 1 else 0)
        }

        /** 设置系统的时区是否自动获取 */
        fun setTimeZoneAuto(context: Context, autoEnabled: Boolean) {
            Settings.Global.putInt(context.contentResolver, Settings.Global.AUTO_TIME_ZONE, if (autoEnabled) 1 else 0)
        }

        /** 设置时间是否使用24小时制 */
        fun set24HourFormat(context: Context, is24HourFormat: Boolean) {
            Settings.System.putInt(context.contentResolver, Settings.System.TIME_12_24, if (is24HourFormat) 24 else 12)
        }

        /** 设置系统日期,需要有系统签名才可以 */
        @RequiresPermission(android.Manifest.permission.SET_TIME)
        fun setDate(context: Context, year: Int, month: Int, day: Int) {
            thread {
                val calendar = Calendar.getInstance()
                calendar[Calendar.YEAR] = year
                calendar[Calendar.MONTH] = month // 注意:月份从0开始的
                calendar[Calendar.DAY_OF_MONTH] = day
                val timeInMillis = calendar.timeInMillis
                if (timeInMillis / 1000 < Int.MAX_VALUE) {
                    (context.getSystemService(Context.ALARM_SERVICE) as AlarmManager).setTime(timeInMillis)
                }
            }
        }

        /** 设置系统时间,需要有系统签名才可以 */
        @RequiresPermission(android.Manifest.permission.SET_TIME)
        fun setTime(context: Context, hour: Int, minute: Int) {
            thread {
                val calendar = Calendar.getInstance()
                calendar[Calendar.HOUR_OF_DAY] = hour
                calendar[Calendar.MINUTE] = minute
                calendar[Calendar.SECOND] = 0
                calendar[Calendar.MILLISECOND] = 0
                val timeInMillis = calendar.timeInMillis
                if (timeInMillis / 1000 < Int.MAX_VALUE) {
                    (context.getSystemService(Context.ALARM_SERVICE) as AlarmManager).setTime(timeInMillis)
                }
            }
        }

        /**
         * 设置系统时区
         * 获取以及设置时区用到的都是TimezoneID,它们以字符串的形式存在。
         * 可以用诸如"GMT+05:00", "GMT+0500", "GMT+5:00","GMT+500","GMT+05", and"GMT+5","GMT-05:00"的ID
         * Android系统用的ID一般为:
         * <timezone id="Asia/Shanghai">中国标准时间 (北京)</timezone>
         * <timezone id="Asia/Hong_Kong">香港时间 (香港)</timezone>
         * <timezone id="Asia/Taipei">台北时间 (台北)</timezone>
         * <timezone id="Asia/Seoul">首尔</timezone>
         * <timezone id="Asia/Tokyo">日本时间 (东京)</timezone>
         * */
        @RequiresPermission(android.Manifest.permission.SET_TIME_ZONE)
        fun setTimeZone(context: Context, timeZoneId: String) {
            thread {
                (context.getSystemService(Context.ALARM_SERVICE) as AlarmManager).setTimeZone(timeZoneId)
            }
            //DO not need send Intent.ACTION_TIMEZONE_CHANGED
            //Because system will send itself, and we do not have permission
        }

        /** 设置时区为上海 */
        @RequiresPermission(android.Manifest.permission.SET_TIME_ZONE)
        fun setAsChinaTimeZone(context: Context) {
            val chinaTimeZoneId = "Asia/Shanghai"
            if (getTimeZoneId() != chinaTimeZoneId) {
                setTimeZone(context, chinaTimeZoneId)
            }
        }

        /** 获取系统当前的时区 */
        fun getTimeZoneId(): String = TimeZone.getDefault().id

        /** 设置系统日期和时间,需要有系统签名才可以 */
        fun setDateAndTime(millis: Long) {
            thread {
                // 据说AlarmManager.setTime()检测权限之后也是调用SystemClock.setCurrentTimeMillis(millis)来设置时间的
                SystemClock.setCurrentTimeMillis(millis)
            }
        }
    }
}
class App : Application() {

    override fun onCreate() {
        super.onCreate()
        Timber.init(this, BuildConfig::class.java)
        if (!TimeUtil.isDateTimeAuto(this)) {
            TimeUtil.setDateTimeAuto(this, true)
        }
        if (!TimeUtil.isTimeZoneAuto(this)) {
            TimeUtil.setTimeZoneAuto(this, true)
        }
    }

}
import android.os.SystemClock
import android.text.format.DateFormat
import com.evendai.loglibrary.Timber
import okhttp3.Interceptor
import okhttp3.Response
import java.util.Calendar
import kotlin.math.abs

/**
 * 时间同步拦截器。主要功能为获取响应头中的时间,然后与本地时间对比,相差超过1分钟的则进行同步。
 */
class TimeSynchronizationInterceptor: Interceptor {

    private val oneMinute = 1000 * 60

    override fun intercept(chain: Interceptor.Chain): Response {
        val response = chain.proceed(chain.request())
        val webServerDate = response.headers.getDate("Date")
        val is1970 = Calendar.getInstance().get(Calendar.YEAR) == 1970
        // 如果当前时间为1970年或者当前时间和服务器时间相差大于1分钟则同步服务器时间
        if (webServerDate != null && (is1970 || abs(webServerDate.time - System.currentTimeMillis()) > oneMinute)) {
            Timber.fi("当前系统时间为:${DateFormat.format("yyyy-MM-dd HH:mm:ss", Calendar.getInstance())}")
            SystemClock.setCurrentTimeMillis(webServerDate.time)
            Timber.fi("更新系统时间为:${DateFormat.format("yyyy-MM-dd HH:mm:ss", webServerDate)}")
            Timber.fi("当前系统时间为:${DateFormat.format("yyyy-MM-dd HH:mm:ss", Calendar.getInstance())}")
        }
        return response
    }
}
class MainActivity : AppCompatActivity(), Handler.Callback {

    private val binding: ActivityMainBinding by lazy { ActivityMainBinding.inflate(layoutInflater) }
    private val handler: Handler = Handler(Looper.getMainLooper(), this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        binding.autoDateAndTimeCheckBox.setOnCheckedChangeListener { _, isChecked -> TimeUtil.setDateTimeAuto(this, isChecked) }
        binding.autoTimeZoneTimeCheckBox.setOnCheckedChangeListener { _, isChecked -> TimeUtil.setTimeZoneAuto(this, isChecked) }
        binding.use24HourCheckBox.setOnCheckedChangeListener { _, isChecked -> TimeUtil.set24HourFormat(this, isChecked) }
        binding.setDateButton.setOnClickListener { TimeUtil.setDate(this, 2008, 11, 31) }
        binding.setTimeButton.setOnClickListener { TimeUtil.setTime(this, 12, 30) }
        binding.setTimeZoneButton.setOnClickListener { TimeUtil.setTimeZone(this, "Asia/Seoul") }
        binding.setUseNetTimeButton.setOnClickListener {
            thread {
//                val url = "https://www.baidu.com"
                val url = "https://10.238.113.50"
                val okHttpClient = OkHttpClientBuilder.createOkHttpClientBuilder()
                    .addInterceptor(TimeSynchronizationInterceptor())
                    .callTimeout(1, TimeUnit.SECONDS)
                    .build()
                okHttpClient.newCall(Request.Builder().url(url).build()).enqueue(object: Callback {
                    override fun onFailure(call: Call, e: IOException) { }

                    override fun onResponse(call: Call, response: Response) {
                        Timber.fi("responseCode = ${response.code}")
                    }
                })
            }
        }
        Timber.fi("当前年份:${Calendar.getInstance().get(Calendar.YEAR)}")
    }

    override fun onResume() {
        super.onResume()
        handler.removeMessages(0)
        handler.sendEmptyMessage(0)
        binding.autoDateAndTimeCheckBox.isChecked = TimeUtil.isDateTimeAuto(this)
        binding.autoTimeZoneTimeCheckBox.isChecked = TimeUtil.isTimeZoneAuto(this)
        binding.use24HourCheckBox.isChecked = TimeUtil.is24HourFormat(this)
    }

    override fun onStop() {
        super.onStop()
        handler.removeMessages(0)
    }

    override fun handleMessage(msg: Message): Boolean {
        val timeFormat = if (TimeUtil.is24HourFormat(this)) "HH:mm:ss" else "hh:mm:ss a"
        val dateTimeFormat = "yyyy-MM-dd $timeFormat"
        val dateTime = DateFormat.format(dateTimeFormat, System.currentTimeMillis())
        binding.timeText.text = String.format("%s, %s", dateTime, TimeUtil.getTimeZoneId())
        handler.sendEmptyMessageDelayed(0, 1000)
        return true
    }
}

这里我用到了自己定义的日志库和OkHttp库,在使用时大家可以改成使用Android标准的日志和标准的OkHttp即可。

2023-10-24续:

  1. 今天我在另一台球机设备上运行代码,由于没有系统签名,调用各种获取状态的方法似乎都不会有问题(比如TimeUtil.isDateTimeAuto()),只有调用设置函数时才会出异常。
  2. 在调用TimeUtil.setDateTimeAuto()TimeUtil.setTimeZoneAuto()时都报异常提示需要android.permission.WRITE_SECURE_SETTINGS权限,把这个权限加上之后,虽然我没有系统签名,但是也可以正常调用这两个函数了,而且可以正常修改设置,而且这个权限也不需要动态申请。
  3. 在调用TimeUtil.set24HourFormat()时报缺少android.permission.WRITE_SETTINGS,跟WRITE_SECURE_SETTINGS权限不太一样,在清单文件上看它也是ProtectedPermissions类型的,但是我加上这个权限后还是会挂,提示如下:
SecurityException: cn.android666.settimedemo was not granted  this permission: android.permission.WRITE_SETTINGS.

查看此系统权限的官方文档声明如下:

WRITE_SETTINGS 允许应用程序读取或写入系统设置。注意:如果应用程序以 API 级别 23
或更高级别为目标,则应用程序用户必须通过权限管理屏幕明确向应用程序授予此权限。该应用程序通过发送 intent with action
Settings.ACTION_MANAGE_WRITE_SETTINGS来请求用户的批准 。应用可以通过调用
来检查自己是否有这个权限Settings.System.canWrite()。

按照文档声明去申请权限之后就可以正常修改了。

  1. 在调用SystemClock.setCurrentTimeMillis()mAlarmManager.setTime()来设置时间会报异常,但是程序不会崩,异常如下:
SecurityException: setTime: Neither user 10154 nor current process has android.permission.SET_TIME.

这就有点奇怪, 因为SET_TIME权限和WRITE_SECURE_SETTINGS我都声明有了,而且看类型他们是一个类型的,都是ProtectedPermissions,为什么一个声明了有用,一个没用,搞不懂。对应的TimeUtil.setTimeZone()需要SET_TIME_ZONE权限,和SET_TIME一样,声明了也不管用,一样会挂。

简单总结下:

android 设置window flag_android时间设置_02


总结一下,如上图,在没有系统签名的情况下,上图中4个按钮的功能都无法使用,程序会崩,3个复选框的功能则可以正常使用。