一种简单的护眼模式实现

概述: 不少健康管理类的app都有护眼模式的功能,在以往做过的项目中也曾经做过护眼模式功能的开发,当时参考和总结了市面上部分护眼类app的实现,顺便利用自身OS厂商可给与系统权限的优势实现了护眼模式,现简单记录如下。
实现原理 android自7.0之后提供了一个夜间模式的功能,只是该功能不是所有设备都默认开启,需要依赖硬件条件,所以不是所有7.0以上的设备都支持该功能,如果当前设备支持夜间模式,那开启护眼模式就等同于开启夜间模式,如果在不支持夜间模式的设备上,开启护眼模式,则通过添加一层蒙版的方式来实现护眼模式。
实现过程 1.权限支持: 如果不支持夜间模式,则使用蒙版需要有浮窗权限支持,先检查是否拥有该权限,如果没有,则弹窗提示,点击跳转到开启浮窗权限的页面去。

//检查是否拥有浮窗权限 public static boolean checkAllowAlert(Context cn) { try { AppOpsManager appOpsManager = (AppOpsManager) cn.getSystemService(Context.APP_OPS_SERVICE); PackageManager pm = cn.getPackageManager(); ApplicationInfo info = pm.getApplicationInfo(cn.getPackageName(),PackageManager.GET_UNINSTALLED_PACKAGES); int uid = info.uid; int alertMode =appOpsManager.checkOp(AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW,uid, cn.getPackageName()); LogUtil.d("EyeModeActivity:","alertMode:" + alertMode); if(alertMode == AppOpsManager.MODE_ALLOWED || alertMode == AppOpsManager.MODE_DEFAULT) { return true; } } catch (Exception e) { LogUtil.d(TAG,"check alert permission fail:" + e.getMessage()); } return false; }

跳转到开启浮窗权限的页面
Intent intent = new Intent("android.settings.action.MANAGE_OVERLAY_PERMISSION"); intent.setData(Uri.parse("package:" + getPackageName())); startActivity(intent);

因夜间模式的开启关闭涉及到系统数据库的操作所以还需要申请以下系统权限:

2.支持夜间模式的情况 首先需要确认该设备是否支持夜间模式,这个可从系统配置文件中查找:
\frameworks\base\core\res\res\values\config.xml
false

/**是否支持夜间模式 * 返回true支持,返回不支持 */ private boolean isNightModeSupport() { Resources res = getResources(); int allowId = res.getIdentifier("config_nightDisplayAvailable","bool","android"); return res.getBoolean(allowId); }

开启或者关闭夜间模式,这里有点差异,主要是控制类在不同android版本上不同,参数open为true,则开启夜间模式,为false,则关闭夜间模式。
private void openOrCloseNightMode(boolean open) { try { Class cl; if(Build.VERSION.SDK_INT > 27) { cl = Class.forName("com.android.internal.app.ColorDisplayController"); } else { cl = Class.forName("com.android.internal.app.NightDisplayController"); } Constructor constructor = cl.getConstructor(Context.class); Object control = constructor.newInstance(getApplicationContext()); Method md = cl.getDeclaredMethod("setActivated",boolean.class); md.invoke(control,open); if(open) { Method setMode = cl.getDeclaredMethod("setAutoMode",int.class); setMode.invoke(control,0); } } catch (ClassNotFoundException e) { LogUtil.d(TAG, "ClassNotFoundException:" + e.getMessage()); } catch (NoSuchMethodException e) { LogUtil.d(TAG, "NoSuchMethodException:"+e.getMessage()); } catch (InvocationTargetException e) { LogUtil.d(TAG, "InvocationTargetException:"+e.getMessage()); } catch (IllegalAccessException e) { LogUtil.d(TAG, "IllegalAccessException:"+e.getMessage()); } catch (InstantiationException e) { LogUtil.d(TAG, "InstantiationException:"+e.getMessage()); } destroySelf(); }

支持夜间模式的话,还需要考虑一种情况,就是应用内的护眼模式开关需要与设置中的夜间模式开关同步,如果用户从设置中开启了夜间模式,那进入自身应用护眼模式页面的时候,护眼模式的开关也需要是开启的,同理,如果用户从设置中关闭了护眼模式,那进入应用后,护眼模式的开关也需要是保持关闭状态的。这个可以通过在页面控件初始化之后添加数据库监听来实现:
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_huyanmoshi); initReceiver(); initView(); //初始化自定义监听后将其注册 nightModeObserver = new NightModeObserver(new Handler()); getContentResolver().registerContentObserver(Uri.parse("content://settings/secure/night_display_activated"), false,nightModeObserver); }

收到回调后,会根据当前控件的状态与获取到的当前夜间模式的开启状态来重新对控件状态进行设置。
public class NightModeObserver extends ContentObserver {public NightModeObserver(Handler handler) { super(handler); }@Override public void onChange(boolean selfChange) { super.onChange(selfChange); boolean buttonChecked = eyemode.isChecked(); boolean modeOn = InvokeUtil.isNightModeOn(getApplicationContext()); if(modeOn && !buttonChecked) { //夜间模式是开启的,但是控件不是开启状态,则将控件状态改为开启 changeByCode = true; eyemode.setChecked(true); } else if(!modeOn && buttonChecked) { //夜间模式是关闭的,但是控件状态是开启的,则将控件状态改为关闭 changeByCode = true; eyemode.setChecked(false); } } }

获取夜间模式是否开通,通过反射isActivated方法来获取,返回true是开启,false是关闭。
public static boolean isNightModeOn(Context context) { boolean isOpen = false; try { Class cl; if(Build.VERSION.SDK_INT > 27) { cl = Class.forName("com.android.internal.app.ColorDisplayController"); } else { cl = Class.forName("com.android.internal.app.NightDisplayController"); } Constructor constructor = cl.getConstructor(Context.class); Object control = constructor.newInstance(context); Method md = cl.getDeclaredMethod("isActivated"); isOpen = (boolean)md.invoke(control); } catch (ClassNotFoundException e) { LogUtil.d(TAG, "ClassNotFoundException:" + e.getMessage()); } catch (NoSuchMethodException e) { LogUtil.d(TAG, "NoSuchMethodException:"+e.getMessage()); } catch (InvocationTargetException e) { LogUtil.d(TAG, "InvocationTargetException:"+e.getMessage()); } catch (IllegalAccessException e) { LogUtil.d(TAG, "IllegalAccessException:"+e.getMessage()); } catch (InstantiationException e) { LogUtil.d(TAG, "InstantiationException:"+e.getMessage()); } LogUtil.d(TAG,"isNightModeOn:" + isOpen); return isOpen; }

支持夜间模式的情况实现护眼模式较为简单,基本就是通过反射ColorDisplayController或者NightDisplayController两个类中的方法来实现,这两个类也较简单,代码很少,可自行查看下。
3.不支持夜间模式,需要使用蒙版的情况 不支持夜间模式的设备上,开启护眼模式,就通过启动一个后台服务,在服务中,添加一个全屏的蒙版浮窗来实现,该浮窗不获取焦点,不拦截处理任何点击触摸事件,同时为了兼容系统的服务常驻规则,需要发送一个通知挂在前台。
首先需要确定该应用具有发送通知的权限,通过appopsmanager来实现:
public static boolean checkAllowNotification(Context cn) { AppOpsManager mAppOps = (AppOpsManager) cn.getSystemService(Context.APP_OPS_SERVICE); ApplicationInfo appInfo = cn.getApplicationInfo(); String pkg = cn.getApplicationContext().getPackageName(); int uid = appInfo.uid; Class appOpsClass = null; try { appOpsClass = Class.forName(AppOpsManager.class.getName()); Method checkOpNoThrowMethod = appOpsClass.getMethod("checkOpNoThrow", Integer.TYPE, Integer.TYPE, String.class); Field opPostNotificationValue = https://www.it610.com/article/appOpsClass.getDeclaredField("OP_POST_NOTIFICATION"); int value = https://www.it610.com/article/(Integer) opPostNotificationValue.get(Integer.class); int mode = (Integer) checkOpNoThrowMethod.invoke(mAppOps, value, uid, pkg); LogUtil.d(TAG,"checkAllowNotification mode:" + mode); //9.x系统不管是否允许通知都是返回0 if(mode == AppOpsManager.MODE_ALLOWED || mode == AppOpsManager.MODE_DEFAULT) { return true; } } catch (Exception e) { LogUtil.d(TAG,"check notification fail:" + e.getMessage()); } return false; }

如果没有通知权限,则需要弹窗提示,引导用户开启通知权限,如果当前系统支持android.settings.APP_NOTIFICATION_SETTINGS,则引导用户跳转到通知开启页面,否则,就跳转到应用详情页面,让用户自己“通知”进入通知设置页面。
public static boolean isActionAvailable(Context cn,String action) { final PackageManager packageManager = cn.getPackageManager(); Intent intent = new Intent(action); List list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); return list.size() > 0; }

if(PkgUtil.isActionAvailable(getApplicationContext(),"android.settings.APP_NOTIFICATION_SETTINGS")) { PkgUtil.goToNotificationMan(getApplicationContext()); } else { PkgUtil.gotoAppDetail(getApplicationContext()); }

【一种简单的护眼模式实现】跳转到通知设置页面:
/**go to application notification manager * * @param cn */ public static void goToNotificationMan(Context cn) { int appUid = -1; try { PackageManager packageManager = cn.getPackageManager(); ApplicationInfo ai = packageManager.getApplicationInfo(cn.getPackageName(), PackageManager.GET_UNINSTALLED_PACKAGES); appUid = ai.uid; } catch (Exception e) { e.printStackTrace(); } Intent intent = new Intent("android.settings.APP_NOTIFICATION_SETTINGS"); if(Build.VERSION.SDK_INT >= 26) { intent.putExtra("android.provider.extra.APP_PACKAGE", cn.getPackageName()); } else { intent.putExtra("app_package", cn.getPackageName()); } intent.putExtra("app_uid", appUid); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); cn.startActivity(intent); }

跳转到应用详情页面:
/**go to app detail * * @param cn */ public static void gotoAppDetail(Context cn) { Intent intent = new Intent(); intent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS"); intent.setData(Uri.fromParts("package", cn.getPackageName(), null)); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); cn.startActivity(intent); }

通知权限之所以写在后面,是因为通知权限可有可无,如果用户没有提供通知权限,对使用蒙版实现护眼模式的功能也不会有太大影响。
权限准备之后,就是浮窗的添加与移除了
public void addFullScreenWin() { LogUtil.d(TAG,"addFullScreenWin eyeView:" + eyeView); if(eyeView!=null) { return; } if(windowManager == null) { windowManager = (WindowManager)getSystemService(Context.WINDOW_SERVICE); } eyeView = new View(getApplicationContext()); eyeView.setBackgroundColor(Util.getColor(20)); final WindowManager.LayoutParams params = new WindowManager.LayoutParams(); params.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY; int flag=WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL| WindowManager.LayoutParams.FLAG_FULLSCREEN| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE|WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; params.format = PixelFormat.TRANSLUCENT; params.flags = flag; Display display = windowManager.getDefaultDisplay(); Point p = new Point(); display.getRealSize(p); params.width = p.x*4; params.height = p.y*2; if(android.os.Build.VERSION.SDK_INT >= 26) { params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; } else { params.type = WindowManager.LayoutParams.TYPE_PHONE; } windowManager.addView(eyeView, params); showNotification(); SharedInfo.getInstance().setInEyeMode(true); }

移除浮窗:
public void removeFullScreenWin() { LogUtil.d(TAG,"removeFullScreenWin eyeView:" + eyeView); if(windowManager!=null && eyeView!=null) { windowManager.removeView(eyeView); removeNotification(); eyeView = null; SharedInfo.getInstance().setInEyeMode(false); } }

弹出通知:
public void showNotification() { Intent intent = new Intent(getApplicationContext(), EyeModeActivity.class); PendingIntent pd = PendingIntent.getActivity(this,1001,intent,PendingIntent.FLAG_UPDATE_CURRENT); NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); NotificationChannel notificationChannel = null; if (android.os.Build.VERSION.SDK_INT >= 26) { notificationChannel = new NotificationChannel("eyeProtectMode", "com.healthguard.channel", NotificationManager.IMPORTANCE_LOW); notificationChannel.enableLights(true); notificationChannel.setShowBadge(true); notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); manager.createNotificationChannel(notificationChannel); } Notification.Builder notibuild = new Notification.Builder(this) .setSmallIcon(R.drawable.jiankang) .setContentTitle(getResources().getString(R.string.eye_protection_mode)) .setContentText(getResources().getString(R.string.eye_protection_mode_notice)) .setContentIntent(pd) .setAutoCancel(false) .setDefaults(Notification.DEFAULT_ALL); if (android.os.Build.VERSION.SDK_INT >= 26) { notibuild.setChannelId("eyeProtectMode"); } //startForeground(1001,notibuild.build()); manager.notify(1001,notibuild.build()); }

移除通知:
public void removeNotification() { NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(1001); }

使用蒙版的情况下还需要注意一个情况,就是需要处理插入usb接口时候,如果弹出允许调试的弹窗,这个时候该弹窗是点击不到的,所以需要监听usb插入与拔出广播,当插入usb时候移除蒙版,拔出后重新添加。
public class UsbReceiver extends BroadcastReceiver {@Override public void onReceive(Context context, Intent intent) { if(intent == null) { return; } String action = intent.getAction(); if(TextUtils.isEmpty(action)) { return; } if(action.equals("android.hardware.usb.action.USB_STATE")) { boolean cn = intent.getBooleanExtra("connected",false); if(!cn && eyeView==null) { //usb拔出,且浮窗已经移除,则重新添加 addFullScreenWin(); } else if(cn && eyeView!=null) { //usb插入,且浮窗已经添加,则将其移除 removeFullScreenWin(); } } } }

实现护眼模式功能的主要思路和方法,就基本都记录如上了,剩下的是些页面处理和同步之类的业务逻辑处理。

    推荐阅读