当前位置: 首页 > news >正文

Android项目实现自动获取手机号一键登录功能

功能概述

在 Android 应用中实现自动获取本机手机号进行一键登录,同时支持手动登录和历史账号选择功能。这个功能大大提升了用户体验,减少了用户输入成本。

1. 权限配置

首先在 AndroidManifest.xml 中添加必要的权限:


<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/><!-- 读取手机状态权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
<uses-permission android:name="android.permission.READ_SMS"/><!-- 声明电话功能为可选,解决Chrome OS兼容性问题 -->
<uses-feature android:name="android.hardware.telephony" android:required="false"/>

2. 登录处理

主要功能包括通过系统权限获取本机手机号进行一键登录、支持手动输入账号密码登录、显示历史登录账号列表供用户快速选择。系统首先会动态申请读取手机状态的权限来获取本机号码,然后提供三种登录方式:一键登录使用获取到的本机号码自动登录,手动登录允许用户输入其他手机号和密码,历史账号功能展示之前登录过的账号供用户选择。无论采用哪种登录方式,系统都会在本地数据库和远程服务器之间同步用户信息,确保数据一致性,并在登录成功后跳转到主界面。

点击查看代码
package com.example.ui.screensimport android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.telephony.TelephonyManager
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.example.MainActivity
import com.example.R
import com.example.model.User
import com.example.network.RetrofitClient
import com.example.ui.theme.MentalTheme
import com.example.util.DatabaseHelper
import coil.compose.AsyncImage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody
import timber.log.Timber// 登录活动类,负责处理用户登录相关的所有功能
class LoginActivity : ComponentActivity() {// 权限请求码和必要的变量声明private val READ_PHONE_PERMISSION = 1001private lateinit var dbHelper: DatabaseHelperprivate var devicePhoneNumber: String? = nullprivate var allUserPhones: List<String> = emptyList()private val apiService = RetrofitClient.apiService// 活动创建时的初始化工作override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)dbHelper = DatabaseHelper(this)// 从数据库获取所有历史登录用户的手机号allUserPhones = dbHelper.getAllUserPhones()// 检查是否有从其他界面传递过来的预填充手机号val prefilledPhone = intent.getStringExtra("PREFILLED_PHONE") ?: ""val isManualLogin = intent.getBooleanExtra("IS_MANUAL_LOGIN", false)// 检查并请求获取手机号的权限checkPhonePermission()// 设置Compose UI界面setContent {MentalTheme {LoginScreen(devicePhoneNumber = devicePhoneNumber,allUserPhones = allUserPhones,prefilledPhone = prefilledPhone,isManualLogin = isManualLogin,onOneKeyLogin = { phoneNumber ->handleOneKeyLogin(phoneNumber)},onManualLogin = { phone, password ->handleManualLogin(phone, password)},onHistoryPhoneSelected = { phone ->// 当用户选择历史账号时,重新启动登录界面并预填充选中的手机号startActivity(Intent(this, LoginActivity::class.java).apply {putExtra("PREFILLED_PHONE", phone)putExtra("IS_MANUAL_LOGIN", true)})finish()})}}}// 检查获取手机号所需的权限private fun checkPhonePermission() {// 定义需要的权限数组val permissions = arrayOf(android.Manifest.permission.READ_PHONE_STATE,android.Manifest.permission.READ_PHONE_NUMBERS,android.Manifest.permission.READ_SMS)// 筛选出尚未授予的权限val missingPermissions = permissions.filter {ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED}// 如果有未授权的权限,则请求授权if (missingPermissions.isNotEmpty()) {ActivityCompat.requestPermissions(this,missingPermissions.toTypedArray(),READ_PHONE_PERMISSION)} else {// 所有权限都已授予,直接获取手机号getDevicePhoneNumber()}}// 获取设备手机号码private fun getDevicePhoneNumber() {try {val telephonyManager = getSystemService(TELEPHONY_SERVICE) as TelephonyManagerif (ActivityCompat.checkSelfPermission(this,android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {// 获取本机号码devicePhoneNumber = telephonyManager.line1Number// 处理国家代码,移除中国的+86前缀if (!devicePhoneNumber.isNullOrEmpty() && devicePhoneNumber!!.startsWith("+86")) {devicePhoneNumber = devicePhoneNumber!!.substring(3)}// 对手机号进行脱敏处理(实际函数中直接返回原始号码)devicePhoneNumber = maskPhoneNumber(devicePhoneNumber!!)}} catch (e: Exception) {Timber.e(e, "获取本机号码失败")}}// 处理权限请求结果override fun onRequestPermissionsResult(requestCode: Int,permissions: Array<out String>,grantResults: IntArray) {super.onRequestPermissionsResult(requestCode, permissions, grantResults)if (requestCode == READ_PHONE_PERMISSION) {// 检查是否有任何一个权限被授予val hasAnyPermission = grantResults.any { it == PackageManager.PERMISSION_GRANTED }if (hasAnyPermission) {// 至少有一个权限被授予,尝试获取手机号getDevicePhoneNumber()} else {// 所有权限都被拒绝,提示用户手动输入Toast.makeText(this, "无法获取本机号码,您可以手动输入或选择历史登录账号", Toast.LENGTH_SHORT).show()}}}// 处理一键登录逻辑private fun handleOneKeyLogin(phoneNumber: String) {// 首先根据手机号检查用户是否在本地数据库中存在val existingUser = dbHelper.getUserByPhone(phoneNumber)if (existingUser != null) {// 用户已存在,更新登录状态val updatedUser = existingUser.copy(isLogin = true)dbHelper.addOrUpdateUser(updatedUser)// 异步调用API更新服务器上的用户信息GlobalScope.launch(Dispatchers.IO) {try {// 准备用户信息JSON字符串val userJson = """{"username": "${updatedUser.username}","password": "${updatedUser.password}","phone": "${updatedUser.phone}","email": ${if (updatedUser.email != null) "\"${updatedUser.email}\"" else "null"},"nickname": ${if (updatedUser.nickname != null) "\"${updatedUser.nickname}\"" else "null"},"gender": "${updatedUser.gender}","age": ${if (updatedUser.age != null) updatedUser.age else "null"}}"""val userMediaType = "application/json".toMediaTypeOrNull()val userRequestBody = userMediaType?.let {RequestBody.create(it, userJson)} ?: throw IllegalStateException("Invalid media type")// 调用API更新用户信息val apiResponse = apiService.updateUser(phone = phoneNumber,user = userRequestBody)// 从响应中获取用户数据val responseUser = apiResponse.data ?: updatedUser// 处理头像URL,将localhost替换为实际IP地址val processedAvatarUrl = responseUser.avatarUrl?.replace("http://localhost:8080", "http://192.168.94.109:8080")// 更新本地用户信息val finalUser = responseUser.copy(isLogin = true,avatarUrl = processedAvatarUrl)dbHelper.addOrUpdateUser(finalUser)} catch (e: Exception) {Timber.e(e, "更新用户API调用失败")}}// 跳转到主界面navigateToMain()} else {// 用户不存在,创建新用户val maskedPhone = maskPhoneNumber(phoneNumber)val newUser = User(username = "用户" + (10000..99999).random(), // 生成随机用户名phone = phoneNumber,password = "", // 一键登录可以不设置密码isLogin = true)// 先保存到本地数据库dbHelper.addOrUpdateUser(newUser)// 然后异步调用API保存到远端服务器GlobalScope.launch(Dispatchers.IO) {try {// 准备用户信息JSON字符串val userJson = """{"username": "${newUser.username}","password": "${newUser.password}","phone": "${newUser.phone}","email": null,"nickname": null,"gender": "${newUser.gender}","age": null}""".trimIndent()val userMediaType = "application/json".toMediaTypeOrNull()val userRequestBody = userMediaType?.let {RequestBody.create(it, userJson)} ?: throw IllegalStateException("Invalid media type")// 调用API创建用户val apiResponse = apiService.createUser(user = userRequestBody)// 从响应中获取用户数据val responseUser = apiResponse.data ?: newUser// 更新本地用户信息val updatedLocalUser = responseUser.copy(isLogin = true)dbHelper.addOrUpdateUser(updatedLocalUser)} catch (e: Exception) {Timber.e(e, "创建用户API调用失败")}}navigateToMain()}}// 处理手动登录逻辑private fun handleManualLogin(phone: String, password: String) {// 检查本地数据库中是否存在匹配的用户val user = dbHelper.checkUser(phone, password)if (user != null) {// 用户存在,更新登录状态val updatedUser = user.copy(isLogin = true)dbHelper.addOrUpdateUser(updatedUser)// 异步调用API更新服务器上的用户信息GlobalScope.launch(Dispatchers.IO) {try {// 准备用户信息JSON字符串val userJson = """{"username": "${updatedUser.username}","password": "${updatedUser.password}","phone": "${updatedUser.phone}","email": ${if (updatedUser.email != null) "\"${updatedUser.email}\"" else "null"},"nickname": ${if (updatedUser.nickname != null) "\"${updatedUser.nickname}\"" else "null"},"gender": "${updatedUser.gender}","age": ${if (updatedUser.age != null) updatedUser.age else "null"}}""".trimIndent()val userMediaType = "application/json".toMediaTypeOrNull()val userRequestBody = userMediaType?.let {RequestBody.create(it, userJson)} ?: throw IllegalStateException("Invalid media type")// 调用API更新用户val apiResponse = apiService.updateUser(phone = phone,user = userRequestBody)// 从响应中获取用户数据val responseUser = apiResponse.data ?: updatedUser// 更新本地用户信息val finalUser = responseUser.copy(isLogin = true)dbHelper.addOrUpdateUser(finalUser)} catch (e: Exception) {Timber.e(e, "更新用户API调用失败")}}navigateToMain()} else {// 用户不存在,创建新用户val newUser = User(username = "用户" + (10000..99999).random(),phone = phone,password = password,isLogin = true)// 先保存到本地数据库dbHelper.addOrUpdateUser(newUser)// 然后异步调用API保存到远端服务器GlobalScope.launch(Dispatchers.IO) {try {// 准备用户信息JSON字符串val userJson = """{"username": "${newUser.username}","password": "${newUser.password}","phone": "${newUser.phone}","email": null,"nickname": null,"gender": "${newUser.gender}","age": null}""".trimIndent()val userMediaType = "application/json".toMediaTypeOrNull()val userRequestBody = userMediaType?.let {RequestBody.create(it, userJson)} ?: throw IllegalStateException("Invalid media type")// 调用API创建用户val apiResponse = apiService.createUser(user = userRequestBody)// 从响应中获取用户数据val responseUser = apiResponse.data ?: newUser// 更新本地用户信息val updatedLocalUser = responseUser.copy(isLogin = true)dbHelper.addOrUpdateUser(updatedLocalUser)} catch (e: Exception) {Timber.e(e, "创建用户API调用失败")}}navigateToMain()}}// 跳转到主界面private fun navigateToMain() {startActivity(Intent(this, MainActivity::class.java))finish()}
}// 手机号脱敏函数,当前实现直接返回原始手机号
private fun maskPhoneNumber(phoneNumber: String): String {return phoneNumber
}// Compose登录界面组件
@Composable
fun LoginScreen(devicePhoneNumber: String?, // 设备手机号allUserPhones: List<String>, // 所有历史登录手机号prefilledPhone: String = "", // 预填充的手机号isManualLogin: Boolean = false, // 是否手动登录模式onOneKeyLogin: (String) -> Unit, // 一键登录回调onManualLogin: (String, String) -> Unit, // 手动登录回调onHistoryPhoneSelected: (String) -> Unit // 历史账号选择回调
) {// 定义各种状态变量var phoneNumber by remember { mutableStateOf(prefilledPhone) }var password by remember { mutableStateOf("") }var isAgreed by remember { mutableStateOf(false) }var showManualLogin by remember { mutableStateOf(isManualLogin) }var showHistoryPhones by remember { mutableStateOf(false) }val context = LocalContext.currentvar showAgreementDialog by remember { mutableStateOf(false) } // 协议确认对话框显示状态// 显示的设备手机号,如果没有则为空字符串val displayedPhoneNumber = devicePhoneNumber ?: ""// 界面布局Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF0F4FF)).padding(24.dp)) {Column(modifier = Modifier.fillMaxSize(),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {// 应用图标Image(painter = painterResource(id = R.drawable.img),contentDescription = "应用图标",modifier = Modifier.size(100.dp).background(Color(0xFF5A67D8), RoundedCornerShape(20.dp)).padding(20.dp))Spacer(modifier = Modifier.height(40.dp))// 如果设备手机号不为空且不是手动登录模式,显示一键登录界面if (displayedPhoneNumber.isNotEmpty() && !isManualLogin) {Text(text = displayedPhoneNumber,fontSize = 20.sp,fontWeight = FontWeight.Bold,color = Color(0xFF333333))Spacer(modifier = Modifier.height(24.dp))// 本机号码一键登录按钮Button(onClick = {if (isAgreed) {onOneKeyLogin(displayedPhoneNumber)} else {// 未同意协议,显示提示对话框showAgreementDialog = true}},modifier = Modifier.fillMaxWidth().height(56.dp),shape = RoundedCornerShape(28.dp),enabled = true // 按钮始终可用) {Text(text = "本机号码一键登录",fontSize = 16.sp,fontWeight = FontWeight.Bold)}}// 手动登录表单(当设备手机号为空或用户选择手动登录时显示)if (displayedPhoneNumber.isEmpty() || showManualLogin) {Spacer(modifier = Modifier.height(8.dp))// 手机号输入框OutlinedTextField(value = phoneNumber,onValueChange = { phoneNumber = it },label = { Text("手机号") },modifier = Modifier.fillMaxWidth(),shape = RoundedCornerShape(12.dp))Spacer(modifier = Modifier.height(16.dp))// 密码输入框OutlinedTextField(value = password,onValueChange = { password = it },label = { Text("密码") },modifier = Modifier.fillMaxWidth(),visualTransformation = PasswordVisualTransformation(),shape = RoundedCornerShape(12.dp))Spacer(modifier = Modifier.height(24.dp))// 登录按钮Button(onClick = {if (isAgreed) {onManualLogin(phoneNumber, password)} else {// 未同意协议,显示提示对话框showAgreementDialog = true}},modifier = Modifier.fillMaxWidth().height(56.dp),shape = RoundedCornerShape(28.dp),enabled = phoneNumber.isNotEmpty() && password.isNotEmpty() // 手机号和密码不为空时启用) {Text(text = "登录",fontSize = 16.sp,fontWeight = FontWeight.Bold)}}Spacer(modifier = Modifier.height(16.dp))// 切换登录方式(仅在设备手机号不为空时显示)if (displayedPhoneNumber.isNotEmpty()) {Text(text = if (showManualLogin) "使用本机号码登录" else "其他手机号码登录",fontSize = 14.sp,color = Color(0xFF5A67D8),modifier = Modifier.clickable {showManualLogin = !showManualLogin})}// 显示历史登录手机号选项if (allUserPhones.isNotEmpty() && !showManualLogin && displayedPhoneNumber.isNotEmpty()) {Text(text = if (showHistoryPhones) "隐藏历史账号" else "选择历史账号",fontSize = 14.sp,color = Color(0xFF5A67D8),modifier = Modifier.clickable {showHistoryPhones = !showHistoryPhones})// 显示历史手机号列表if (showHistoryPhones) {Spacer(modifier = Modifier.height(8.dp))// 过滤掉当前显示的设备号码val filteredPhones = allUserPhones.filter { it != displayedPhoneNumber }if (filteredPhones.isNotEmpty()) {Column {filteredPhones.forEachIndexed { index, phone ->val maskedPhone = maskPhoneNumber(phone)val dbHelper = remember { DatabaseHelper(context) }val user = remember { dbHelper.getUserByPhone(phone) }// 历史账号项Row(modifier = Modifier.fillMaxWidth().clickable {onHistoryPhoneSelected(phone)}.padding(8.dp),verticalAlignment = Alignment.CenterVertically) {// 显示用户头像if (user?.avatarUrl != null && user.avatarUrl.isNotEmpty()) {AsyncImage(model = user.avatarUrl,contentDescription = "用户头像",modifier = Modifier.size(32.dp).padding(4.dp),placeholder = painterResource(id = R.drawable.img),error = painterResource(id = R.drawable.img))} else {Image(painter = painterResource(id = R.drawable.img),contentDescription = "用户头像",modifier = Modifier.size(32.dp).padding(4.dp))}Spacer(modifier = Modifier.width(8.dp))Text(text = maskedPhone,fontSize = 14.sp,color = Color(0xFF333333))}// 最后一个不显示分割线if (index < filteredPhones.size - 1) {Spacer(modifier = Modifier.height(4.dp))Box(modifier = Modifier.fillMaxWidth().height(1.dp).background(Color(0xFFEEEEEE)))Spacer(modifier = Modifier.height(4.dp))}}}} else {Text(text = "暂无其他历史账号",fontSize = 14.sp,color = Color(0xFF999999),modifier = Modifier.padding(8.dp))}}}Spacer(modifier = Modifier.weight(1f))// 协议同意复选框Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth()) {Checkbox(checked = isAgreed,onCheckedChange = { isAgreed = it })Text(text = "同意《中国移动认证服务条款》和《用户协议》和《隐私政策》",fontSize = 12.sp,color = Color(0xFF666666))}// 协议确认对话框if (showAgreementDialog) {androidx.compose.material3.AlertDialog(onDismissRequest = { showAgreementDialog = false },title = { Text("提示") },text = { Text("请先同意《中国移动认证服务条款》和《用户协议》和《隐私政策》") },confirmButton = {Button(onClick = { showAgreementDialog = false }) {Text("确定")}})}}}
}
http://www.rkmt.cn/news/14956.html

相关文章:

  • Qt编程: 正则表达式分析 - 实践
  • Manim实现渐变填充特效
  • Spring Boot 集成 Redis 全方位详解 - 指南
  • 十月牛气冲天计数题没做
  • datadome 隐私模式 ck设置
  • CPU温度查看(Core Temp)
  • 深入解析:python学智能算法(三十九)|使用PyTorch模块的normal()函数绘制正态分布函数图
  • 2025污水处理设备厂家 TOP 企业品牌推荐排行榜,一体化,生活,工业,养殖,医疗,农村,学校,餐厨,隧洞,高速污水处理设备公司推荐!
  • 详细介绍:告别“下次注意”,用这套结构化事故复盘方案就对了
  • 关于树状数组的一些东西
  • [问题记录] vmagent 增加 aggregation 表达式后,CPU 上升 2.43 倍, 内存上升 3.82 倍
  • CF1081F Tricky Interactor
  • JAVA SE 基础语法 —— A / 初识 - 指南
  • 2025机械加工供货厂家权威口碑排行:实力与服务深度解析!
  • 2025七水硫酸锌厂家权威推荐榜:优质供应与专业定制首选
  • CustomKD论文阅读 - 实践
  • 2025 年水质测定仪厂家 TOP 企业品牌推荐排行榜,多参数,便携式,cod 快速,台式,污水,自来水,养殖,便携式总磷总氮,余氯总氯,废水水质测定仪公司推荐
  • AI+Decodo:构建智能电商价格监控系统的完整实战指南 - 实践
  • 2025公考培训机构权威推荐榜:实力师资与高效备考口碑之选
  • Mapper.xml中SQL语句的用法示例
  • MX-J24 题解(T1 - T4) - 指南
  • 2025球墨铸铁管厂家TOP企业品牌推荐排行榜,k9球墨铸铁管,c25球墨铸铁管,c30球墨铸铁管,c级国标离心球墨铸铁管,c级供水球墨铸铁管,dn900球墨铸铁管公司推荐!
  • 10/2
  • 使用 VictoriaLogs 存储和查询服务器日志
  • 详细介绍:Git 基础 - 查看提交历史
  • 2025年光亮剂源头厂家最新推荐榜单:聚焦实力厂商,为电镀企业精选高口碑品牌
  • 详细介绍:机器学习+数字孪生:从诊断到自主决策的跨越
  • vue3 知识点快速入门整理
  • 红色面纱
  • 创建 SQL Server 数据库