尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

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

Android项目实现自动获取手机号一键登录功能
📅 发布时间:2026/6/19 18:32:01

功能概述

在 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("确定")}})}}}
}

相关新闻

  • Qt编程: 正则表达式分析 - 实践
  • Manim实现渐变填充特效
  • Spring Boot 集成 Redis 全方位详解 - 指南

最新新闻

  • 武汉家具安装推荐良匠千艺2026口碑榜 - 我叫一
  • 2026昆山卫生间防水服务商适配指南:昆山鼎壹万机构解析及5家优质服务商推荐 专业瓷砖空鼓维修公司排名推荐(2026年5月瓷砖空鼓维修最新TOP权威排名) - 鼎壹万修缮说
  • 166、模组来料检验标准:外观、MTF 抽检、IRCF 透过率测试的 IQC 流程
  • 马鞍山GEO服务商代理加盟选型靠谱推荐?2026年马鞍山GEO代理服务商选型排名与合作路径解析 - 子柔传媒
  • 大连家电维修平台推荐:本地用户实测较好的几家服务商深度对比——2026年6月最新发布 - 一步到家
  • 3步解锁老旧Mac新生命:OpenCore Legacy Patcher终极升级指南

日新闻

  • 5分钟掌握Python进化算法:Geatpy高性能优化工具完全指南
  • Microchip 24AA044 EEPROM选型与应用全指南:从参数解析到实战编程
  • 华为的鸿蒙到底有多牛?为什么称作遥遥领先?

周新闻

  • 3步解锁iOS设备:applera1n激活锁绕过完全指南
  • 39 2026 人工智能证书终极盘点,普通人选 AI 证书可以从这些方向入手
  • Redis 暴露公网有多危险?从端口检查到补救步骤

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号