/** * */ package org.orange.familylink.fragment; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.SortedMap; import java.util.TreeMap; import org.orange.familylink.R; import org.orange.familylink.data.Message; import org.orange.familylink.data.Message.Code; import org.orange.familylink.data.MessageLogRecord.Direction; import org.orange.familylink.data.MessageLogRecord.Status; import org.orange.familylink.data.Settings; import org.orange.familylink.data.UrgentMessageBody; import org.orange.familylink.database.Contract; import org.orange.familylink.fragment.MessagesFragment.MessagesSender.MessageWrapper; import org.orange.familylink.sms.SmsMessage; import android.app.ListFragment; import android.app.LoaderManager; import android.app.LoaderManager.LoaderCallbacks; import android.content.AsyncQueryHandler; import android.content.ContentUris; import android.content.Context; import android.content.CursorLoader; import android.content.Loader; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.text.format.DateFormat; import android.util.SparseBooleanArray; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView.MultiChoiceModeListener; import android.widget.CursorAdapter; import android.widget.ImageView; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.google.gson.Gson; /** * 日志{@link ListFragment} * @author Team Orange */ public class MessagesFragment extends ListFragment { /** 参数Key:需要选中的消息IDs */ public static final String ARGUMENT_KEY_IDS = MessagesFragment.class.getName() + ".argument.IDS"; /** 参数Key:需要设置的 <em>消息状态</em> 筛选条件,用{@link Status}设置 */ public static final String ARGUMENT_KEY_STATUS = MessagesFragment.class.getName() + ".argument.STATUS"; /** 参数Key:需要设置的 <em>消息方向</em> 筛选条件,用{@link Direction}设置 */ public static final String ARGUMENT_KEY_DIRECTION = MessagesFragment.class.getName() + ".argument.DIRECTION"; private static final String STATE_CHECKED_ITEM_IDS = MessagesFragment.class.getName() + ".state.CHECKED_ITEM_IDS"; private static final int LOADER_ID_CONTACTS = 1; private static final int LOADER_ID_LOG = 2; /** 当前启动的{@link ActionMode};如果没有启动,则为null */ private ActionMode mActionMode; /** 最近选中的消息的IDs,用于恢复之前的选中状态。仅在{@link #mActionMode} != null时有效 */ private long[] mCheckedItemids; /** 用于把联系人ID映射为联系人名称的{@link Map} */ private Map<Long, String> mContactIdToNameMap; /** 用于显示消息日志的{@link ListView}的{@link ListAdapter} */ private CursorAdapter mAdapterForLogList; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // 尝试恢复选中状态 if(savedInstanceState != null) { long[] ids = savedInstanceState.getLongArray(STATE_CHECKED_ITEM_IDS); if(ids != null) mCheckedItemids = ids; } // 处理Fragment的参数 Bundle arguments = getArguments(); if(arguments != null) { long[] argumentIds = arguments.getLongArray(ARGUMENT_KEY_IDS); if(argumentIds != null) { mCheckedItemids = argumentIds; } } // Give some text to display if there is no data. setEmptyText(getResources().getText(R.string.no_message_record)); // 设置ListView ListView listView = getListView(); // 设置ListView为多选模式 listView.setItemsCanFocus(false); listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); listView.setMultiChoiceModeListener(mMultiChoiceModeListener); // 设置ListView的FastScrollBar listView.setFastScrollEnabled(true); listView.setFastScrollAlwaysVisible(false); listView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_LEFT); // We have a menu item to show in action bar. setHasOptionsMenu(true); // Create an empty adapter we will use to display the loaded data. mAdapterForLogList = new LogAdapter(getActivity(), null, 0); setListAdapter(mAdapterForLogList); // Start out with a progress indicator. setListShown(false); // Prepare the loader. Either re-connect with an existing one, // or start a new one. LoaderManager loaderManager = getLoaderManager(); loaderManager.initLoader(LOADER_ID_CONTACTS, null, mLoaderCallbacksForContacts); loaderManager.initLoader(LOADER_ID_LOG, null, mLoaderCallbacksForLogList); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if(mActionMode != null) outState.putLongArray(STATE_CHECKED_ITEM_IDS, getListView().getCheckedItemIds()); else outState.putLongArray(STATE_CHECKED_ITEM_IDS, null); } @Override public void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); if(mActionMode == null) l.setItemChecked(position, true); //触发ActionModes } /** * 选中指定ID的日志消息记录 * <p> * <strong>Note</strong>:此方法 <em>不会</em> 自动清除以前选中的内容,只会选中指定的消息(如果有的话) * @param ids 应当选中的消息(如果有的话)的ID * @return 实际选中的消息的个数 * @see #setItemsCheckedByIds(long[]) */ public int checkItemsByIds(long[] ids) { ListView listView = getListView(); if(listView == null || mAdapterForLogList == null || mAdapterForLogList.isEmpty() || ids == null || ids.length == 0) return 0; int counter = 0; // 对ids、LogItems排序 SortedMap<Long, Integer> items = new TreeMap<Long, Integer>(); for(int i = 0 ; i < mAdapterForLogList.getCount() ; i++) { items.put(mAdapterForLogList.getItemId(i), i); } Arrays.sort(ids); // 依次序比较ids和items中的元素 int idIndex = 0; Iterator<Entry<Long, Integer>> iterator = items.entrySet().iterator(); long id = ids[idIndex++]; // idIndex和iterator类似指针,id和item类似上个元素的值 Entry<Long, Integer> item = iterator.next(); while(idIndex < ids.length && iterator.hasNext()) { if(id == item.getKey()) { listView.setItemChecked(item.getValue(), true); counter++; id = ids[idIndex++]; item = iterator.next(); } else if(id > item.getKey()) { item = iterator.next(); } else { // id < item.getKey() id = ids[idIndex++]; } } // id或者item是最后一个元素,比较一下这个边界元素(如果另一个还有,可能还需比较后续元素) if(id == item.getKey()) { listView.setItemChecked(item.getValue(), true); counter++; } else if(id > item.getKey()) { // 尝试后移item while(id > item.getKey() && iterator.hasNext()) { item = iterator.next(); if(id == item.getKey()) { listView.setItemChecked(item.getValue(), true); counter++; } } } else { // id < item.getKey() while(id < item.getKey() && idIndex < ids.length) { id = ids[idIndex++]; if(id == item.getKey()) { listView.setItemChecked(item.getValue(), true); counter++; } } } return counter; } /** * 选中指定ID的日志消息记录,并移动到第一个选中的消息处 * <p> * <strong>Note</strong>:此方法 <em>会</em> 自动清除以前选中的内容,再选中指定的消息(如果有的话) * @param ids 应当选中的消息(如果有的话)的ID * @return 实际选中的消息的个数 * @see #checkItemsByIds(long[]) */ public int setItemsCheckedByIds(long[] ids) { ListView listView = getListView(); if(listView == null) return 0; listView.clearChoices(); int checkedCount = checkItemsByIds(ids); if(mActionMode != null) mMultiChoiceModeListener.updateTitle(mActionMode); if(checkedCount >= 1) { List<Integer> positions = getCheckedItemPositions(); int min = positions.get(0); for(Integer position : positions) { if(min > position) min = position; } if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) listView.smoothScrollToPositionFromTop(min, 0); else listView.smoothScrollToPosition(min); } return checkedCount; } /** 取得选中的Items的位置 */ public List<Integer> getCheckedItemPositions() { List<Integer> checkeditems = new ArrayList<Integer>(); ListView listView = getListView(); if(listView == null) return checkeditems; SparseBooleanArray checkedPositionsBool = listView.getCheckedItemPositions(); if(checkedPositionsBool == null) return checkeditems; for (int i = 0; i < checkedPositionsBool.size(); i++) { if (checkedPositionsBool.valueAt(i)) { checkeditems.add(checkedPositionsBool.keyAt(i)); } } return checkeditems; } private int getImportantCode(Integer code) { if(code == null) { return R.string.undefined; } else if(Code.isInform(code)){ if(Code.Extra.Inform.hasSetUrgent(code)) return R.string.urgent; else if(Code.Extra.Inform.hasSetRespond(code)) return R.string.respond; else if(Code.Extra.Inform.hasSetPulse(code)) return R.string.pulse; else return R.string.inform; } else if(Code.isCommand(code)){ if(Code.Extra.Command.hasSetLocateNow(code)) return R.string.locate_now; else return R.string.command; } else if(!Code.isLegalCode(code)) { return R.string.illegal_code; } else { throw new IllegalArgumentException("May be method Code.isLegalCode() not correct"); } } private String valueOfUrgentMessageBodyType(UrgentMessageBody.Type type) { if(type == null) return getString(R.string.undefined); switch (type) { case SEEK_HELP: return getString(R.string.seek_help); case FALL_DOWN_ALARM: return getString(R.string.fall_down_alarm); default: throw new UnsupportedOperationException("unsupported type: " + type); } } private String valueOfSendStatus(Status status) { if(status == null || status.getDirection() != Direction.SEND) return ""; switch(status) { case SENDING: return getString(R.string.sending); case SENT: return getString(R.string.sent); case DELIVERED: return getString(R.string.delivered); case FAILED_TO_SEND: return getString(R.string.failed_to_send); default: throw new UnsupportedOperationException("unsupport "+status+" now."); } } protected final LoaderCallbacks<Cursor> mLoaderCallbacksForContacts = new LoaderCallbacks<Cursor>(){ private final Uri baseUri = Contract.Contacts.CONTACTS_URI;; private final String[] projection = {Contract.Contacts._ID, Contract.Contacts.COLUMN_NAME_NAME}; private static final String sortOrder = Contract.Contacts.COLUMN_NAME_NAME; @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { // One callback only to one loader, so we don't care about the ID. return new CursorLoader(getActivity(), baseUri, projection, null, null, sortOrder); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { // setup Map Map<Long, String> id2nameNew = new HashMap<Long, String>(data.getCount()); int indexId = data.getColumnIndex(Contract.Contacts._ID); int indexName = data.getColumnIndex(Contract.Contacts.COLUMN_NAME_NAME); data.moveToPosition(-1); while(data.moveToNext()) { id2nameNew.put(data.getLong(indexId), data.getString(indexName)); } mContactIdToNameMap = id2nameNew; mAdapterForLogList.notifyDataSetChanged(); } @Override public void onLoaderReset(Loader<Cursor> loader) { mContactIdToNameMap = null; } }; protected final LoaderCallbacks<Cursor> mLoaderCallbacksForLogList = new LoaderCallbacks<Cursor>() { private final Uri baseUri = Contract.Messages.MESSAGES_URI; private final String[] projection = { Contract.Messages._ID, Contract.Messages.COLUMN_NAME_ADDRESS, Contract.Messages.COLUMN_NAME_BODY, Contract.Messages.COLUMN_NAME_CODE, Contract.Messages.COLUMN_NAME_CONTACT_ID, Contract.Messages.COLUMN_NAME_STATUS, Contract.Messages.COLUMN_NAME_TIME }; private final String sortOrder = Contract.Messages.COLUMN_NAME_TIME + " DESC"; @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { // This class only has one Loader, so we don't care about the ID. // 根据当前筛选条件,构造where子句 String selection = null; Bundle arguments = getArguments(); if(arguments != null) { if(arguments.containsKey(ARGUMENT_KEY_STATUS)) selection = Contract.Messages.getWhereClause( (Status) arguments.getSerializable(ARGUMENT_KEY_STATUS)); else if(arguments.containsKey(ARGUMENT_KEY_DIRECTION)) selection = Contract.Messages.getWhereClause( (Direction) arguments.getSerializable(ARGUMENT_KEY_DIRECTION)); } // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. return new CursorLoader(getActivity(), baseUri, projection, selection, null, sortOrder); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) Cursor oldCursor = mAdapterForLogList.swapCursor(data); // The list should now be shown. if (isResumed()) { setListShown(true); } else { setListShownNoAnimation(true); } // 如果处于多选状态或第一次获得日志信息,尝试恢复以前的状态 if((mActionMode != null || oldCursor == null) && mCheckedItemids != null) { setItemsCheckedByIds(mCheckedItemids); mCheckedItemids = null; } } @Override public void onLoaderReset(Loader<Cursor> loader) { // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed. We need to make sure we are no // longer using it. mAdapterForLogList.swapCursor(null); } }; protected final LogMultiChoiceModeListener mMultiChoiceModeListener = new LogMultiChoiceModeListener(); protected class LogMultiChoiceModeListener implements MultiChoiceModeListener { private int mUnretransmittableCount = 0; private MessagesSender mMessagesSender = null; private List<AsyncQueryHandler> mDeletehandlers = new LinkedList<AsyncQueryHandler>(); @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { mActionMode = mode; // Inflate the menu for the contextual action bar MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.fragment_log_action_mode, menu); return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { // Here you can perform updates to the CAB due to // an invalidate() request MenuItem retransmit = menu.findItem(R.id.retransmit); if(canRetransmit()) { retransmit.setVisible(true); retransmit.setEnabled(true); } else { retransmit.setVisible(false); retransmit.setEnabled(false); } return true; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { // Respond to clicks on the actions in the CAB switch (item.getItemId()) { case R.id.retransmit: if(mMessagesSender == null) retransmitSelectedItems(); else { Toast.makeText( getActivity(), R.string.prompt_one_retransmission_at_a_time, Toast.LENGTH_LONG) .show(); return true; } break; case R.id.delete: deletetSelectedItems(); break; default: return false; } mode.finish(); return true; } @Override public void onDestroyActionMode(ActionMode mode) { // Here you can make any necessary updates to the activity when // the CAB is removed. By default, selected items are deselected/unchecked. mActionMode = null; } @Override public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { updateTitle(mode); Cursor cursor = (Cursor) getListView().getItemAtPosition(position); String statusString = cursor.getString( cursor.getColumnIndex(Contract.Messages.COLUMN_NAME_STATUS)); Status status = statusString != null ? Status.valueOf(statusString) : null; if(!isRetransmittable(status)) { boolean oldCanRetransmit = canRetransmit(); if(checked) mUnretransmittableCount++; else mUnretransmittableCount--; boolean newCanRetransmit = canRetransmit(); if(oldCanRetransmit != newCanRetransmit) mode.invalidate(); } } public void updateTitle(ActionMode mode) { int count = getListView().getCheckedItemCount(); String title = getString( count != 1 ? R.string.checked_n_messages : R.string.checked_one_message, count); mode.setTitle(title); } protected boolean isRetransmittable(Status status) { return status == Status.FAILED_TO_SEND; } protected boolean canRetransmit() { return mUnretransmittableCount == 0; } protected void retransmitSelectedItems() { List<Integer> items = getCheckedItemPositions(); MessageWrapper[] messages = new MessageWrapper[items.size()]; int index = 0; for(int position : items) { MessageWrapper message = new MessageWrapper(); message.message = new SmsMessage(); Cursor cursor = (Cursor) mAdapterForLogList.getItem(position); int indexId = cursor.getColumnIndex(Contract.Messages._ID); int indexCode = cursor.getColumnIndex(Contract.Messages.COLUMN_NAME_CODE); int indexBody = cursor.getColumnIndex(Contract.Messages.COLUMN_NAME_BODY); int indexDest = cursor.getColumnIndex(Contract.Messages.COLUMN_NAME_ADDRESS); if(!cursor.isNull(indexCode)) message.message.setCode(cursor.getInt(indexCode)); message.message.setBody(cursor.getString(indexBody)); message.dest = cursor.getString(indexDest); // 设置Uri long _id = cursor.getLong(indexId); Uri uri = Contract.Messages.MESSAGES_ID_URI; uri = ContentUris.withAppendedId(uri, _id); message.uri = uri; messages[index++] = message; } mMessagesSender = new MessagesSender(getActivity()) { @Override protected void onPostExecute(Void result) { super.onPostExecute(result); mMessagesSender = null; } }; mMessagesSender.execute(messages); } protected void deletetSelectedItems() { // 构造删除条件(where子句) long[] ids = getListView().getCheckedItemIds(); if(ids == null) throw new NullPointerException("ids is null"); if(ids.length == 0) return; StringBuilder sb = new StringBuilder(); for(long id : ids) sb.append(id + ","); sb.deleteCharAt(sb.length() - 1); String selection = Contract.Messages._ID + " IN ( " + sb.toString() + " )"; AsyncQueryHandler handler = new AsyncQueryHandler( getActivity().getContentResolver()) { @Override protected void onDeleteComplete(int token, Object cookie, int result) { mDeletehandlers.remove(this); if(mDeletehandlers.isEmpty()) getActivity().setProgressBarIndeterminateVisibility(false); Toast.makeText( getActivity(), getString(R.string.prompt_delete_messages_successfully, result), Toast.LENGTH_LONG) .show(); } }; mDeletehandlers.add(handler); getActivity().setProgressBarIndeterminateVisibility(true); handler.startDelete(-1, null, Contract.Messages.MESSAGES_URI, selection, null); } } protected class LogAdapter extends CursorAdapter { private final Context mContext; private final LayoutInflater mInflater; public LogAdapter(Context context, Cursor c, int flags) { super(context, c, flags); mContext = context; mInflater = LayoutInflater.from(context); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { View rootView = mInflater.inflate(R.layout.fragment_messages_list_item, parent, false); // Creates a ViewHolder and store references to the two children views // we want to bind data to. ViewHolder holder = new ViewHolder(); holder.senderAndReceiver = (TextView) rootView.findViewById(R.id.sender_and_receiver); holder.body = (TextView) rootView.findViewById(R.id.body); holder.send_status = (TextView) rootView.findViewById(R.id.send_status); holder.date = (TextView) rootView.findViewById(R.id.date); holder.type_icon = (ImageView) rootView.findViewById(R.id.type_icon); rootView.setTag(holder); return rootView; } @Override public void bindView(View view, Context context, Cursor cursor) { long contactId = cursor.getLong(cursor.getColumnIndex(Contract.Messages.COLUMN_NAME_CONTACT_ID)); String address = cursor.getString(cursor.getColumnIndex(Contract.Messages.COLUMN_NAME_ADDRESS)); Date date = null; if(!cursor.isNull(cursor.getColumnIndex(Contract.Messages.COLUMN_NAME_TIME))) { long time = cursor.getLong(cursor.getColumnIndex(Contract.Messages.COLUMN_NAME_TIME)); date = new Date(time); } String statusString = cursor.getString(cursor.getColumnIndex(Contract.Messages.COLUMN_NAME_STATUS)); Status status = statusString != null ? Status.valueOf(statusString) : null; String body = cursor.getString(cursor.getColumnIndex(Contract.Messages.COLUMN_NAME_BODY)); Integer code = null; if(!cursor.isNull(cursor.getColumnIndex(Contract.Messages.COLUMN_NAME_CODE))) { code = cursor.getInt(cursor.getColumnIndex(Contract.Messages.COLUMN_NAME_CODE)); } ViewHolder holder = (ViewHolder) view.getTag(); // message code setViewColor(code, holder.type_icon); // message body holder.body.setText(formatBody(code, body)); // 联系人 String contactName = null; if(mContactIdToNameMap != null) contactName = mContactIdToNameMap.get(contactId); if(contactName == null) contactName = getString(R.string.unknown); // address String senderAndReceiver; if(address != null) senderAndReceiver = getString(R.string.contact_formatter, contactName, address); else senderAndReceiver = contactName; // direction (in status) if(status != null) { Direction dirct = status.getDirection(); if(dirct == Direction.SEND) { senderAndReceiver = getString(R.string.me) + "→" + senderAndReceiver; } else if(dirct == Direction.RECEIVE) { senderAndReceiver = senderAndReceiver + " → " + getString(R.string.me); } else throw new IllegalStateException("unknown Direction: " + dirct.name()); } holder.senderAndReceiver.setText(senderAndReceiver); // date if(date != null) { holder.date.setVisibility(View.VISIBLE); holder.date.setText(DateFormat.getDateFormat(mContext).format(date) + " " + DateFormat.getTimeFormat(mContext).format(date)); } else { holder.date.setVisibility(View.INVISIBLE); } // is it sent if(status == null || status.getDirection() != Direction.SEND) holder.send_status.setVisibility(View.GONE); else { holder.send_status.setText( getString(R.string.send_status_formatter, valueOfSendStatus(status))); holder.send_status.setVisibility(View.VISIBLE); } } private CharSequence formatBody(Integer code, String body) { if(body == null) return ""; if(code == null || !Code.Extra.Inform.hasSetUrgent(code)) return body; final UrgentMessageBody urgentBody = new Gson().fromJson(body, UrgentMessageBody.class); StringBuilder sb = new StringBuilder(); final UrgentMessageBody.Type type = urgentBody.getType(); if(type != null) sb.append(getString(R.string.type) + ": " + valueOfUrgentMessageBodyType(type) + "\n"); if(urgentBody.getContent() != null) sb.append(getString(R.string.content) + ": " + urgentBody.getContent() + "\n"); if(urgentBody.containsPosition()) { String location = urgentBody.getPositionLatitude() + "," + urgentBody.getPositionLongitude(); sb.append(getString(R.string.position_of_sender) + ": " + location); } return sb; } /** * 根据消息类型,设置消息视图的颜色 */ private void setViewColor(Integer code, ImageView typeIcon) { Integer colorResId = null; switch(getImportantCode(code)) { case R.string.urgent: colorResId = R.color.urgent; break; case R.string.respond: colorResId = R.color.respond; break; case R.string.locate_now: case R.string.command: colorResId = R.color.command; break; default: colorResId = android.R.color.transparent; } typeIcon.setImageResource(colorResId); } /** * 保存对 每项记录的视图中各元素 的引用,避免每次重复执行<code>findViewById()</code>,也方便使用 * @author Team Orange */ private class ViewHolder { TextView senderAndReceiver; TextView body; TextView send_status; TextView date; ImageView type_icon; } } /** * 用于发送消息的{@link AsyncTask}。支持批量发送。 * @author Team Orange */ protected static class MessagesSender extends AsyncTask<MessageWrapper, Integer, Void> { private final Context mContext; public MessagesSender(Context context) { super(); mContext = context; } @Override protected Void doInBackground(MessageWrapper... messages) { if(messages == null) return null; final int total = messages.length; int finished = 0; String password = Settings.getPassword(mContext); for(MessageWrapper message : messages) { if(message.uri != null) message.message.send(mContext, message.uri, message.dest, password); else message.message.sendAndSave(mContext, message.contactId, message.dest, password); publishProgress(total, ++finished); if(isCancelled()) break; } return null; } /** * {@link Message}的包装器,作为{@link MessagesSender}的泛型参数,用于传递发送参数。 * @author Team Orange */ public static class MessageWrapper { /** 要发送的消息主体,也将使用此对象的方法发送消息 */ public Message message; /** 如果用于重发,可以不设置此字段;如果发送新消息,请设置为对方联系人ID */ public Long contactId; /** 目标地址。如对方手机号 */ public String dest; /** 如果用于重发,此属性为原消息的{@link Uri};如果发送新消息,此属性应设置为null */ public Uri uri; } } }