編輯:關於Android編程
本文主要講述Android 6.0 SIM卡初始化流程,這個過程也涉及到UICC框架的初始化,UICC(Universal Integrated Circuit Card)的架構圖如下:
/**
* This class is responsible for keeping all knowledge about
* Universal Integrated Circuit Card (UICC), also know as SIM's,
* in the system. It is also used as API to get appropriate
* applications to pass them to phone and service trackers.
*
* UiccController is created with the call to make() function.
* UiccController is a singleton and make() must only be called once
* and throws an exception if called multiple times.
*
* Once created UiccController registers with RIL for "on" and "unsol_sim_status_changed"
* notifications. When such notification arrives UiccController will call
* getIccCardStatus (GET_SIM_STATUS). Based on the response of GET_SIM_STATUS
* request appropriate tree of uicc objects will be created.
*
* Following is class diagram for uicc classes:
*
* UiccController
* #
* |
* UiccCard
* # #
* | ------------------
* UiccCardApplication CatService
* # #
* | |
* IccRecords IccFileHandler
* ^ ^ ^ ^ ^ ^ ^ ^
* SIMRecords---- | | | | | | ---SIMFileHandler
* RuimRecords----- | | | | ----RuimFileHandler
* IsimUiccRecords--- | | -----UsimFileHandler
* | ------CsimFileHandler
* ----IsimFileHandler
*
* Legend: # stands for Composition
* ^ stands for Generalization
*
* See also {@link com.android.internal.telephony.IccCard}
* and {@link com.android.internal.telephony.uicc.IccCardProxy}
*/
UiccController是整個UICC相關信息的控制接口,UiccController的實例化就是在RIL與UiccController 之間建立監聽關系,這樣的話,當SIM卡狀態發生變化時,UiccController就可以馬上知道並且做出相應的操作。
UiccController對象是在PhoneFacotry.java中的makeDefaultPhone()方法中初始化的,有個細節值得注意的是sCommandsInterfaces數組的i對應的是PhoneId。
public static void makeDefaultPhone(Context context) {
// Instantiate UiccController so that all other classes can just
// call getInstance()
mUiccController = UiccController.make(context, sCommandsInterfaces);
for (int i = 0; i < numPhones; i++) {
PhoneBase phone = null;
int phoneType = TelephonyManager.getPhoneType(networkModes[i]);
if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
//sCommandsInterfaces的i對應的是PhoneId
phone = TelephonyPluginDelegate.getInstance().makeGSMPhone(context,
sCommandsInterfaces[i], sPhoneNotifier, i);
} else if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
phone = TelephonyPluginDelegate.getInstance().makeCDMALTEPhone(context,
sCommandsInterfaces[i], sPhoneNotifier, i);
}
Rlog.i(LOG_TAG, "Creating Phone with type = " + phoneType + " sub = " + i);
sProxyPhones[i] = TelephonyPluginDelegate.getInstance().makePhoneProxy(phone);
}
}
@TelephonyPluginBase.java
public PhoneBase makeGSMPhone(Context context, CommandsInterface ci,
PhoneNotifier notifier, int phoneId) {
return new GSMPhone(context, ci, notifier, phoneId);
}
}
在UiccController.java的make()方法中new了一個UiccController對象,
public static UiccController make(Context c, CommandsInterface[] ci) {
synchronized (mLock) {
if (mInstance != null) {
throw new RuntimeException("MSimUiccController.make() should only be called once");
}
mInstance = new UiccController(c, ci);
return (UiccController)mInstance;
}
}
private UiccController(Context c, CommandsInterface []ci) {
if (DBG) log("Creating UiccController");
mContext = c;
mCis = ci;
for (int i = 0; i < mCis.length; i++) {
Integer index = new Integer(i);
mCis[i].registerForIccStatusChanged(this, EVENT_ICC_STATUS_CHANGED, index);
mCis[i].registerForAvailable(this, EVENT_ICC_STATUS_CHANGED, index);
mCis[i].registerForNotAvailable(this, EVENT_RADIO_UNAVAILABLE, index);
mCis[i].registerForIccRefresh(this, EVENT_SIM_REFRESH, index);
}
}
在上面UiccController的構造方法中可以看到,注冊了三個事件EVENT_ICC_STATUS_CHANGED(監聽SIM卡的狀態變化),EVENT_RADIO_UNAVAILABLE(一旦radio變成不可用狀態,就清空SIM卡的信息),EVENT_SIM_REFRESH。index對應的是PhoneId,當上面這三種消息上來時,就知道對應哪個Phone對象,也就對應那張卡。
當接收到EVENT_ICC_STATUS_CHANGED消息後,UiccController調用RIL.java的getIccCardStatus()方法給MODEM發送RIL_REQUEST_GET_SIM_STATUS消息,查詢SIM卡的狀態。
public void handleMessage (Message msg) {
.....
//首先從Message中取出PhoneId
Integer index = getCiIndex(msg);
.....
case EVENT_ICC_STATUS_CHANGED:
if (DBG) log("Received EVENT_ICC_STATUS_CHANGED, calling getIccCardStatus");
//查詢當前SIM卡的狀態
mCis[index].getIccCardStatus(obtainMessage(EVENT_GET_ICC_STATUS_DONE, index));
break;
case EVENT_GET_ICC_STATUS_DONE:
if (DBG) log("Received EVENT_GET_ICC_STATUS_DONE");
onGetIccCardStatusDone(ar, index);
break;
}
當查詢SIM卡的狀態完畢後,先從result中解析出IccCardStatus,依據IccCardStatus來創建UiccCard對象,一個UiccCard 對象代表著一張SIM卡;如果UiccCard對象已存在就直接調它的update()方法更新UiccCard的信息。
private synchronized void onGetIccCardStatusDone(AsyncResult ar, Integer index) {
if (ar.exception != null) {
Rlog.e(LOG_TAG,"Error getting ICC status. "
+ "RIL_REQUEST_GET_ICC_STATUS should "
+ "never return an error", ar.exception);
return;
}
if (!isValidCardIndex(index)) {
Rlog.e(LOG_TAG,"onGetIccCardStatusDone: invalid index : " + index);
return;
}
IccCardStatus status = (IccCardStatus)ar.result;
if (mUiccCards[index] == null) {
//Create new card(Android6.0 調的是4個參數的構造方法)
mUiccCards[index] = new UiccCard(mContext, mCis[index], status, index);
} else {
//Update already existing card
mUiccCards[index].update(mContext, mCis[index] , status);
}
if (DBG) log("Notifying IccChangedRegistrants");
//通知監聽UiccController的監聽者
mIccChangedRegistrants.notifyRegistrants(new AsyncResult(null, index, null));
}
在UiccCard的構造方法中,最終還是調用了update()方法
public UiccCard(Context c, CommandsInterface ci, IccCardStatus ics, int phoneId) {
mCardState = ics.mCardState;
mPhoneId = phoneId;
update(c, ci, ics);
}
public void update(Context c, CommandsInterface ci, IccCardStatus ics) {
synchronized (mLock) {
CardState oldState = mCardState;
mCardState = ics.mCardState;
mUniversalPinState = ics.mUniversalPinState;
mGsmUmtsSubscriptionAppIndex = ics.mGsmUmtsSubscriptionAppIndex;
mCdmaSubscriptionAppIndex = ics.mCdmaSubscriptionAppIndex;
mImsSubscriptionAppIndex = ics.mImsSubscriptionAppIndex;
mContext = c;
mCi = ci;
//update applications
if (DBG) log(ics.mApplications.length + " applications");
for ( int i = 0; i < mUiccApplications.length; i++) {
if (mUiccApplications[i] == null) {
//Create newly added Applications
if (i < ics.mApplications.length) {
mUiccApplications[i] = new UiccCardApplication(this,
ics.mApplications[i], mContext, mCi);
}
} else if (i >= ics.mApplications.length) {
//Delete removed applications
mUiccApplications[i].dispose();
mUiccApplications[i] = null;
} else {
//Update the rest
mUiccApplications[i].update(ics.mApplications[i], mContext, mCi);
}
}
//創建CatService,用於讀取STK的信息
createAndUpdateCatService();
// Reload the carrier privilege rules if necessary.
log("Before privilege rules: " + mCarrierPrivilegeRules + " : " + mCardState);
if (mCarrierPrivilegeRules == null && mCardState == CardState.CARDSTATE_PRESENT) {
mCarrierPrivilegeRules = new UiccCarrierPrivilegeRules(this,
mHandler.obtainMessage(EVENT_CARRIER_PRIVILIGES_LOADED));
} else if (mCarrierPrivilegeRules != null && mCardState != CardState.CARDSTATE_PRESENT) {
mCarrierPrivilegeRules = null;
}
sanitizeApplicationIndexes();
RadioState radioState = mCi.getRadioState();
if (DBG) log("update: radioState=" + radioState + " mLastRadioState="
+ mLastRadioState);
// No notifications while radio is off or we just powering up
if (radioState == RadioState.RADIO_ON && mLastRadioState == RadioState.RADIO_ON) {
if (oldState != CardState.CARDSTATE_ABSENT &&
mCardState == CardState.CARDSTATE_ABSENT) {
if (DBG) log("update: notify card removed");
mAbsentRegistrants.notifyRegistrants();
mHandler.sendMessage(mHandler.obtainMessage(EVENT_CARD_REMOVED, null));
} else if (oldState == CardState.CARDSTATE_ABSENT &&
mCardState != CardState.CARDSTATE_ABSENT) {
if (DBG) log("update: notify card added");
mHandler.sendMessage(mHandler.obtainMessage(EVENT_CARD_ADDED, null));
}
}
mLastRadioState = radioState;
}
}
在UiccCard.java的update()方法中,實例化了UiccCardApplication對象,或者調UiccCardApplication的update()方法更新狀態。mCardState記錄著卡的狀態,根據新舊mCardState就可以知道CARD_ADDED或者CARD_REMOVED。
UiccCardApplication(UiccCard uiccCard,
IccCardApplicationStatus as,
Context c,
CommandsInterface ci) {
if (DBG) log("Creating UiccApp: " + as);
mUiccCard = uiccCard;
mAppState = as.app_state;
mAppType = as.app_type;
mAuthContext = getAuthContext(mAppType);
mPersoSubState = as.perso_substate;
mAid = as.aid;
mAppLabel = as.app_label;
mPin1Replaced = (as.pin1_replaced != 0);
mPin1State = as.pin1;
mPin2State = as.pin2;
mContext = c;
mCi = ci;
//根據AppType實例化IccFileHandler
mIccFh = createIccFileHandler(as.app_type);
//根據AppType實例化IccRecords
mIccRecords = createIccRecords(as.app_type, mContext, mCi);
if (mAppState == AppState.APPSTATE_READY) {
queryFdn();
//查詢PIN1碼的狀態
queryPin1State();
}
mCi.registerForNotAvailable(mHandler, EVENT_RADIO_UNAVAILABLE, null);
}
在UiccCardApplication初始化的時候,會調用自身的createIccRecords()方法,根據AppType創建對應的Records對象。
private IccRecords createIccRecords(AppType type, Context c, CommandsInterface ci) {
if (type == AppType.APPTYPE_USIM || type == AppType.APPTYPE_SIM) {
return TelephonyPluginDelegate.getInstance().makeSIMRecords(this, c, ci);
} else if (type == AppType.APPTYPE_RUIM || type == AppType.APPTYPE_CSIM){
return new RuimRecords(this, c, ci);
} else if (type == AppType.APPTYPE_ISIM) {
return new IsimUiccRecords(this, c, ci);
} else {
// Unknown app type (maybe detection is still in progress)
return null;
}
}
接著會進入TelephonyPluginDelegate.java和TelephonyPluginBase.java的makeSIMRecords()方法
@TelephonyPluginDelegate.java
public SIMRecords makeSIMRecords (UiccCardApplication app, Context c, CommandsInterface ci) {
return sPlugin.makeSIMRecords (app, c, ci);
}
@TelephonyPluginBase.java
public SIMRecords makeSIMRecords (UiccCardApplication app, Context c, CommandsInterface ci) {
return new SIMRecords(app, c, ci);
}
到這裡就可以看到直接new 了一個SIMRecords對象。
public SIMRecords(UiccCardApplication app, Context c, CommandsInterface ci) {
super(app, c, ci);
mAdnCache = new AdnRecordCache(mFh);
mVmConfig = new VoiceMailConstants();
//從spn-conf.xml文件中加載SPN
mSpnOverride = new SpnOverride();
// No load request is made till SIM ready
mRecordsRequested = false;
// recordsToLoad is set to 0 because no requests are made yet
mRecordsToLoad = 0;
mCi.setOnSmsOnSim(this, EVENT_SMS_ON_SIM, null);
mCi.registerForIccRefresh(this, EVENT_SIM_REFRESH, null);
// Start off by setting empty state
resetRecords();
//注冊監聽,如果已ready,那麼開始加載數據
mParentApp.registerForReady(this, EVENT_APP_READY, null);
mParentApp.registerForLocked(this, EVENT_APP_LOCKED, null);
if (DBG) log("SIMRecords X ctor this=" + this);
}
等到AppState變成APPSTATE_READY是,UiccCardApplication會在notifyReadyRegistrantsIfNeeded()方法裡通知SIMRecords,那麼在SIMRecords的handleMessage()方法就會收到EVENT_APP_READY消息。
public void handleMessage(Message msg) {
case EVENT_APP_READY:
onReady();
break;
}
@Override
public void onReady() {
fetchSimRecords();
}
當執行到fetchSimRecords()方法時,才真正開始加載EF文件信息。具體的讀取SIM卡EF文件信息的過程是由 IccFileHandler 來實現的,根據EF文件的類型,調用不用的方法,loadEFTransparent()和loadEFLinearFixed()最終都會調用RIL.java的iccIOForApp()。某一項的加載流程如下流程圖。
protected void fetchSimRecords() {
mRecordsRequested = true;
if (DBG) log("fetchSimRecords " + mRecordsToLoad);
//讀取IMSI
mCi.getIMSIForApp(mParentApp.getAid(), obtainMessage(EVENT_GET_IMSI_DONE));
mRecordsToLoad++;
//從EF_ICCID(0x2fe2)讀取ICCID
mFh.loadEFTransparent(EF_ICCID, obtainMessage(EVENT_GET_ICCID_DONE));
mRecordsToLoad++;
// FIXME should examine EF[MSISDN]'s capability configuration
// to determine which is the voice/data/fax line
new AdnRecordLoader(mFh).loadFromEF(EF_MSISDN, getExtFromEf(EF_MSISDN), 1,
obtainMessage(EVENT_GET_MSISDN_DONE));
mRecordsToLoad++;
// Record number is subscriber profile
mFh.loadEFLinearFixed(EF_MBI, 1, obtainMessage(EVENT_GET_MBI_DONE));
mRecordsToLoad++;
mFh.loadEFTransparent(EF_AD, obtainMessage(EVENT_GET_AD_DONE));
mRecordsToLoad++;
// Record number is subscriber profile
mFh.loadEFLinearFixed(EF_MWIS, 1, obtainMessage(EVENT_GET_MWIS_DONE));
mRecordsToLoad++;
// Also load CPHS-style voice mail indicator, which stores
// the same info as EF[MWIS]. If both exist, both are updated
// but the EF[MWIS] data is preferred
// Please note this must be loaded after EF[MWIS]
mFh.loadEFTransparent(
EF_VOICE_MAIL_INDICATOR_CPHS,
obtainMessage(EVENT_GET_VOICE_MAIL_INDICATOR_CPHS_DONE));
mRecordsToLoad++;
// Same goes for Call Forward Status indicator: fetch both
// EF[CFIS] and CPHS-EF, with EF[CFIS] preferred.
loadCallForwardingRecords();
//從EF_SPN(0x6F46)、EF_SPN_CPHS(0x6f14)、EF_SPN_SHORT_CPHS(0x6f18)三個地址上讀取SPN
getSpnFsm(true, null);
//從EF_SPDI(0x6fcd)讀取SPDI
mFh.loadEFTransparent(EF_SPDI, obtainMessage(EVENT_GET_SPDI_DONE));
mRecordsToLoad++;
//從EF_PNN(0x6fc5)讀取PNN
mFh.loadEFLinearFixed(EF_PNN, 1, obtainMessage(EVENT_GET_PNN_DONE));
mRecordsToLoad++;
mFh.loadEFTransparent(EF_SST, obtainMessage(EVENT_GET_SST_DONE));
mRecordsToLoad++;
mFh.loadEFTransparent(EF_INFO_CPHS, obtainMessage(EVENT_GET_INFO_CPHS_DONE));
mRecordsToLoad++;
mFh.loadEFTransparent(EF_CSP_CPHS,obtainMessage(EVENT_GET_CSP_CPHS_DONE));
mRecordsToLoad++;
mFh.loadEFTransparent(EF_GID1, obtainMessage(EVENT_GET_GID1_DONE));
mRecordsToLoad++;
mFh.loadEFTransparent(EF_GID2, obtainMessage(EVENT_GET_GID2_DONE));
mRecordsToLoad++;
loadEfLiAndEfPl();
// XXX should seek instead of examining them all
if (false) { // XXX
mFh.loadEFLinearFixedAll(EF_SMS, obtainMessage(EVENT_GET_ALL_SMS_DONE));
mRecordsToLoad++;
}
if (CRASH_RIL) {
String sms = "0107912160130310f20404d0110041007030208054832b0120"
+ "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+ "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+ "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+ "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+ "ffffffffffffffffffffffffffffff";
byte[] ba = IccUtils.hexStringToBytes(sms);
mFh.updateEFLinearFixed(EF_SMS, 1, ba, null,
obtainMessage(EVENT_MARK_SMS_READ_DONE, 1));
}
if (DBG) log("fetchSimRecords " + mRecordsToLoad + " requested: " + mRecordsRequested);
}
讀取ICCID : ICCID通常有20位,根據TS 102.221,ICCID 是BCD編碼的,只允許出現數字和字符F,如果出現其他字母,就會在下面IccUtils.bcdToString()方法中被截斷。(如中國移動的SIM卡,會使用字符b代表188號段,那麼應用層得到的ICCID就不是20位的了)
case EVENT_GET_ICCID_DONE:
isRecordLoadResponse = true;
ar = (AsyncResult)msg.obj;
data = (byte[])ar.result;
if (ar.exception != null) {
break;
}
mIccId = IccUtils.bcdToString(data, 0, data.length);
log("iccid: " + mIccId);
break;
mIccId的值最後會通過IccRecords.java的getIccId()方法,提供給應用層使用。
public String getIccId() {
return mIccId;
}
讀取SPDI : SPDI存儲的是由numeric network ID組成的列表。
case EVENT_GET_SPDI_DONE:
isRecordLoadResponse = true;
ar = (AsyncResult)msg.obj;
data = (byte[])ar.result;
if (ar.exception != null) {
break;
}
parseEfSpdi(data);
break;
/**
* Parse TS 51.011 EF[SPDI] record
* This record contains the list of numeric network IDs that
* are treated specially when determining SPN display
*/
private void
parseEfSpdi(byte[] data) {
SimTlv tlv = new SimTlv(data, 0, data.length);
byte[] plmnEntries = null;
for ( ; tlv.isValidObject() ; tlv.nextObject()) {
// Skip SPDI tag, if existant
if (tlv.getTag() == TAG_SPDI) {
tlv = new SimTlv(tlv.getData(), 0, tlv.getData().length);
}
// There should only be one TAG_SPDI_PLMN_LIST
if (tlv.getTag() == TAG_SPDI_PLMN_LIST) {
plmnEntries = tlv.getData();
break;
}
}
if (plmnEntries == null) {
return;
}
mSpdiNetworks = new ArrayList(plmnEntries.length / 3);
for (int i = 0 ; i + 2 < plmnEntries.length ; i += 3) {
String plmnCode;
plmnCode = IccUtils.bcdToString(plmnEntries, i, 3);
// Valid operator codes are 5 or 6 digits
if (plmnCode.length() >= 5) {
log("EF_SPDI network: " + plmnCode);
mSpdiNetworks.add(plmnCode);
}
}
}
最終會把值存儲到mSpdiNetworks這個列表中,mSpdiNetworks在判斷PLMN和SPN顯示規則時會使用到。
讀取SPN:後續寫SPN相關文章時會詳細講解。
讀取PNN:讀取到PNN的值後,存儲在mPnnHomeName中,但是mPnnHomeName並沒有被其他地方調用。
case EVENT_GET_PNN_DONE:
isRecordLoadResponse = true;
ar = (AsyncResult)msg.obj;
data = (byte[])ar.result;
if (ar.exception != null) {
break;
}
SimTlv tlv = new SimTlv(data, 0, data.length);
for ( ; tlv.isValidObject() ; tlv.nextObject()) {
if (tlv.getTag() == TAG_FULL_NETWORK_NAME) {
mPnnHomeName
= IccUtils.networkNameToString(
tlv.getData(), 0, tlv.getData().length);
break;
}
}
break;
可以自己新增代碼,把mPnnHomeName通過setSystemProperty(key,value)方法存儲到一個key裡,然後在應用層就可以通過這個key,獲取到PNN的值。
//存儲mPnnHomeName的值
setSystemProperty(TelephonyProperties.PNN_NAME, mPnnHomeName);
//獲取PNN的值
TelephonyManager.getTelephonyProperty(mPhone.getPhoneId(),
TelephonyProperties.PNN_NAME, "");
回到fetchSimRecords()方法,每加載一項,mRecordsToLoad就加1;等到某一項讀取數據完畢,handleMessage()方法被執行,就會調onRecordLoaded()方法
protected void onRecordLoaded() {
// One record loaded successfully or failed, In either case
// we need to update the recordsToLoad count
mRecordsToLoad -= 1;
if (DBG) log("onRecordLoaded " + mRecordsToLoad + " requested: " + mRecordsRequested);
if (mRecordsToLoad == 0 && mRecordsRequested == true) {
onAllRecordsLoaded();
} else if (mRecordsToLoad < 0) {
loge("recordsToLoad <0, programmer error suspected");
mRecordsToLoad = 0;
}
}
mRecordsToLoad的值會減1,直到mRecordsToLoad的值為0時,說明在fetchSimRecords()中啟動加載的數據都已異步讀取完成。就會進入onAllRecordsLoaded()方法。
@Override
protected void onAllRecordsLoaded() {
if (DBG) log("record load complete");
Resources resource = Resources.getSystem();
if (resource.getBoolean(com.android.internal.R.bool.config_use_sim_language_file)) {
setSimLanguage(mEfLi, mEfPl);
} else {
if (DBG) log ("Not using EF LI/EF PL");
}
setVoiceCallForwardingFlagFromSimRecords();
if (mParentApp.getState() == AppState.APPSTATE_PIN ||
mParentApp.getState() == AppState.APPSTATE_PUK) {
// reset recordsRequested, since sim is not loaded really
mRecordsRequested = false;
// lock state, only update language
return ;
}
// Some fields require more than one SIM record to set
String operator = getOperatorNumeric();
if (!TextUtils.isEmpty(operator)) {
log("onAllRecordsLoaded set 'gsm.sim.operator.numeric' to operator='" +
operator + "'");
log("update icc_operator_numeric=" + operator);
mTelephonyManager.setSimOperatorNumericForPhone(
mParentApp.getPhoneId(), operator);
final SubscriptionController subController = SubscriptionController.getInstance();
subController.setMccMnc(operator, subController.getDefaultSmsSubId());
} else {
log("onAllRecordsLoaded empty 'gsm.sim.operator.numeric' skipping");
}
if (!TextUtils.isEmpty(mImsi)) {
log("onAllRecordsLoaded set mcc imsi" + (VDBG ? ("=" + mImsi) : ""));
mTelephonyManager.setSimCountryIsoForPhone(
mParentApp.getPhoneId(), MccTable.countryCodeForMcc(
Integer.parseInt(mImsi.substring(0,3))));
} else {
log("onAllRecordsLoaded empty imsi skipping setting mcc");
}
setVoiceMailByCountry(operator);
setSpnFromConfig(operator);
mRecordsLoadedRegistrants.notifyRegistrants(
new AsyncResult(null, null, null));
}
在onAllRecordsLoaded()方法中會對讀取到的數據進行處理和存儲,到這裡,SIM卡初始化的流程就結束了。
Android實習札記(8)---ViewPager+Fragment實例講解 在札記(5)中我們就說過要弄一個模仿微信頁面切換的東東,就是V
今天在逛安智的時候看到一個軟件,我對注冊碼驗證的程序比較感興趣哈,- -那個帖子的軟件是通過爆破法實現破解的,之前我在這個帖子講過http://www.52pojie.c
為了避免看視頻時影響到他人休息,很多用戶會考慮購買無線耳機遠程接收PC的音頻信號。問題是,有多少人會為這種小概率事件去購買無線耳機?如果你身邊有台Andro
本文實例講述了Android編程實現Toast自定義布局的方法。分享給大家供大家參考,具體如下:不知道各位客官是不是覺得系統的toast的信息很難看呢,默認的但黑色背景,