Android 高仿UC浏览器监控剪切板弹出悬浮窗功能
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 等等。