mb61ab4a9335a30 2022-01-15 03:39:45 阅读数:450
package?com.scott.provider;??
import?java.text.SimpleDateFormat;??
import?android.content.ContentResolver;??
import?android.database.Cursor;??
import?android.database.CursorWrapper;??
import?android.net.Uri;??
import?android.test.AndroidTestCase;??
import?android.util.Log;??
public?class?SMSTest?extends?AndroidTestCase?{??
????private?static?final?String?TAG?=?"SMSTest";??
????// conversation ??
????private?static?final?String?CONVERSATIONS?=?"content://sms/conversations/";??
????// Query contact ??
????private?static?final?String?CONTACTS_LOOKUP?=?"content://com.android.contacts/phone_lookup/";??
????// All SMS ??
????private?static?final?String?SMS_ALL???=?"content://sms/";??
????// inbox ??
//??private?static?final?String?SMS_INBOX?=?"content://sms/inbox";??
????// Has been sent ??
//??private?static?final?String?SMS_SENT??=?"content://sms/sent";??
????// Draft box ??
//??private?static?final?String?SMS_DRAFT?=?"content://sms/draft";??
????private?SimpleDateFormat?dateFormat?=?new?SimpleDateFormat("yyyy-MM-dd?HH:mm:ss");??
????/**?
?????*? Read session information ?
?????*/??
????public?void?testReadConversation()?{??
????????ContentResolver?resolver?=?getContext().getContentResolver();??
????????Uri?uri?=?Uri.parse(CONVERSATIONS);??
????????String[]?projection?=?new?String[]{"groups.group_thread_id?AS?group_id",?"groups.msg_count?AS?msg_count",??
????????????????????????"groups.group_date?AS?last_date",?"sms.body?AS?last_msg",?"sms.address?AS?contact"};??
????????Cursor?thinc?=?resolver.query(uri,?projection,?null,?null,?"groups.group_date?DESC");???// Query and reverse by date ??
????????Cursor?richc?=?new?CursorWrapper(thinc)?{???// Yes Cursor To deal with , Get the corresponding contact name after encountering the number ??
????????????public?String?getString(int?columnIndex)?{??
????????????????if(super.getColumnIndex("contact")?==?columnIndex){??
????????????????????String?contact?=?super.getString(columnIndex);??
????????????????????// Read contacts , Query the corresponding name ??
????????????????????Uri?uri?=?Uri.parse(CONTACTS_LOOKUP?+?contact);??
????????????????????Cursor?cursor?=?getContext().getContentResolver().query(uri,?null,?null,?null,?null);??
????????????????????if(cursor.moveToFirst()){??
????????????????????????String?contactName?=?cursor.getString(cursor.getColumnIndex("display_name"));??
????????????????????????return?contactName;??
????????????????????}??
????????????????????return?contact;??
????????????????}??
????????????????return?super.getString(columnIndex);??
????????????}??
????????};??
????????while?(richc.moveToNext())?{??
????????????String?groupId?=?"groupId:?"?+?richc.getInt(richc.getColumnIndex("group_id"));??
????????????String?msgCount?=?"msgCount:?"?+?richc.getLong(richc.getColumnIndex("msg_count"));??
????????????String?lastMsg?=?"lastMsg:?"?+?richc.getString(richc.getColumnIndex("last_msg"));??
????????????String?contact?=?"contact:?"?+?richc.getString(richc.getColumnIndex("contact"));??
????????????String?lastDate?=?"lastDate:?"?+?dateFormat.format(richc.getLong(richc.getColumnIndex("last_date")));??
????????????printLog(groupId,?contact,?msgCount,?lastMsg,?lastDate,?"---------------END---------------");??
????????}??
????????richc.close();??
????}??
????/**?
?????*? Read SMS ?
?????*/??
????public?void?testReadSMS()?{??
????????ContentResolver?resolver?=?getContext().getContentResolver();??
????????Uri?uri?=?Uri.parse(SMS_ALL);??
????????String[]?projection?=?{"thread_id?AS?group_id",?"address?AS?contact",?"body?AS?msg_content",?"date",?"type"};??
????????Cursor?c?=?resolver.query(uri,?projection,?null,?null,?"date?DESC");????// Query and reverse by date ??
????????while?(c.moveToNext())?{??
????????????String?groupId?=?"groupId:?"?+?c.getInt(c.getColumnIndex("group_id"));??
????????????String?contact?=?"contact:?"?+?c.getString(c.getColumnIndex("contact"));??
????????????String?msgContent?=?"msgContent:?"?+?c.getString(c.getColumnIndex("msg_content"));??
????????????String?date?=?"date:?"?+?dateFormat.format(c.getLong(c.getColumnIndex("date")));??
????????????String?type?=?"type:?"?+?getTypeById(c.getInt(c.getColumnIndex("type")));??
????????????printLog(groupId,?contact,?msgContent,?date,?type,?"---------------END---------------");??
????????}??
????????c.close();??
????}??
????private?String?getTypeById(int?typeId)?{??
????????switch?(typeId)?{??
????????case?1:?return?"receive";??
????????case?2:?return?"send";??
????????case?3:?return?"draft";??
????????default:?return?"none";??
????????}??
????}??
????private?void?printLog(String...strings)?{??
????????for?(String?s?:?strings)?{??
????????????Log.i(TAG,?s?==?null???"NULL"?:?s);??
????????}??
????}??
package com.scott.provider;
import java.text.SimpleDateFormat;
import android.content.ContentResolver;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.net.Uri;
import android.test.AndroidTestCase;
import android.util.Log;
public class SMSTest extends AndroidTestCase {
private static final String TAG = "SMSTest";
// conversation
private static final String CONVERSATIONS = "content://sms/conversations/";
// Query contact
private static final String CONTACTS_LOOKUP = "content://com.android.contacts/phone_lookup/";
// All SMS
private static final String SMS_ALL = "content://sms/";
// inbox
// private static final String SMS_INBOX = "content://sms/inbox";
// Has been sent
// private static final String SMS_SENT = "content://sms/sent";
// Draft box
// private static final String SMS_DRAFT = "content://sms/draft";
private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
*/
public void testReadConversation() {
ContentResolver resolver = getContext().getContentResolver();
Uri uri = Uri.parse(CONVERSATIONS);
String[] projection = new String[]{"groups.group_thread_id AS group_id", "groups.msg_count AS msg_count",
"groups.group_date AS last_date", "sms.body AS last_msg", "sms.address AS contact"};
Cursor thinc = resolver.query(uri, projection, null, null, "groups.group_date DESC"); // Query and reverse by date
Cursor richc = new CursorWrapper(thinc) { // Yes Cursor To deal with , Get the corresponding contact name after encountering the number
@Override
public String getString(int columnIndex) {
if(super.getColumnIndex("contact") == columnIndex){
String contact = super.getString(columnIndex);
// Read contacts , Query the corresponding name
Uri uri = Uri.parse(CONTACTS_LOOKUP + contact);
Cursor cursor = getContext().getContentResolver().query(uri, null, null, null, null);
if(cursor.moveToFirst()){
String contactName = cursor.getString(cursor.getColumnIndex("display_name"));
return contactName;
}
return contact;
}
return super.getString(columnIndex);
}
};
while (richc.moveToNext()) {
String groupId = "groupId: " + richc.getInt(richc.getColumnIndex("group_id"));
String msgCount = "msgCount: " + richc.getLong(richc.getColumnIn
《Android Summary of learning notes + Latest mobile architecture video + Big Android interview questions + Project actual combat source code handout 》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 Open source sharing of complete information
dex("msg_count"));
String lastMsg = "lastMsg: " + richc.getString(richc.getColumnIndex("last_msg"));
String contact = "contact: " + richc.getString(richc.getColumnIndex("contact"));
String lastDate = "lastDate: " + dateFormat.format(richc.getLong(richc.getColumnIndex("last_date")));
printLog(groupId, contact, msgCount, lastMsg, lastDate, "---------------END---------------");
}
richc.close();
}
/**
*/
public void testReadSMS() {
ContentResolver resolver = getContext().getContentResolver();
Uri uri = Uri.parse(SMS_ALL);
String[] projection = {"thread_id AS group_id", "address AS contact", "body AS msg_content", "date", "type"};
Cursor c = resolver.query(uri, projection, null, null, "date DESC"); // Query and reverse by date
while (c.moveToNext()) {
String groupId = "groupId: " + c.getInt(c.getColumnIndex("group_id"));
String contact = "contact: " + c.getString(c.getColumnIndex("contact"));
String msgContent = "msgContent: " + c.getString(c.getColumnIndex("msg_content"));
String date = "date: " + dateFormat.format(c.getLong(c.getColumnIndex("date")));
String type = "type: " + getTypeById(c.getInt(c.getColumnIndex("type")));
printLog(groupId, contact, msgContent, date, type, "---------------END---------------");
}
c.close();
}
private String getTypeById(int typeId) {
switch (typeId) {
case 1: return "receive";
case 2: return "send";
case 3: return "draft";
default: return "none";
}
}
private void printLog(String...strings) {
for (String s : strings) {
Log.i(TAG, s == null ? "NULL" : s);
}
}
}
Let's analyze it testReadConversation() Method , It is used to read all session information , according to “content://sms/conversations/” This URI Read session data , When you get the data , Further packaging of data , The specific method is to meet the number according to “content://com.android.contacts/phone_lookup/” Find the corresponding name in the contact , If present, the name is displayed instead of the number . We noticed that... Was used when querying sessions projection, What are these based on ? This requires us to take a look at the source code .
We find TelephonyProvider Medium com/android/providers/telephony/SmsProvider.java file , Let's see what :
[java] [view plain]( ) [copy]( ) [print]( ) [?]( )
@Override??
public?Cursor?query(Uri?url,?String[]?projectionIn,?String?selection,??
????????String[]?selectionArgs,?String?sort)?{??
????SQLiteQueryBuilder?qb?=?new?SQLiteQueryBuilder();??
????//?Generate?the?body?of?the?query.??
????int?match?=?sURLMatcher.match(url);??
????switch?(match)?{??
????...??
????case?SMS_CONVERSATIONS:??
????????qb.setTables("sms,?(SELECT?thread_id?AS?group_thread_id,?MAX(date)AS?group_date,"??
???????????????+?"COUNT(*)?AS?msg_count?FROM?sms?GROUP?BY?thread_id)?AS?groups");??
????????qb.appendWhere("sms.thread_id?=?groups.group_thread_id?AND?sms.date?="??
???????????????+?"groups.group_date");??
????????qb.setProjectionMap(sConversationProjectionMap);??
????????break;??
????????...??
????}??
????String?orderBy?=?null;??
????if?(!TextUtils.isEmpty(sort))?{??
????????orderBy?=?sort;??
????}?else?if?(qb.getTables().equals(TABLE_SMS))?{??
????????orderBy?=?Sms.DEFAULT_SORT_ORDER;??
????}??
????SQLiteDatabase?db?=?mOpenHelper.getReadableDatabase();??
????Cursor?ret?=?qb.query(db,?projectionIn,?selection,?selectionArgs,??
??????????????????????????null,?null,?orderBy);??
????//?TODO:?Since?the?URLs?are?a?mess,?always?use?content://sms??
????ret.setNotificationUri(getContext().getContentResolver(),??
????????????NOTIFICATION_URI);??
????return?ret;??
@Override
public Cursor query(Uri url, String[] projectionIn, String selection,
String[] selectionArgs, String sort) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
// Generate the body of the query.
int match = sURLMatcher.match(url);
switch (match) {
...
case SMS_CONVERSATIONS:
qb.setTables("sms, (SELECT thread_id AS group_thread_id, MAX(date)AS group_date,"
qb.appendWhere("sms.thread_id = groups.group_thread_id AND sms.date ="
qb.setProjectionMap(sConversationProjectionMap);
break;
...
}
String orderBy = null;
if (!TextUtils.isEmpty(sort)) {
orderBy = sort;
} else if (qb.getTables().equals(TABLE_SMS)) {
orderBy = Sms.DEFAULT_SORT_ORDER;
}
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
Cursor ret = qb.query(db, projectionIn, selection, selectionArgs,
null, null, orderBy);
// TODO: Since the URLs are a mess, always use content://sms
ret.setNotificationUri(getContext().getContentResolver(),
NOTIFICATION_URI);
return ret;
}
We see , stay query Methodical case In the sentence , If it is SMS_CONVERSATIONS Type words , for SQLiteQueryBuilder Instance object qb Set the corresponding query table and where sentence , In addition, a basic query mapping will be set for it map namely sConversationProjectionMap, This variable is embodied in the following code :
[java] [view plain]( ) [copy]( ) [print]( ) [?]( )
static?{??
????...??
????sURLMatcher.addURI("sms",?"conversations",?SMS_CONVERSATIONS);??
????sURLMatcher.addURI("sms",?"conversations/*",?SMS_CONVERSATIONS_ID);??
????...??
????sConversationProjectionMap.put(Sms.Conversations.SNIPPET,??
????????"sms.body?AS?snippet");??
????sConversationProjectionMap.put(Sms.Conversations.THREAD_ID,??
????????"sms.thread_id?AS?thread_id");??
????sConversationProjectionMap.put(Sms.Conversations.MESSAGE_COUNT,??
????????"groups.msg_count?AS?msg_count");??
????sConversationProjectionMap.put("delta",?null);??
static {
...
sURLMatcher.addURI("sms", "conversations", SMS_CONVERSATIONS);
sURLMatcher.addURI("sms", "conversations/*", SMS_CONVERSATIONS_ID);
...
sConversationProjectionMap.put(Sms.Conversations.SNIPPET,
"sms.body AS snippet");
sConversationProjectionMap.put(Sms.Conversations.THREAD_ID,
"sms.thread_id AS thread_id");
sConversationProjectionMap.put(Sms.Conversations.MESSAGE_COUNT,
"groups.msg_count AS msg_count");
sConversationProjectionMap.put("delta", null);
}
What is the use of these data ? If we query projection by null Words ,sConversationProjectionMap It will be converted to the default projection, Finally, the query result contains only these three basic fields :snippet、thread_id、msg_count, The most concise information that can represent a conversation , Friends can try it themselves .
Of course , If you want to run the above test case , Two permissions need to be configured : Read SMS permission and read contact permission , as follows :
[html] [view plain]( ) [copy]( ) [print]( ) [?]( )
<!--? Read SMS ?-->??
<uses-permission?android:name\="android.permission.READ_SMS"?/>??
<!--? Read contacts ?-->??
<!-- Read SMS -->
<uses-permission android:name="android.permission.READ_SMS" />
<!-- Read contacts -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
Then let's run the test case , give the result as follows :
That's all about reading the session , Now let's introduce one of them testReadSMS() Method . In this method, we try to get all short messages , Used “content://sms/” The query , This query is relatively simple . In addition, there are several unused in the code URI, They are inbox 、 Sent and drafts , These queries are “content://sms/” Subset , Different selection conditions are used to query the short message table , Let's take a look at the specific source code :
[java] [view plain]( ) [copy]( ) [print]( ) [?]( )
??public?Cursor?query(Uri?url,?String[]?projectionIn,?String?selection,??
??????????String[]?selectionArgs,?String?sort)?{??
??????SQLiteQueryBuilder?qb?=?new?SQLiteQueryBuilder();??
??????//?Generate?the?body?of?the?query.??
??????int?match?=?sURLMatcher.match(url);??
??????switch?(match)?{??
??????case?SMS_ALL:??
??????????constructQueryForBox(qb,?Sms.MESSAGE_TYPE_ALL);??
??????????break;??
??????case?SMS_INBOX:??
??????????constructQueryForBox(qb,?Sms.MESSAGE_TYPE_INBOX);??
??????????break;??
??????case?SMS_SENT:??
??????????constructQueryForBox(qb,?Sms.MESSAGE_TYPE_SENT);??
??????????break;??
??????case?SMS_DRAFT:??
??????????constructQueryForBox(qb,?Sms.MESSAGE_TYPE_DRAFT);??
??????????break;??
??????}??
...??
@Override
public Cursor query(Uri url, String[] projectionIn, String selection,
String[] selectionArgs, String sort) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
// Generate the body of the query.
int match = sURLMatcher.match(url);
switch (match) {
case SMS_ALL:
constructQueryForBox(qb, Sms.MESSAGE_TYPE_ALL);
break;
case SMS_INBOX:
constructQueryForBox(qb, Sms.MESSAGE_TYPE_INBOX);
break;
case SMS_SENT:
constructQueryForBox(qb, Sms.MESSAGE_TYPE_SENT);
break;
case SMS_DRAFT:
constructQueryForBox(qb, Sms.MESSAGE_TYPE_DRAFT);
break;
}
...
}
You can see , They all called constructQueryForBox Method , What is this method for ?
[java] [view plain]( ) [copy]( ) [print]( ) [?]( )
private?void?constructQueryForBox(SQLiteQueryBuilder?qb,?int?type)?{??
????qb.setTables(TABLE_SMS);??
????if?(type?!=?Sms.MESSAGE_TYPE_ALL)?{??
????????qb.appendWhere("type="?+?type);??
????}??
private void constructQueryForBox(SQLiteQueryBuilder qb, int type) {
qb.setTables(TABLE_SMS);
if (type != Sms.MESSAGE_TYPE_ALL) {
qb.appendWhere("type=" + type);
}
}
We found that it actually adds filter conditions , If not all , Then add type filter information , Therefore, query different SMS sets . Friends can also try different types of queries in person .
in addition , If we want to query the corresponding SMS collection according to the session , We can do it in two ways :
1.“content://sms/”(selection:“thread_id=3”)
2.“content://sms/conversations/3”
The first one is easier to think of the query process , That is to add... To the above “thread_id=3” This article where Sentence can be used ; The second is in conversation path Follow the conversation id that will do , The specific logic is as follows :
[java] [view plain]( ) [copy]( ) [print]( ) [?]( )
case?SMS_CONVERSATIONS_ID:??
int?threadID;??
try?{??
????threadID?=?Integer.parseInt(url.getPathSegments().get(1));??
????if?(Log.isLoggable(TAG,?Log.VERBOSE))?{??
????????Log.d(TAG,?"query?conversations:?threadID="?+?threadID);??
????}??
}??
catch?(Exception?ex)?{??
????Log.e(TAG,??
??????????"Bad?conversation?thread?id:?"??
??????????+?url.getPathSegments().get(1));??
????return?null;??
}??
qb.setTables(TABLE_SMS);??
qb.appendWhere("thread_id?=?"?+?threadID);??
case SMS_CONVERSATIONS_ID:
int threadID;
try {
threadID = Integer.parseInt(url.getPathSegments().get(1));
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.d(TAG, "query conversations: threadID=" + threadID);
}
}
catch (Exception ex) {
Log.e(TAG,
"Bad conversation thread id: "
return null;
}
qb.setTables(TABLE_SMS);
qb.appendWhere("thread_id = " + threadID);
break;
We can see , It finally went the same way as the first way , There is no essential difference between the two . But in terms of simplicity and ease of use , This way is better , Friends can compare .
The above is to get all the information about the conversation content and SMS content , Let's introduce the writing operation of SMS .
Send and write SMS
On certain occasions , We need to send text messages , And write the SMS to the data source , At this time, we need to understand the sending SMS mechanism and writing SMS mechanism .
We will try to send a text message to the specified address , At the same time, the content of the short message is written to the short message data source , After the SMS is sent successfully , We inform the user that the transmission is successful , After the other party receives the message , We inform the user that the other party has received successfully .
To implement these functions , We need to understand the following key contents :
1. Use android.telephony.SmsManager Of API Send a text message
2. Use ContentProvider The mechanism is right “content://sms/sent” This URI Write operation
3. register “SENT_SMS_ACTION” This broadcast address , Receive this broadcast after the SMS is successfully sent
4. register “DELIVERED_SMS_ACTION” This broadcast address , When the other party receives the message, it receives the broadcast
Let's use code to realize these functions , Create a file called SMSActivity Of Activity, as follows :
[java] [view plain]( ) [copy]( ) [print]( ) [?]( )
package?com.scott.provider;??
import?java.util.List;??
import?android.app.Activity;??
import?android.app.PendingIntent;??
import?android.content.BroadcastReceiver;??
import?android.content.ContentValues;??
import?android.content.Context;??
import?android.content.Intent;??
import?android.content.IntentFilter;??
import?android.net.Uri;??
import?android.os.Bundle;??
import?android.telephony.SmsManager;??
import?android.view.View;??
import?android.widget.EditText;??
import?android.widget.Toast;??
public?class?SMSActivity?extends?Activity?{??
????private?SendReceiver?sendReceiver?=?new?SendReceiver();??
????private?DeliverReceiver?deliverReceiver?=?new?DeliverReceiver();??
????private?EditText?address;??
????private?EditText?body;??
????protected?void?onCreate(Bundle?savedInstanceState)?{??
????????super.onCreate(savedInstanceState);??
????????setContentView(R.layout.sms);??
????????address?=?(EditText)?findViewById(R.id.address);??
????????body?=?(EditText)?findViewById(R.id.body);??
????????// Register to send a successful broadcast ??
????????registerReceiver(sendReceiver,?new?IntentFilter("SENT_SMS_ACTION"));??
????????// Register to receive successful broadcasts ??
????????registerReceiver(deliverReceiver,?new?IntentFilter("DELIVERED_SMS_ACTION"));??
????}??
????protected?void?onDestroy()?{??
????????super.onDestroy();??
????????unregisterReceiver(sendReceiver);??
????????unregisterReceiver(deliverReceiver);??
????}??
????public?void?sendSMS(View?view)?{??
????????String?address?=?this.address.getText().toString();??
????????String?body?=?this.body.getText().toString();??
????????//android.telephony.SmsManager,?not?[android.telephony.gsm.SmsManager]??
????????SmsManager?smsManager?=?SmsManager.getDefault();??
????????// A message will be generated after successful or failed SMS sending SENT_SMS_ACTION Broadcast of ??
????????PendingIntent?sendIntent?=?PendingIntent.getBroadcast(this,?0,?new?Intent("SENT_SMS_ACTION"),?0);??
????????// After receiving the message successfully , The sender will generate a message DELIVERED_SMS_ACTION radio broadcast ??
????????PendingIntent?deliveryIntent?=?PendingIntent.getBroadcast(this,?0,?new?Intent("DELIVERED_SMS_ACTION"),?0);??
????????if?(body.length()?>?70)?{????// If the number of words exceeds 70, It needs to be split into multiple SMS messages to send ??
????????????List<String>?msgs?=?smsManager.divideMessage(body);??
????????????for?(String?msg?:?msgs)?{??
????????????????smsManager.sendTextMessage(address,?null,?msg,?sendIntent,?deliveryIntent);??????????????????????????
????????????}??
????????}?else?{??
????????????smsManager.sendTextMessage(address,?null,?body,?sendIntent,?deliveryIntent);??
????????}??
????????// Write to SMS data source ??
????????ContentValues?values?=?new?ContentValues();??
????????values.put("address",address);??// Sending address ??
????????values.put("body",?body);???// The message content ??
????????values.put("date",?System.currentTimeMillis());?// Creation time ??
????????values.put("read",?0);??//0: unread ;1: read ??
????????values.put("type",?2);??//1: receive ;2: send out ??
????????getContentResolver().insert(Uri.parse("content://sms/sent"),?values);???// insert data ??
????}??
????private?class?SendReceiver?extends?BroadcastReceiver?{??
If you want to be a good Android Developer , Please focus on , Do in-depth research on basic and important things .
For many primary and intermediate Android For Engineers , Want to improve skills , Often is oneself gropes for growth , Unsystematic learning is inefficient, long and helpless . I hope that these architecture technologies can be applied to Android Development of friends have reference and less detours , The point of this article is whether you have gained and grown up , The rest is unimportant , I hope readers will keep this in mind .
here , The author shares a video and data from the perspective of architecture philosophy to share with you the experience of many years of architecture , Near preparation 6 The latest recording of , I believe this video can give you different inspiration 、 Harvest .
PS: Because of the Internet's second line collection Android The real question of the interview ( contain BAT、 millet 、 Huawei 、 Meituan 、 sound of dripping water ) And I sort it out myself Android Review notes ( contain Android Basic knowledge points 、Android Expand your knowledge 、Android The source code parsing 、 Design pattern summary 、Gradle Knowledge point 、 Summary of common algorithm questions .)
《Jetpack The whole family bucket creates a new Google Standard architecture mode 》
This article has been CODING Open source project :《Android Summary of learning notes + Mobile architecture video + The real interview question of Dachang + Project source code 》 Included
版权声明:本文为[mb61ab4a9335a30]所创,转载请带上原文链接,感谢。 https://javamana.com/2021/12/202112122318461766.html