zeerd's blog         Search     Categories     Tags     Feed

闲来生雅趣,无事乐逍遥。对窗相望雪,一盏茶香飘。

Android消息中心信息获取与发布

#Notification @Android


Contents:
现如今,绝大部分腕表等可穿戴设备所能起到的主要作用还仅仅是手机第二屏(部分支持语音的设备除外)。
本文讲述一下如何在Android手机中获取到消息中心的实时数据,并发送给其他设备。

因为本人对蓝牙部分不熟悉,所以这里采用了UDP的形式进行数据的发送。
实际上,此处主要的难点是icon图片的封装和解包。至于通讯是依赖于蓝牙还是UDP还是其他什么方式,就仅仅是发送命令不同罢了。

下面是消息中心数据的获取。实际上非常简单,只要实现了NotificationListenerService 里面的几个函数就可以了。
MyNotificationService.java
package com.zeerd.emneg.mynotificationcenter;

import android.annotation.TargetApi;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;

public class MyNotificationService extends NotificationListenerService {
@Override
public void onNotificationPosted(StatusBarNotification arg0) {
// TODO Auto-generated method stub
SendNotification sn = new SendNotification(this);
sn.posted(arg0);
}

@Override
public void onNotificationRemoved(StatusBarNotification arg0) {
// TODO Auto-generated method stub
SendNotification sn = new SendNotification(this);
sn.removed(arg0);
}
}


需要注意的就是要在AndroidManifest.xml文件中配置service:
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zeerd.emneg.mynotificationcenter"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="19" />

<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.zeerd.emneg.mynotificationcenter.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".MyNotificationService"
android:label="@string/service_name"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
</application>

</manifest>


下面的类用于消息的打包和发送。为了便于测试,这里将消息数据通过sendBroadcast发送到了MainActivity一份。实际的使用中是不需要的。
SendNotification.java
package com.zeerd.emneg.mynotificationcenter;

import java.lang.NullPointerException;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import android.annotation.TargetApi;
import android.app.Notification;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import android.util.Log;

public class SendNotification {
final static private String TAG = "SendOfMyNotification";

private Context mContext = null;
private UdpClient udpClient = null;

public SendNotification(Context context) {
mContext = context;
udpClient = new UdpClient();
new Thread(udpClient).start();

SharedPreferences settings = mContext
.getSharedPreferences("Setting", 0);
String ip = settings.getString("server_ip", "127.0.0.1");

udpClient.setServerIp(ip, 5678);
Log.d(TAG, "Connect to " + ip + ":" + 5678);
}

public boolean posted(StatusBarNotification arg0) {
toActivity.posted(mContext, arg0.getNotification().extras);
udpClient.posted(mContext, arg0.getId(), arg0.isOngoing(),
arg0.getNotification().extras);
return true;
}

public boolean removed(StatusBarNotification arg0) {
toActivity.removed(mContext, arg0.getNotification().extras);
udpClient.removed(mContext, arg0.getId(), arg0.isOngoing(),
arg0.getNotification().extras);
return true;
}

private NotificationInfo getInfo(Context c, Bundle extras) {
NotificationInfo info = new NotificationInfo();
info.action = NotificationInfo.ACTION_UNDEF;
try {
info.title = extras.getString(Notification.EXTRA_TITLE);
} catch (NullPointerException e) {
// e.printStackTrace();
}

try {
info.icon = extras.getInt(Notification.EXTRA_SMALL_ICON);
} catch (NullPointerException e) {
// e.printStackTrace();
}

try {
info.largeIcon = ((Bitmap) extras
.getParcelable(Notification.EXTRA_LARGE_ICON));
} catch (NullPointerException e) {
// e.printStackTrace();
try {
info.largeIcon = ((Bitmap) extras
.getParcelable(Notification.EXTRA_LARGE_ICON_BIG));
} catch (NullPointerException e1) {
// e1.printStackTrace();
try {
info.largeIcon = ((Bitmap) extras
.getParcelable(Notification.EXTRA_PICTURE));
} catch (NullPointerException e2) {
// e2.printStackTrace();
}
}
}

try {
info.text = extras.getCharSequence(Notification.EXTRA_TEXT)
.toString();
} catch (NullPointerException e) {
// e.printStackTrace();
}

try {
info.subText = extras.getCharSequence(Notification.EXTRA_SUB_TEXT)
.toString();
} catch (NullPointerException e) {
// e.printStackTrace();
}

Log.d(TAG, "Title[" + info.title + "] Text[" + info.text + "] SubText["
+ info.subText + "] Icon[" + info.icon + "] LargeIcon["
+ ((info.largeIcon == null) ? "false]" : "true]"));
return info;
}

private static class NotificationInfo {
public static final int ACTION_UNDEF = 0;
public static final int ACTION_POSTED = 1;
public static final int ACTION_REMOVED = 2;

public String title = "";
public String text = "";
public String subText = "";
public int icon = -1;
public Bitmap largeIcon = null;
public int action = ACTION_UNDEF;
public int id = -1;
boolean isOngoing = false;
}

// for easy testing , we send the notification's infos to the MainActivity,
// too.
public static class toActivity {
public static boolean posted(Context c, Bundle extras) {
Intent intent = new Intent(MainActivity.INTENT_ACTION_NOTIFICATION);
intent.putExtras(extras);
c.sendBroadcast(intent);
return true;
}

public static boolean removed(Context c, Bundle extras) {
Intent intent = new Intent(MainActivity.INTENT_ACTION_NOTIFICATION);
intent.putExtras(extras);
c.sendBroadcast(intent);
return true;
}
}

public class UdpClient implements Runnable {
private InetAddress serverAddr = null;
DatagramSocket socket = null;
private int port = 0;
private NotificationInfo info = null;

public void setServerIp(String ip, int p) {
try {
serverAddr = InetAddress.getByName(ip);
} catch (UnknownHostException e) {
e.printStackTrace();
}
port = p;
}

public boolean posted(Context c, int id, boolean isOngoing,
Bundle extras) {
info = getInfo(c, extras);
info.action = NotificationInfo.ACTION_POSTED;
info.id = id;
info.isOngoing = isOngoing;
return true;
}

public boolean removed(Context c, int id, boolean isOngoing,
Bundle extras) {
info = getInfo(c, extras);
info.action = NotificationInfo.ACTION_REMOVED;
info.id = id;
info.isOngoing = isOngoing;
return true;
}

private byte[] makeSendBuf() {
byte[] buf;
byte[] id = intToByteArray(info.id);
byte[] idLen = intToByteArray(id.length);
byte[] isOngoing = intToByteArray(info.isOngoing ? 1 : 0);
byte[] isOngoingLen = intToByteArray(isOngoing.length);
byte[] title = info.title.getBytes();
byte[] titleLen = intToByteArray(title.length);
byte[] text = info.text.getBytes();
byte[] textLen = intToByteArray(text.length);
byte[] sub = info.subText.getBytes();
byte[] subLen = intToByteArray(sub.length);
byte[] icon = intToByteArray(info.icon);
byte[] iconLen = intToByteArray(icon.length);
byte[] largeIcon = bitmapToByteArray(info.largeIcon);
byte[] largeIconWidth = null;
byte[] largeIconHeight = null;
if (info.largeIcon != null) {
largeIconWidth = intToByteArray(info.largeIcon.getWidth());
largeIconHeight = intToByteArray(info.largeIcon.getHeight());
}
byte[] largeIconLen = intToByteArray((largeIcon == null) ? 0
: info.largeIcon.getByteCount());

buf = concatBytes(idLen, id);
buf = concatBytes(buf, isOngoingLen);
buf = concatBytes(buf, isOngoing);
buf = concatBytes(buf, titleLen);
buf = concatBytes(buf, title);
buf = concatBytes(buf, textLen);
buf = concatBytes(buf, text);
buf = concatBytes(buf, subLen);
buf = concatBytes(buf, sub);
buf = concatBytes(buf, iconLen);
buf = concatBytes(buf, icon);
buf = concatBytes(buf, largeIconLen);
buf = concatBytes(buf, largeIconWidth);
buf = concatBytes(buf, largeIconHeight);
buf = concatBytes(buf, largeIcon);

return buf;
}

@Override
public void run() {
if (serverAddr == null) {
return;
}

Log.d(TAG, "Client: Start connecting\\n");
try {
socket = new DatagramSocket();
} catch (SocketException e2) {
e2.printStackTrace();
}

while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
if (info != null) {
try {
byte[] buf = makeSendBuf();
// the max size of the send buffer is 4097. we made the
// send buffer to 4000 for prefix and reserve.
// the prefix protocol format is:
// byte1 : action
// byte2~5 : the start offset (unit by byte) in the
// whole sending buffer
// byte6~9 : the total size of the sending buffer
for (int i = 0; i < buf.length; i += 4000) {
byte[] s = new byte[4009], n, t;
int sLen = ((buf.length - i) < 4000) ? buf.length
- i : 4000;
n = intToByteArray(i);
t = intToByteArray(buf.length);
s[0] = (byte) info.action;
System.arraycopy(n, 0, s, 1, 4);
System.arraycopy(t, 0, s, 5, 4);
System.arraycopy(buf, i, s, 9, sLen);

DatagramPacket packet = new DatagramPacket(s,
sLen + 8, serverAddr, port);
socket.send(packet);
}
Log.d(TAG, "Client: Message sent\\n");
} catch (Exception e) {
Log.d(TAG, "Client: Error!\\n");
e.printStackTrace();
}
info = null;
}
}
}
}

private static byte[] intToByteArray(int a) {
byte[] ret = new byte[4];
ret[3] = (byte) (a & 0xFF);
ret[2] = (byte) ((a >> 8) & 0xFF);
ret[1] = (byte) ((a >> 16) & 0xFF);
ret[0] = (byte) ((a >> 24) & 0xFF);
return ret;
}

private static byte[] bitmapToByteArray(Bitmap b) {
if (b != null) {
int bytes = b.getByteCount();
ByteBuffer buffer = ByteBuffer.allocate(bytes);
b.copyPixelsToBuffer(buffer);
return buffer.array();
} else {
return null;
}
}

private static byte[] concatBytes(byte[] a, byte[] b) {
if (b == null) {
return a;
} else {
byte[] c = new byte[a.length + b.length];
System.arraycopy(a, 0, c, 0, a.length);
System.arraycopy(b, 0, c, a.length, b.length);
return c;
}
}
}


MainActivity.java
package com.zeerd.emneg.mynotificationcenter;

import java.util.Properties;

import android.os.Bundle;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;

@TargetApi(19)
public class MainActivity extends Activity {

public static String INTENT_ACTION_NOTIFICATION = "com.zeerd.emneg.mynotificationcenter.notification";
NotificationManager mNotificationMgr = null;
Button mBtn = null;
Context mContext = this;
protected Properties properties = null;

protected TextView title;
protected TextView text;
protected TextView subtext;
protected ImageView largeIcon;
protected EditText etIp;

protected MyReceiver mReceiver = new MyReceiver();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

properties = new Properties();
mNotificationMgr = (NotificationManager) this
.getSystemService(Context.NOTIFICATION_SERVICE);

title = (TextView) findViewById(R.id.nt_title);
text = (TextView) findViewById(R.id.nt_text);
subtext = (TextView) findViewById(R.id.nt_subtext);
largeIcon = (ImageView) findViewById(R.id.nt_largeicon);
etIp = (EditText) findViewById(R.id.editText1);

mBtn = (Button) findViewById(R.id.button1);
mBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
writeProperties();

Bitmap aBitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.ic_launcher);
Notification.Builder b = new Notification.Builder(mContext);
b.setContentTitle("Test");
b.setContentText("subject");
b.setSubText("content");
b.setSmallIcon(R.drawable.ic_launcher);
b.setLargeIcon(aBitmap);
Notification n = b.build();

mNotificationMgr.notify(null, 0, n);
}
});
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}

@Override
protected void onResume() {
super.onResume();
if (mReceiver == null)
mReceiver = new MyReceiver();
registerReceiver(mReceiver,
new IntentFilter(INTENT_ACTION_NOTIFICATION));
}

@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mReceiver);
}

@TargetApi(19)
public class MyReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {

if (intent != null) {
Bundle extras = intent.getExtras();
String notificationTitle = extras
.getString(Notification.EXTRA_TITLE);
Bitmap notificationLargeIcon = ((Bitmap) extras
.getParcelable(Notification.EXTRA_LARGE_ICON));
CharSequence notificationText = extras
.getCharSequence(Notification.EXTRA_TEXT);
CharSequence notificationSubText = extras
.getCharSequence(Notification.EXTRA_SUB_TEXT);

title.setText(notificationTitle);
text.setText(notificationText);
subtext.setText(notificationSubText);

if (notificationLargeIcon != null) {
largeIcon.setImageBitmap(notificationLargeIcon);
}
}
}
}

private void writeProperties() {
SharedPreferences settings = mContext
.getSharedPreferences("Setting", 0);
Editor editor = settings.edit();
editor.putString("server_ip", etIp.getText().toString());
editor.commit();
}
}


接收端的代码如下:
package com.zeerd.emneg.mynotificationreceiver;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class MainActivity extends Activity {
private static final String TAG = "ReceiverOfMyNotification";
public static final int SERVERPORT = 5678;
public Context mContext = this;

public ListView mListViewOngoing;
public ListView mListViewNonOngoing;

public List<NotificationInfo> mOngoingList;
public List<NotificationInfo> mNonOngoingList;

public Handler handler;

@SuppressLint("HandlerLeak")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mOngoingList = new ArrayList<NotificationInfo>();
mNonOngoingList = new ArrayList<NotificationInfo>();

mListViewOngoing = (ListView) findViewById(R.id.listView2);
mListViewNonOngoing = (ListView) findViewById(R.id.listView1);

handler = new Handler() {
@Override
public void handleMessage(Message msg) {
NotificationInfo info = (NotificationInfo) msg.obj;

if (info.isOngoing) {
mOngoingList.add(0, info);
ListAdapter list = new OngoingAdapter(mContext);
mListViewOngoing.setAdapter(list);
} else {
mNonOngoingList.add(0, info);
ListAdapter list1 = new NonOngoingAdapter(mContext);
mListViewNonOngoing.setAdapter(list1);
}

}
};

new Thread(new Server()).start();
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}

public class Server implements Runnable {
byte[] fullBuf = new byte[100 * 1024];

@Override
public void run() {
try {
Log.d(TAG, "Server: Start connecting");
@SuppressWarnings("resource")
DatagramSocket socket = new DatagramSocket(SERVERPORT);
byte[] buf = new byte[4097];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
while (true) {
socket.receive(packet);

int index = byteArrayToInt(buf, 1);
int total = byteArrayToInt(buf, 5);

if ((total - index) < 4000) {

Log.d(TAG, "Server: Receiving" + index + "/" + total);
System.arraycopy(buf, 9, fullBuf, index, total - index);

String debug_log = "";
for (int i = 0; i < 100; i++) {
debug_log += " "
+ Integer.toString((int) fullBuf[i], 16);
}
Log.d(TAG, "Server: Receiving[0..100] " + debug_log);

NotificationInfo info = getInfo(fullBuf);
info.action = (int) buf[0];

Log.d(TAG, "Title["
+ info.title
+ "] Text["
+ info.text
+ "] SubText["
+ info.subText
+ "] Icon["
+ info.icon
+ "] LargeIcon["
+ ((info.largeIcon == null) ? "false]"
: "true]"));

Message msg = handler.obtainMessage(0, info);
handler.sendMessage(msg);

} else {
Log.d(TAG, "Server: Receiving" + index + "/" + total);
System.arraycopy(buf, 9, fullBuf, index, 4000);
}

}

} catch (Exception e) {
e.printStackTrace();
}
}
}

private static class NotificationInfo {
public static final int ACTION_UNDEF = 0;
public static final int ACTION_POSTED = 1;
@SuppressWarnings("unused")
public static final int ACTION_REMOVED = 2;

public String title = "";
public String text = "";
public String subText = "";
public int icon = -1;
public Bitmap largeIcon = null;
public int action = ACTION_UNDEF;
public int id = -1;
boolean isOngoing = false;
}

private NotificationInfo getInfo(byte[] buf) {
int idx = 0;
int len = 0;
byte[] text = null;
NotificationInfo info = new NotificationInfo();

try {
len = byteArrayToInt(buf, idx);
text = new byte[len];
System.arraycopy(buf, idx + 4, text, 0, len);
idx += (4 + len);
info.id = byteArrayToInt(text, 0);

len = byteArrayToInt(buf, idx);
text = new byte[len];
System.arraycopy(buf, idx + 4, text, 0, len);
idx += (4 + len);
info.isOngoing = (byteArrayToInt(text, 0) == 0) ? false : true;

len = byteArrayToInt(buf, idx);
text = new byte[len];
System.arraycopy(buf, idx + 4, text, 0, len);
idx += (4 + len);
info.title = new String(text);

len = byteArrayToInt(buf, idx);
text = new byte[len];
System.arraycopy(buf, idx + 4, text, 0, len);
idx += (4 + len);
info.text = new String(text);

len = byteArrayToInt(buf, idx);
text = new byte[len];
System.arraycopy(buf, idx + 4, text, 0, len);
idx += (4 + len);
info.subText = new String(text);

len = byteArrayToInt(buf, idx);
Log.d(TAG, "len of icon=" + len + " idx=" + idx);
if (len > 0) {
text = new byte[len];
System.arraycopy(buf, idx + 4, text, 0, len);
info.icon = byteArrayToInt(text, 0);
}
idx += (4 + len);

len = byteArrayToInt(buf, idx);
Log.d(TAG, "len of large icon=" + len + " idx=" + idx);
if (len > 0) {
byte[] num = new byte[4];
System.arraycopy(buf, idx + 4, num, 0, 4);
int w = byteArrayToInt(num, 0);
System.arraycopy(buf, idx + 8, num, 0, 4);
int h = byteArrayToInt(num, 0);

text = new byte[len];
System.arraycopy(buf, idx + 12, text, 0, len);
ByteBuffer buffer = ByteBuffer.allocate(len);
buffer.put(text);
buffer.rewind();

info.largeIcon = Bitmap.createBitmap(w, h,
Bitmap.Config.ARGB_8888);
info.largeIcon.copyPixelsFromBuffer(buffer);
}
} catch (Exception e) {
e.printStackTrace();
}

return info;
}

public static int byteArrayToInt(byte[] b, int idx) {
return b[3 + idx] & 0xFF | (b[2 + idx] & 0xFF) << 8
| (b[1 + idx] & 0xFF) << 16 | (b[0 + idx] & 0xFF) << 24;
}

public class OngoingAdapter extends BaseAdapter {

LayoutInflater mInflator;
private Context mContext;

public OngoingAdapter(Context c) {
mContext = c;
mInflator = LayoutInflater.from(mContext);
}

@Override
public int getCount() {
return mOngoingList.size();
}

@Override
public Object getItem(int arg0) {
if (mOngoingList != null) {
return mOngoingList.get(arg0);
} else {
return null;
}
}

@Override
public long getItemId(int arg0) {
return arg0;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
convertView = mInflator.inflate(R.layout.listitem, parent, false);

if ((convertView != null) && (mOngoingList != null)) {
NotificationInfo info = (NotificationInfo) mOngoingList
.get(position);
getListVIew(convertView, info);
}

return convertView;
}

}

public class NonOngoingAdapter extends BaseAdapter {

LayoutInflater mInflator;
private Context mContext;

public NonOngoingAdapter(Context c) {
mContext = c;
mInflator = LayoutInflater.from(mContext);
}

@Override
public int getCount() {
return mNonOngoingList.size();
}

@Override
public Object getItem(int arg0) {
if (mNonOngoingList != null) {
return mNonOngoingList.get(arg0);
} else {
return null;
}
}

@Override
public long getItemId(int arg0) {
return arg0;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
convertView = mInflator.inflate(R.layout.listitem, parent, false);

if ((convertView != null) && (mNonOngoingList != null)) {
NotificationInfo info = (NotificationInfo) mNonOngoingList
.get(position);
getListVIew(convertView, info);
}

return convertView;
}
}

private void getListVIew(View convertView, NotificationInfo info) {
TextView tvTitle = (TextView) convertView.findViewById(R.id.text1);
TextView tvText = (TextView) convertView.findViewById(R.id.text2);
ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);

Log.d(TAG, "Set Title[" + info.title + "] Text[" + info.text
+ "] SubText[" + info.subText + "] Icon[" + info.icon
+ "] LargeIcon["
+ ((info.largeIcon == null) ? "false]" : "true]"));

tvTitle.setText(((info.action == NotificationInfo.ACTION_POSTED) ? "[+]"
: "[-]") + info.title);
tvText.setText("(" + info.id + ")" + info.text + "[" + info.subText
+ "]");
iconView.setImageBitmap(info.largeIcon);
}
}


listitem.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content" >

<ImageView
android:id="@+id/icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" />

<TextView
android:id="@+id/text1"
android:textStyle="bold"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginTop="6dp"
android:layout_toRightOf="@id/icon" />

<TextView
android:id="@+id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignStart="@id/text1"
android:layout_below="@id/text1"
android:layout_toRightOf="@id/icon"
android:textAppearance="?android:attr/textAppearanceSmall" />

</RelativeLayout>


效果如下:
nc