我想从设备中检索并显示SMS消息?
当前回答
谷歌Play服务有两个api,可用于简化基于短信的验证过程
短信检索器接口
提供完全自动化的用户体验,不需要用户手动输入验证码,也不需要任何额外的应用权限,应该在可能的情况下使用。但是,它要求您在消息体中放置自定义哈希代码,因此您还必须控制服务器端。
消息要求-唯一标识应用程序的11位哈希码 发件人要求—无 用户交互—无
在Android应用程序中请求短信验证
在服务器上进行短信验证
短信用户同意API
不需要自定义哈希码,但需要用户批准你的应用程序的请求,以访问包含验证码的消息。为了最大限度地减少向用户显示错误消息的机会,SMS用户同意将从用户的联系人列表中过滤出发件人的消息。
消息要求-至少包含一个数字的4-10位字母数字代码 发件人要求-发件人不能在用户的联系人列表中 用户交互-一键批准
短信用户同意API是谷歌播放服务的一部分。要使用它,你至少需要这些库的17.0.0版本:
implementation "com.google.android.gms:play-services-auth:17.0.0"
implementation "com.google.android.gms:play-services-auth-api-phone:17.1.0"
第一步:开始监听短信
SMS用户同意将监听包含一次性代码的传入SMS消息长达五分钟。它不会查看在启动之前发送的任何消息。如果知道发送一次性代码的电话号码,则可以指定senderPhoneNumber,如果不指定,则null将匹配任何号码。
smsRetriever.startSmsUserConsent(senderPhoneNumber /* or null */)
步骤2:请求同意阅读消息
一旦你的应用程序接收到包含一次性代码的消息,它就会被广播通知。在这一点上,你没有同意阅读消息-相反,你给了一个意图,你可以开始提示用户同意。在你的BroadcastReceiver内部,你使用extras中的Intent来显示提示。 当您启动该意图时,它将提示用户允许读取单个消息。他们将会看到与你的应用共享的全部文本。
val consentIntent = extras.getParcelable<Intent>(SmsRetriever.EXTRA_CONSENT_INTENT)
startActivityForResult(consentIntent, SMS_CONSENT_REQUEST)
第三步:解析一次性代码,完成短信验证
当用户点击“允许”-是时候真正阅读消息了!在onActivityResult内部,你可以从数据中获得短信的全文:
val message = data.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE)
然后解析SMS消息并将一次性代码传递到后端!
其他回答
String WHERE_CONDITION = unreadOnly ? SMS_READ_COLUMN + " = 0" : null;
改变了:
String WHERE_CONDITION = unreadOnly ? SMS_READ_COLUMN + " = 0 " : SMS_READ_COLUMN + " = 1 ";
用1个Python函数获取所有短信:
我写了一个python函数,从f-droid安装SMS导入/导出,然后使用adb通过USB将手机中的SMS文件导入到pc中。
"""Ensures sms messages can be read through adb."""
import hashlib
import os
import time
from typing import List
import requests # type: ignore[import]
from src.helper import create_and_write_file, load_dict_from_file
from src.sender.helper_sms import cmd_res, device_ready
def sms_ie_config_content(int_time: int, output_dir: str) -> List[str]:
"""Creates the sms_ie .xml config file content to schedule an export at the
time: int time.
int_time contains the number of minutes after midnight on which an
sms export is scheduled.
:param int_time: int:
:param output_dir: str:
:param int_time: int:
:param output_dir: str:
"""
content = [
"<?xml version='1.0' encoding='utf-8' standalone='yes' ?>",
"<map>",
' <boolean name="include_binary_data" value="true" />',
' <boolean name="mms" value="true" />',
' <string name="max_messages"></string>',
' <boolean name="schedule_export" value="true" />',
' <boolean name="sms" value="true" />',
' <boolean name="debugging" value="false" />',
' <boolean name="export_call_logs" value="true" />',
f' <int name="export_time" value="{int_time}" />',
' <boolean name="export_messages" value="true" />',
' <string name="export_dir">content://com.android.providers.'
+ "downloads.documents/tree/raw%3A%2Fstorage%2Femulated%2F0%2FDo"
+ f"wnload%2F{output_dir}</string>",
"</map>",
]
return content
def sha256(fname: str) -> str:
"""Computes the sha256 sum of a file.
:param fname:
"""
hash_sha256 = hashlib.sha256()
with open(fname, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_sha256.update(chunk)
return hash_sha256.hexdigest()
def get_file(url: str, filename: str, expected_hash: str) -> None:
"""Downloads a file and verifies its filecontent is as expected.
:param url: param filename:
:param expected_hash:
:param filename:
"""
if not sms_ie_apk_file_exists(filename, expected_hash):
response = requests.get(url)
with open(filename, "wb") as some_file:
some_file.write(response.content)
assert_sms_ie_apk_file_exists(filename, expected_hash)
def assert_sms_ie_apk_file_exists(filename: str, expected_hash: str) -> None:
"""Verifies a file exists and that its content is as expected. Throws error
if file does not exist or if the content is not expected.
:param filename: param expected_hash:
:param expected_hash:
"""
if not os.path.exists(filename):
raise Exception(f"Error, filename={filename} did not exist.")
if expected_hash is not None:
if sha256(filename) != expected_hash:
raise Exception(f"Download is corrupted, {sha256(filename)}.")
def sms_ie_apk_file_exists(filename: str, expected_hash: str) -> bool:
"""Returns True a file exists and that its content is as expected. Returns
False otherwise.
:param filename: param expected_hash:
:param expected_hash:
"""
if not os.path.exists(filename):
return False
if sha256(filename) != expected_hash:
return False
return True
def app_is_installed(package_name: str) -> bool:
"""Verifies an application is installed on the phone.
:param package_name:
"""
installation_response = cmd_res(
f"adb shell pm list packages {package_name}"
)
return installation_response == f"package:{package_name}\n"
def adb_install_apk(filename: str, package_name: str) -> None:
"""Installs an application and verifies it is installed.
:param filename: param package_name:
:param package_name:
"""
if not app_is_installed(package_name):
installation_response = cmd_res(f"adb install {filename}")
print(f"installation_response={installation_response}")
if not app_is_installed(package_name):
raise Exception(
f"Error, after installation, package:{package_name} "
+ "was not found."
)
def ensure_sms_ie_config_file_exists(xml_path: str) -> None:
"""Ensures the configuration for the SMS Import/Export applictation exists.
:param xml_path:
"""
# Ensure the app directory exists.
assert_dir_exists_on_phone("/data/data/com.github.tmo1.sms_ie/")
ensure_dir_exists_on_phone(
"/data/data/com.github.tmo1.sms_ie/shared_prefs/"
)
ensure_file_exists_on_phone(xml_path)
def create_sms_ie_output_folder(output_path: str) -> None:
"""Creates the output directory on the phone to which the sms messages will
be exported.
:param output_path: str:
:param output_path: str:
"""
ensure_dir_exists_on_phone(output_path)
def ensure_dir_exists_on_phone(path: str) -> None:
"""Creates a directory if it does not yet exist on the phone.
:param path:
"""
command = f"adb shell mkdir -p {path}"
cmd_res(command)
assert_dir_exists_on_phone(path)
def assert_dir_exists_on_phone(path: str) -> None:
"""Throws error if a directory does not yet exist on the phone.
:param path:
"""
command = f'adb shell [ -d {path} ] && echo "exists."'
output = cmd_res(command)
if output != "exists.\n":
raise Exception(f"Error, app dir:{path} is not found.")
def file_exists_on_phone(filepath: str) -> bool:
"""Returns True if a file exists on the phone. Returns False otherwise.
:param filepath:
"""
command = f'adb shell [ -f {filepath} ] && echo "exists."'
output = cmd_res(command)
if output != "exists.\n":
return False
return True
def remove_file_if_exists_on_phone(filepath: str) -> None:
"""Removes file from phone if it exists.
:param filepath:
"""
if file_exists_on_phone(filepath):
command = f"adb shell rm {filepath}"
cmd_res(command)
assert_file_does_not_exists_on_phone(filepath)
def assert_file_does_not_exists_on_phone(filepath: str) -> None:
"""Throws error if file exists on phone.
:param filepath:
"""
if file_exists_on_phone(filepath):
raise Exception("Error file:{filepath} still exists on phone.")
def assert_file_exists_on_phone(filepath: str) -> None:
"""Throws error if a file does not exist on the phone.
# TODO: verify this is not a duplicate function.
:param filepath:
"""
if not file_exists_on_phone(filepath):
raise Exception("Error file:{filepath} still exists on phone.")
def ensure_file_exists_on_phone(path: str) -> None:
"""Creates a file if it does not yet exist on the phone.
# TODO: verify this is not a duplicate function.
:param path:
"""
command = f"adb shell touch {path}"
cmd_res(command)
assert_file_exists_on_phone(path)
def copy_file_from_pc_to_phone(
local_filepath: str, phone_filepath: str
) -> None:
"""Copies a file from the pc to the phone.
:param local_filepath: param phone_filepath:
:param phone_filepath:
"""
# Overwrite file content
command = f"adb push {local_filepath} {phone_filepath}"
print(f"command={command}")
# TODO: verify mdf5 values are identical
cmd_res(command)
def copy_file_from_phone_to_pc(
phone_filepath: str, local_filepath: str
) -> None:
"""Copies a file from the phone to the pc.
:param phone_filepath: param local_filepath:
:param local_filepath:
"""
# Overwrite file content
command = f"adb pull {phone_filepath} {local_filepath}"
print(f"command={command}")
# TODO: verify mdf5 values are identical
cmd_res(command)
def verify_app_is_in_foreground(package_name: str) -> None:
"""Verify an app is opened and into the foreground.
:param package_name:
"""
command = (
"adb shell dumpsys activity recents | grep 'Recent #0' | cut -d="
+ " -f2 | sed 's| .*||' | cut -d '/' -f1"
)
output = cmd_res(command)
if output != f"{package_name}\n":
raise Exception(
"Error, app is not running in the foreground. Please try again."
)
def restart_application(package_name: str) -> None:
"""Restarts an application.
:param package_name: str:
:param package_name: str:
"""
command = f"adb shell am force-stop {package_name}"
cmd_res(command)
# command=f"adb shell monkey -p '{package_name}' 1"
# Works but does not export according to schedule.
command = (
f"adb shell am start -n {package_name}/{package_name}.MainActivity"
)
print(f"command={command}")
cmd_res(command)
time.sleep(2)
def screen_is_locked() -> bool:
"""Returns True if the screen is locked.
Returns False if the screen is unlocked.
"""
command = "adb shell dumpsys power | grep 'mHolding'"
output = cmd_res(command)
lines = output.split()
if (
lines[0] == "mHoldingWakeLockSuspendBlocker=true"
and lines[1] == "mHoldingDisplaySuspendBlocker=true"
):
return False
if (
lines[0] == "mHoldingWakeLockSuspendBlocker=true"
and lines[1] == "mHoldingDisplaySuspendBlocker=false"
):
return True
raise Exception(f"Unexpected output in check if screen is locked:{output}")
def clear_app_data(package_name) -> None:
"""Deletes application data.
:param package_name:
"""
command = f"adb shell pm clear {package_name}"
cmd_res(command)
def send_export_sms_inputs(
package_name, keystrokes: List[int], pause_time: int
) -> None:
"""Sends keystrokes to phone.
:param package_name: param keystrokes: List[int]:
:param pause_time: int:
:param keystrokes: List[int]:
:param pause_time: int:
"""
time.sleep(pause_time)
for keystroke in keystrokes:
verify_app_is_in_foreground(package_name)
command = f"adb shell input keyevent {keystroke}"
cmd_res(command)
time.sleep(pause_time)
def get_phone_date():
"""Gets current date from phone in format yyyy-mm-dd."""
command = "adb shell date +%F"
date = cmd_res(command)
return date.strip() # removes newlines
def get_phone_time(buffer_sec: int) -> tuple[int, int]:
"""Gets the time from the phone, and computes whether the time up to the
next whole minute is enough or whether the code should wait an additional
minute.
:param buffer_sec: int:
:param buffer_sec: int:
"""
# TODO: change to: date +%T for HH:MM:SS format without parsing
command = "adb shell date"
date_and_time = cmd_res(command)
time_elements = date_and_time.split(" ")
# Extract the 18:19:20 time from the date and time string.
for element in time_elements:
if ":" in element:
time_str = element
# Split 18:19:20 into hrs, mins and seconds
if time_str is None:
raise Exception("Phone time not found")
[hrs, mins, secs] = list(map(int, time_str.split(":")))
print(f"{hrs}:{mins}:{secs}")
wait_time = 60 - secs
# Ensure a buffer in when to export, is taken into account.
if secs + buffer_sec > 60:
mins = mins + 1
wait_time = wait_time + 60
# Expected time:
return (
hrs * 60 + mins + 1,
wait_time,
) # Plus 1 because the export should happen at the next minute.
def get_sms_messages_from_phone(sms_ie_dict) -> dict:
"""Gets sms messages from phone and stores them locally in a .json file.
:param sms_ie_dict:
"""
# Get the sms_ie .apk file from the release page.
url = (
"https://github.com/tmo1/sms-ie/releases/download/v1.4.1/com.github"
+ ".tmo1.sms_ie-v1.4.1.apk"
)
filename = "sms_ie.apk"
expected_hash = (
"185afc567ea5fce2df4045925687a79a717bd31185cd944a4c80c51e64ce77ec"
)
get_file(url, filename, expected_hash=expected_hash)
# Connect to phone through adb
# check if device connected
if device_ready():
# Install sms_ie.apk file.
adb_install_apk(filename, sms_ie_dict["package_name"])
clear_app_data(sms_ie_dict["package_name"])
# Verify sms_ie config file is found and exists.
ensure_sms_ie_config_file_exists(sms_ie_dict["phone_xml_path"])
# Specify output directory on android device.
# Verify output directory exists on android device.
create_sms_ie_output_folder(sms_ie_dict["output_dirpath"])
# Delete sms_ie output file if it exists on phone.
output_filepath = (
f'{sms_ie_dict["output_dirpath"]}messages-{get_phone_date()}.json'
)
print(f"output_filepath={output_filepath}")
remove_file_if_exists_on_phone(output_filepath)
# Get the time on the phone and add buffer of 10 seconds.
export_time, wait_time = get_phone_time(5)
print(f"export_time={export_time}")
print(f"wait_time={wait_time}")
# Verify config file contains schedule, and if not overwrite config.
config_content = sms_ie_config_content(
export_time, sms_ie_dict["output_dir"]
)
# Create local copy of file content:
create_and_write_file(sms_ie_dict["local_xml_path"], config_content)
# Push config file to device.
copy_file_from_pc_to_phone(
sms_ie_dict["local_xml_path"], sms_ie_dict["phone_xml_path"]
)
if not screen_is_locked():
clear_app_data(sms_ie_dict["package_name"])
# Restart sms_ie application
restart_application(sms_ie_dict["package_name"])
# enter, enter
time.sleep(2)
print("pressing enter 4 times, to grant permissions")
send_export_sms_inputs(
sms_ie_dict["package_name"], [23, 23, 23, 23], 1
)
print("pressing enter,tab, enter to export sms output")
# enter, tab, enter
send_export_sms_inputs(
sms_ie_dict["package_name"], [23, 61, 23], 1
)
print("Waiting 15 seconds for the export of data to be completed.")
time.sleep(15)
# Wait for wait time+file creation duration buffer
print(
f"Waiting for:{wait_time} seconds until sms data export file"
+ " is created."
)
time.sleep(wait_time)
# Verify output file exists
assert_file_exists_on_phone(output_filepath)
# Copy file from phone to local storage
copy_file_from_phone_to_pc(
output_filepath, sms_ie_dict["local_sms_filepath"]
)
# Verify the sms messages file exists locally.
if not os.path.exists(sms_ie_dict["local_sms_filepath"]):
raise Exception(
f"Error, filename={sms_ie_dict['local_sms_filepath']} did"
+ " not exist."
)
else:
raise Exception("Error, please unlock screen and try again.")
else:
raise Exception("Please connect phone and enable adb, and try again.")
# Load sms_ie output file content.
sms_messages = load_dict_from_file(sms_ie_dict["local_sms_filepath"])
return sms_messages
它向应用程序发送击键来进行手动导出。
它被称为:
output_dir = "sms_ie_output"
sms_ie_dict = {
"private_dir": "private_data",
# ensure_private_data_templates_exist(private_dir)
"output_dir": output_dir,
"output_dirpath": f"/sdcard/Download/{output_dir}/",
"phone_xml_path": "/data/data/com.github.tmo1.sms_ie/shared_prefs/com."
+ "github.tmo1.sms_ie_preferences.xml",
"local_xml_path": "installation/com.github.tmo1.sms_ie_preferences.xml",
"package_name": "com.github.tmo1.sms_ie",
"local_sms_messages_dir": "private_data/",
"local_sms_filepath": "private_data/messages.json",
}
sms_messages = get_sms_messages_from_phone(sms_ie_dict)
改进空间
我最初修改了配置,将计划设置为在下一分钟导出,然后等待那一分钟,然而什么都没有发生。因此,我发送手动击键来导出sms消息。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
final String myPackageName = getPackageName();
if (!Telephony.Sms.getDefaultSmsPackage(this).equals(myPackageName)) {
Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT);
intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, myPackageName);
startActivityForResult(intent, 1);
}else {
List<Sms> lst = getAllSms();
}
}else {
List<Sms> lst = getAllSms();
}
将app设置为默认短信app
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 1) {
if (resultCode == RESULT_OK) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
final String myPackageName = getPackageName();
if (Telephony.Sms.getDefaultSmsPackage(mActivity).equals(myPackageName)) {
List<Sms> lst = getAllSms();
}
}
}
}
}
获取短信的功能
public List<Sms> getAllSms() {
List<Sms> lstSms = new ArrayList<Sms>();
Sms objSms = new Sms();
Uri message = Uri.parse("content://sms/");
ContentResolver cr = mActivity.getContentResolver();
Cursor c = cr.query(message, null, null, null, null);
mActivity.startManagingCursor(c);
int totalSMS = c.getCount();
if (c.moveToFirst()) {
for (int i = 0; i < totalSMS; i++) {
objSms = new Sms();
objSms.setId(c.getString(c.getColumnIndexOrThrow("_id")));
objSms.setAddress(c.getString(c
.getColumnIndexOrThrow("address")));
objSms.setMsg(c.getString(c.getColumnIndexOrThrow("body")));
objSms.setReadState(c.getString(c.getColumnIndex("read")));
objSms.setTime(c.getString(c.getColumnIndexOrThrow("date")));
if (c.getString(c.getColumnIndexOrThrow("type")).contains("1")) {
objSms.setFolderName("inbox");
} else {
objSms.setFolderName("sent");
}
lstSms.add(objSms);
c.moveToNext();
}
}
// else {
// throw new RuntimeException("You have no SMS");
// }
c.close();
return lstSms;
}
短信类如下:
public class Sms{
private String _id;
private String _address;
private String _msg;
private String _readState; //"0" for have not read sms and "1" for have read sms
private String _time;
private String _folderName;
public String getId(){
return _id;
}
public String getAddress(){
return _address;
}
public String getMsg(){
return _msg;
}
public String getReadState(){
return _readState;
}
public String getTime(){
return _time;
}
public String getFolderName(){
return _folderName;
}
public void setId(String id){
_id = id;
}
public void setAddress(String address){
_address = address;
}
public void setMsg(String msg){
_msg = msg;
}
public void setReadState(String readState){
_readState = readState;
}
public void setTime(String time){
_time = time;
}
public void setFolderName(String folderName){
_folderName = folderName;
}
}
不要忘记在AndroidManifest.xml中定义权限
<uses-permission android:name="android.permission.READ_SMS" />
谷歌Play服务有两个api,可用于简化基于短信的验证过程
短信检索器接口
提供完全自动化的用户体验,不需要用户手动输入验证码,也不需要任何额外的应用权限,应该在可能的情况下使用。但是,它要求您在消息体中放置自定义哈希代码,因此您还必须控制服务器端。
消息要求-唯一标识应用程序的11位哈希码 发件人要求—无 用户交互—无
在Android应用程序中请求短信验证
在服务器上进行短信验证
短信用户同意API
不需要自定义哈希码,但需要用户批准你的应用程序的请求,以访问包含验证码的消息。为了最大限度地减少向用户显示错误消息的机会,SMS用户同意将从用户的联系人列表中过滤出发件人的消息。
消息要求-至少包含一个数字的4-10位字母数字代码 发件人要求-发件人不能在用户的联系人列表中 用户交互-一键批准
短信用户同意API是谷歌播放服务的一部分。要使用它,你至少需要这些库的17.0.0版本:
implementation "com.google.android.gms:play-services-auth:17.0.0"
implementation "com.google.android.gms:play-services-auth-api-phone:17.1.0"
第一步:开始监听短信
SMS用户同意将监听包含一次性代码的传入SMS消息长达五分钟。它不会查看在启动之前发送的任何消息。如果知道发送一次性代码的电话号码,则可以指定senderPhoneNumber,如果不指定,则null将匹配任何号码。
smsRetriever.startSmsUserConsent(senderPhoneNumber /* or null */)
步骤2:请求同意阅读消息
一旦你的应用程序接收到包含一次性代码的消息,它就会被广播通知。在这一点上,你没有同意阅读消息-相反,你给了一个意图,你可以开始提示用户同意。在你的BroadcastReceiver内部,你使用extras中的Intent来显示提示。 当您启动该意图时,它将提示用户允许读取单个消息。他们将会看到与你的应用共享的全部文本。
val consentIntent = extras.getParcelable<Intent>(SmsRetriever.EXTRA_CONSENT_INTENT)
startActivityForResult(consentIntent, SMS_CONSENT_REQUEST)
第三步:解析一次性代码,完成短信验证
当用户点击“允许”-是时候真正阅读消息了!在onActivityResult内部,你可以从数据中获得短信的全文:
val message = data.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE)
然后解析SMS消息并将一次性代码传递到后端!
第一步:首先我们必须在清单文件中添加权限 就像
<uses-permission android:name="android.permission.RECEIVE_SMS" android:protectionLevel="signature" />
<uses-permission android:name="android.permission.READ_SMS" />
步骤2:添加服务短信接收类,用于接收短信
<receiver android:name="com.aquadeals.seller.services.SmsReceiver">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
步骤3:添加运行时权限
private boolean checkAndRequestPermissions()
{
int sms = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS);
if (sms != PackageManager.PERMISSION_GRANTED)
{
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_SMS}, REQUEST_ID_MULTIPLE_PERMISSIONS);
return false;
}
return true;
}
第四步:在应用程序中添加这些类并进行测试 接口类
public interface SmsListener {
public void messageReceived(String messageText);
}
SmsReceiver.java
public class SmsReceiver extends BroadcastReceiver {
private static SmsListener mListener;
public Pattern p = Pattern.compile("(|^)\\d{6}");
@Override
public void onReceive(Context context, Intent intent) {
Bundle data = intent.getExtras();
Object[] pdus = (Object[]) data.get("pdus");
for(int i=0;i<pdus.length;i++)
{
SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) pdus[i]);
String sender = smsMessage.getDisplayOriginatingAddress();
String phoneNumber = smsMessage.getDisplayOriginatingAddress();
String senderNum = phoneNumber ;
String messageBody = smsMessage.getMessageBody();
try{
if(messageBody!=null){
Matcher m = p.matcher(messageBody);
if(m.find()) {
mListener.messageReceived(m.group(0));
}
}
}
catch(Exception e){}
}
}
public static void bindListener(SmsListener listener) {
mListener = listener;
}
}
推荐文章
- 如何隐藏动作栏之前的活动被创建,然后再显示它?
- 是否有一种方法以编程方式滚动滚动视图到特定的编辑文本?
- 在Android中将字符串转换为Uri
- 如何在NestedScrollView内使用RecyclerView ?
- 移动到另一个EditText时,软键盘下一步点击Android
- Android应用中的GridView VS GridLayout
- Activity和FragmentActivity的区别
- 右对齐文本在android TextView
- 权限拒绝:start前台需要android.permission.FOREGROUND_SERVICE
- 如何更改android操作栏的标题和图标
- Android Split字符串
- 让一个链接在安卓浏览器启动我的应用程序?
- 如何在Android工作室的外部库中添加一个jar ?
- GridLayout(不是GridView)如何均匀地拉伸所有子元素
- 如何让一个片段删除自己,即它的等效完成()?