Android 高仿UC瀏覽器監控剪下板彈出懸浮窗功能

希爾瓦娜斯女神發表於2015-11-26

UC瀏覽器應該是android手機裡 最流行的瀏覽器之一了,他們有一個功能 相信大家都體驗過,就是如果你複製了什麼文字,(在其他app中 複製也有這個效果!,所以能猜到肯定是監控了剪下板),就會彈出一個懸浮窗。

懸浮窗這個東西 相信大家很多人都使用過,但是在小米的手機上,應該很多人的懸浮窗是無法使用的,因為小米預設是關閉這個懸浮窗許可權的。但是uc往往能繞過小米這個懸浮窗許可權控制。除此之外 剪下板在api 11以下

和11以上都是不一樣的實現。所以我們要完全複製uc瀏覽器的這個功能,我們主要需要解決2個問題:

1.對剪下板 這個api 做版本相容處理。

2.如何繞過懸浮窗許可權檢查 去實現在小米等收緊懸浮窗許可權的手機裡依然正常顯示懸浮窗。

首先來看api相容處理怎麼做:

在以往app開發的時候,我們基本上都會使用到一個本地物理快取資料夾,這個裡面存放著我們本app的快取圖片啊 之類的其他資訊,但是考慮到使用者手機容量有限,我們在相當多的時候在操作這個快取路徑的時候是會判斷他的大小的,

如果太大了,我們就刪除一部分快取。通常我們會這麼做:

 1  /**
 2      * 返回path路徑下的 所有檔案大小
 3      * @param path 全路徑
 4      * @return 返回-1代表path值為null
 5      */
 6     public static long getTotalSpace(File path)
 7     {
 8         if (path==null)
 9         {
10             return -1;
11         }
12         return path.getTotalSpace();
13     }

看上去程式碼很完美對吧,但是如果我們改一個地方minSdkVersion 改成8

 1 android {
 2     compileSdkVersion 23
 3     buildToolsVersion "23.0.1"
 4 
 5     defaultConfig {
 6         applicationId "com.example.administrator.clipboardmanagertest"
 7         minSdkVersion 8
 8         targetSdkVersion 23
 9         versionCode 1
10         versionName "1.0"
11     }
12     buildTypes {
13         release {
14             minifyEnabled false
15             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
16         }
17     }
18 }

我們再看:

你看 ide直接報錯了,原來這個api要求是

public static final int GINGERBREAD = 9;




也就是說,這個getTotalSpace這個函式 一定得在9或者9以上的手機裡才能正常使用 在9之下的比如8 ,是沒有這個api的。
有些人為了懶,他就這麼做了:
 1  /**
 2      * 返回path路徑下的 所有檔案大小
 3      * @param path 全路徑
 4      * @return 返回-1代表path值為null
 5      */
 6     @TargetApi(Build.VERSION_CODES.GINGERBREAD)
 7     public static long getTotalSpace(File path)
 8     {
 9         if (path==null)
10         {
11             return -1;
12         }
13         return path.getTotalSpace();
14     }

加了一個註解,這樣編譯能通過了,但實際上這並沒有什麼卵用,因為這段程式碼只要在api小於9的手機裡執行 依然會報錯的。

因為小於9的手機裡 沒有這個方法。所以這裡要做一個簡單的api相容:

 1  @TargetApi(Build.VERSION_CODES.GINGERBREAD)
 2     public static long getTotalSpace(File path)
 3     {
 4         if (path==null)
 5         {
 6             return -1;
 7         }
 8         //如果這個sdk大於9 那就使用系統的api
 9         if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.GINGERBREAD)
10         {
11 
12             return path.getTotalSpace();
13         }else//小於9 系統沒有這個api 我們就自己算吧。
14         {
15             final StatFs statFs=new StatFs(path.getPath());
16             return statFs.getBlockSize()*statFs.getBlockCount();
17         }
18     }

你看這樣做就很完美了。同樣的,我們在剪下板這個api 上也一樣要做相容處理:

你想一下 uc的那個功能,其實肯定就是開啟了一個服務,然後在服務裡 監聽剪下板的變化對吧,那就看看剪下板的變化 怎麼監聽:

 1  public void testCliboardApi()
 2     {
 3         ClipboardManager clipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
 4         clipboard.addPrimaryClipChangedListener(new ClipboardManager.OnPrimaryClipChangedListener() {
 5             @Override
 6             public void onPrimaryClipChanged() {
 7 
 8             }
 9         });
10     }

實際上就是通過這個api進行監聽剪下板的變化的,但是 這個api只能支援11或者11以上啊你改成10 就會報錯了:

所以我們的目標就是讓這個api在11以下的版本也能相容。好 現在就來完成這個功能,我們首先來自定義一個介面,這個介面實際上就只是寫了5個方法 這5個方法在api 》11的 原始碼裡面是都有實現的。

在<11的原始碼裡,實際上只有3個方法實現了,還有2個沒有實現(我們主要就是要在小於api11的裡面 實現這2個方法)

 

 1 package com.example.administrator.clipboardmanagertest;
 2 
 3 /**
 4  * Created by Administrator on 2015/11/25.
 5  */
 6 //這裡我們就定義一個介面,這個介面囊括了 所有我們需要使用的方法
 7 //注意後三個方法 api11以下也是有的,而前2個方法 11或者11以上才有
 8 public interface ClipboardManagerInterfaceCompat {
 9 
10     //注意這裡的引數 我們使用的是自己定義的介面 而不是sdk裡面的ClipboardManager.OnPrimaryClipChangedListener
11     void addPrimaryClipChangedListener(OnPrimaryClipChangedListener listener);
12 
13     void removePrimaryClipChangedListener(OnPrimaryClipChangedListener listener);
14 
15     CharSequence getText();
16 
17     void setText(CharSequence text);
18 
19     boolean hasText();
20 
21 
22 }

然後我們可以看一下高於api11的版本里面,這個監聽剪下板變化的功能是怎麼做的,來稍微看一下原始碼:

其實也很簡單,無非就是發生內容變化的時候 回撥一下這個介面的onPrimaryClipChanged方法罷了。

為了相容 我們也定義一個這樣的介面,實際上就是把這段程式碼給摳出來。

 1 package com.example.administrator.clipboardmanagertest;
 2 
 3 /**
 4  * Created by Administrator on 2015/11/25.
 5  */
 6 
 7 //注意這個OnPrimaryClipChangedListener 是在api11以後才有的
 8 //我們這裡就是把這個介面給拿出來 定義一下 看下CliboardManager的原始碼就知道了(注意要看api11 以後的原始碼)
 9 public interface OnPrimaryClipChangedListener {
10     void onPrimaryClipChanged();
11 }

然後繼續,我們可以想一下 既然是要對api11 以上和以下做2個版本,但實際上這2個版本 都得實現我們上面一開始的那個介面,所以可以定義一個抽象類 幫助我們完成這個功能:

 1 package com.example.administrator.clipboardmanagertest;
 2 
 3 import java.util.ArrayList;
 4 
 5 /**
 6  * Created by Administrator on 2015/11/25.
 7  */
 8 //既然我們是要對api11 以上和以下 分別做2個 實體類出來,而且這2個實體類 都必須實現我們的自定義介面。
 9 //所以不妨先定義一個base 的抽象類
10 public  abstract class ClipboardManagerInterfaceCompatBase implements ClipboardManagerInterfaceCompat{
11 
12     //這個抽象類實際上就只做了一件事 維持一個監聽器的list 罷了。
13     //注意OnPrimaryClipChangedListener 這個類 是我們自定義的,不是高於api11的原始碼裡的
14     protected final ArrayList<OnPrimaryClipChangedListener> mPrimaryClipChangedListeners
15             = new ArrayList<OnPrimaryClipChangedListener>();
16 
17     @Override
18     public void addPrimaryClipChangedListener(OnPrimaryClipChangedListener listener) {
19         synchronized (mPrimaryClipChangedListeners) {
20             mPrimaryClipChangedListeners.add(listener);
21         }
22     }
23 
24    //這個方法其實還挺重要的 就是通知所有在這個上面的listenser 內容發生了變化
25     //注意這裡的mPrimaryClipChangedListeners是自定義的 不是系統的
26     protected final void notifyPrimaryClipChanged() {
27         synchronized (mPrimaryClipChangedListeners) {
28             for (int i = 0; i < mPrimaryClipChangedListeners.size(); i++) {
29                 mPrimaryClipChangedListeners.get(i).onPrimaryClipChanged();
30             }
31         }
32     }
33 
34     @Override
35     public void removePrimaryClipChangedListener(OnPrimaryClipChangedListener listener) {
36         synchronized (mPrimaryClipChangedListeners) {
37             mPrimaryClipChangedListeners.remove(listener);
38         }
39     }
40 }

好,抽象類也有了,我們就來寫一下實體類,首先來實現一個高於api11的 類,這個比較簡單:實際上就是引用原來系統的程式碼就可以了:

 1 package com.example.administrator.clipboardmanagertest;
 2 
 3 import android.annotation.TargetApi;
 4 import android.content.ClipboardManager;
 5 import android.content.Context;
 6 import android.os.Build;
 7 
 8 /**
 9  * Created by Administrator on 2015/11/25.
10  */
11 //注意這個實際上對應的就是api11 以上的ClipboardManager了,其實這個是最簡單的,你只要呼叫系統的ClipboardManager 即可
12 //不要遺漏註解 TargetApi 因為遺漏的話 編譯會不過的
13 public class ClipboardManagerInterfaceCompatImplNormal extends ClipboardManagerInterfaceCompatBase {
14 
15     ClipboardManager.OnPrimaryClipChangedListener mOnPrimaryClipChangedListener = new ClipboardManager.OnPrimaryClipChangedListener() {
16         @Override
17         public void onPrimaryClipChanged() {
18             notifyPrimaryClipChanged();
19         }
20     };
21     private ClipboardManager mClipboardManager;
22 
23     public ClipboardManagerInterfaceCompatImplNormal(Context context) {
24         mClipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
25     }
26 
27     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
28     @Override
29     public void addPrimaryClipChangedListener(OnPrimaryClipChangedListener listener) {
30         super.addPrimaryClipChangedListener(listener);
31         synchronized (mPrimaryClipChangedListeners) {
32             if (mPrimaryClipChangedListeners.size() == 1) {
33                 mClipboardManager.addPrimaryClipChangedListener(mOnPrimaryClipChangedListener);
34             }
35         }
36     }
37 
38     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
39     @Override
40     public void removePrimaryClipChangedListener(OnPrimaryClipChangedListener listener) {
41         super.removePrimaryClipChangedListener(listener);
42         synchronized (mPrimaryClipChangedListeners) {
43             if (mPrimaryClipChangedListeners.size() == 0) {
44                 mClipboardManager.removePrimaryClipChangedListener(mOnPrimaryClipChangedListener);
45             }
46         }
47     }
48 
49     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
50     @Override
51     public CharSequence getText() {
52         return mClipboardManager.getText();
53     }
54 
55     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
56     @Override
57     public void setText(CharSequence text) {
58         if (mClipboardManager != null) {
59             mClipboardManager.setText(text);
60         }
61     }
62 
63     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
64     @Override
65     public boolean hasText() {
66         return mClipboardManager != null && mClipboardManager.hasText();
67     }
68 
69 }

你看這個高於api11的 實體類 無非就是把api11的 給包了一層罷了。很簡單。那我們來看看如何做api11 向下的相容實體類。

  1 package com.example.administrator.clipboardmanagertest;
  2 
  3 import android.content.Context;
  4 import android.os.Handler;
  5 import android.os.Looper;
  6 import android.text.ClipboardManager;
  7 import android.text.TextUtils;
  8 import android.util.Log;
  9 
 10 import java.util.logging.LogRecord;
 11 
 12 /**
 13  * Created by Administrator on 2015/11/25.
 14  */
 15 //這個就是對應的api11 以下的ClipboardManager 實體類了,實際上這裡主要就是要實現api11 以上的那個監聽
 16 //我們就用一個最簡單的方法 不斷監視text變化就可以了
 17 //思路其實也挺簡單的 就是把這個 ClipboardManagerInterfaceCompatImplCustom
 18 public class ClipboardManagerInterfaceCompatImplCustom extends ClipboardManagerInterfaceCompatBase implements Runnable {
 19 
 20     //靜態的不會導致記憶體洩露
 21     private static Handler mHandler;
 22     private CharSequence mLastText;
 23     //這個是設定間隔多少毫秒去檢查一次 預設我們設定成1000ms檢查一次
 24     public static int CHECK_TIME_INTERVAL = 1000;
 25 
 26 
 27     static {
 28         mHandler = new Handler(Looper.getMainLooper());
 29     }
 30 
 31     //api11 以下 是android.text.ClipboardManager; 注意和api11以上的android.content.ClipboardManager是 有區別的
 32     ClipboardManager clipboardManager;
 33 
 34     public ClipboardManagerInterfaceCompatImplCustom(Context context) {
 35         clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
 36     }
 37 
 38 
 39     @Override
 40     public void addPrimaryClipChangedListener(OnPrimaryClipChangedListener listener) {
 41         super.addPrimaryClipChangedListener(listener);
 42         synchronized (mPrimaryClipChangedListeners) {
 43             if (mPrimaryClipChangedListeners.size() == 1) {
 44                 startListenDataChange();
 45             }
 46         }
 47     }
 48 
 49     @Override
 50     public void removePrimaryClipChangedListener(OnPrimaryClipChangedListener listener) {
 51         super.removePrimaryClipChangedListener(listener);
 52         synchronized (mPrimaryClipChangedListeners) {
 53             if (mPrimaryClipChangedListeners.size() == 0) {
 54                 stopListenDataChange();
 55             }
 56         }
 57     }
 58 
 59     private void stopListenDataChange() {
 60         mHandler.removeCallbacks(this);
 61     }
 62 
 63     private void startListenDataChange() {
 64         mLastText = getText();
 65         mHandler.post(this);
 66     }
 67 
 68 
 69     @Override
 70     public CharSequence getText() {
 71 
 72         if (clipboardManager == null) {
 73             return null;
 74         }
 75 
 76         return clipboardManager.getText();
 77     }
 78 
 79     @Override
 80     public void setText(CharSequence text) {
 81         if (clipboardManager != null) {
 82             clipboardManager.setText(text);
 83         }
 84     }
 85 
 86     @Override
 87     public boolean hasText() {
 88         if (clipboardManager==null)
 89         {
 90             return false;
 91         }
 92         return clipboardManager.hasText();
 93     }
 94 
 95     @Override
 96     public void run() {
 97 
 98         CharSequence data=getText();
 99         isChanged(data);
100         mHandler.postDelayed(this,CHECK_TIME_INTERVAL);
101 
102     }
103 
104     private void isChanged(CharSequence data)
105     {
106         if (TextUtils.isEmpty(mLastText) && TextUtils.isEmpty(data)) {
107             return;
108         }
109         if (!TextUtils.isEmpty(mLastText) && data != null && mLastText.toString().equals(data.toString())) {
110             return;
111         }
112         mLastText = data;
113         //如果發生了變化 就通知
114         notifyPrimaryClipChanged();
115     }
116 }

最後定義一個util

 1 package com.example.administrator.clipboardmanagertest;
 2 
 3 import android.content.Context;
 4 import android.os.Build;
 5 
 6 /**
 7  * Created by Administrator on 2015/11/25.
 8  */
 9 public class CliboardManagerUtils {
10     public static ClipboardManagerInterfaceCompat create(Context context) {
11         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
12             return new ClipboardManagerInterfaceCompatImplNormal(context);
13         } else {
14             return new ClipboardManagerInterfaceCompatImplCustom(context);
15         }
16     }
17 }

然後,我們開啟一個服務 來監聽下 即可:

package com.example.administrator.clipboardmanagertest;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;

public class MonitorService extends Service {

    private ClipboardManagerInterfaceCompat clipboardManagerInterfaceCompat;

    public MonitorService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onCreate() {
        clipboardManagerInterfaceCompat = CliboardManagerUtils.create(this);
        clipboardManagerInterfaceCompat.addPrimaryClipChangedListener(new OnPrimaryClipChangedListener() {
            @Override
            public void onPrimaryClipChanged() {

                Toast.makeText(MonitorService.this, "監聽到剪下板發生了變化", Toast.LENGTH_LONG).show();
            }
        });
        super.onCreate();
    }
}

最後我們來看一下效果,高於11的版本的效果我就不放了,因為是呼叫系統的所以肯定成功的,我們看看2.3這個低於11版本的效果 就好了:

 

剪下的api相容 我們做完了,那最後再看一下如何彈出懸浮窗 :

 

 1 package com.example.administrator.clipboardmanagertest;
 2 
 3 import android.app.Service;
 4 import android.content.Context;
 5 import android.content.Intent;
 6 import android.graphics.PixelFormat;
 7 import android.os.Build;
 8 import android.os.IBinder;
 9 import android.view.Gravity;
10 import android.view.View;
11 import android.view.WindowManager;
12 import android.widget.TextView;
13 
14 public class MonitorService extends Service {
15 
16     private ClipboardManagerInterfaceCompat clipboardManagerInterfaceCompat;
17 
18     public MonitorService() {
19     }
20 
21     @Override
22     public IBinder onBind(Intent intent) {
23         // TODO: Return the communication channel to the service.
24         throw new UnsupportedOperationException("Not yet implemented");
25     }
26 
27     @Override
28     public void onCreate() {
29         mWindowManager = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE);
30         clipboardManagerInterfaceCompat = CliboardManagerUtils.create(this);
31         clipboardManagerInterfaceCompat.addPrimaryClipChangedListener(new OnPrimaryClipChangedListener() {
32             @Override
33             public void onPrimaryClipChanged() {
34                 showText(clipboardManagerInterfaceCompat.getText().toString());
35             }
36         });
37         super.onCreate();
38     }
39     private WindowManager mWindowManager;
40 
41     public void showText(String mContent)
42     {
43         final View rootView =  View.inflate(this, R.layout.content_view, null);
44 
45         rootView.setOnClickListener(new View.OnClickListener() {
46 
47             @Override
48             public void onClick(View v) {
49                 mWindowManager.removeView(rootView);
50             }
51         });
52         final TextView mTextView;
53         mTextView = (TextView) rootView.findViewById(R.id.contentTv);
54         mTextView.setText(mContent);
55 
56         int w = WindowManager.LayoutParams.MATCH_PARENT;
57         int h = WindowManager.LayoutParams.WRAP_CONTENT;
58 
59         int flags = 0;
60         int type = 0;
61         //api版本大於19的時候 TYPE_TOAST用這個引數 可以繞過絕大多數對懸浮窗許可權的限制,比如miui
62         //在小於19的時候 其實也是可以繞過的,只不過小於19你繞過了以後 點選事件就無效了 所以小於19的時候
63         //還是用TYPE_PHONE
64         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
65             type = WindowManager.LayoutParams.TYPE_TOAST;
66         } else {
67             type = WindowManager.LayoutParams.TYPE_PHONE;
68         }
69 
70         WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(w, h, type, flags, PixelFormat.TRANSLUCENT);
71         layoutParams.gravity = Gravity.TOP;
72         mWindowManager.addView(rootView, layoutParams);
73     }
74 
75 }

別忘記許可權:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
最後看下效果


到這裡應該來說模仿的就差不多了,當然你要完全做的和UC一樣還是要稍微潤色一下ui的,此外,還要監聽下啟動手機時候的廣播,當手機啟動的時候 接收到廣播
就啟動這個監聽剪下板的服務即可。點選事件也要稍微修改一下,比如點選以後去你自己的業務邏輯activity 等等。

相關文章