這是一篇我個人在EOE發的帖子《綁定系統圖標,類似各種電話本的綁定圖標功能》,由於eoe的blog系統實在是無言以對,我就把eoe上面的帖子以及blog是都轉到csdn上來,原帖地址:http://www.eoeandroid.com/thread-542422-1-1.html
先說下背景把,之前要做一個融合的Contacts+Mms+ Dialer的一個項目,這個項目最初是要往第三方方面發展,然後要做一個功能:點擊系統的聯繫人,信息,撥號的icon然後把我們的app啓動起來,然後切換到相應的tab(做了類似功能的目前市面的app有:微信電話本,有你短信,觸寶號碼助手,360電話本,上面幾個都是我自己體驗並反編譯過他們的apk的,看的code太多了,就不貼截圖了)
這個功能呢,其實非常流氓,我也是通過看了一篇文章(文章地址:http://imid.me/blog/2013/08/16/weixin-phonebook-case-study/)才知道他們是具體怎麼做了,花了3天時間把基本功能做出來了,後面斷斷續續的優化功能也有一段時間
現在的情況是,我們的產品要進我們的rom了,進rom的話,我們就不需要這個功能了,所以把我之前研究的時候對整個流程的分析和我自己實現的demo分享出來
但是,希望大家不要利用這個去做一些不好的事情
通過上面那篇文章的分析,主要的做法就是利用WindowManager在整個屏幕窗口上添加一個透明且優先級比較高的窗口(是一個自定義View),然後監控屏幕的touch,用戶touch一次就去檢測一下系統目前運行的所有app,在RunningTask裏看棧頂的app是不是我們要綁定的app,如果是就啓動我們的app,如果不是就不用管了,而判斷是不是我們要綁定的app,是通過
RunningTaskInfo runningTaskInfo = localObject.get(0);
ComponentName topActivity = runningTaskInfo.topActivity;
String topActivityClassName = topActivity.getClassName();
通過topActivity的類名,我們在啓動這個監控的時候會獲取一下我們需要的綁定的app的集合,然後在這個集合中過濾
基本思路就是這樣那下面就開始具體實現:(另外本帖主要分析微信電話本{以下稱wxdhb}的一些邏輯流程和我自己的實現,其他的app我也反編譯過,基本差不多的思路)
1、首先UI上有三個開關
開關的作用是發送自定義的廣播,接收到廣播之後來初始化一些數據,然後開啓service,然後添加透明窗口(其實可以直接添加透明窗口,wxdhb中好像就是直接搞的),(demo裏有些地方code寫的有些冗餘了,後期優化的時候直接在項目裏做的,就沒優化demo的code了,所以大家可以看着修改)//發送廣播
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
SharedPreferences sp = getActivity().getSharedPreferences(BindActions.SCREEN_BIND_ACTIONS, Context.MODE_PRIVATE);
Intent i = new Intent(BindActions.BIND_ACTION);
switch (buttonView.getId()) {
case R.id.sw_call:
sp.edit().putBoolean(BindActions.BIND_ICON_ACTION_+"Call", buttonView.isChecked()).commit();
break;
case R.id.sw_contacts:
sp.edit().putBoolean(BindActions.BIND_ICON_ACTION_+"Contacts", buttonView.isChecked()).commit();
break;
case R.id.sw_mms:
sp.edit().putBoolean(BindActions.BIND_ICON_ACTION_+"Mms", buttonView.isChecked()).commit();
break;
default:
break;
}
getActivity().sendBroadcast(i);
}
public class ScreenStateReceiver extends BroadcastReceiver {
……
@Override
public void onReceive(Context context, Intent intent) {
//接受到上面發送的廣播,而且我們的不止是需要接收自己的廣播,還需要監聽系統的一些廣播,比如屏幕點亮和屏幕解鎖,且我們還要自啓動的時候就開始監聽了,所以需要接收多個action
String intent_action = intent.getAction();
if(BindActions.BIND_ACTION.equals(intent_action)){
handleBindEvent(context, intent);
} else if(Intent.ACTION_USER_PRESENT.equals(intent_action)){
//Log.i("LM", "ACTION_USER_PRESENT");
SystemClock.sleep(200);
handleBindEvent(context, intent);
} else if(Intent.ACTION_SCREEN_ON.equals(intent_action)){
//Log.i("LM", "ACTION_SCREEN_ON");
handleBindEvent(context, intent);
} else if(Intent.ACTION_SCREEN_OFF.equals(intent_action)){
//Log.i("LM", "ACTION_SCREEN_OFF");
}else if(Intent.ACTION_BOOT_COMPLETED.equals(intent_action)){
//Log.i("LM", "ACTION_BOOT_COMPLETED");
handleBindEvent(context, intent);
}
2、那麼下面service中,我們註冊廣播,和判斷是否需要添加透明窗口,然後來進行窗口的添加和移除
public class BindIconsService extends Service {
……
@Override
public void onCreate() {
super.onCreate();
receiver = new ScreenStateReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_USER_PRESENT);
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_SCREEN_ON);
registerReceiver(receiver, filter );// 屏幕點亮的廣播,必須在code中註冊纔有效
}
@Override
public void onDestroy() {
super.onDestroy();
if(receiver != null){
unregisterReceiver(receiver);
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
SharedPreferences sp = getSharedPreferences(BindActions.BIND_SERVICE_STATE,Context.MODE_PRIVATE);
boolean b = sp.getBoolean(BIND_TAG, false);// 獲取是否需要綁定
if (b) {
AlertView.removeAlertWindow(this);//不需要綁定,刪除AlerView,並停止service
stopSelf();
} else {
createView();
}
return START_STICKY;
}
/**
* 添加透明窗口
*/
private void createView() {
SharedPreferences sp = getSharedPreferences(BindActions.SCREEN_BIND_ACTIONS, Context.MODE_PRIVATE);
boolean _call = sp.getBoolean(BindActions.BIND_ICON_ACTION_+"Call", true);
boolean _contacts = sp.getBoolean(BindActions.BIND_ICON_ACTION_+"Contacts", true);
boolean _mms = sp.getBoolean(BindActions.BIND_ICON_ACTION_+"Mms", true);
AlertView.addAlertWindow(this, _call, _contacts, _mms);//alertview就是那個自定義VIew
}
3、關鍵的這個AlertView,這個view是透明的,懸浮在系統窗口之上,然後接收touch的時候調用一下我們自己的一個方法來進行一些判斷的邏輯,但是注意不要把touchevent攔截掉了(要不然用戶無法使用手機了,像我們開發的知道原因的話,還可以cmd下使用adb命令卸載掉,給小白用戶用,估計要哭了)
public class AlertView extends View {
……
/**
* 停止接受信息
*/
private void stopHandleTouch(){
if(this.eventHandler == null){
return;
}
this.eventHandler.quit();
this.eventHandler = null;
}
/**
* 刪除透明窗口
* @param context
*/
public static void removeAlertWindow(Context context) {
if (singleAlertView == null)
return;
if(singleAlertView != null){
singleAlertView.stopHandleTouch();
}
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
windowManager.removeView(singleAlertView);
singleAlertView = null;
}
/**
* 添加透明窗口
* @param paramContext
* @param is_bind_call
* @param is_bind_contacts
* @param is_bind_mms
*/
public static void addAlertWindow(Context paramContext,boolean is_bind_call,boolean is_bind_contacts,boolean is_bind_mms) {
if(singleAlertView != null){
removeAlertWindow(paramContext);
}
singleAlertView = new AlertView(paramContext, is_bind_call, is_bind_contacts, is_bind_mms);
addAlertView(paramContext);
}
/**
* 開始接受屏幕touch事件的信息
* @param paramContext
*/
private void startHandleTouch(Context paramContext) {
this.eventHandler = new TouchEventHandlerThread(this, paramContext);
this.eventHandler.start();
this.eventHandler.checkTop();
}
private static int exception_count = 0;
/**
* 添加窗口,<font color="#ff0000">這個方法的一些參數,我自己研究了很久,也去對應api了,上面幾個都可以對應出來具體參數,但** 是</font>localLayoutParams.flags <font color="#ff0000">這個參數,我真的不知道是個什麼鳥意思,但是這個值很關鍵,換其他值view沒效果(這段code我記得是從wxdhb和觸寶電話本里摳出來結合了一下吧)</font>
* @param paramContext
*/
private static void addAlertView(Context paramContext) {
if (singleAlertView == null)
return;
WindowManager localWindowManager = (WindowManager) paramContext.getSystemService(Context.WINDOW_SERVICE);
singleAlertView.setBackgroundColor(0);
WindowManager.LayoutParams localLayoutParams = new WindowManager.LayoutParams();
localLayoutParams.width = 1;
localLayoutParams.height = 1;
localLayoutParams.gravity = 51;
localLayoutParams.x = 0;
localLayoutParams.y = 0;
localLayoutParams.format = 1;
localLayoutParams.type = 2010;
localLayoutParams.flags = 262152;
try {
localWindowManager.addView(singleAlertView, localLayoutParams);
singleAlertView.startHandleTouch(paramContext);
return;
} catch (Exception localException1) {
if(DEBUG){
logMsg("Exception->"+localException1.getMessage());
}
removeAlertWindow(paramContext);
if(exception_count > 5){
if(DEBUG){
logMsg("can not add alertview ,exception:-->"+localException1.getMessage());
}
return;
}
addAlertView(paramContext);
exception_count++;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if ((this.eventHandler != null) && (this.eventHandler.isAlive()))
this.eventHandler.checkTop();
logMsg("onTouchEvent---");
return super.onTouchEvent(event);
}
4、當然整個判斷的邏輯的過程要放在線程中執行,所以在這個類中還會用到TouchEventHandlerThread,該類中也有很變態的檢車棧頂的方法,做出來的效果,和其他的差不多,可能在app切換的時候做的還是有點差距,不過功能實現了,就是慢慢優化的事了
public class TouchEventHandlerThread extends HandlerThread {
……
/**
* 判斷是否有需要綁定
* @return
*/
private boolean hasBind() {
if (alertView == null) {
return false;
} else {
return alertView.isIs_bind_call() || alertView.isIs_bind_contacts()
|| alertView.isIs_bind_mms();
}
}
/**
* 添加需要過濾的類名信息
*/
public void initFilterActions() {
Intent i_call = new Intent();
i_call.setAction("android.intent.action.CALL_BUTTON");
addFilterString(i_call, bind_set_call);
Intent i_dial = new Intent();
i_dial.setAction("android.intent.action.DIAL");
addFilterString(i_dial, bind_set_call);
Intent localIntent5 = new Intent();
localIntent5.setAction("android.intent.action.CALL");
addFilterString(localIntent5, bind_set_call);
// s6
bind_set_call.add("com.yulong.android.contacts.dial.DialActivity");
// suning
bind_set_call.add("com.android.contacts.activities.DialtactsActivity");
Intent i_contents = new Intent();
i_contents.setAction("android.intent.action.VIEW");
i_contents.setType("vnd.android.cursor.dir/contact");
addFilterString(i_contents, bind_set_contacts);
Intent i_person = new Intent("android.intent.action.VIEW");
i_person.setType("vnd.android.cursor.dir/person");
addFilterString(i_person, bind_set_contacts);
// meizu
bind_set_contacts.add("com.meizu.mzsnssyncservice.ui.SnsTabActivity");
bind_set_contacts.add("com.sec.android.app.contacts.PhoneBookTopMenuActivity");
// xiaomi
bind_set_contacts.add("com.android.contacts.activities.TwelveKeyDialer");
bind_set_contacts.add("com.android.mms.ui.MmsTabActivity");
bind_set_contacts.add("com.android.contacts.activities.PeopleActivity");
// s6
bind_set_contacts.add("com.yulong.android.contacts.ui.main.ContactMainActivity");
Intent i_mms = new Intent();
i_mms.setAction("android.intent.action.MAIN");
i_mms.setType("vnd.android.cursor.dir/mms");
addFilterString(i_mms, bind_set_mms);
Intent i_mms_2 = new Intent("android.intent.action.VIEW");
i_mms_2.setType("vnd.android-dir/mms-sms");
addFilterString(i_mms_2, bind_set_mms);
bind_set_mms.add("com.android.mms.ui.ConversationList");
bind_set_mms.add("com.android.mms");
// huawei
bind_set_mms.add("com.huawei.message");
// sony
bind_set_mms.add("com.sonyericsson.conversations");
// motorola
bind_set_mms.add("com.motorola.blur.conversations");
bind_set_mms.add("com.android.mms.ui.SingleRecipientConversationActivity");
bind_set_mms.add("com.android.mms.ui.NewMessagePopupActivity");
// s6
bind_set_mms.add("com.yulong.android.mms.ui.MmsMainListFormActivity");
// 360
bind_set_call.add("com.qihoo360.contacts.contacts");
bind_set_contacts.add("com.qihoo360.contacts.safecontacts");
bind_set_mms.add("com.qihoo360.contacts.mms");
bind_set_mms.add("com.google.android.talk.SigningInActivity");
bind_set_mms.add("com.google.android.apps.babel.fragments.SmsOobActivity");
}
/**
* 根據Intent去系統中查詢可以啓動該類intent的程序信息
* @param paramIntent
* @param bind_set
*/
private void addFilterString(Intent paramIntent, HashSet<String> bind_set) {
try {
ContextWrapper contextWrapper = (ContextWrapper) mContext;
PackageManager packageManager = contextWrapper.getPackageManager();
List<ResolveInfo> queryIntentActivities = packageManager.queryIntentActivities(paramIntent,PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : queryIntentActivities) {
String packageName = resolveInfo.activityInfo.packageName;
String name = resolveInfo.activityInfo.name;
String targetActivity = resolveInfo.activityInfo.targetActivity;
if (!TextUtils.isEmpty(name)) {
bind_set.add(name);
}
if (!TextUtils.isEmpty(targetActivity)) {
bind_set.add(targetActivity);
}
}
} catch (Exception e) {
logMsg("----------"+e.getMessage());
}
}
boolean isSuccess = false;
private int temp = 10;
/**
* 檢查棧頂
*/
public void checkTop() {
isSuccess = false;
if (this.mHandler == null)
return;
this.mHandler.removeMessages(TouchHandler.MSG_WHAT_COMMENT);
if(!hasBind()){
return;
}
long i = 400L;
while (true) {
if(isSuccess){
break;
}
if (i >= 600) {
this.mHandler.sendEmptyMessageDelayed(TouchHandler.MSG_WHAT_COMMENT_FAILED, i);
return;
}
this.mHandler.sendEmptyMessageDelayed(TouchHandler.MSG_WHAT_COMMENT, i);
i += temp;
}
}
/**
* 檢查棧頂的Acitivity的類型(是否需要綁定)
* @return
*/
private int checkTopType() {
SystemClock.sleep(200);
int lanch_type = LANCH_TYPE_NONE;
List<RunningTaskInfo> localObject = mActivityManager.getRunningTasks(1);
if (localObject != null && localObject.size() > 0) {
RunningTaskInfo runningTaskInfo = localObject.get(0);
ComponentName topActivity = runningTaskInfo.topActivity;
String topActivityClassName = topActivity.getClassName();
if(mContext == null){
return lanch_type;
}
if (mContext.getPackageName().equals(topActivity.getPackageName())) {
isSuccess = true;
}
logMsg("topActivityClassName->"+topActivityClassName);
if (bind_set_call.contains(topActivityClassName)) {
if (alertView.isIs_bind_call()) {
lanch_type = LANCH_TYPE_CALL;
}
}else if (bind_set_contacts.contains(topActivityClassName)) {
if (alertView.isIs_bind_contacts()) {
lanch_type = LANCH_TYPE_CONTACTS;
}
}else if (bind_set_mms.contains(topActivityClassName)) {
if (alertView.isIs_bind_mms()) {
lanch_type = LANCH_TYPE_MMS;
}
}
}
return lanch_type;
}
/**
* 幹掉其他程序,並啓動我們的app,<font color="#ff0000">這裏code寫了,但是沒有什麼效果,大家有好的殺其他app的辦法可以告訴我</font>
*/
public void killTopAndExcuteMine() {
if (isSuccess) {
return;
}
int type = checkTopType();
switch (type) {
case LANCH_TYPE_CALL:
startOurApp(TwoActivity.class);
break;
case LANCH_TYPE_CONTACTS:
startOurApp(TwoActivity.class);
break;
case LANCH_TYPE_MMS:
startOurApp(TwoActivity.class);
break;
case LANCH_TYPE_NONE:
// mHandler.removeMessages(TouchHandler.MSG_WHAT_COMMENT);
// isSuccess = true;
return;
}
}
/**
* 啓動我們的app
* @param targetClass
*/
private void startOurApp(Class<?> targetClass){
List<RunningTaskInfo> topRunningTask = mActivityManager.getRunningTasks(1);
if (topRunningTask != null && topRunningTask.size() > 0) {
RunningTaskInfo topRunningTaskInfo = topRunningTask.get(0);
String packagename = topRunningTaskInfo.topActivity.getPackageName();
if (mContext.getPackageName().equals(packagename)) {
return;
}
mActivityManager.killBackgroundProcesses(packagename);
}
Intent i = new Intent();
i.setAction(Intent.ACTION_MAIN);
i.addCategory(Intent.CATEGORY_DEFAULT);
// i.addCategory(Intent.CATEGORY_LAUNCHER);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
i.setClassName(mContext, targetClass.getName());
mContext.startActivity(i);
isSuccess = true;
}
@Override
protected void onLooperPrepared() {
Looper localLooper = getLooper();
this.mHandler = new TouchHandler(this, localLooper);
}
@Override
public boolean quit() {
this.mHandler = null;
this.mContext = null;
return super.quit();
}
public class TouchHandler extends Handler {
……
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_WHAT_COMMENT:
if (paramd == null){
logMsg("TouchEventHandlerThread is null");
return;
}
paramd.killTopAndExcuteMine();
break;
case MSG_WHAT_COMMENT_FAILED:
logMsg("FAILED");
break;
default:
break;
}
}
主要的核心code主要就是上面這些了,具體的流程分析大家可以看我上面留的那麼文章和反編譯上面幾個app來具體分析吧
demo下載地址:http://download.csdn.net/detail/lsmfeixiang/7859557
github地址:https://github.com/teffy/bind-system-app
需要依賴appcompat_v7,大家可以去sdk.dir:android sdk\extras\android\support\v7目錄下面找到