Android SDK 網絡問題解析
Android 客戶端網絡不穩定,會導致App 有時候無法及時收到 Push 消息。
很多開發者認爲這是因爲 JPush 推送不穩定、延遲,甚至有時候認爲 JPush 後臺推送系統出問題了。
本文目的是從各個方面來分析 Android 網絡導致的 JPush 不能正常工作的問題。
JPush 正常工作的必要條件
首先,我們需要知道,JPush SDK 並不是集成到App 後就必然一直工作的。
其正常工作的必要條件是:JPush SDK 與 JPush Server 的網絡保持着連接。請參考這篇文章來做進一步的理解:極光推送技術原理:移動無線網絡長連接。
而 Android 設備的網絡的複雜性、不穩定性,是 Android 設備開發最複雜的地方之一。
另外,每款手機的網絡能力也是千差萬別的。國內很多雜牌手機在網絡方面甚至會有嚴重的問題。大品牌廠商的手機則要好很多。
只要 JPush 的網絡連接是正常的,則:
- JPush 收到消息一定是及時的。其延遲是秒級的,一般在 1 秒之內。如果超過 10 秒,則一定是客戶端網絡出了問題。
- 手機休眠時,也能夠及時地收到推送消息。
部分系統的特殊處理導致問題
MIUI V5 系統
-
自啓動管理:默認情況下,手機開機後,只有系統默認的服務可以啓動起來。除非在自啓動管理界面,設置允許第三方程序自啓動。
-
網絡助手:可以手動禁止已安裝的第三方程序訪問2G/3G和WIFI的網絡和設置以後新安裝程序是否允許訪問2G/3G和WIFI的網絡。
4.0以上的android系統
- 在設置->應用,強行停止 應用程序後該程序無法再自啓動,就算重新開機也一樣,一定要手動開啓才能運行起來。
讓我們從目前得到的反饋來整理調試的思路
手機休眠時收不到 JPush 消息,解鎖或屏幕燈亮則可以成功接收
這個現象表明,手機休眠時,JPush SDK “被迫”與服務器端的網絡失去了連接。
JPush SDK 的工作原理是要確保在手機休眠時也能正常的工作,即休眠時也可以及時地收到Push消息。實際上JPush在大部分上手機上都能達到此效果。
這個“被迫”,是由 Android 設備的環境所導致的。涉及的原因有如下幾個方面:
- 手機本身的網絡設置。標準版本的 Android ROM 是沒有這個設置的,但某些特殊的 ROM 可能會有這方面的設置。
- 手機上的安全、省電工具軟件額外做的事情
上述的特殊機制會關閉網絡。網絡一旦連接上,JPush也會連接上服務器,從而Push消息就會收到。
有時候收到 JPush 消息很及時,有時候則要等幾分鐘
JPush 會監聽網絡切換廣播。當網絡關閉時,把原來JPush連接關閉。當有新的網絡時,創建JPush連接。
另外,RTC會定時發送心跳。如果之前的網絡已經斷了,則會重新連接。
應該說,當前的網絡連接策略還是相對簡單的,這樣做的目的是:省電、省流量。
不 好之處就是:網絡沒有切換時,因爲當時網絡過差,JPush連接會被中斷。這種情況下,就只能等 RTC 心跳去觸發連接。這也是有時候JPush 無法及時接收Push消息的原因。根據網絡條件的不同,出現這個情況的概率也會不同。但據我們自己的測試,90% 的時候是可以及時地收到Push消息的。
JPush 目前在網絡策略方面沒有像微信這種聊天工具做得積極。如果這樣做到,電量和流量的消耗必然會成倍地增加。
完全收不到 JPush 消息
如果集成之後就完全收不到Push消息,則很有可能是某個地方配置錯誤。請根據文檔仔細檢查:Android SDK 集成指南,iOS SDK 集成指南,或者根據參考教程:Android SDK 調試指南,iOS SDK 調試指南。
Android SDK 調試指南
SDK啓動過程
- 檢查AndroidManifest.xml中是否有配置AppKey,如果沒有,則啓動失敗
- 檢查 Androidmanifest.xml文件配置的正確性,必須要保證“Android SDK 集成指南”中所有標註“
- Required”的部分都正確配置,否則啓動失敗
- 檢查 JPush SDK庫文件的有效性,如果庫文件無效,則啓動失敗
- 檢查網絡是否可用,如果網絡可用則連接服務器登錄,否則啓動失敗
- 登陸成功後可以從log中看到如下log
id="iframe_0.1429851669818163" src="data:text/html;charset=utf8,%3Cimg%20id=%22img%22%20src=%22http://docs.jpush.io/client/image/jpush.jpg?_=4685948%22%20style=%22border:none;max-width:939px%22%3E%3Cscript%3Ewindow.onload%20=%20function%20()%20%7Bvar%20img%20=%20document.getElementById('img');%20window.parent.postMessage(%7BiframeId:'iframe_0.1429851669818163',width:img.width,height:img.height%7D,%20'http://www.cnblogs.com');%7D%3C/script%3E" frameborder="0" scrolling="no" style="border-style: none; width: 18px; height: 20px;">
測試確認
- 確認 Androidmanifest.xml 中所需的所有 “Required” 項都已經添加。如果有 "Required" 項未添加,日誌會提示錯誤。
- 確認 AppKey (在Portal上生成的) 已經正確的寫入 Androidmanifest.xml 中,沒寫會有日誌提示錯誤。
- 確認在程序啓動時候調用了init(context) 接口
- 確認測試手機(或者模擬器)的網絡可用,如果網絡正常可用,客戶端調用 init 後不久,應有登錄成功(Login succeed)的日誌信息,如 SDK 啓動過程所示
- 啓動應用程序,登陸 Portal 系統,並嚮應用程序發送自定義消息或者通知欄提示。在幾秒內,客戶端應可收到下發的通知或者正定義消息.
別名與標籤使用教程
爲什麼需要別名與標籤
推送消息時,要指定推送的對象:全部,某一個人,或者某一羣人。
全部很好辦,針對某應用“羣發”就好了。Portal與API都支持向指定的 appKey 羣發消息。
要指定向某一個特定的人,或者某一羣特定的人,則相對複雜。因爲對於 JPush 來說,某一個人就是一個註冊ID,這個註冊ID與開發者App沒有任何關係,或者說對開發者App是沒有意義的。
如果要對開發者App有意義的某個特定的用戶推送消息,則需要:把 JPush 註冊用戶與開發者App 用戶綁定起來。
這個綁定有兩個基本思路:
- 把綁定關係保存到 JPush 服務器端
- 把綁定關係保存到開發者應用服務器中
前者,就是這裏要說到的:別名與標籤的功能。這個機制簡單易用,適用於大多數開發者。
後者,則是 JPush 提供的另外一套 RegistrationID 機制。這套機制開發者需要有應用服務器來維護綁定關係,不適用於普通開發者。Android SDK r1.6.0 版本開始支持。
使用方式
別名與標籤的機制,其工作方式是:
- 客戶端開發者App調用 setAliasAndTags API 來設置關係
- JPush SDK 把該關係設置保存到 JPush Server 上
- 在服務器端推送消息時,指定向之前設置過的別名或者標籤推送
SDK 支持的 setAliasAndTags 請參考相應的文檔:別名與標籤 API
使用過程中有幾個點做特別說明:
-
App 調用 SDK setAliasAndTags API 時,r1.5.0 版本提供了 Callback 來返回設置狀態。如果返回 6002 (超時)則建議重試
- 老版本沒有提供 Callback 無設置狀態返回,從而沒有機制確定一定成功。建議升級到新版本
-
Portal 上推送或者 API 調用向別名或者標籤推送時,可能會報錯:不存在推送目標用戶。該報錯表明,JPush Server 上還沒有針對你所推送的別名或者標籤的用戶綁定關係,所以沒有推送目標。這時請開發者檢查確認,開發者App是否正確地調用了 setAliasAndTags API,以及調用時是否網絡不好,JPush SDK 暫時未能保存成功。
使用別名
用於給某特定用戶推送消息。
所謂別名,可以近似地被認爲,是用戶帳號裏的暱稱。
使用標籤
用於給某一羣人推送消息。
標籤類似於博客裏爲文章打上 tag ,即爲某資源分類。
動態標籤
JPush 提供的設置標籤的 API 是在客戶端的。開發者如何做到在自己的服務器端動態去設置分組呢? 比如一個企業OA系統,經常需要去變更部門人員分組。以下是大概的思路:
- 設計一種自定義消息格式(業務協議),App解析後可以調用 JPush SDK setAliasAndTags API 來重新設置標籤(分組)
- 例:{"action":"resetTags", "newTags":["dep_level_1":"A公司", "dep_level_2":"技術部", "dep_level_3":"Android開發組", "address":"深圳", "lang":"zh"]}
- 要動態設置分組時,推送這條自定義消息給指定的用戶
- 使用別名的機制,推送到指定的用戶。
- 客戶端App 調用 JPush SDK API 來設置新的標籤
別名與標籤設置異常處理
由於網絡連接不穩定的原因,有一定的概率 JPush SDK 設置別名與標籤會失敗。
App 開發者合理地處理設置失敗,則偶爾失敗對應用的正常使用 JPush 影響是有限的。
以下以 Android SDK 作爲示例。
基本思路:
- 設置成功時,往 SharePreference 裏寫狀態,以後不必再設置
-
遇到 6002 超時,則稍延遲重試。
// 這是來自 JPush Example 的設置別名的 Activity 裏的代碼。一般 App 的設置的調用入口,在任何方便的地方調用都可以。 private void setAlias() { EditText aliasEdit = (EditText) findViewById(R.id.et_alias); String alias = aliasEdit.getText().toString().trim(); if (TextUtils.isEmpty(alias)) { Toast.makeText(PushSetActivity.this,R.string.error_alias_empty, Toast.LENGTH_SHORT).show(); return; } if (!ExampleUtil.isValidTagAndAlias(alias)) { Toast.makeText(PushSetActivity.this,R.string.error_tag_gs_empty, Toast.LENGTH_SHORT).show(); return; } // 調用 Handler 來異步設置別名 mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_ALIAS, alias)); } private final TagAliasCallback mAliasCallback = new TagAliasCallback() { @Override public void gotResult(int code, String alias, Set<String> tags) { String logs ; switch (code) { case 0: logs = "Set tag and alias success"; Log.i(TAG, logs); // 建議這裏往 SharePreference 裏寫一個成功設置的狀態。成功設置一次後,以後不必再次設置了。 break; case 6002: logs = "Failed to set alias and tags due to timeout. Try again after 60s."; Log.i(TAG, logs); // 延遲 60 秒來調用 Handler 設置別名 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SET_ALIAS, alias), 1000 * 60); break; default: logs = "Failed with errorCode = " + code; Log.e(TAG, logs); } ExampleUtil.showToast(logs, getApplicationContext()); } }; private static final int MSG_SET_ALIAS = 1001; private final Handler mHandler = new Handler() { @Override public void handleMessage(android.os.Message msg) { super.handleMessage(msg); switch (msg.what) { case MSG_SET_ALIAS: Log.d(TAG, "Set alias in handler."); // 調用 JPush 接口來設置別名。 JPushInterface.setAliasAndTags(getApplicationContext(), (String) msg.obj, null, mAliasCallback); break; default: Log.i(TAG, "Unhandled msg - " + msg.what); } } };
自定義通知欄樣式教程
關於自定義通知欄樣式
JPush 通知推送到客戶端時,默認使用手機的默認設置來顯示通知欄,包括鈴聲、震動等效果。
如果開發者想要達到如下的效果,則需要使用“自定義通知欄樣式”功能:
- 通知欄樣式使用與默認不一樣的設置,比如想要控制:
- 鈴聲、震動
- 顯示圖標
- 替換默認的通知欄樣式。
推送消息指定通知欄樣式編號
通知欄樣式在服務器端向下推送時,只體現爲一個編號(數字)。
推送通知的樣式編號,應該是在客戶端做了自定義通知欄樣式設置的。
如果通知上的樣式編號,在客戶端檢查不存在,則使用默認的通知欄樣式。
開發者不自定義通知欄樣式時,則此編號默認爲 0。
開發者自定義的通知欄樣式編號應大於 0,小於 1000。
在 Portal 上發送通知時,最下邊的“可選”部分展開,開發者可指定當前要推送的通知的樣式編號。如下圖所示:
id="iframe_0.08811786910519004" src="data:text/html;charset=utf8,%3Cimg%20id=%22img%22%20src=%22http://docs.jpush.io/client/image/image2012-11-6_9_16_45.png?_=4685948%22%20style=%22border:none;max-width:939px%22%3E%3Cscript%3Ewindow.onload%20=%20function%20()%20%7Bvar%20img%20=%20document.getElementById('img');%20window.parent.postMessage(%7BiframeId:'iframe_0.08811786910519004',width:img.width,height:img.height%7D,%20'http://www.cnblogs.com');%7D%3C/script%3E" frameborder="0" scrolling="no" style="border-style: none; width: 18px; height: 20px;">
客戶端定義通知欄樣式
自定義的通知欄樣式,是在客戶端進行的。請參考 通知欄樣式定製API 來看所支持的功能。
自定義通知欄樣式設計
- 有個 PushNotificationBuilder 概念,開發者使用 setPushNotificationBuilder 方法爲某種類型的 PushNotificationBuilder 指定編號。
- setPushNotificationBuilder 可以在 JPushInterface.init() 之後任何地方調用,可以是開發者應用的邏輯來觸發調用,或者初始化時調用。
- 只需要設置一次,JPush SDK 會記住這個設置。在下次收到推送通知時,就根據通知裏指定的編號來找到 PushNotificationBuilder 來展現、執行。
API - setDefaultPushNotificationBuilder 設置默認
此 API 改變默認的編號爲 0 的通知欄樣式。
API - setPushNotificationBuilder 指定編號
此 API 爲開發者指定的編號,設置一個自定義的 PushNotificationBuilder(通知樣式構建器)。
Example - 基礎的 PushNotificationBuilder
定製聲音、震動、閃燈等 Notification 樣式。
BasicPushNotificationBuilder builder = new BasicPushNotificationBuilder(MainActivity.this);
builder.statusBarDrawable = R.drawable.jpush_notification_icon;
builder.notificationFlags = Notification.FLAG_AUTO_CANCEL; //設置爲自動消失
builder.notificationDefaults = Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS; // 設置爲鈴聲與震動都要
JPushInterface.setPushNotificationBuilder(1, builder);
Example - 高級自定義的 PushNotificationBuilder
基於基礎的 PushNotificationBuilder,可進一步地定製 Notification 的 Layout。
這裏作爲 example 的 customer_notitfication_layout 在我們的 example 項目的 /res/layout/ 下可以找到。你完全可以用自己的 layout。
CustomPushNotificationBuilder builder = new
CustomPushNotificationBuilder(MainActivity.this,
R.layout.customer_notitfication_layout,
R.id.icon,
R.id.title,
R.id.text);
// 指定定製的 Notification Layout
builder.statusBarDrawable = R.drawable.your_notification_icon;
// 指定最頂層狀態欄小圖標
builder.layoutIconDrawable = R.drawable.your_2_notification_icon;
// 指定下拉狀態欄時顯示的通知圖標
JPushInterface.setPushNotificationBuilder(2, builder);
通知欄樣式定義不符合要求?
以上提供的自定義通知欄樣式的功能是有限的。比如:Android SDK 4.0 以後的 Notification 支持指定 Style ,而這種複雜的通知樣式定義 JPush SDK 還未有支持。
或者你想要自定義的複雜的通知樣式,但不願意使用上述高級的自定義通知欄定製功能。
建議不要使用 JPush 提供的通知功能,而使用自定義消息功能。
即:推送自定義消息到客戶端後,App取到自定義消息全部內容,然後App自己來寫代碼做通知的展示。請參考文檔:通知 vs. 自定義消息。
通知 vs 自定義消息
極光推送包含有通知與自定義消息兩種類型的推送。本文描述他們的區別,以及建議的應用場景。
兩者的區別 - 功能角度
通知
或者說 Push Notification,即指在手機的通知欄(狀態欄)上會顯示的一條通知信息。這是 Android / iOS 的基本功能。
一條通知,簡單的填寫純文本的通知內容即可。
通知主要用於提示用戶的目的。應用加上通知功能,有利於提高應用的活躍度。
自定義消息
是極光推送自己的概念。
自定義消息不是通知,所以不會被SDK展示到通知欄上。其內容完全由開發者自己定義。
自定義消息主要用於應用的內部業務邏輯。一條自定義消息推送過來,有可能沒有任何界面顯示。
本質上:
自定義消息是原始的消息,JPush SDK 不做處理。而通知,則 JPush SDK 會做通知展示處理,其目的是爲了減輕開發人員的工作量。
所以,如果通知功能不太符合您的需求,你都可以使用自定義消息來實現(客戶端展現App自己來做)。
兩者的區別 - 開發者使用角度
通知
簡單場景下的通知,用戶可以不寫一行代碼,而完全由 SDK 來負責默認的效果展示,以及默認用戶點擊時打開應用的主界面。
JPush Android SDK 提供了 API 讓開發者來定製通知欄的效果,請參考:自定義通知欄樣式教程;也提供了 接收推送消息Receiver 讓你來定製在收到通知時與用戶點擊通知時的不同行爲。
自定義消息
SDK 不會把自定義消息展示到通知欄。
所以調試時,需要到日誌裏纔可以看到服務器端推送的自定義消息。
自定義消息一定要由開發者寫 接收推送消息Receiver 來處理收到的消息。
注意:
當自定義消息內容msg_content爲空時,SDK不會對消息進行廣播,使得app無法接收到推送的消息,因此建議在使用自定義消息推送時添
加內容
使用通知
請參考以下示例代碼。
public class MyReceiver extends BroadcastReceiver {
private static final String TAG = "MyReceiver";
private NotificationManager nm;
@Override
public void onReceive(Context context, Intent intent) {
if (null == nm) {
nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
Bundle bundle = intent.getExtras();
Logger.d(TAG, "onReceive - " + intent.getAction() + ", extras: " + AndroidUtil.printBundle(bundle));
if (JPushInterface.ACTION_REGISTRATION_ID.equals(intent.getAction())) {
Logger.d(TAG, "JPush用戶註冊成功");
} else if (JPushInterface.ACTION_MESSAGE_RECEIVED.equals(intent.getAction())) {
Logger.d(TAG, "接受到推送下來的自定義消息");
} else if (JPushInterface.ACTION_NOTIFICATION_RECEIVED.equals(intent.getAction())) {
Logger.d(TAG, "接受到推送下來的通知");
receivingNotification(context,bundle);
} else if (JPushInterface.ACTION_NOTIFICATION_OPENED.equals(intent.getAction())) {
Logger.d(TAG, "用戶點擊打開了通知");
openNotification(context,bundle);
} else {
Logger.d(TAG, "Unhandled intent - " + intent.getAction());
}
}
private void receivingNotification(Context context, Bundle bundle){
String title = bundle.getString(JPushInterface.EXTRA_NOTIFICATION_TITLE);
Logger.d(TAG, " title : " + title);
String message = bundle.getString(JPushInterface.EXTRA_ALERT);
Logger.d(TAG, "message : " + message);
String extras = bundle.getString(JPushInterface.EXTRA_EXTRA);
Logger.d(TAG, "extras : " + extras);
}
private void openNotification(Context context, Bundle bundle){
String extras = bundle.getString(JPushInterface.EXTRA_EXTRA);
String myValue = "";
try {
JSONObject extrasJson = new JSONObject(extras);
myValue = extrasJson.optString("myKey");
} catch (Exception e) {
Logger.w(TAG, "Unexpected: extras is not a valid json", e);
return;
}
if (TYPE_THIS.equals(myValue)) {
Intent mIntent = new Intent(context, ThisActivity.class);
mIntent.putExtras(bundle);
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(mIntent);
} else if (TYPE_ANOTHER.equals(myValue)){
Intent mIntent = new Intent(context, AnotherActivity.class);
mIntent.putExtras(bundle);
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(mIntent);
}
}
}
使用自定義消息
使用自定義消息,在客戶端App裏一定要寫代碼,去接受 JPush SDK 的廣播,從而取得推送下來的消息內容。具體請參考文檔:接收推送消息Receiver。
以下代碼來自於推聊。
public class TalkReceiver extends BroadcastReceiver {
private static final String TAG = "TalkReceiver";
private NotificationManager nm;
@Override
public void onReceive(Context context, Intent intent) {
if (null == nm) {
nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
Bundle bundle = intent.getExtras();
Logger.d(TAG, "onReceive - " + intent.getAction() + ", extras: " + AndroidUtil.printBundle(bundle));
if (JPushInterface.ACTION_REGISTRATION_ID.equals(intent.getAction())) {
Logger.d(TAG, "JPush用戶註冊成功");
} else if (JPushInterface.ACTION_MESSAGE_RECEIVED.equals(intent.getAction())) {
Logger.d(TAG, "接受到推送下來的自定義消息");
// Push Talk messages are push down by custom message format
processCustomMessage(context, bundle);
} else if (JPushInterface.ACTION_NOTIFICATION_RECEIVED.equals(intent.getAction())) {
Logger.d(TAG, "接受到推送下來的通知");
receivingNotification(context,bundle);
} else if (JPushInterface.ACTION_NOTIFICATION_OPENED.equals(intent.getAction())) {
Logger.d(TAG, "用戶點擊打開了通知");
openNotification(context,bundle);
} else {
Logger.d(TAG, "Unhandled intent - " + intent.getAction());
}
}
private void processCustomMessage(Context context, Bundle bundle) {
String title = bundle.getString(JPushInterface.EXTRA_TITLE);
String message = bundle.getString(JPushInterface.EXTRA_MESSAGE);
if (StringUtils.isEmpty(title)) {
Logger.w(TAG, "Unexpected: empty title (friend). Give up");
return;
}
boolean needIncreaseUnread = true;
if (title.equalsIgnoreCase(Config.myName)) {
Logger.d(TAG, "Message from myself. Give up");
needIncreaseUnread = false;
if (!Config.IS_TEST_MODE) {
return;
}
}
String channel = null;
String extras = bundle.getString(JPushInterface.EXTRA_EXTRA);
try {
JSONObject extrasJson = new JSONObject(extras);
channel = extrasJson.optString(Constants.KEY_CHANNEL);
} catch (Exception e) {
Logger.w(TAG, "Unexpected: extras is not a valid json", e);
}
// Send message to UI (Webview) only when UI is up
if (!Config.isBackground) {
Intent msgIntent = new Intent(MainActivity.MESSAGE_RECEIVED_ACTION);
msgIntent.putExtra(Constants.KEY_MESSAGE, message);
msgIntent.putExtra(Constants.KEY_TITLE, title);
if (null != channel) {
msgIntent.putExtra(Constants.KEY_CHANNEL, channel);
}
JSONObject all = new JSONObject();
try {
all.put(Constants.KEY_TITLE, title);
all.put(Constants.KEY_MESSAGE, message);
all.put(Constants.KEY_EXTRAS, new JSONObject(extras));
} catch (JSONException e) {
}
msgIntent.putExtra("all", all.toString());
context.sendBroadcast(msgIntent);
}
String chatting = title;
if (!StringUtils.isEmpty(channel)) {
chatting = channel;
}
String currentChatting = MyPreferenceManager.getString(Constants.PREF_CURRENT_CHATTING, null);
if (chatting.equalsIgnoreCase(currentChatting)) {
Logger.d(TAG, "Is now chatting with - " + chatting + ". Dont show notificaiton.");
needIncreaseUnread = false;
if (!Config.IS_TEST_MODE) {
return;
}
}
if (needIncreaseUnread) {
unreadMessage(title, channel);
}
NotificationHelper.showMessageNotification(context, nm, title, message, channel);
}
// When received message, increase unread number for Recent Chat
private void unreadMessage(final String friend, final String channel) {
new Thread() {
public void run() {
String chattingFriend = null;
if (StringUtils.isEmpty(channel)) {
chattingFriend = friend;
}
Map<String, String> params = new HashMap<String, String>();
params.put("udid", Config.udid);
params.put("friend", chattingFriend);
params.put("channel_name", channel);
try {
HttpHelper.post(Constants.PATH_UNREAD, params);
} catch (Exception e) {
Logger.e(TAG, "Call pushtalk api to report unread error", e);
}
}
}.start();
}
}