weijunfeng
4/4/2019 - 12:16 PM

android 8.0适配

Andriod版本适配 check list(持续更新)
96  Bug总柴 
 0.6 2018.11.16 19:40* 字数 845 阅读 1232评论 2喜欢 7
前言
Android官方的迁移适配文档有点混乱,这篇文章旨在给开发者在适配中对代码做快速检查。适配变化将分为运行版本影响和Target版本影响,并提供可能影响的功能以便测试参考。

Android Q (API level 29)
沙箱机制(scoped-storage)
在Android Q中变化比较大的是对外置sdcard的访问权限变化,这个变化将会影响大部分需要访问外置存储的应用。

沙箱机制解读
external storage在Android Q开始被设置成像internal storage那种只能访问自己包名下的空间,无法直接访问sdcard其他位置内容。就算声明了READ_EXTERNAL_STORAGE权限,在应用中通过File.listFiles只能看到/storage/emulated/0/Android/data/<package> , /storage/emulated/0/Android/media/<package> , /storage/emulated/0/Android/obb/<package> 三个文件夹。
READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE的通用访问外置sdcard的权限被拆分为访问音乐READ_MEDIA_AUDIO、照片READ_MEDIA_IMAGES和视频READ_MEDIA_VIDEO三种权限,而访问应用沙箱的内容无需额外申请权限。
沙箱生效时机
如果target版本小于等于28并且应用是安装在从Android 9升级到Andoid Q的手机上,则会启用兼容模式,仍然可以随意访问external存储的内容。
意味着当target版本大于28,或者应用是在Android Q的手机上新安装都会使沙箱机制生效。这里需要说明,不管是否target到28以上,只要是在Android Q上新安装的应用都会使沙箱机制生效。
对于模拟器里面的Andorid Q Beta 1版本,需要执行adb shell sm set-isolated-storage on开启沙箱机制
影响范围
各种为了实现离线使用功能的离线下载文件
各种缓存文件(例如信息流缓存、广告缓存等)
需要注意某些三方库可能会使用外置sdcard(例如log或者crash统计等)
四、处理办法

对于图片视频音乐和下载文件可以通过MediaStore类访问,或者使用Storage Access Framework
对于之前存储在外置sdcard的其他数据,需要迁移存储到getExternalFilesDir目录中
对于新增的文件尽量保存在getExternalFilesDir和getExternalCacheDir
Api检查
Context.getExternalFilesDir(null) -> /storage/emulated/0/Android/data/<package>/files
Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) -> /storage/emulated/0/Android/data/<package>/files/Pictures
Context.externalCacheDir -> /storage/emulated/0/Android/data/<package>/cache
Context.obbDir -> /storage/emulated/0/Android/obb/<package>
Environment.getExternalStorageDirectory() -> /storage/emulated/0 (沙箱机制下无法访问)
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) -> /storage/emulated/0/Pictures (沙箱机制下无法访问)

Android 9 (API level 28)
官方行为变更文档

非SDK接口使用限制
使用 veridex工具测试apk是否有调用非SDK接口

➜  veridex-mac ./appcompat.sh --dex-file=test.apk
实例结果如下:

6889 hidden API(s) used: 6817 linked against, 72 through reflection
       0 in blacklistgetConnectionInfo
       3 in dark greylist
       47 in light greylist
To run an analysis that can give more reflection accesses, 
but could include false positives, pass the --imprecise flag.
其中:

类型	描述
blacklist	不管是否target到28,都会报NoSuchMethodError/NoSuchFieldException
dark greylist	如果target在28一下没问题,但是target到28及以上会报NoSuchMethodError/NoSuchFieldException
light greylist	暂时没有问题,可以使用
处理办法:
去除blacklist以及dark greylist的非android sdk调用的反射调用,有些是android support包内部调用的可以考虑升级support包版本

隐私&权限相关
运行在9.0受到影响	可能受到影响的功能
不能在后台访问麦克风和摄像头	后台录音、后台拍照
加速器陀螺仪等传感器不能在后台持续获取数据	步数计算
通过变化模式或者单次模式的传感器收不到事件	显著运动检测、计步器、近程传感器和心率传感器
通话记录权限组别由PHONE组调整到CALL_LOG组	需要获通过记录权限的功能
通过android.intent.action.PHONE_STATE或TelephonyManager.listen方法获取手机号码需要申请READ_CALL_LOG 权限	例如来电归属地显示或者来电拦截等需要获取通话手机号的功能
wifi扫描频率限制更为严格,getConnectionInfo WifiManager.getScanResults()以及WifiManager.startScan()需要而外权限详见	需要wifi扫描匹配等功能
WifiManager.getConnectionInfo() 要获得SSID和BSSID,要求定位权限并要求设备打开定位功能,NETWORK_STATE_CHANGED_ACTION不再能获得SSID和BSSID	需要获取wifi信息的功能
WifiManager与WifiP2pManager中getScanResults() getConnectionInfo()和discoverServices() addServiceRequest()和NETWORK_STATE_CHANGED_ACTION不再包含用户定位信息	使用wifi定位功能
TelephonyManager中getAllCellInfo() listen() getCellLocation() getNeighboringCellInfo()不返回结果,除非用户打开了定位功能	使用移动信号定位
Target在9.0受到影响	可能受到影响的功能
启动前台服务要去注册android.permission.FOREGROUND_SERVICE权限	前台服务启动
获取序列号不能通过Build.SERIAL,需要注册android.permission.READ_PHONE_STATE然后使用Build.getSerial()	获取序列号相关功能
安全相关
运行在9.0受到影响	可能受到影响的功能
SSLSocket出错不返回NullPointerException,改成返回IOException	https网络错误处理
加密函数Cipher.getInstance("AES/CBC/PKCS7PADDING", "BC") Cipher.getInstance("AES/CBC/PKCS7PADDING",Security.getProvider("BC")) SecureRandom.getInstance("SHA1PRNG", "Crypto");移除	加密功能
Android secure encrypted files移除	移动app到sdcard功能
Target在9.0受到影响	可能受到影响的功能
DNS客户端需要根据系统使用加密DNS查找与系统相同的主机名,或改由系统解析程序	DNS自解析功能
默认要求使用https,如果需要使用http需要设置cleartextTrafficPermitted="true"详见	所有http网络请求
webview的数据包括cookies和caches不允许多进程共享	多进程使用webview
不用通过设置全局Unix权限共享数据文件,不用应用的文件共享需要使用ContentProvider	应用间文件共享
国际化相关
运行在9.0受到影响	可能受到影响的功能
java.text.SimpleDateFormat 使用zzzz格式、java.text.DateFormatSymbols.getZoneStrings()格式、NumberFormat.getInstance(ULocale, PLURALCURRENCYSTYLE).parse(String)格式修改	时区、货币显示相关功能
网络相关
运行在9.0受到影响	可能受到影响的功能
NetworkCapabilities支持返回NET_CAPABILITY_NOT_VPN	vpn设置功能
Apache HTTP client不能使用system ClassLoader加载,若要使用需要实现自定义ClassLoader	使用旧Apache Http client网络功能
Target在9.0受到影响	可能受到影响的功能
NetworkStatsManager能获取非当前正在使用的流量情况	网络使用统计
ConnectivityManager.getMultipathPreference() 可以获取是否超过了移动流量使用限制	网络使用情况提醒
Apache Http背去除,要使用需要加上<uses-library android:name="org.apache.http.legacy" android:required="false"/>或者想apache.http相关类包通过jar方式引入	使用旧Apache Http client网络功能
界面相关
运行在9.0受到影响	可能受到影响的功能
通过非activity的context启动activity强制要求intent带上FLAG_ACTIVITY_NEW_TASK	后台启动页面
屏幕旋转方式由原来的“自动旋转”和“纵向”改为“自动旋转”和“固定旋转”	屏幕旋转功能
Target在9.0受到影响	可能受到影响的功能
长或宽为0的view不再可以获取焦点,新开页面不默认获取焦点	交互过程通过特殊焦点实现的功能
webview可以支持带透明度的8位颜色css	webview css 颜色透明度功能
webview中document的root元素滚动位置得到支持	webview 相关
暂停挂起app的通知会在app resumed之后重新通知	通知相关
设备相关
运行在9.0受到影响	可能受到影响的功能
多摄像头支持getCameraIdList()前后摄像头切换需要选择合适的摄像头	摄像头相关功能
其他
运行在9.0受到影响	可能受到影响的功能
UTF-8解码更加严格按照Unicode标准详见	UTF-8解码相关的功能
实用参考地址
权限组级别

Android 8 (API level 26)
官方行为变更文档

后台限制
运行在8.0受到影响	可能受到影响的功能
后台应用通过startService()方法启动服务,
包括IntentService会受到限制并抛出IllegalStateException异常,
需要改成使用 JobScheduler 或者JobIntentService	所有启动后台服务的行为,包括但不限于后台下载、后台数据更新、后台初始化等等
前台服务启动不能通过启动后台服务再将其转换为前台,
需要通过startForegroundService()方法,
并在5s内调用startForeground()方法显示前台通知,否则会ANR	所有前台服务,包括音乐播放功能、其他有通知的服务
自定义action广播以及其他系统非指向性的广播接收受到限制,
可通过manifests注册指向性广播或者通过Context.registerReceiver()动态注册,
系统性的广播事件可考虑通过JobScheduler配置实现	例如软件安装后的广播处理以及网络变化通知处理功能
后台应用获取位置受到限制,包括FusedLocationProviderApi、
GnssMeasurement、GnssNavigationMessage、
WifiManager.startScan()、LocationManager,需要使用前台服务保持应用前台状态	后台动作检测功能、后台需要用到地理位置的功能例如后台导航之类
隐私&权限相关
运行在8.0受到影响	可能受到影响的功能
ANDROID_ID从之前的仅与设备相关,改为与应用签名、设备、设备登录用户相关。	使用ANDROID_ID的功能
获取系统属性net.hostname将返回null	wifi hostname获取功能
Target在8.0受到影响	可能受到影响的功能
系统属性net.dns*不再支持	通过系统属性获取dns功能
需要获取DNS信息需要ACCESS_NETWORK_STATE权限,通过
NetworkRequest或者NetworkCallback获取	DNS获取功能
获取序列号不能通过Build.SERIAL,需要注册android.permission.READ_PHONE_STATE然后使用Build.getSerial()	获取序列号相关功能
LauncherApps获取不同用户的应用信息时,会当做没有任何应用安装,而不是抛出异常	桌面启动器相关功能
相同权限组的其他权限会在真正需要时才被自动授予,之前是整个权限组同时授予	权限授予相关
安全相关
运行在8.0受到影响	可能受到影响的功能
不再支持SSLv3	使用SSLv3的地方
当HTTPS使用错误的TLS协议与服务交互时,不再使用其他TLS协议重试	HTTPS相关
在bionic之外的系统调用将被禁止	bionic系统调用
WebView被运行在多进程空间	WebView间数据共享
APKs安装路径可能会被修改	APKs管理
判断是否能安装应用需使用PackageManager.canRequestPackageInstalls(),
INSTALL_NON_MARKET_APPS失效	应用安装
8.0系统默认禁止应用安装未知应用	应用安装功能
Thread.UncaughtExceptionHandler 会记录在stacktrace中,但不会杀死应用	线程异常处理
Target在8.0受到影响	可能受到影响的功能
registerContentObserver(Uri, boolean, ContentObserver)中的Uri必须使用ContentProvider注册	以Uri来通知变化的功能
network_security_config.xml 配置禁止明文传输将同样影响WebView	Https功能
AccountManager不能只通过申明GET_ACCOUNTS来获取账号,需要调用
AccountManager.newChooseAccountIntent()让用户选择,
再通过AccountManager.getAccounts()来获取	Account Services相关
native库若包含可执行文件则不会加载	native库相关
JNI调用会检查反射的类或方法是否存在,否则会抛出异常	JNI调用
DexFile API已经过时,建议使用系统默认PathClassLoader 或者 BaseDexClassLoader。
如果需要用到DexFile,不应该进行压缩,否则会解压消耗内存。
多线程加载相同类由最先加载的类的加载器决定。	Dex 加载相关
国际化相关
运行在8.0受到影响	可能受到影响的功能
Currency.getDisplayName()、Currency.getSymbol()、
Locale.getDisplayScript()
默认调用Locale.getDefault(Category.DISPLAY)	国际化显示
Currency.getDisplayName(null)将会抛出异常	国际化单位显示
对于SimpleDateFormat的时区获取由原来在设备第一次启动时候获取,改为每次实时获取	时区显示
升级ICU到58版本	国际化单位标准
网络相关
运行在8.0受到影响	可能受到影响的功能
无正文的 OPTIONS 请求具有 Content-Length: 0 头部	options请求相关
HttpURLConnection会保证请求最后带上“/”	HttpURLConnection
ProxySelector.setDefault()设置的代理仅处理scheme/host/port,不会处理请求参数	代理设置相关功能
不再支持空lable的URI	使用URI相关功能
HttpsURLConnection不会执行不安全的TLS/SSL协议版本回退	HttpsURLConnection
隧道Https协议改变,具体见Networking and HTTP(S) connectivity	隧道Https
如果DatagramSocket.connect()返回错误,DatagramSocket.send()也会返回错误	socket相关
InetAddress.isReachable() 会在会退到TCP Echo协议之前尝试ICMP协议,若不可达会消耗更多时间	IP地址判断是否可达等网络功能
在支持设备上wifi连接当有强度大且已经保存的网络时可以自动切换	需保证网络切换不会影响应用功能
界面相关
运行在8.0受到影响	可能受到影响的功能
TYPE_PHONE、TYPE_PRIORITY_PHONE、
TYPE_SYSTEM_ALERT、TYPE_SYSTEM_OVERLAY、
TYPE_SYSTEM_ERROR这些类型的窗口都会显示在TYPE_APPLICATION_OVERLAY之下	悬浮球、快速查词等需要弹窗弹窗的地方
使用键盘导航时,获取焦点的view将会加上ripple高亮,
如果不需要这种默认的高亮,
需要设置android:defaultFocusHighlightEnabled
或者setDefaultFocusHighlightEnabled(false)	键盘导航
webview中WebSettings.getSaveFormData()返回false,
WebSettings.setSaveFormData()没有任何作用,
WebViewDatabase.clearFormData()没有任何作用,
WebViewDatabase.hasFormData()返回false	网页相关
Target在8.0受到影响	可能受到影响的功能
TYPE_PHONE、TYPE_PRIORITY_PHONE、
TYPE_SYSTEM_ALERT、TYPE_SYSTEM_OVERLAY、
TYPE_SYSTEM_ERROR
不能用在alert window上,必须使用
TYPE_APPLICATION_OVERLAY	悬浮球、快速查词等需要弹窗弹窗的地方
可点击的View默认拥有可获取焦点属性	View焦点显示
Notificaiton通知必须指定Notificaiton Channels,否则不会显示通知,详见notifications	通知相关
设备相关
运行在8.0受到影响	可能受到影响的功能
蓝牙ScanRecord.getBytes()返回长度不受限制	蓝牙相关功能
Target在8.0受到影响	可能受到影响的功能
音频获取焦点时会自动降低其他音频音量,现在支持暂停而不是降低音量,详见automatic ducking	音频播放相关功能
当来电时,自动静音音频播放	音频播放相关功能
需要使用AudioAttributes实现音频回放功能,AudioTrack过期	音频回放功能
音量按键事件会优先给前台activity,如果前台activity不处理会给最近一次播放音频的应用	音量控制
其他
运行在8.0受到影响	可能受到影响的功能
应用快捷方式不能通过com.android.launcher.action.INSTALL_SHORTCUT创建,
需要使用ShortcutManager,具体如何创建可以看这篇文章	快捷方式创建功能
无障碍功能中双击动作转换为点击动作、
能识别TextView中的ClickableSpan	无障碍功能
findViewById() 返回类型由View改为<T extends View> T	覆盖findViewById() 的地方需要相应修改
从2019年1月7日起,将无法通过
LAST_TIME_CONTACTED
/TIMES_CONTACTED
/LAST_TIME_USED
/TIMES_USED
获取联系人使用情况	联系人联系情况获取功能
AbstractCollection.removeAll(java.util.Collection)
/AbstractCollection.retainAll(java.util.Collection)
当传入参数为null时会报NullPointerException	集合操作
Target在8.0受到影响	可能受到影响的功能
浏览器ua会包含OPR有可能导致判断是否Opera浏览器失效	根据ua判断浏览器
Collections.sort()改为在List.sort()基础上实现,之前是恰好相反。
如果在List.sort()中调用Collections.sort()会产生死循环	集合排序
在遍历的过程中进行排序,现在使用无论使用List.sort()还是Collections.sort()都会报错	集合排序
记 Android 7.0 8.0版本更新安装遇到的坑
96  淡雅如兰_往事随风 
 0.7 2018.06.28 18:07* 字数 1670 阅读 5681评论 7喜欢 34
前言:7.0版本更新FileProvider的使用网上很多就不讲了,本文主要讲述这次发版遇到的一系列坑。。。
前天喜滋滋的发布了新版本,木有想到昨天就出现问题了,那就是Android 8.0系统居然不能下载安装,或是下载成功了也没有跳出应用安装界面。于是我不管三七二十一先百度了一波,大概意思就是Android 8.0的系统中,“未知来源应用权限”的开关被移除掉了,取而代之的是未知来源应用的管理列表,如果你想要安装某个被自己所信任的开发者的app,则需要在每一次都手动授权“安装未知应用”的许可。

网上的解决其实很简单:
1.在AndroidManifest.xml文件中,添加REQUEST_INSTALL_PACKAGES权限

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
2.在打开安装包的代码中添加“兼容Android 8.0”的代码

       //兼容8.0
        boolean installAllowed;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            installAllowed = context.getPackageManager().canRequestPackageInstalls();
            if (installAllowed) {
                installApk(file);
            } else {
                Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + context.getPackageName()));
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                context.startActivity(intent);
                installApk(file);
                return;
            }
        } else {
            installApk(file);
        }

    //安装apk,兼容7.0
    protected void installApk(File file) {
        if (!file.exists()) {
            return;
        }
        Intent intent = new Intent(Intent.ACTION_VIEW);
        //版本在7.0以上是不能直接通过uri访问的
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            //参数1 上下文, 参数2 Provider主机地址和清单文件中保持一致   参数3 共享的文件
            Uri apkUri =
                    FileProvider.getUriForFile(context, "com.xxx.fileProvider", file);
            //添加这一句表示对目标应用临时授权该Uri所代表的文件
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
        } else {
            intent.setDataAndType(Uri.fromFile(file),
                    "application/vnd.android.package-archive");
        }
//        intent.setDataAndType(Uri.parse("file://" + file.toString()), "application/vnd.android.package-archive");
        // 由于没有在Activity环境下启动Activity,设置下面的标签
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }

在这个过程中我还遇到了一个坑,那就是canRequestPackageInstalls一直返回false。查了资料才发现targetSdkVersion是26以上才能获取正确的canRequestPackageInstalls,否则就一直返回false。(ps:我之前的targetSdkVersion是24)。
做完这两步后可以下载并自动跳转到未知来源应用权限界面,你以为这样就完了吗?并没有!!!
运行了几次后直接下载进度没有了,换言之就是下载不了! what??? 一阵烦躁。。。
想不到到底是哪儿出现问题了呢,刚刚还好好的运行,咋地突然就不能下载了。。。
查看了报错日志,如下:


报错日志.png

意思就是用户拒绝了权限,可是我明明一开始就动态设置权限允许了呀,为什么还会出现这个??不信邪的我特意去看这个APP下看了权限,确实存储权限是开了的呀,一脸懵逼。。。
第一感觉会不会又是Android 8.0的问题呢,结果确实是,是因为代码中动态申请的其实是READ_EXTERNAL_STORAGE读存储权限,这在Android O(Android 8.0)之前是没有任何问题的,因为读写是一组权限,同属存储权限,只要申请了同组权限中的一个,同组中的其他在清单文件中列出了的权限也就被授予了。但是Android O(Android 8.0)运行时权限有了变动,就是系统只会授予应用明确请求的权限,
然而一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准,但是还是需要去申请,这点和Android O(Android 8.0)之前不同。
由于这里创建下载文件,实际上是往存储中写文件,需要写存储权限WRITE_EXTERNAL_STORAGE,于是将代码点击更新时申请READ_EXTERNAL_STORAGE改为申请WRITE_EXTERNAL_STORAGE。运行测试,APK文件是可以下载成功了。至于之前几次为什么可以下载,我还是想不明白,可能和targetSdkVersion有关。

如果你以为我这次的坑彻底结束了,那你就错了!!!因为这些解决完之后结果解析包安装失败了,,why???
查了资料大体就是Android Studio打包问题,现在Android Studio打包出现了两个选择signature versions:V1(Jar Signature) and V2(Full APK Signature) 。以下是官方说法:
Android 7.0 引入一项新的应用签名方案 APK Signature Scheme v2,它能提供更快的应用安装时间和更多针对未授权 APK 文件更改的保护。在默认情况下,Android Studio 2.2 和 Android Plugin for Gradle 2.2 会使用 APK Signature Scheme v2 和传统签名方案来签署应用。
这项新方案并非强制性的,如果应用在使用 APK Signature Scheme v2 时不能正确开发,可以停用这项新方案。禁用过程会导致 Android Studio 2.2 和 Android Plugin for Gradle 2.2 仅使用传统签名方案来签署应用。要仅用传统方案签署,打开模块级 build.gradle 文件,然后将行 v2SigningEnabled false 添加到版本签名配置中:

android { 
… 
defaultConfig { … } 
 signingConfigs {
        debug {
            storeFile file("./xxx.keystore")
            storePassword 'password'
            keyAlias 'xxx'
            keyPassword 'password'
        }
        release {
            storeFile file("./xxx.keystore")
            storePassword 'password'
            keyAlias 'xxx'
            keyPassword 'password'
            v2SigningEnabled false
        }
    } 
根据官方文档,就是在我们的gradle文件里的相应位置添加这行代码

v2SigningEnabled false
but!!!我添加了之后还是出现了解析包安装失败,,蓝瘦香菇。。。路漫漫~~~只能继续摸索
当发现是 7.0 系统上才会出现的问题之后,再联系报错信息,很容易就想到 FileProvider 的权限问题,然而并没有什么用,我还是不知道怎么回事。对比之前实现的版本更新的代码,定位到 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)这句代码,因为我把intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)放在了intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)前面,当把intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)这句话放在intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)前面,就是正常的。

这是因为对 setFlags() 和 addFlags() 认知不清,错误使用导致的,最后,将 setFlags()操作放在 addFlags() 之前解决了这个问题。

  //安装apk,兼容7.0
    protected void installApk(File file) {
        if (!file.exists()) {
            return;
        }
        Intent intent = new Intent(Intent.ACTION_VIEW);
      // 由于没有在Activity环境下启动Activity,设置下面的标签   setFlags要放在addFlags之前
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        //版本在7.0以上是不能直接通过uri访问的
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            //参数1 上下文, 参数2 Provider主机地址和清单文件中保持一致   参数3 共享的文件
            Uri apkUri =
                    FileProvider.getUriForFile(context, "com.xxx.fileProvider", file);
            //添加这一句表示对目标应用临时授权该Uri所代表的文件
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
        } else {
            intent.setDataAndType(Uri.fromFile(file),
                    "application/vnd.android.package-archive");
        }
//        intent.setDataAndType(Uri.parse("file://" + file.toString()), "application/vnd.android.package-archive");
        context.startActivity(intent);
    }
intent.setFlags() 和 intent.addFlags() 的区别
setFlags():为intent 设置特殊的标志,会覆盖 intent 已经设置的所有标志。

 public Intent setFlags(int flags) {
     mFlags = flags;
     return this;
 }
addFlags():为intent 添加特殊的标志,不会覆盖,只会追加。

 public Intent addFlags(int flags) {
      mFlags |= flags;
      return this;
  }
之前更新Android 7.0并没有这个问题,应该也是和targetSdkVersion有关。
到此,,此次版本更新遇到的坑总算填完了。。。
总结此次的坑:
一、Android 8.0下载失败。解决方案:把Android 6.0的动态申请权限READ_EXTERNAL_STORAGE改为申请WRITE_EXTERNAL_STORAGE。
二、Android 8.0下载成功后无法跳到自动更新页面。解决方案:授权“安装未知应用”的许可。
三、授权“安装未知应用”的许可的时候获取canRequestPackageInstalls一直返回false。解决方案:targetSdkVersion必须大于等于26(我之前是24)。
四、Android 7.0 8.0 解析包安装失败。解决方案:安装时把intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)这句话放在intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)前面。

好了,,这次真的是讲完了,共勉。。。
Android 8.0后台执行限制
2017年05月08日 14:12:35 陈辰臣 阅读数:24187
 版权声明:本文为博主原创文章,未经博主允许不得转载。	https://blog.csdn.net/chenshengfa/article/details/71407704
Android O对应用在后台运行时可以执行的操作施加了限制,称为后台执行限制(Background Execution Limits),这可以大大减少应用的内存使用和耗电量,提高用户体验。后台执行限制分为两个部分:后台服务限制(Background Service Limitations)、广播限制(BroadcastLimitations)。



后台服务限制

如何才算是后台应用?除了下面情况外都是后台应用

1. 具有可见的Activity

2. 具有前台服务

3. 另一个前台应用已关联到该应用(通过bindService或者使用该应用的ContentProvider)。



当应用处于后台时:

1.在后台运行的服务在几分钟内会被stop掉(模拟器测试在1分钟左右后被kill掉)。在这段时间内,应用仍可以创建和使用服务。

2.在应用处于后台几分钟后(模拟器测试1分钟左右),应用将不能再通过startService创建后台服务,如果创建则抛出以下异常


Caused by: java.lang.IllegalStateException: Not allowed to start service Intent { cmp=com.example.samsung.test/.TestService }: app is in background
应用处于后台时,虽然不能通过startService创建后台服务,但仍可以通过下面的方式创建前台服务。
NotificationManager noti = (NotificationManager)getApplicationContext().getSystemService(NOTIFICATION_SERVICE);
noti.startServiceInForeground();
后台服务会被kill掉,官方推荐可使用AlarmManager、SyncAdapter、JobScheduler代替后台服务。



广播限制

如果应用监听一些系统广播,当系统发出广播时,很多应用都会被唤醒,这会导致所有应用快速地连续消耗资源,从而降低用户体验。其实,大部分应用都不会处理这个广播,应用只是唤醒一下看看和自己是否有关,为了缓解这一问题,Android N对一些广播做出了限制:

1.targetSdkVersion为Android N(API level 24)及以上的应用,如果应用在AndroidManifest.xml中静态注册CONNECTIVITY_ACTION这个receiver,应用将不能收到此广播。如果应用使用Context.registerReceiver()动态注册receiver,应用仍可以收到这个广播。

2.运行在Android N及以上设备的应用,无论是targetSdkVersion是否是Android N,应用都不能发送或者接收ACTION_NEW_PICTURE和ACTION_NEW_VIDEO这两个广播。



而Android O执行了更为严格的限制。

1.动态注册的receiver,可接收任何显式和隐式广播。

2.targetSdkVersion为Android O(API level 26)及以上的应用,静态注册的receiver将不能收到隐式广播,但可以收到显式广播。下面例子说明

清单文件的receiver声明

<receiver
    android:name=".TestReceiver"
    android:enabled="true">
    <intent-filter>
        <action android:name="com.test.example.testreceiver.action"></action>
    </intent-filter>
</receiver>


Receiver类定义

public class TestReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "onReceive", Toast.LENGTH_SHORT).show();
    }
}


①如果这样发送隐式广播,receiver将接收不到广播。

Intent intent = new Intent();
intent.setAction("com.test.example.testreceiver.action");
sendBroadcast(intent);


②如果这样发送显式广播,receiver仍可以接收到广播

Intent intent = new Intent();
intent.setClassName("com.test.example.testreceiver", "com.test.example.testreceiver.TestReceiver");
sendBroadcast(intent);


虽然静态注册的receiver不能接收隐式广播,但Google考虑到一些广播也不频繁发生,也有一些例外,对这些例外的静态注册的receiver,仍可接收隐式和显式广播。

ACTION_LOCKED_BOOT_COMPLETED、ACTION_BOOT_COMPLETED

ACTION_USER_INITIALIZE

ACTION_TIMEZONE_CHANGED

ACTION_LOCALE_CHANGED

ACTION_USB_ACCESSORY_ATTACHED

ACTION_USB_ACCESSORY_DETACHED

ACTION_USB_DEVICE_ATTACHED

ACTION_USB_DEVICE_DETACHED

ACTION_HEADSET_PLUG

……

详情请参考

https://developer.android.com/preview/features/background-broadcasts.html



据Google工程师透露,Google的目标是充满一次电,手机可用2~3天,这也可看到Google在手机续航方面的努力。

目前只有Android Studio2.4可以下载Android O 预览版SDK,下载地址

https://dl.google.com/dl/android/studio/ide-zips/2.4.0.6/android-studio-ide-171.3934896-windows.zip
Android O(8.0)Notification Channel使用姿势
96  蓅哖伊人为谁笑 
 0.1 2018.01.29 12:25* 字数 524 阅读 6019评论 0喜欢 11
 NotificationGroup通知渠道组示意图.png
上面这张图,将具有形同形态,相同表现形式的通知渠道A ,通知渠道B归纳到了同一个渠道组中。
通知渠道的引入可以很方便的管理,和归纳同一种类型的通知Notification.
通知渠道组的引入同样可以方便的管理,归纳同一种类型的通知渠道Channel.
1.首先看一下Android8.0之前的普通Notification的样式以及使用姿势
普通多个notification.png
可以看到是多个通知 ,无论是不是同一个应用的通知,逐个排列下来,占满了屏幕,不太友好
接下来我们来看一下代码
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
                .setSmallIcon(android.R.drawable.stat_notify_chat)
                .setContentTitle("你有一条新的消息")
                .setContentText("this is normal notification style")
                .setTicker("notification ticker")
                .setPriority(1000)
                .setAutoCancel(true)
                .setVibrate(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400})
                .setNumber(3)
                .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE)
                .setContentIntent(pendingResult)
                .setOngoing(true);


        Notification notification = mBuilder.build();
        getNotificationManager().notify(notifyId, notification);
2.再来看一下带下载进度的通知栏Notification
带下载进度Notification.png
起一个线程来模拟 下载进度,间隔1秒更新一下 通知进度。
 final NotificationManager mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, CHANNEL_ID);
        mBuilder.setContentTitle("Picture Download")
                .setContentText("Download in progress")
                .setOngoing(true)
                .setVibrate(new long[]{0})
                .setSound(null)
                .setTicker("notification ticker")
                .setDefaults(NotificationCompat.FLAG_LOCAL_ONLY)
                .setSmallIcon(android.R.drawable.stat_notify_chat);
        new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        int incr;
                        for (incr = 0; incr <= 100; incr += 5) {
                            mBuilder.setProgress(100, incr, false);
                            mBuilder.setSound(null);
                            mNotifyManager.notify(0, mBuilder.build());
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                            }
                        }
                        mBuilder.setContentText("Download complete").setProgress(0, 0, false);
                        mBuilder.setAutoCancel(true);
                        mBuilder.setOngoing(false);
                        mNotifyManager.notify(0, mBuilder.build());
                    }
                }
        ).start();
3.再来看一下自定义布局的Notification
自定义布局Notification.png
创建一个布局资源,通过RemoteViews来 渲染,同时也绑定了点击事件
  RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.remote_custom_notification);
        remoteViews.setTextViewText(R.id.title, "Custom Notification---title");
        remoteViews.setTextViewText(R.id.content, "Custom Notification---content");

        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("image/*");
        intent.addCategory(Intent.CATEGORY_OPENABLE);

        PendingIntent pendingIntent = PendingIntent.getActivity(this, REQUEST_CODE_NOTIFY, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setSmallIcon(android.R.drawable.stat_notify_chat)
                .setContent(remoteViews)
                .setTicker("ticker")
                .setContentIntent(pendingIntent).build();

        getNotificationManager().notify(notifyId, notification);
  
以上【1】【2】【3】就是常用的通知栏Notification的样式和使用姿势

4.下面来看一下Android 8.0上通知渠道NotificationChannel 的应用
NotificationChannel.png
上图可以看到,相同通知渠道的通知已经被合并,而不是一一全部展开。
如果你的项目TargetSDK在26以上,那么你在使用Notification的时候必须指定一个ChannelId,否则当然会报错
下面是Android 8.0上通知渠道NotificationChannel 的使用代码段,注意点需要传入CHANNEL_ID(随意指定),CHANNEL_NAME(随意指定)
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
        channel.setBypassDnd(true);    //设置绕过免打扰模式
        channel.canBypassDnd();       //检测是否绕过免打扰模式
        channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);//设置在锁屏界面上显示这条通知
        channel.setDescription("description of this notification");
        channel.setLightColor(Color.GREEN);
        channel.setName("name of this notification");
        channel.setShowBadge(true);
        channel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
        channel.enableVibration(true);
        getNotificationManager().createNotificationChannel(channel);
5.如果要使用通知渠道组NotificationChannelGroup,那么它的样式跟上图一样,使用姿势是下面这样
这里在渠道组NotificationChannelGroup上绑定了两个通知渠道NotificationChannel ,每个渠道下各有一个通知Notification.
getNotificationManager().createNotificationChannelGroup(new NotificationChannelGroup(GROUP_ID, "GROUP_CHANNEL"));

        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
        channel.setGroup(GROUP_ID);
        channel.setShowBadge(true);
        channel.setLightColor(Color.RED);
        channel.enableLights(true);
        getNotificationManager().createNotificationChannel(channel);

        NotificationChannel channel2 = new NotificationChannel(CHANNEL_ID_2, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
        channel2.setGroup(GROUP_ID);
        channel2.setShowBadge(true);
        channel2.setLightColor(Color.RED);
        channel2.enableLights(true);
        getNotificationManager().createNotificationChannel(channel2);


        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("image/*");
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        PendingIntent pendingResult = PendingIntent.getActivity(this, REQUEST_CODE_NOTIFY, intent, PendingIntent.FLAG_UPDATE_CURRENT);

        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
                .setSmallIcon(android.R.drawable.stat_notify_chat)
                .setContentTitle("notification title_9")
                .setContentText("notification content_9")
                .setPriority(1000)
                .setAutoCancel(true)
                .setVibrate(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400})
                .setNumber(3)
                .setDefaults(Notification.DEFAULT_LIGHTS)
                .setContentIntent(pendingResult)
                .setOngoing(true);

        NotificationCompat.Builder mBuilder1 = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID_2)
                .setSmallIcon(android.R.drawable.stat_notify_chat)
                .setContentTitle("notification title_10")
                .setContentText("notification content_10")
                .setPriority(1000)
                .setAutoCancel(true)
                .setVibrate(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400})
                .setNumber(15)
                .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE)
                .setContentIntent(pendingResult)
                .setOngoing(true);


        getNotificationManager().notify(9, mBuilder.build());
        getNotificationManager().notify(10, mBuilder1.build());
6.通知渠道的管理
1.删除渠道

 getNotificationManager().deleteNotificationChannel(CHANNEL_ID);
2.删除渠道组

 getNotificationManager().deleteNotificationChannelGroup(GROUP_ID);
以上便是老版本上使用Notification 和 Android 8.0上通知渠道,渠道组的姿势。

Android通知栏微技巧,8.0系统中通知栏的适配
2018年04月17日 07:39:11 guolin 阅读数:41155
版权声明:本文出自郭霖的博客,转载必须注明出处。	https://blog.csdn.net/sinyu890807/article/details/79854070
转载请注明出处:https://blog.csdn.net/guolin_blog/article/details/79854070

本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新。

大家好,今天我们继续来学习Android 8.0系统的适配。

之前我们已经讲到了,Android 8.0系统最主要需要进行适配的地方有两处:应用图标和通知栏。在上一篇文章当中,我们学习了Android 8.0系统应用图标的适配,还没有看过这篇文章的朋友可以先去阅读 Android应用图标微技巧,8.0系统中应用图标的适配 。

那么本篇文章,我们自然要将重点放在通知栏上面了,学习一下Android 8.0系统的通知栏适配。

其实在8.0系统之前,还有一次通知栏变动比较大的版本,就是5.0系统。关于5.0系统需要对通知栏进行适配的内容,我也整理了一篇文章,感兴趣的朋友可以去阅读 Android通知栏微技巧,那些你所没关注过的小细节 。

那么下面我们就开始进入本篇文章的正题。



为什么要进行通知栏适配?
不得不说,通知栏真是一个让人又爱又恨的东西。

通知栏是Android系统原创的一个功能,虽说乔布斯一直认为Android系统是彻彻底底抄袭iOS的一个产品,但是通知栏确实是Android系统原创的,反而苹果在iOS 5之后也加入了类似的通知栏功能。

通知栏的设计确实非常巧妙,它默认情况下不占用任何空间,只有当用户需要的时候用手指在状态栏上向下滑动,通知栏的内容才会显示出来,这在智能手机发展的初期极大地解决了手机屏幕过小,内容展示区域不足的问题。

可是随着智能手机发展的逐渐成熟,通知栏却变得越来越不讨人喜欢了。各个App都希望能抢占通知栏的空间,来尽可能地宣传和推广自己的产品。现在经常是早上一觉醒来拿起手机一看,通知栏上全是各种APP的推送,不胜其烦。



我个人虽然是Android应用开发者,但同时也是Android手机的资深用户。我已经使用了8年的Android手机,目前我对于通知栏的这种垃圾推送是零容忍的。现在每当我安装一个新的App时,我都会先到设置里面去找一找有没有推送开关,如果有的话我会第一时间把它关掉。而如果一个App经常给我推送垃圾信息却又无法关闭时,我会直接将它的通知总开关给关掉,如果还不是什么重要的App的话,那么我可能就直接将它卸载掉了。

为什么一个很好的通知栏功能现在却变得这么遭用户讨厌?很大一部分原因都是因为开发者没有节制地使用导致的。就好像App保活一样,直到今天还是不断有人问我该如何保活App,试想如何每个人都能保活自己的App,那么最终受害的人是谁?还不是使用Android手机的用户。大家的手机只会越来越卡,最后只想把手机丢掉,变成iPhone用户了。也是因为开发者没节制地使用,Android现在的每个版本都会不断收缩后台权限。

回到通知栏上也是一样,每个开发者都只想着尽可能地去宣传自己的App,最后用户的手机就乱得跟鸡窝一样了。但是通知栏又还是有用处的,比如我们收到微信、短信等消息的时候,确实需要通知栏给我们提醒。因此分析下来,通知栏目前最大的问题就是,无法让用户对感兴趣和不感兴趣的消息进行区分。就比如说,我希望淘宝向我推送卖家发货和物流的相关消息,但是我不想收到那些打折促销或者是让我去买衣服的这类消息。那么就目前来说,是没有办法对这些消息做区分的,我要么同意接受所有消息,要么就屏蔽所有消息,这是当前通知栏的痛点。

那么在Android 8.0系统中,Google也是从这个痛点开始下手的。 


8.0系统的通知栏适配
从Android 8.0系统开始,Google引入了通知渠道这个概念。

什么是通知渠道呢?顾名思义,就是每条通知都要属于一个对应的渠道。每个App都可以自由地创建当前App拥有哪些通知渠道,但是这些通知渠道的控制权都是掌握在用户手上的。用户可以自由地选择这些通知渠道的重要程度,是否响铃、是否振动、或者是否要关闭这个渠道的通知。

拥有了这些控制权之后,用户就再也不用害怕那些垃圾推送消息的打扰了,因为用户可以自主地选择自己关心哪些通知、不关心哪些通知。举个具体的例子,我希望可以即时收到支付宝的收款信息,因为我不想错过任何一笔收益,但是我又不想收到支付宝给我推荐的周围美食,因为我没钱只吃得起公司食堂。这种情况,支付宝就可以创建两种通知渠道,一个收支,一个推荐,而我作为用户对推荐类的通知不感兴趣,那么我就可以直接将推荐通知渠道关闭,这样既不影响我关心的通知,又不会让那些我不关心的通知来打扰我了。

对于每个App来说,通知渠道的划分是非常需要仔细考究的,因为通知渠道一旦创建之后就不能再修改了,因此开发者需要仔细分析自己的App一共有哪些类型的通知,然后再去创建相应的通知渠道。这里我们来参考一下Twitter的通知渠道划分:


可以看到,Twitter就是根据自己的通知类型,对通知渠道进行了非常详细的划分,这样用户的自主选择性就比较高了,也就大大降低了用户不堪其垃圾通知的骚扰而将App卸载的概率。



我一定要适配吗?
Google这次对于8.0系统通知渠道的推广态度还是比较强硬的。

首先,如果你升级了appcompat库,那么所有使用appcompat库来构建通知的地方全部都会进行废弃方法提示,如下所示:


上图告诉我们,此方法已废弃,需要使用带有通知渠道的方法才行。

当然,Google也并没有完全做绝,即使方法标为了废弃,但还是可以正常使用的。可是如果你将项目中的targetSdkVersion指定到了26或者更高,那么Android系统就会认为你的App已经做好了8.0系统的适配工作,当然包括了通知栏的适配。这个时候如果还不使用通知渠道的话,那么你的App的通知将完全无法弹出。因此这里给大家的建议就是,一定要适配。

好了,前面向大家介绍了这么多的背景知识,那么现在开始我们就正式进入正题,来学习一下如何进行8.0系统中通知栏的适配。 


创建通知渠道
首先我们使用Android Studio来新建一个项目,就叫它NotificationTest吧。

创建好项目之后,打开app/build.gradle文件检查一下,确保targetSdkVersion已经指定到了26或者更高,如下所示:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.example.notificationtest"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
可以看到,这里我在创建新项目的时候默认targetSdkVersion就是26,如果你是低于26的话,说明你的Android SDK有些老了,最好还是更新一下。当然如果你懒得更新也没关系,手动把它改成26就可以了。

接下来修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            String channelId = "chat";
            String channelName = "聊天消息";
            int importance = NotificationManager.IMPORTANCE_HIGH;
            createNotificationChannel(channelId, channelName, importance);

            channelId = "subscribe";
            channelName = "订阅消息";
            importance = NotificationManager.IMPORTANCE_DEFAULT;
            createNotificationChannel(channelId, channelName, importance);
        }
    }


    @TargetApi(Build.VERSION_CODES.O)
    private void createNotificationChannel(String channelId, String channelName, int importance) {
        NotificationChannel channel = new NotificationChannel(channelId, channelName, importance);
        NotificationManager notificationManager = (NotificationManager) getSystemService(
                NOTIFICATION_SERVICE);
        notificationManager.createNotificationChannel(channel);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
代码不长,我来简单解释下。这里我们在MainActivity中创建了两个通知渠道,首先要确保的是当前手机的系统版本必须是Android 8.0系统或者更高,因为低版本的手机系统并没有通知渠道这个功能,不做系统版本检查的话会在低版本手机上造成崩溃。

创建一个通知渠道的方式非常简单,这里我封装了一个createNotificationChannel()方法,里面的逻辑相信大家都看得懂。需要注意的是,创建一个通知渠道至少需要渠道ID、渠道名称以及重要等级这三个参数,其中渠道ID可以随便定义,只要保证全局唯一性就可以。渠道名称是给用户看的,需要能够表达清楚这个渠道的用途。重要等级的不同则会决定通知的不同行为,当然这里只是初始状态下的重要等级,用户可以随时手动更改某个渠道的重要等级,App是无法干预的。

上述代码我是模拟了这样一个场景。想象一下我们正在开发一个类似于微信的App,其中App通知主要可以分为两类,一类是我和别人的聊天消息,这类消息非常重要,因此重要等级我设为了IMPORTANCE_HIGH。另一类是公众号的订阅消息,这类消息不是那么重要,因此重要等级我设为了IMPORTANCE_DEFAULT。除此之外,重要等级还可以设置为IMPORTANCE_LOW、IMPORTANCE_MIN,分别对应了更低的通知重要程度。

现在就可以运行一下代码了,运行成功之后我们关闭App,进入到设置 -> 应用 -> 通知当中,查看NotificationTest这个App的通知界面,如下图所示:


刚才我们创建的两个通知渠道这里已经显示出来了。可以看到,由于这两个通知渠道的重要等级不同,通知的行为也是不同的,聊天消息可以发出提示音并在屏幕上弹出通知,而订阅消息只能发出提示音。

当然,用户还可以点击进去对该通知渠道进行任意的修改,比如降低聊天消息的重要等级,甚至是可以完全关闭该渠道的通知。

至于创建通知渠道的这部分代码,你可以写在MainActivity中,也可以写在Application中,实际上可以写在程序的任何位置,只需要保证在通知弹出之前调用就可以了。并且创建通知渠道的代码只在第一次执行的时候才会创建,以后每次执行创建代码系统会检测到该通知渠道已经存在了,因此不会重复创建,也并不会影响任何效率。



让通知显示出来
触发通知的代码和之前版本基本是没有任何区别的,只是在构建通知对象的时候,需要多传入一个通知渠道ID,表示这条通知是属于哪个渠道的。

那么下面我们就来让通知显示出来。

首先修改activity_main.xml中的代码,如下所示:

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

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="发送聊天消息"
        android:onClick="sendChatMsg"
        />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="发送订阅消息"
        android:onClick="sendSubscribeMsg"
        />
</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这里我们在布局文件中加入了两个按钮,很显然,一个是用于触发聊天消息渠道通知的,一个是用于触发订阅消息渠道通知的。

接下来修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {

    ...

    public void sendChatMsg(View view) {
        NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        Notification notification = new NotificationCompat.Builder(this, "chat")
                .setContentTitle("收到一条聊天消息")
                .setContentText("今天中午吃什么?")
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.drawable.icon)
                .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon))
                .setAutoCancel(true)
                .build();
        manager.notify(1, notification);
    }

    public void sendSubscribeMsg(View view) {
        NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        Notification notification = new NotificationCompat.Builder(this, "subscribe")
                .setContentTitle("收到一条订阅消息")
                .setContentText("地铁沿线30万商铺抢购中!")
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.drawable.icon)
                .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon))
                .setAutoCancel(true)
                .build();
        manager.notify(2, notification);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
这里我们分别在sendChatMsg()和sendSubscribeMsg()方法中触发了两条通知,创建通知的代码就不再多做解释了,和传统创建通知的方法没什么两样,只是在NotificationCompat.Builder中需要多传入一个通知渠道ID,那么这里我们分别传入了chat和subscribe这两个刚刚创建的渠道ID。

现在重新运行一下代码,并点击发送聊天消息按钮,效果如下图所示:


由于这是一条重要等级高的通知,因此会使用这种屏幕弹窗的方式来通知用户有消息到来。然后我们可以下拉展开通知栏,这里也能查看到通知的详细信息:


用户可以通过快速向左或者向右滑动来关闭这条通知。

接下来点击发送订阅消息按钮,你会发现现在屏幕上不会弹出一条通知提醒了,只会在状态栏上显示一个小小的通知图标:


因为订阅消息通知的重要等级是默认级别,这就是默认级别通知的展示形式。当然我们还是可以下拉展开通知栏,查看通知的详细信息:


不过上面演示的都是通知栏的传统功能,接下来我们看一看Android 8.0系统中通知栏特有的功能。

刚才提到了,快速向左或者向右滑动可以关闭一条通知,但如果你缓慢地向左或者向右滑动,就会看到这样两个按钮:


其中,左边那个时钟图标的按钮可以让通知延迟显示。比方说这是一条比较重要的通知,但是我暂时没时间看,也不想让它一直显示在状态栏里打扰我,我就可以让它延迟一段后时间再显示,这样我就暂时能够先将精力放在专注的事情上,等过会有时间了这条通知会再次显示出来,我不会错过任何信息。如下所示:


而右边那个设置图标的按钮就可以用来对通知渠道进行屏蔽和配置了,用户对每一个App的每一个通知渠道都有绝对的控制权,可以根据自身的喜好来进行配置和修改。如下所示:


比如说我觉得订阅消息老是向我推荐广告,实在是太烦了,我就可以将订阅消息的通知渠道关闭掉。这样我以后就不会再收到这个通知渠道下的任何消息,而聊天消息却不会受到影响,这就是8.0系统通知渠道最大的特色。

另外,点击上图中的所有类别就可以进入到当前应用程序通知的完整设置界面。



管理通知渠道
在前面的内容中我们已经了解到,通知渠道一旦创建之后就不能再通过代码修改了。既然不能修改的话那还怎么管理呢?为此,Android赋予了开发者读取通知渠道配置的权限,如果我们的某个功能是必须按照指定要求来配置通知渠道才能使用的,那么就可以提示用户去手动更改通知渠道配置。

只讲概念总是不容易理解,我们还是通过具体的例子来学习一下。想一想我们开发的是一个类似于微信的App,聊天消息是至关重要的,如果用户不小心将聊天消息的通知渠道给关闭了,那岂不是所有重要的信息全部都丢了?为此我们一定要保证用户打开了聊天消息的通知渠道才行。

修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {

    ...

    public void sendChatMsg(View view) {
        NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = manager.getNotificationChannel("chat");
            if (channel.getImportance() == NotificationManager.IMPORTANCE_NONE) {
                Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
                intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
                intent.putExtra(Settings.EXTRA_CHANNEL_ID, channel.getId());
                startActivity(intent);
                Toast.makeText(this, "请手动将通知打开", Toast.LENGTH_SHORT).show();
            }
        }

        Notification notification = new NotificationCompat.Builder(this, "chat")
                ...
                .build();
        manager.notify(1, notification);
    }

    ...

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
这里我们对sendChatMsg()方法进行了修改,通过getNotificationChannel()方法获取到了NotificationChannel对象,然后就可以读取该通知渠道下的所有配置了。这里我们判断如果通知渠道的importance等于IMPORTANCE_NONE,就说明用户将该渠道的通知给关闭了,这时会跳转到通知的设置界面提醒用户手动打开。

现在重新运行一下程序,效果如下图所示:


可以看到,当我们将聊天消息的通知渠道关闭后,下次再次发送聊天消息将会直接跳转到通知设置界面,提醒用户手动将通知打开。

除了以上管理通知渠道的方式之外,Android 8.0还赋予了我们删除通知渠道的功能,只需使用如下代码即可删除:

NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.deleteNotificationChannel(channelId);
1
2
但是这个功能非常不建议大家使用。因为Google为了防止应用程序随意地创建垃圾通知渠道,会在通知设置界面显示所有被删除的通知渠道数量,如下图所示:


这样是非常不美观的,所以对于开发者来说最好的做法就是仔细规划好通知渠道,而不要轻易地使用删除功能。



显示未读角标
前面我们提到过,苹果是从iOS 5开始才引入了通知栏功能,那么在iOS 5之前,iPhone都是怎么进行消息通知的呢?使用的就是未读角标功能,效果如下所示:


实际上Android系统之前是从未提供过这种类似于iOS的角标功能的,但是由于很多国产手机厂商都喜欢跟风iOS,因此各种国产手机ROM都纷纷推出了自己的角标功能。

可是国产手机厂商虽然可以订制ROM,但是却没有制定API的能力,因此长期以来都没有一个标准的API来实现角标功能,很多都是要通过向系统发送广播来实现的,而各个手机厂商的广播标准又不一致,经常导致代码变得极其混杂。

值得高兴的是,从8.0系统开始,Google制定了Android系统上的角标规范,也提供了标准的API,长期让开发者头疼的这个问题现在终于可以得到解决了。

那么下面我们就来学习一下如何在Android系统上实现未读角标的效果。

修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {

    ...

    @TargetApi(Build.VERSION_CODES.O)
    private void createNotificationChannel(String channelId, String channelName, int importance) {
        NotificationChannel channel = new NotificationChannel(channelId, channelName, importance);
        channel.setShowBadge(true);
        NotificationManager notificationManager = (NotificationManager) getSystemService(
                NOTIFICATION_SERVICE);
        notificationManager.createNotificationChannel(channel);
    }

    public void sendSubscribeMsg(View view) {
        NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        Notification notification = new NotificationCompat.Builder(this, "subscribe")
                ...
                .setNumber(2)
                .build();
        manager.notify(2, notification);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
可以看到,这里我们主要修改了两个地方。第一是在创建通知渠道的时候,调用了NotificationChannel的setShowBadge(true)方法,表示允许这个渠道下的通知显示角标。第二是在创建通知的时候,调用了setNumber()方法,并传入未读消息的数量。

现在重新运行一下程序,并点击发送订阅消息按钮,然后在Launcher中找到NotificationTest这个应用程序,如下图所示:


可以看到,在图标的右上角有个绿色的角标,说明我们编写的角标功能已经生效了。

需要注意的是,即使我们不调用setShowBadge(true)方法,Android系统默认也是会显示角标的,但是如果你想禁用角标功能,那么记得一定要调用setShowBadge(false)方法。

但是未读数量怎么没有显示出来呢?这个功能还需要我们对着图标进行长按才行,效果如下图所示:


这样就能看到通知的未读数量是2了。

可能有些朋友习惯了iOS上的那种未读角标,觉得Android上这种还要长按的方式很麻烦。这个没有办法,因为这毕竟是Android原生系统,Google没有办法像国内手机厂商那样可以肆无忌惮地模仿iOS,要不然可能会吃官司的。但是我相信国内手机厂商肯定会将这部分功能进行定制,风格应该会类似于iOS。不过这都不重要,对于我们开发者来说,最好的福音就是有了统一的API标准,不管国内手机厂商以后怎么定制ROM,都会按照这个API的标准来定制,我们只需要使用这个API来进行编程就可以了。

好的,关于Android 8.0系统适配的上下两篇文章到这里就结束了,感谢大家阅读。

文章中的示例源码点击 这里 下载。
Android8.0 启动后台Service
96  南国生红豆 
 0.2 2018.02.06 11:19 字数 506 阅读 18463评论 13喜欢 14
在一加手机上,用户升级了新版8.0的系统,用户将app切到后台,过一会儿就弹出“xxx app 已停止运行”的弹窗。

通过定位分析,发现下面俩前置条件

8.0系统杀服务杀的很频繁
为了保活,我们使用了俩Service互保的方式
马上跑了26的模拟器,果然复现,日志如下:

main
Process: com.icourt.alpha.service.LocalService, PID: 4343
java.lang.IllegalStateException: Not allowed to start service Intent { cmp=com.icourt.alpha/.service.LocalService }: app is in background uid UidRecord{81da92c u0a91 SVC  bg:+1m0s13ms idle procs:4 seq(0,0,0)}
at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1505)
at android.app.ContextImpl.startService(ContextImpl.java:1461)
at android.content.ContextWrapper.startService(ContextWrapper.java:644)
at com.icourt.alpha.service.RemoteService$RemoteServiceConnection.onServiceDisconnected(RemoteService.java:100)
at android.app.LoadedApk$ServiceDispatcher.doConnected(LoadedApk.java:1627)
at android.app.LoadedApk$ServiceDispatcher$RunConnection.run(LoadedApk.java:1663)
at android.os.Handler.handleCallback(Handler.java:789)
at android.os.Handler.dispatchMessage(Handler.java:98)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6541)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
我查阅了android官网,有如下一段描述:

Android 8.0 还对特定函数做出了以下变更:

如果针对 Android 8.0 的应用尝试在不允许其创建后台服务的情况下使用 startService() 函数,则该函数将引发一个 IllegalStateException。
新的 Context.startForegroundService() 函数将启动一个前台服务。现在,即使应用在后台运行,系统也允许其调用 Context.startForegroundService()。不过,应用必须在创建服务后的五秒内调用该服务的 startForeground() 函数。
解决方法就很简单了,把Service互启的逻辑块改为:

if (Build.VERSION.SDK_INT >= 26) {
    context.startForegroundService(intent);
} else {
    // Pre-O behavior.
    context.startService(intent);
}
有个简写:ContextCompat.startForegroundService(context, intent)
因为我不想让用户在控制面板看到通知栏,我没有在创建服务后的五秒内调用startForeground()。我马上试了一下,果然没有了“已停止运行”,但是日志爆出了另一个ANR的错误,虽然不是在主线程,也没有弹窗,但毕竟是个错误,我想一探究竟。

回到官方的描述:在被启动的Service创建服务后的五秒内调用startForground(0, new Notification()),如果不调用或调用时间超过5秒会抛出一个ANR。
果不其然,我只有调用了,这样更好,有前台进程,系统想杀都杀不掉了。

但仔细看一下官方文档,你会发现,官方推荐使用JobScheduler,这是api25的新特性。
(四十二)Context.startForegroundService() did not then call Service.startForeground?
2018年07月21日 13:01:35 i加加 阅读数 15065 标签: Android o service startForeground startForegroundService  更多
个人分类: Android
 版权声明:本文为博主原创文章,未经授权禁止转载,O(∩_∩)O谢谢 https://blog.csdn.net/sinat_20059415/article/details/80584487
前言:最近在处理Android O的应用crash和anr问题,其中遇到比较多的就是“Context.startForegroundService() did not then call Service.startForeground()”,将自己的处理心得总结回顾一下。

 

demo:https://github.com/happyjiatai/demo_csdn/tree/master/demo_42_startforegroundservice

-------------------------------------------------------6月21日更新------------------------------------------------------

PS:我比较注重报错及解决,有愿意看代码弄清代码层次的前因后果的下面这篇写的很好

https://blog.csdn.net/lylddinghffw/article/details/80366791

-------------------------------------------------------6月21日更新------------------------------------------------------

 

1.报错堆栈
startForegroundService相关报错堆栈分为两种:

1)

05-28 17:49:49.693516  3986  3986 E AndroidRuntime: FATAL EXCEPTION: main
05-28 17:49:49.693516  3986  3986 E AndroidRuntime: Process: packageName,,,,,,,,,,,,,,,,,,,,,,,,, PID: 3986
05-28 17:49:49.693516  3986  3986 E AndroidRuntime: java.lang.RuntimeException: java.lang.IllegalStateException: Not allowed to start service Intent { flg=0x1000000 cmp=packagename/.servicename (has extras) }: app is in background uid UidRecord{52db80 u2357s1000 TRNB bg:+2m42s199ms idle procs:3 seq(0,0,0)}
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.app.job.JobServiceEngine$JobHandler.handleMessage(JobServiceEngine.java:112)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.os.Handler.dispatchMessage(Handler.java:106)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.os.Looper.loop(Looper.java:168)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.app.ActivityThread.main(ActivityThread.java:6555)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at java.lang.reflect.Method.invoke(Native Method)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:857)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime: Caused by: java.lang.IllegalStateException: Not allowed to start service Intent { flg=0x1000000 cmp=packagename/.servicename (has extras) }: app is in background uid UidRecord{52db80 u2357s1000 TRNB bg:+2m42s199ms idle procs:3 seq(0,0,0)}
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1522)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.app.ContextImpl.startService(ContextImpl.java:1478)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.content.ContextWrapper.startService(ContextWrapper.java:661)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at packageName.ConnectionChangeJobService.onStartJob(ConnectionChangeJobService.java:102)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.app.job.JobService$1.onStartJob(JobService.java:71)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.app.job.JobServiceEngine$JobHandler.handleMessage(JobServiceEngine.java:108)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     ... 6 more
 

2)

 

06-11 15:48:15.602772 13768 13768 E AndroidRuntime: Process: packagename, PID: 13768
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: android.app.RemoteServiceException: Context.startForegroundService() did not then call Service.startForeground()
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: 	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1803)
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: 	at android.os.Handler.dispatchMessage(Handler.java:106)
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: 	at android.os.Looper.loop(Looper.java:168)
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: 	at android.app.ActivityThread.main(ActivityThread.java:6555)
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: 	at java.lang.reflect.Method.invoke(Native Method)
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:857)
 

在解决问题之前先打一下基础,看一下Android O对后台的限制。

 

 

2.Android O 后台执行限制
2.1 后台执行限制
Android 8.0 为提高电池续航时间而引入的变更之一是,当您的应用进入已缓存状态时,如果没有活动的组件,系统将解除应用具有的所有唤醒锁。

此外,为提高设备性能,系统会限制未在前台运行的应用的某些行为。具体而言:

现在,在后台运行的应用对后台服务的访问受到限制。
应用无法使用其清单注册大部分隐式广播(即,并非专门针对此应用的广播)。
默认情况下,这些限制仅适用于针对 O 的应用。不过,用户可以从 Settings 屏幕为任意应用启用这些限制,即使应用并不是以 O 为目标平台。

Android 8.0 还对特定函数做出了以下变更:


如果针对 Android 8.0 的应用尝试在不允许其创建后台服务的情况下使用 startService() 函数,则该函数将引发一个 IllegalStateException。(对应于堆栈一的报错)

 

新的 Context.startForegroundService() 函数将启动一个前台服务。现在,即使应用在后台运行,系统也允许其调用 Context.startForegroundService()。不过,应用必须在创建服务后的五秒内调用该服务的 startForeground() 函数。(对应于堆栈二的报错,需要补充)

 

 

2.2 后台服务限制
在后台中运行的服务会消耗设备资源,这可能降低用户体验。 为了缓解这一问题,系统对这些服务施加了一些限制。

系统可以区分 前台 和 后台 应用。 (用于服务限制目的的后台定义与内存管理使用的定义不同;一个应用按照内存管理的定义可能处于后台,但按照能够启动服务的定义又处于前台。)如果满足以下任意条件,应用将被视为处于前台:
 

具有可见 Activity(不管该 Activity 已启动还是已暂停)。
具有前台服务。
另一个前台应用已关联到该应用(不管是通过绑定到其中一个服务,还是通过使用其中一个内容提供程序)。 例如,如果另一个应用绑定到该应用的服务,那么该应用处于前台:

    IME
    壁纸服务
    通知侦听器
    语音或文本服务
    如果以上条件均不满足,应用将被视为处于后台。

绑定服务不受影响
这些规则不会对绑定服务产生任何影响。 如果您的应用定义了绑定服务,则不管应用是否处于前台,其他组件都可以绑定到该服务。

处于前台时,应用可以自由创建和运行前台服务与后台服务。 进入后台时,在一个持续数分钟的时间窗内,应用仍可以创建和使用服务。

在该时间窗结束后,应用将被视为处于 空闲 状态。 此时,系统将停止应用的后台服务,就像应用已经调用服务的“Service.stopSelf()”方法。

在这些情况下,后台应用将被置于一个临时白名单中并持续数分钟。 位于白名单中时,应用可以无限制地启动服务,并且其后台服务也可以运行。

处理对用户可见的任务时,应用将被置于白名单中,例如:

处理一条高优先级 Firebase 云消息传递 (FCM) 消息。

接收广播,例如短信/彩信消息。

从通知执行 PendingIntent。

在很多情况下,您的应用都可以使用 JobScheduler 作业替换后台服务。 例如,CoolPhotoApp 需要检查用户是否已经从朋友那里收到共享的照片,即使该应用未在前台运行。

之前,应用使用一种会检查其云存储的后台服务。 为了迁移到 Android 8.0,开发者使用一个计划作业替换了这种后台服务,该作业将按一定周期启动,查询服务器,然后退出。

在 Android 8.0 之前,创建前台服务的方式通常是先创建一个后台服务,然后将该服务推到前台。

Android 8.0 有一项复杂功能;系统不允许后台应用创建后台服务。 因此,Android 8.0 引入了一种全新的方法,即 Context.startForegroundService(),以在前台启动新服务。

在系统创建服务后,应用有五秒的时间来调用该服务的 startForeground() 方法以显示新服务的用户可见通知。
 

如果应用在此时间限制内未调用 startForeground(),则系统将停止服务并声明此应用为 ANR。

 

3. 报错解决方案
3.1 堆栈一报错解决方案
将 调用 startService启动Service 改为调用 startForegroundService,这只是第一步,后续步骤请参考堆栈二报错解决方案。

 

05-28 17:49:49.693516  3986  3986 E AndroidRuntime: FATAL EXCEPTION: main
05-28 17:49:49.693516  3986  3986 E AndroidRuntime: Process: packageName,,,,,,,,,,,,,,,,,,,,,,,,, PID: 3986
05-28 17:49:49.693516  3986  3986 E AndroidRuntime: java.lang.RuntimeException: java.lang.IllegalStateException: Not allowed to start service Intent { flg=0x1000000 cmp=packagename/.servicename (has extras) }: app is in background uid UidRecord{52db80 u2357s1000 TRNB bg:+2m42s199ms idle procs:3 seq(0,0,0)}
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.app.job.JobServiceEngine$JobHandler.handleMessage(JobServiceEngine.java:112)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.os.Handler.dispatchMessage(Handler.java:106)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.os.Looper.loop(Looper.java:168)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.app.ActivityThread.main(ActivityThread.java:6555)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at java.lang.reflect.Method.invoke(Native Method)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:857)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime: Caused by: java.lang.IllegalStateException: Not allowed to start service Intent { flg=0x1000000 cmp=com.mediatek.providers.drm/.DrmSyncTimeService (has extras) }: app is in background uid UidRecord{52db80 u2357s1000 TRNB bg:+2m42s199ms idle procs:3 seq(0,0,0)}
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1522)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.app.ContextImpl.startService(ContextImpl.java:1478)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.content.ContextWrapper.startService(ContextWrapper.java:661)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at com.mediatek.providers.drm.ConnectionChangeJobService.onStartJob(ConnectionChangeJobService.java:102)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.app.job.JobService$1.onStartJob(JobService.java:71)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     at android.app.job.JobServiceEngine$JobHandler.handleMessage(JobServiceEngine.java:108)
05-28 17:49:49.693516  3986  3986 E AndroidRuntime:     ... 6 more
 

3.2 堆栈二报错解决方案
 

堆栈二简单来看就是调用了startForegroundService后需要在Service里继续调用Service.startForeground()即可,但有种情况是即使调用了还是报一样的错。

 

06-11 15:48:15.602772 13768 13768 E AndroidRuntime: Process: packagename, PID: 13768
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: android.app.RemoteServiceException: Context.startForegroundService() did not then call Service.startForeground()
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: 	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1803)
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: 	at android.os.Handler.dispatchMessage(Handler.java:106)
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: 	at android.os.Looper.loop(Looper.java:168)
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: 	at android.app.ActivityThread.main(ActivityThread.java:6555)
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: 	at java.lang.reflect.Method.invoke(Native Method)
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
06-11 15:48:15.602772 13768 13768 E AndroidRuntime: 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:857)
如果认真debug,会发现Service启动的时候不会报错,在Service.stopSelf的时候报错,并且catch不到异常。5s内不停止服务还会有anr问题。

原因:

看下面的代码,有种豁然开朗学到什么东西的感觉,但是Google把这条路封掉了,Google本意就是让没有可见通知的应用不可以偷偷启动服务在后台干着见不得人的事,怎么可能留下后门。

        Notification notification = new Notification.Builder(mContext).build();
        startForeground(0, notification);
 
     * @param id The identifier for this notification as per
     * {@link NotificationManager#notify(int, Notification)
     * NotificationManager.notify(int, Notification)}; must not be 0.
     * @param notification The Notification to be displayed.
     * 
     * @see #stopForeground(boolean)
     */
    public final void startForeground(int id, Notification notification) {
结合Service的startForeground api,其中重点强调了must not be 0,即禁止是0,既然使用了0,就不要怪Google让应用crash了。但是是在Service.stopSelf时crash,代码分析见后文框架修改解决方案。

 

3.2.1 正统解决方案
正统解决方案肯定是Google让怎么做就怎么做呀,Google让新建一个通知那就新建一个通知。

写了一个demo,包含正确方式(btn1)和错误方式(btn2)

activity:

package com.example.demo_42_startforegroundservice;
 
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
 
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "jiatai";
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btn = findViewById(R.id.btn);
        Button btn2 = findViewById(R.id.btn2);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG, "start service");
                Intent intent = new Intent(MainActivity.this,MyService.class);
                intent.putExtra("type",1);
                startForegroundService(intent);
            }
        });
 
        btn2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG, "start service");
                Intent intent = new Intent(MainActivity.this,MyService.class);
                intent.putExtra("type",2);
                startForegroundService(intent);
            }
        });
    }
}
Service:

package com.example.demo_42_startforegroundservice;
 
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;
import android.widget.Toast;
 
public class MyService extends Service {
    private static final String TAG = "jiatai";
    private MyHandler handler;
    public MyService() {
    }
 
    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }
 
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "service oncreate");
        handler = new MyHandler();
    }
 
    @Override
    public int onStartCommand(Intent intent, int flags, final int startId) {
        int type = intent.getIntExtra("type",1);
        Log.d(TAG, "the create notification type is " + type + "----" + (type == 1 ? "true" : "false"));
        if(type == 1){
            createNotificationChannel();
        }else{
            createErrorNotification();
        }
        new Thread(){
            @Override
            public void run() {
                super.run();
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                handler.sendEmptyMessage(startId);
            }
        }.start();
        return super.onStartCommand(intent, flags, startId);
    }
 
    private void createErrorNotification() {
        Notification notification = new Notification.Builder(this).build();
        startForeground(0, notification);
    }
 
    private void createNotificationChannel() {
        NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        // 通知渠道的id
        String id = "my_channel_01";
        // 用户可以看到的通知渠道的名字.
        CharSequence name = getString(R.string.channel_name);
//         用户可以看到的通知渠道的描述
        String description = getString(R.string.channel_description);
        int importance = NotificationManager.IMPORTANCE_HIGH;
        NotificationChannel mChannel = new NotificationChannel(id, name, importance);
//         配置通知渠道的属性
        mChannel.setDescription(description);
//         设置通知出现时的闪灯(如果 android 设备支持的话)
        mChannel.enableLights(true); mChannel.setLightColor(Color.RED);
//         设置通知出现时的震动(如果 android 设备支持的话)
        mChannel.enableVibration(true);
        mChannel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
//         最后在notificationmanager中创建该通知渠道 //
        mNotificationManager.createNotificationChannel(mChannel);
 
        // 为该通知设置一个id
        int notifyID = 1;
        // 通知渠道的id
        String CHANNEL_ID = "my_channel_01";
        // Create a notification and set the notification channel.
        Notification notification = new Notification.Builder(this)
                .setContentTitle("New Message") .setContentText("You've received new messages.")
                .setSmallIcon(R.drawable.ic_launcher_foreground)
                .setChannelId(CHANNEL_ID)
                .build();
        startForeground(1,notification);
    }
 
    private class MyHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            stopSelf(msg.what);
        }
    }
 
    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "5s onDestroy");
        Toast.makeText(this, "this service destroy", 1).show();
        stopForeground(true);
    }
}
效果图:



贴一下btn2 报错堆栈:

 

06-16 09:52:12.109 783-907/system_process I/AnrManager: ANR in com.example.demo_42_startforegroundservice, time=689836
    Reason: Context.startForegroundService() did not then call Service.startForeground()
    Load: 10.13 / 9.55 / 5.83
    Android time :[2018-06-16 09:52:12.10] [694.364]
    --------- beginning of crash
06-16 09:52:12.132 7015-7015/com.example.demo_42_startforegroundservice E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.demo_42_startforegroundservice, PID: 7015
    android.app.RemoteServiceException: Context.startForegroundService() did not then call Service.startForeground()
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1803)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:168)
        at android.app.ActivityThread.main(ActivityThread.java:6555)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:857)
 

Google有点狠,crash+anr来了个全套,anr其实是我将Service延迟到了5s做完导致的,5s内如果没有正统的调用startForeground就会anr。

 

 

PS: 网上所传的notification隐藏是否可以?

有的需求是启动服务,但是又不想有通知,这和Google定的规则有冲突呀,有没有什么办法呢?网上2年前的方案是启动两个Service,一个Service干活,另外一个Service把通知隐藏掉。

Android O Google应该考虑到这个漏洞了:

    private void cancelForegroundNotificationLocked(ServiceRecord r) {
        if (r.foregroundId != 0) {
            // First check to see if this app has any other active foreground services
            // with the same notification ID.  If so, we shouldn't actually cancel it,
            // because that would wipe away the notification that still needs to be shown
            // due the other service.
            ServiceMap sm = getServiceMapLocked(r.userId);
            if (sm != null) {
                for (int i = sm.mServicesByName.size()-1; i >= 0; i--) {
                    ServiceRecord other = sm.mServicesByName.valueAt(i);
                    if (other != r && other.foregroundId == r.foregroundId
                            && other.packageName.equals(r.packageName)) {
                        // Found one!  Abort the cancel.
                        return;
                    }
                }
            }
            r.cancelNotification();
        }
    }
如果前台服务的通知还有被占用,那就别想用其他服务把它干掉了。

 

3.2.2 框架规避方案
修改方案(仅供参考):ActiveServices.java如下加一个packageName的crash的规避,anr同理,发出消息的地方可以修改为不发出timeout消息,也可以在startForeground的时候就移除。(如果Service耗时小于5s,Service在stop流程的时候会将anr消息移除,可不修改)

        // Check to see if the service had been started as foreground, but being
        // brought down before actually showing a notification.  That is not allowed.
        if (r.fgRequired) {
            Slog.w(TAG_SERVICE, "Bringing down service while still waiting for start foreground: "
                    + r);
            r.fgRequired = false;
            r.fgWaiting = false;
            mAm.mHandler.removeMessages(
                    ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG, r);
            if (r.app != null && !"packageName".equals(r.packageName)) {
                Message msg = mAm.mHandler.obtainMessage(
                        ActivityManagerService.SERVICE_FOREGROUND_CRASH_MSG);
                msg.obj = r.app;
                mAm.mHandler.sendMessage(msg);
            }
        }
 

 

 

修改原理:

编译一个service.jar,打印报错堆栈

01-01 07:02:17.669   918  1334 W ActivityManager: Bringing down service while still waiting for start foreground: ServiceRecord{2d44a2d u0 packageName/.servicename}
01-01 07:02:17.669   918  1334 W ActivityManager: java.lang.Throwable
01-01 07:02:17.669   918  1334 W ActivityManager: 	at com.android.server.am.ActiveServices.bringDownServiceLocked(ActiveServices.java:2612)
01-01 07:02:17.669   918  1334 W ActivityManager: 	at com.android.server.am.ActiveServices.bringDownServiceIfNeededLocked(ActiveServices.java:2559)
01-01 07:02:17.669   918  1334 W ActivityManager: 	at com.android.server.am.ActiveServices.stopServiceTokenLocked(ActiveServices.java:792)
01-01 07:02:17.669   918  1334 W ActivityManager: 	at com.android.server.am.ActivityManagerService.stopServiceToken(ActivityManagerService.java:18789)
01-01 07:02:17.669   918  1334 W ActivityManager: 	at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:759)
01-01 07:02:17.669   918  1334 W ActivityManager: 	at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:3080)
01-01 07:02:17.669   918  1334 W ActivityManager: 	at android.os.Binder.execTransact(Binder.java:697)
找到对应抛出Context.startForegroundService() did not then call Service.startForeground()的逻辑代码:

ActiveServices.java bringDownServiceLocked

      // Check to see if the service had been started as foreground, but being
        // brought down before actually showing a notification.  That is not allowed.
        if (r.fgRequired) {
            Slog.w(TAG_SERVICE, "Bringing down service while still waiting for start foreground: "
                    + r);
            r.fgRequired = false;
            r.fgWaiting = false;
            mAm.mHandler.removeMessages(
                    ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG, r);
            if (r.app != null) {
                Message msg = mAm.mHandler.obtainMessage(
                        ActivityManagerService.SERVICE_FOREGROUND_CRASH_MSG);
                msg.obj = r.app;
                mAm.mHandler.sendMessage(msg);
            }
        }
走到这里面继而会由ams发出一个service_foreground_crash_msg的消息,导致crash。

至于为嘛会走到这里呢,都是id = 0 的过,既没有走前台服务的流程也没有将r.fgRequired设为false,anr的msg也没有移除掉。

 private void setServiceForegroundInnerLocked(ServiceRecord r, int id,
            Notification notification, int flags) {
        if (id != 0) {
            if (notification == null) {
                throw new IllegalArgumentException("null notification");
            }
            // Instant apps need permission to create foreground services.
            ...
            if (r.fgRequired) {
                if (DEBUG_SERVICE || DEBUG_BACKGROUND_CHECK) {
                    Slog.i(TAG, "Service called startForeground() as required: " + r);
                }
                r.fgRequired = false;
                r.fgWaiting = false;
                mAm.mHandler.removeMessages(
                        ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG, r);
            }
            if (r.foregroundId != id) {
                cancelForegroundNotificationLocked(r);
                r.foregroundId = id;
            }
            notification.flags |= Notification.FLAG_FOREGROUND_SERVICE;
            r.foregroundNoti = notification;
            if (!r.isForeground) {
                final ServiceMap smap = getServiceMapLocked(r.userId);
                if (smap != null) {
                    ActiveForegroundApp active = smap.mActiveForegroundApps.get(r.packageName);
                    if (active == null) {
                        active = new ActiveForegroundApp();
                        active.mPackageName = r.packageName;
                        active.mUid = r.appInfo.uid;
                        active.mShownWhileScreenOn = mScreenOn;
                        if (r.app != null) {
                            active.mAppOnTop = active.mShownWhileTop =
                                    r.app.uidRecord.curProcState
                                            <= ActivityManager.PROCESS_STATE_TOP;
                        }
                        active.mStartTime = active.mStartVisibleTime
                                = SystemClock.elapsedRealtime();
                        smap.mActiveForegroundApps.put(r.packageName, active);
                        requestUpdateActiveForegroundAppsLocked(smap, 0);
                    }
                    active.mNumActive++;
                }
                r.isForeground = true;
            }
            r.postNotification();
            if (r.app != null) {
                updateServiceForegroundLocked(r.app, true);
            }
            getServiceMapLocked(r.userId).ensureNotStartingBackgroundLocked(r);
            mAm.notifyPackageUse(r.serviceInfo.packageName,
                                 PackageManager.NOTIFY_PACKAGE_USE_FOREGROUND_SERVICE);
        } else {
            if (r.isForeground) {
                final ServiceMap smap = getServiceMapLocked(r.userId);
                if (smap != null) {
                    decActiveForegroundAppLocked(smap, r);
                }
                r.isForeground = false;
                if (r.app != null) {
                    mAm.updateLruProcessLocked(r.app, false, null);
                    updateServiceForegroundLocked(r.app, true);
                }
            }
            if ((flags & Service.STOP_FOREGROUND_REMOVE) != 0) {
                cancelForegroundNotificationLocked(r);
                r.foregroundId = 0;
                r.foregroundNoti = null;
            } else if (r.appInfo.targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) {
                r.stripForegroundServiceFlagFromNotification();
                if ((flags & Service.STOP_FOREGROUND_DETACH) != 0) {
                    r.foregroundId = 0;
                    r.foregroundNoti = null;
                }
            }
        }
    }
anr的时限为嘛是5s呢?

    void scheduleServiceForegroundTransitionTimeoutLocked(ServiceRecord r) {
        if (r.app.executingServices.size() == 0 || r.app.thread == null) {
            return;
        }
        Message msg = mAm.mHandler.obtainMessage(
                ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG);
        msg.obj = r;
        r.fgWaiting = true;
        mAm.mHandler.sendMessageDelayed(msg, SERVICE_START_FOREGROUND_TIMEOUT);
    }
    // How long the startForegroundService() grace period is to get around to
    // calling startForeground() before we ANR + stop it.
    static final int SERVICE_START_FOREGROUND_TIMEOUT = 5*1000;
这种timeout流程就很熟悉了。

 

4. 总结
Android O 后台应用想启动服务就老老实实的加个notification给用户看,表示你自己在后台占着资源,杀不杀由用户决定,偷偷地在后台跑没有framework帮忙想都别想,一个anr+crash套餐了解一下。

1)activity: Context.startForegroundService()

2)Service:startForeground(int id, Notification notification)(id must not be 0)
targetSdkVersion升级到28一些修改的地方(持续更新)
96  Bug总柴 
 1.9 2018.07.18 20:09* 字数 1331 阅读 12651评论 5喜欢 47
前言
Google Play应用市场对于应用的targetSdkVersion有了更为严a格的要求。从 2018 年 8 月 1 日起,所有向 Google Play 首次提交的新应用都必须针对 Android 8.0 (API 等级 26) 开发; 2018 年 11 月 1 日起,所有 Google Play 的现有应用更新同样必须针对 Android 8.0。

以下记录了我们升级targetSdkVersion的坑以及解决办法,希望对各位开发者有帮助。

错误1. java.lang.IllegalStateException: Not allowed to start service Intent {}: app is in background uid UidRecord{}
原因分析
从Android8.0开始,系统会对后台执行进行限制。初步判断由于我们应用在Application的onCreate过程中使用了IntentService来后台初始化一些任务,这个时候被系统认为是应用还处于后台,从而报出了java.lang.IllegalStateException错误。

解决办法
解决后台服务的限制,首先想到的办法是将服务变成前台服,随即我们又遇到了另一个问题,见错误2

错误2. android.app.RemoteServiceException: Context.startForegroundService() did not then call Service.startForeground(): ServiceRecord{}
原因分析
见Android8.0行为变更。新的 Context.startForegroundService() 函数将启动一个前台服务。现在,即使应用在后台运行,系统也允许其调用 Context.startForegroundService()。不过,应用必须在创建服务后的五秒内调用该服务的 startForeground() 函数。

解决办法
在后台服务启动执行执行之后,通过Service.startForeground()方法传入notification变成前台服务。需要注意的是从Android8.0开始,Notification必须制定Channel才可以正常弹出通知,如果创建Notification Channels详见这里。

由于我们的初衷是在启动程序的过程中后台进行一些初始化,这种前台给用户带来感知的效果并不是我们所希望的,因此我们考虑可以采用另一个后台执行任务的方法。这里官方推荐使用JobScheduler。由于我们引入了ktx以及WorkManager,这里我们采用了OneTimeWorkRequest来实现。具体实现如下:

class InitWorker : Worker(){
    override fun doWork(): Result {
        // 把耗时的启动任务放在这里

        return Result.SUCCESS
    }
}
然后在Applicaiton的onCreate中调用

val initWork = OneTimeWorkRequestBuilder<InitWorker>().build()
WorkManager.getInstance().enqueue(initWork)
来执行后台初始化工作

错误3. java.lang.NoClassDefFoundError: Failed resolution of: Lorg/apache/http/ProtocolVersion; Caused by: java.lang.ClassNotFoundException: Didn't find class "org.apache.http.ProtocolVersion"
原因分析
Android P Developer Preview的bug

解决办法
在AndroidManifest.xml文件中<Application>标签里面加入

<uses-library android:name="org.apache.http.legacy" android:required="false"/>
错误4. java.io.IOException: Cleartext HTTP traffic to dict.youdao.com not permitted
原因分析
从Android 6.0开始引入了对Https的推荐支持,与以往不同,Android P的系统上面默认所有Http的请求都被阻止了。

<application android:usesCleartextTraffic=["true" | "false"]>
原本这个属性的默认值从true改变为false

解决办法
解决的办法简单来说可以通过在AnroidManifest.xml中的application显示设置

<application android:usesCleartextTraffic="true">
更为根本的解决办法是修改应用程序中Http的请求为Https,当然这也需要服务端的支持。

错误5. android.os.FileUriExposedException file exposed beyond app through Intent.getData()
原因分析
主要原因是7.0系统对file uri的暴露做了限制,加强了安全机制。详见:官方文档
代码里出现问题的原因是,在需要安装应用的时候将下载下来的安装包地址传给了application/vnd.android.package-archive的intent

解决办法
使用FileProvider
具体代码可参考这篇文章
简单说明就是要在AndroidManifest里面声明FileProvider,并且在xml中声明需要使用的uri路径

<provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="${applicationId}.fileProvider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
</provider>
对应的xml/file_paths中指定需要使用的目录

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path
        name="download"
        path="yddownload"/>
</paths>
错误6. java.lang.SecurityException: Failed to find provider ** for user 0; expected to find a valid ContentProvider for this authority
原因分析
target到android8.0之后对ContentResolver.notifyChange() 以及 registerContentObserver(Uri, boolean, ContentObserver)做了限制,官方解释在这里

解决办法
参考文章
简单来说解决的办法就是创建一个contentprovider,并在AndroidManifest里面注册的provider的authority声明为registerContentObserver中uri的authority就可以了。

<provider
        android:name=".download.DownloadUriProvider"
        android:authorities="${applicationId}"
        android:enabled="true"
        android:exported="false"/>
public class DownloadUriProvider extends ContentProvider {
    public DownloadUriProvider() {
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        return null;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        return 0;
    }
}
错误7. notification没有显示
原因分析
如果targetsdkversion设定为26或以上,开始要求notification必须知道channel,具体查阅这里。

解决办法
在notify之前先创建notificationChannel

private void createNotificationChannel() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        CharSequence name = "下载提醒";
        String description = "显示下载过程及进度";
        int importance = NotificationManager.IMPORTANCE_DEFAULT;
        NotificationChannel channel = new NotificationChannel(DOWNLOAD_CHANNEL_ID, name, importance);
        channel.setDescription(description);
        mNotificationManager.createNotificationChannel(channel);
    }
}
错误8. 在AndroidManifest中注册的receiver不能收到广播
原因分析
针对targetsdkversion为26的应用,加强对匿名receiver的控制,以至于在manifest中注册的隐式receiver都失效。具体见官方原文

解决办法
将广播从在AndroidManifest中注册移到在Activity中使用registerReceiver注册

错误9. 无法通过“application/vnd.android.package-archive” action安装应用
原因分析
targetsdkversion大于25必须声明REQUEST_INSTALL_PACKAGES权限,见官方说明:
REQUEST_INSTALL_PACKAGES

解决办法
在AndroidManifest中加入

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
错误10. java.lang.RuntimeException: Unable to start activity ComponentInfo{xxxActivity}: java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation
原因分析
targetsdk26以上,对于透明主题的activity不能够通过manifest设定android:screenOrientation。
具体分析见这里

解决办法
检查报错的Activity是否在AndroidManifest中声明了android:screenOrientation,若有需要将其去除。

所有影响checklist
Andriod版本适配 check list
后台执行限制
每次在后台运行时,应用都会消耗一部分有限的设备资源,例如 RAM。 这可能会影响用户体验,如果用户正在使用占用大量资源的应用(例如玩游戏或观看视频),影响尤为明显。

为了提升用户体验,Android 8.0 对应用在后台运行时可以执行的操作施加了限制。 本文档说明了操作系统的一些变更,以及如何更新应用以便在新限制下正常运行。

概览
多个 Android 应用和服务可以同时运行。 例如,用户可以在一个窗口中玩游戏,同时在另一个窗口中浏览网页,并使用第三个应用播放音乐。

同时运行的应用越多,对系统造成的负担越大。 如果还有应用或服务在后台运行,这会对系统造成更大负担,进而可能导致用户体验下降;例如,音乐应用可能会突然关闭。

为了降低发生这些问题的几率,Android 8.0 对应用在用户不与其直接交互时可以执行的操作施加了限制。

应用在两个方面受到限制:

后台服务限制:处于空闲状态时,应用可以使用的后台服务存在限制。 这些限制不适用于前台服务,因为前台服务更容易引起用户注意。

广播限制:除了有限的例外情况,应用无法使用清单注册隐式广播。 它们仍然可以在运行时注册这些广播,并且可以使用清单注册专门针对它们的显式广播。

注:默认情况下,这些限制仅适用于针对 O 的应用。 不过,用户可以从 Settings 屏幕为任意应用启用这些限制,即使应用并不是以 O 为目标平台。

在大多数情况下,应用都可以使用 JobScheduler 作业克服这些限制。 这种方式让应用安排为在未活跃运行时执行工作,不过仍能够使系统可以在不影响用户体验的情况下安排这些作业。

Android 8.0 提供针对 JobScheduler 的多个改进,让您可以更轻松地使用计划作业取代服务和广播接收器;如需了解详细信息,请参阅 JobScheduler 改进。

后台服务限制
在后台中运行的服务会消耗设备资源,这可能降低用户体验。 为了缓解这一问题,系统对这些服务施加了一些限制。

系统可以区分 前台 和 后台 应用。 (用于服务限制目的的后台定义与内存管理使用的定义不同;一个应用按照内存管理的定义可能处于后台,但按照能够启动服务的定义又处于前台。)如果满足以下任意条件,应用将被视为处于前台:

具有可见 Activity(不管该 Activity 已启动还是已暂停)。
具有前台服务。
另一个前台应用已关联到该应用(不管是通过绑定到其中一个服务,还是通过使用其中一个内容提供程序)。 例如,如果另一个应用绑定到该应用的服务,那么该应用处于前台:

IME
壁纸服务
通知侦听器
语音或文本服务
如果以上条件均不满足,应用将被视为处于后台。

绑定服务不受影响
这些规则不会对绑定服务产生任何影响。 如果您的应用定义了绑定服务,则不管应用是否处于前台,其他组件都可以绑定到该服务。

处于前台时,应用可以自由创建和运行前台服务与后台服务。 进入后台时,在一个持续数分钟的时间窗内,应用仍可以创建和使用服务。

在该时间窗结束后,应用将被视为处于 空闲 状态。 此时,系统将停止应用的后台服务,就像应用已经调用服务的“Service.stopSelf()”方法。

在这些情况下,后台应用将被置于一个临时白名单中并持续数分钟。 位于白名单中时,应用可以无限制地启动服务,并且其后台服务也可以运行。

处理对用户可见的任务时,应用将被置于白名单中,例如:

处理一条高优先级 Firebase 云消息传递 (FCM) 消息。

接收广播,例如短信/彩信消息。

从通知执行 PendingIntent。

在很多情况下,您的应用都可以使用 JobScheduler 作业替换后台服务。 例如,CoolPhotoApp 需要检查用户是否已经从朋友那里收到共享的照片,即使该应用未在前台运行。

之前,应用使用一种会检查其云存储的后台服务。 为了迁移到 Android 8.0,开发者使用一个计划作业替换了这种后台服务,该作业将按一定周期启动,查询服务器,然后退出。

在 Android 8.0 之前,创建前台服务的方式通常是先创建一个后台服务,然后将该服务推到前台。

Android 8.0 有一项复杂功能;系统不允许后台应用创建后台服务。 因此,Android 8.0 引入了一种全新的方法,即 Context.startForegroundService(),以在前台启动新服务。

在系统创建服务后,应用有五秒的时间来调用该服务的 startForeground() 方法以显示新服务的用户可见通知。

如果应用在此时间限制内未调用 startForeground(),则系统将停止服务并声明此应用为 ANR。

广播限制
如果应用注册为接收广播,则在每次发送广播时,应用的接收器都会消耗资源。 如果多个应用注册为接收基于系统事件的广播,这会引发问题;触发广播的系统事件会导致所有应用快速地连续消耗资源,从而降低用户体验。

为了缓解这一问题,Android 7.0(API 级别 25)对广播施加了一些限制,如后台优化中所述。

Android 8.0 让这些限制更为严格。

针对 Android 8.0 的应用无法继续在其清单中为隐式广播注册广播接收器。 隐式广播是一种不专门针对该应用的广播。 例如,ACTION_PACKAGE_REPLACED 就是一种隐式广播,因为它将发送到注册的所有侦听器,让后者知道设备上的某些软件包已被替换。

不过,ACTION_MY_PACKAGE_REPLACED 不是隐式广播,因为不管已为该广播注册侦听器的其他应用有多少,它都会只发送到软件包已被替换的应用。

应用可以继续在它们的清单中注册显式广播。

应用可以在运行时使用 Context.registerReceiver() 为任意广播(不管是隐式还是显式)注册接收器。

需要签名权限的广播不受此限制所限,因为这些广播只会发送到使用相同证书签名的应用,而不是发送到设备上的所有应用。

在许多情况下,之前注册隐式广播的应用使用 JobScheduler 作业可以获得类似的功能。

例如,一款社交照片应用可能需要不时地执行数据清理,并且倾向于在设备连接到充电器时执行此操作。

之前,应用已经在清单中为 ACTION_POWER_CONNECTED 注册了一个接收器;当应用接收到该广播时,它会检查清理是否必要。 为了迁移到 Android 8.0,应用将该接收器从其清单中移除。

应用将清理作业安排在设备处于空闲状态和充电时运行。

注:很多隐式广播当前均已不受此限制所限。 应用可以继续在其清单中为这些广播注册接收器,不管应用针对哪个 API 级别。 有关已豁免广播的列表,请参阅隐式广播例外。

迁移指南
默认情况下,这些更改仅影响针对 O 的应用。 不过,用户可以从 Settings 屏幕为任意应用启用这些限制,即使应用并不是以 O 为目标平台。

您可能需要更新应用,使其符合新限制。

了解您的应用如何使用服务。 如果您的应用依赖某些在它处于空闲时于后台运行的服务,您需要替换这些服务。

可能的解决方法包括:

如果处于后台时您的应用需要创建一个前台服务,请使用新的 NotificationManager.startServiceInForeground()

方法,而不是创建一个后台服务,然后尝试将其推到前台。

如果服务容易被用户注意,请将其设为前台服务。 例如,播放音频的服务始终应为前台服务。

使用 NotificationManager.startServiceInForeground()

而不是 startService() 创建服务。

寻找一种使用计划作业实现服务功能的方式。 如果服务未在执行容易立即被用户注意到的操作,一般情况下,您都能够使用计划作业。

发生网络事件时,请使用 FCM 选择性地唤醒您的应用,而不是在后台轮询。

在应用正常处于前台之前,请推迟后台工作。

检查在您应用的清单中定义的广播接收器。 如果您的清单为显式广播声明了接收器,您必须予以替换。 可能的解决方法包括:

通过调用 Context.registerReceiver() 而不是在清单中声明接收器的方式在运行时创建接收器。

使用计划作业检查条件是否会触发隐式广播。

    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    申请写权限时要同时申请读权限