通过代码实现时间、时区的相关设置。在公司的一个android设备中,经常会出现时间不准,比如重启后时间变成1970年,只要设备连上网,会自动同步时间为正确的时间,但是这个同步有时候也没能同步成功,所以需要我们可以自行设置系统时间,或者同步我们自己服务器的时间,因为有些登录操作要求设备的时间和服务器的时间相差不能超过5分钟,一旦超过5分钟则不给登录。
界面如下:
布局代码:
<?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续:
-
今天我在另一台球机设备上运行代码,由于没有系统签名,调用各种获取状态的方法似乎都不会有问题(比如
TimeUtil.isDateTimeAuto()
),只有调用设置函数时才会出异常。 -
在调用
TimeUtil.setDateTimeAuto()
或TimeUtil.setTimeZoneAuto()
时都报异常提示需要android.permission.WRITE_SECURE_SETTINGS
权限,把这个权限加上之后,虽然我没有系统签名,但是也可以正常调用这两个函数了,而且可以正常修改设置,而且这个权限也不需要动态申请。 -
在调用
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()。按照文档声明去申请权限之后就可以正常修改了。
-
在调用
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
一样,声明了也不管用,一样会挂。
简单总结下:
总结一下,如上图,在没有系统签名的情况下,上图中4个按钮的功能都无法使用,程序会崩,3个复选框的功能则可以正常使用。