日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > Android >内容正文

Android

Android开发之Android5.1.1(CM12.1)源码中短信发送流程解析

發布時間:2025/3/15 Android 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Android开发之Android5.1.1(CM12.1)源码中短信发送流程解析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

首先我要從SmsManager開始一步步深入了解,相信大家在學習Android基礎的時候接觸過這個類。它在/frameworks/opt/telephony/src/java/android/telephony路徑下,SmsManager:提供管理短信操作,如發送數據,文本和PDU短信。通過調用靜態方法SmsManager.getDefault() 獲取此對象。它里面提供了一系列發送短信的方法,我們就從sendTextMessage()方法說起,首先我們來看看這個方法:

public void sendTextMessage( String destinationAddress, String scAddress, String text, PendingIntent sentIntent, PendingIntent deliveryIntent) {if (TextUtils.isEmpty(destinationAddress)){throw new IllegalArgumentException("Invalid destinationAddress");}if (TextUtils.isEmpty(text)){throw new IllegalArgumentException("Invalid message body");}try{ISms iccISms = getISmsServiceOrThrow();iccISms.sendTextForSubscriber(getSubscriptionId(), ActivityThread.currentPackageName(),destinationAddress,scAddress, text, sentIntent, deliveryIntent);} catch (RemoteException ex){// ignore it} }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

先判斷地址和短信內容是否為空,并且拋出異常信息,然后通過ISms這樣一個東西調用sendTextForSubscriber()方法將短信往下一個類進行傳遞。

getISmsServiceOrThrow():獲取ISms服務。?
destinationAddress:收短信人的地址。?
scAddress:短信號碼中心,如果傳null則為默認短信號碼中心。?
text:短信內容。?
sentIntent:短信發送成功或者失敗的廣播。?
deliveryIntent:對方收到短信時候的廣播。?
getSubscriptionId():獲取訂閱id。?
ActivityThread.currentPackageName():當前的包名。

在上面接觸到ISms這樣一個東西,那么他是干嘛的呢?其實它一個接口,在frameworks/base/telephony/java/com/android/internal/telephony下面,我們會找到它,會發現它是是一個aidl文件,打開它是一個接口(interface),這是我們就要去找另外一個類了,在android5.1.1中有能力完成短信發送任務的系統服務它就是UiccSmsController.java。它在/frameworks/opt/telephony/src/java/com/android/internal/telephony路徑下面,UiccSmsController:提供一個進程間通信訪問ICC中的短信。打開它會發現這樣的繼承關系:

public class UiccSmsController extends ISms.Stub
  • 1
  • 1

里面有這樣的一段代碼:

public void sendText(String callingPackage, String destAddr, String scAddr,String text, PendingIntent sentIntent, PendingIntent deliveryIntent){sendTextForSubscriber(getDefaultSmsSubId(), callingPackage, destAddr,scAddr, text, sentIntent, deliveryIntent);}public void sendTextForSubscriber(int subId, String callingPackage,String destAddr, String scAddr,String text,PendingIntent sentIntent, PendingIntent deliveryIntent){sendTextWithOptionsUsingSubscriber(subId, callingPackage, destAddr,scAddr, text, sentIntent, deliveryIntent, -1, false, -1);}public void sendTextWithOptionsUsingSubscriber(int subId,String callingPackage, String destAddr,String scAddr, String text,PendingIntent sentIntent, PendingIntent deliveryIntent,int priority, boolean isExpectMore, int validityPeriod){mContext.enforceCallingPermission(android.Manifest.permission.SEND_SMS,"Sending SMS message");IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);if (iccSmsIntMgr.isShortSMSCode(destAddr)){iccSmsIntMgr.sendTextWithOptions(callingPackage, destAddr, scAddr,text, sentIntent, deliveryIntent, priority, isExpectMore,validityPeriod);return;}ArrayList<String> parts = new ArrayList<String>();parts.add(text);ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>();sentIntents.add(sentIntent);ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>();deliveryIntents.add(deliveryIntent);broadcastOutgoingSms(subId, callingPackage, destAddr, scAddr, false,parts, sentIntents, deliveryIntents, priority, isExpectMore,validityPeriod);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

可以看出現進行權限的操作,然后調用了IccSmsInterfaceManager 的sendTextWithOptions方法將短信進一步傳遞,后面進行廣播的處理。通過getDefaultSmsSubId()獲得了一個手機卡的默認SubId,在同級路徑下找到IccSmsInterfaceManager 類打開之后,又會發現,它調用SMSDispatcher的sendText()方法將短信進一步傳遞。在同級路徑下我們打開SMSDispatcher 會發現它是一個抽象類,并且繼承了Handler。如下:

public abstract class SMSDispatcher extends Handler
  • 1
  • 1

既然是抽象類,那肯定就有實現它的派生類,在Android5.1.1中我找到了三個派生類:CdmaSMSDispatcher、GsmSMSDispatcher、ImsSmsDispatcher。但是在IccSmsInterfaceManager 中只創建了ImsSmsDispatcher。

protected IccSmsInterfaceManager(PhoneBase phone){mPhone = phone;mContext = phone.getContext();mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);mDispatcher = new ImsSMSDispatcher(phone, phone.mSmsStorageMonitor,phone.mSmsUsageMonitor);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在同級路徑下打開ImsSmsDispatcher.java我們會發現,ImsSmsDispatcher持有了CdmaSMSDispatcher、GsmSMSDispatcher這兩個對象的實例:

private SMSDispatcher mCdmaDispatcher; private SMSDispatcher mGsmDispatcher;
  • 1
  • 2
  • 1
  • 2

通過判斷網絡制式,分別調用mCdmaDispatcher或者mGsmDispatcher的sendText()方法。?
判斷網絡制式:

/*** Determines whether or not to use CDMA format for MO SMS. If SMS over IMS* is supported, then format is based on IMS SMS format, otherwise format is* based on current phone type.** @return true if Cdma format should be used for MO SMS, false otherwise.*/private boolean isCdmaMo(){if (!isIms() || !shouldSendSmsOverIms()){// Either IMS is not registered or there is an active 1x voice call// while on eHRPD, use Voice technology to determine SMS format.return (PhoneConstants.PHONE_TYPE_CDMA == mPhone.getPhoneType());}// IMS is registered with SMS supportreturn isCdmaFormat(mImsSmsFormat);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

這兒我們以mGsmDispatcher作為下一步講解對象,繼續研究短信的發送。在frameworks/opt/telephony/src/java/com/android/internal/telephony/gsm路徑下打開GsmSMSDispatcher,找到sendText()方法,如下:

@Overrideprotected void sendText(String destAddr, String scAddr, String text,PendingIntent sentIntent,PendingIntent deliveryIntent,Uri messageUri, String callingPkg, int priority,boolean isExpectMore, int validityPeriod){SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(scAddr, destAddr,text,(deliveryIntent != null), validityPeriod);if (pdu != null){HashMap map = getSmsTrackerMap(destAddr, scAddr, text, pdu);SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent,getFormat(),messageUri, isExpectMore,text /* fullMessageText */, true /* isText */,validityPeriod);String carrierPackage = getCarrierAppPackageName();if (carrierPackage != null){Rlog.d(TAG, "Found carrier package.");TextSmsSender smsSender = new TextSmsSender(tracker);smsSender.sendSmsByCarrierApp(carrierPackage,new SmsSenderCallback(smsSender));} else{Rlog.v(TAG, "No carrier package.");sendRawPdu(tracker);}} else{Rlog.e(TAG,"GsmSMSDispatcher.sendText(): getSubmitPdu() returned null");}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

我們會發現,首先將短信組裝成pdu格式,然后進一步將pdu組裝成為一個SmsTracker ,getCarrierAppPackageName():獲取手機內置的載體app,一般手機廠商都不會內置這個,所以一般都會走sendRawPdu()這個方法。這個方法在父類SMSDispatcher 中,所以我們又得去父類看:

protected void sendRawPdu(SmsTracker tracker){HashMap map = tracker.mData;byte pdu[] = (byte[]) map.get("pdu");if (mSmsSendDisabled){Rlog.e(TAG, "Device does not support sending sms.");tracker.onFailed(mContext, RESULT_ERROR_NO_SERVICE, 0/* errorCode */);return;}if (pdu == null){Rlog.e(TAG, "Empty PDU");tracker.onFailed(mContext, RESULT_ERROR_NULL_PDU, 0/* errorCode */);return;}PendingIntent sentIntent = tracker.mSentIntent;// Get calling app package name via UID from Binder callPackageManager pm = mContext.getPackageManager();int callingUid = Binder.getCallingUid();// Special case: We're being proxied by the telephony stack itself,// so use the intent generator's UID if one existsString[] packageNames;if (callingUid == android.os.Process.PHONE_UID && sentIntent != null&& sentIntent.getCreatorPackage() != null){packageNames = new String[]{ sentIntent.getCreatorPackage() };} else{packageNames = pm.getPackagesForUid(callingUid);}if (packageNames == null || packageNames.length == 0){// Refuse to send SMS if we can't get the calling package name.Rlog.e(TAG,"Can't get calling app package name: refusing to send SMS");tracker.onFailed(mContext, RESULT_ERROR_GENERIC_FAILURE, 0/* errorCode */);return;}// Get package info via packagemanagerPackageInfo appInfo;try{// XXX this is lossy- apps can share a UIDappInfo = pm.getPackageInfo(packageNames[0],PackageManager.GET_SIGNATURES);} catch (PackageManager.NameNotFoundException e){Rlog.e(TAG,"Can't get calling app package info: refusing to send SMS");tracker.onFailed(mContext, RESULT_ERROR_GENERIC_FAILURE, 0/* errorCode */);return;}// checkDestination() returns true if the destination is not a premium// short code or the// sending app is approved to send to short codes. Otherwise, a message// is sent to our// handler with the SmsTracker to request user confirmation before// sending.if (checkDestination(tracker)){// check for excessive outgoing SMS usage by this appif (!mUsageMonitor.check(appInfo.packageName, SINGLE_PART_SMS)){sendMessage(obtainMessage(EVENT_SEND_LIMIT_REACHED_CONFIRMATION, tracker));return;}sendSms(tracker);}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

從上面可以得知,先從tracker中取出data,之后再取出pdu。?
判斷短信發送是否可用?
pdu是否為空。?
checkDestination(tracker):判斷內置的short code和發送的short code是否一致。?
然后調用GsmSMSDispatcher的sendSms()方法:

@Overrideprotected void sendSms(SmsTracker tracker){HashMap<String, Object> map = tracker.mData;byte pdu[] = (byte[]) map.get("pdu");if (tracker.mRetryCount > 0){Rlog.d(TAG, "sendSms: " + " mRetryCount=" + tracker.mRetryCount+ " mMessageRef=" + tracker.mMessageRef + " SS="+ mPhone.getServiceState().getState());// per TS 23.040 Section 9.2.3.6: If TP-MTI SMS-SUBMIT (0x01) type// TP-RD (bit 2) is 1 for retry// and TP-MR is set to previously failed sms TP-MRif (((0x01 & pdu[0]) == 0x01)){pdu[0] |= 0x04; // TP-RDpdu[1] = (byte) tracker.mMessageRef; // TP-MR}}Rlog.d(TAG, "sendSms: " + " isIms()=" + isIms() + " mRetryCount="+ tracker.mRetryCount + " mImsRetry=" + tracker.mImsRetry+ " mMessageRef=" + tracker.mMessageRef + " SS="+ mPhone.getServiceState().getState());sendSmsByPstn(tracker);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

可以看到上面需要對短信進行一個處理,判斷重試次數,如果大于0就將第一個字節賦值為0x04 至于為什么是0x04我也不知道,?
TP-RD:是否拒絕相同重復消息。?
TP-MR:消息基準值。?
現在來看GsmSMSDispatcher的sendSmsByPstn()方法又干了些什么事情。

@Overrideprotected void sendSmsByPstn(SmsTracker tracker){int ss = mPhone.getServiceState().getState();// if sms over IMS is not supported on data and voice is not// available...if (!isIms() && ss != ServiceState.STATE_IN_SERVICE){tracker.onFailed(mContext, getNotInServiceError(ss), 0/* errorCode */);return;}HashMap<String, Object> map = tracker.mData;byte smsc[] = (byte[]) map.get("smsc");byte[] pdu = (byte[]) map.get("pdu");Message reply = obtainMessage(EVENT_SEND_SMS_COMPLETE, tracker);// sms over gsm is used:// if sms over IMS is not supported AND// this is not a retry case after sms over IMS failed// indicated by mImsRetry > 0if (0 == tracker.mImsRetry && !isIms()){if (tracker.mRetryCount > 0){// per TS 23.040 Section 9.2.3.6: If TP-MTI SMS-SUBMIT (0x01)// type// TP-RD (bit 2) is 1 for retry// and TP-MR is set to previously failed sms TP-MRif (((0x01 & pdu[0]) == 0x01)){pdu[0] |= 0x04; // TP-RDpdu[1] = (byte) tracker.mMessageRef; // TP-MR}}if (tracker.mRetryCount == 0 && tracker.mExpectMore){mCi.sendSMSExpectMore(IccUtils.bytesToHexString(smsc),IccUtils.bytesToHexString(pdu), reply);} else{mCi.sendSMS(IccUtils.bytesToHexString(smsc),IccUtils.bytesToHexString(pdu), reply);}} else{mCi.sendImsGsmSms(IccUtils.bytesToHexString(smsc),IccUtils.bytesToHexString(pdu), tracker.mImsRetry,tracker.mMessageRef, reply);// increment it here, so in case of SMS_FAIL_RETRY over IMS// next retry will be sent using IMS request again.tracker.mImsRetry++;}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

先是對手機狀態進行判斷,然后又將pdu取出來,然后弄了一個發送完成的消息。?
isIms():判斷IMS是否注冊,和SMS是否支持。自此就開始調用mCi了,也就是CommandsInterface類了,這個類在/frameworks/opt/telephony/src/java/com/android/internal/telephony路徑下,這個就進入額RIL層了,也就不是我研究的范圍了。其實在我修改過中是沒有修改到這一層,基本修改操作基本都是在調用sendRawPdu()方法之前完成。是不是已經腦殼都看糊了?好了,下面我們來看一張圖,總結一下,短信發送的流程!?

自此,希望大家能對短信的發送有個大體的認識,如果需要詳細的了解短信發送的每個細節,可以下載源代碼觀看。結合源代碼,觀看本文效果更佳!



原文地址: http://blog.csdn.net/poison_h/article/details/50972149

總結

以上是生活随笔為你收集整理的Android开发之Android5.1.1(CM12.1)源码中短信发送流程解析的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

主站蜘蛛池模板: 一本视频在线 | aaaaav| 欧美精品一 | 91精品国自产在线偷拍蜜桃 | av片网站 | 中文字幕人妻熟女在线 | 中文字幕 欧美激情 | 一区二区三区精品在线 | 欧美性猛交xxx乱久交 | 精品少妇人妻av一区二区 | 国产精品久久久久久久久久小说 | 亚洲精品成人电影 | 秋霞影院一区二区 | 午夜精品免费视频 | 亚洲国产精品久久 | 亚洲偷偷| 成年人午夜网站 | 亚洲美女av在线 | 中文字幕乱码人妻无码久久95 | 搞黄视频在线观看 | 极品超粉嫩尤物69xx | 日韩欧美久久精品 | 奶水旺盛的少妇在线播放 | 国产精品高潮呻吟久久 | 三级色网站| 麻豆成人在线观看 | 72种无遮挡啪啪的姿势 | 亚洲精品1区 | 亚洲专区欧美专区 | 99久久精品免费看 | 国产精彩视频在线 | 国产一区二| 精品久久中文字幕 | 国产三级全黄 | 国产日本在线观看 | 欧美日韩国产麻豆 | 色戒电影未测减除版 | 足疗店女技师按摩毛片 | 黄网在线看 | 干美女视频| 免费在线看黄网站 | 久久国产视频一区 | 国产精品大全 | 亚洲h | 成人爱爱 | 日韩一区二区免费视频 | 国产91边播边对白在线 | 一级黄色av| 人操人人| 成人毛片av | 看欧美一级片 | 视频在线一区二区三区 | 亚洲天堂手机在线观看 | 在线国产91 | 国产精品视频在线免费观看 | 噼里啪啦免费看 | av福利影院 | 日本做爰全过程免费看 | 小sao货大ji巴cao死你 | 毛片无遮挡 | 成人网在线播放 | 欧洲成人在线 | 夜夜综合网 | 三级黄色视屏 | 国产一区 在线播放 | 国产精品有码 | 中国女人黄色大片 | 国产成人三级在线 | 国产白袜脚足j棉袜在线观看 | 91av福利视频| 欧美在线视频第一页 | 亚洲在线一区二区三区 | 中文二区| 日本91在线 | 亚洲综合图色 | 韩国19主播内部福利vip | 日日狠狠久久偷偷四色综合免费 | 日剧再来一次第十集 | 久久精品成人 | 婷婷射丁香 | 成人在线免费播放 | www.奇米.com | 国产精品视频看看 | 日韩精品一二三四 | a在线免费观看 | 97人人爽人人爽人人爽 | 中文字幕在线看人 | 国产情侣自拍小视频 | 伊伊成人网 | 亚洲色精品三区二区一区 | 国产极品美女高潮无套嗷嗷叫酒店 | 激情综合站 | 本道久久 | 精品一区二区三区四区五区 | 男生插女生视频 | 黄色无遮挡网站 | 欧美xxxx18国产| 黄色片欧美 | 亚洲人午夜射精精品日韩 |