-
Notifications
You must be signed in to change notification settings - Fork 0
Summary in development
开发中遇到的问题和总结
- 1. Can NoticationManager.notify() be called from a worker thread?
- 2. Android: Firebase remoteConfig getString() method is removing quotes from string inside default.xml
- 3. 使用EventBus出现NoClassDefFoundError android/os/PersistableBundle问题
- 4. 应用添加android.permission.PROCESS_OUTGOING_CALLS权限导致支持的设备数减少
- 5. 应用targetSdkVersion升级到26需要注意的问题
- 6. 获取去电号码和来电号码
- 7. Notification布局设置高度问题
- 8. Android java.lang.VerifyError
- 9. 双重校验单例需注意的问题
- 10. Android7.1机型上由Toast引起的BadTokenException错误
- 11. Manifest merger failed with multiple errors, see logs问题处理
- 12. Lottie(版本v2.5.4) animation在s8(7.0),LG(7.0)上不工作的问题
- 13. 小米手机上应用在后台启动不了页面的问题
- 14. 在CoordinatorLayout中AppBarLayout不会随ExpandableListView滑动的问题
- 15. 测试订阅遇到的问题
- 16. 更高效的使用属性动画
- 17. NestedScrollView嵌套RecyclerView
- 18. ViewGroup中clipChildren属性的用法
- 19. Android 开机自启动方式
- 20. 解决android 9.0获取不到wifi名称的问题(附上完整代码)
- 20. Android target sdk升级到28
- 21. Android resource混淆
- 22. Google I/O 2019
- 23. CoordinatorLayout配合AppBarLayout和RecyclerView使用时遇到RecylerView item显示不全的问题
- 24. 优化android.app.RemoteServiceException: Context.startForegroundService() did not then call Service.startForeground()问题
- 25. java.lang.SecurityException: !@Too many alarms (500) registered from uid 10187
- 26. java.lang.RuntimeException: Using WebView from more than one process at once with the same data directory is not supported
- 27. 采用activity-alias方式实现动态更换桌面图标会导致在9.0手机上应用不会出现在recents列表里
- 28. AndroidStudio SSL peer shut down incorrectly 问题
- 29. Android获取顶层应用包名(较全面)
- 30. Android模拟器安装apk报错Failure [INSTALL_FAILED_DEXOPT]
- 31. Vibrate API crash on SamSung(android 8.1.0) mobile.
- 32. Android Studio 3.5后格式化布局代码时错乱
- 33. RecyclerView调用 notifyDataSetChanged()后导致item上的图片闪烁
- 34. Html.fromHtml忽视\n符号问题
- 35. 一条SMS最大字符数
- 36. RecyclerView局部刷新无效问题
- 37. Toolbar标题与返回键之间的间距过大问题
- 38. AndroidStudio升级到Arctic Fox 2020.3.1 版本后,git log窗口中文提交显示乱码
- 39. java.lang.ClassNotFoundException: org.gradle.wrapper.GradleWrapperMain
从工作线程更新通知是可以接受的,因为通知不会存在于应用程序的进程中,因此您不会直接更新其UI。通知在系统进程中维护,通知的UI通过RemoteViews(doc)进行更新,RemoteViews允许操作由您自己以外的进程维护的视图层次结构。如果你查看Notification.Builder的源代码,你会发现它最终构建了一个RemoteViews。
如果您查看RemoteViews的源代码,您会看到当您操作视图时,它实际上只是创建一个Action(source)对象并将其添加到要处理的队列中。 Action是一个Parcelable,它最终通过IPC发送到拥有Notification视图的进程,在那里它可以解压缩值并按照指示更新视图......在它自己的UI线程上。
2. Android: Firebase remoteConfig getString() method is removing quotes from string inside default.xml
获取firebase后台配置的数据是没问题的,只有获取默认配置文件里的数据才有该问题。
config_default.xml配置如下:
<?xml version="1.0" encoding="utf-8"?>
<defaultsMap>
<entry>
<key>LOCAL_JSON</key>
<value>[{"title":"TitleA","path":"pathA","image_url":" Some URL A"},{"title":"TitleB","path":"pathB","image_url":" Some URL B"}]</value>
</entry>
</defaultsMap>
使用Firebase remote config getString()获取回来的数据格式如下:
"[{title:TitleA,path:pathA,image_url: Some URL A},{title:TitleB,path:pathB,image_url: Some URL B}]"
明显,字符串里的双引号被去掉了,已经是一个错误的json数据格式,在解析的时候就会出错。
-
Use
firebaseConfig.setDefaults(Map)
; for setting up the default configuration. -
Use backslash (\) before Double Quotes(") in your String in .xml file.
-
Replace
classpath 'com.android.tools.build:gradle:3.0.1'
withclasspath 'com.android.tools.build:gradle:2.3.3'
and
Relpace
google()
withmaven { url 'https://maven.google.com' }
-
Add
android.enableAapt2 = false
in gradle.properties
- 经验证以上的四种方法,1和3方法是可行的,第2种方法不起作用,第4种方法会出现编译上的问题。
- 另外,个人觉得第3种方法去降低Gradle tools版本的方式也是不可取的,除了第1种方法外,还可以将默认json字符串数据写到
strings.xml
里(注意:json字符串里的双引号前要加反斜线\), 在具体实现的时候判断如果没有获取到后台配的数据,再去获取strings.xml
中的默认数据,当然最好的是等着google尽快解决该问题。
注:该问题只在Android5.0以下会出现
E/AndroidRuntime(28537): java.lang.NoClassDefFoundError: android/os/PersistableBundle
E/AndroidRuntime(28537): at java.lang.Class.getDeclaredMethods(Native Method)
E/AndroidRuntime(28537): at java.lang.Class.getPublicMethodsRecursive(Class.java:894)
E/AndroidRuntime(28537): at java.lang.Class.getMethods(Class.java:877)
E/AndroidRuntime(28537): at org.greenrobot.eventbus.SubscriberMethodFinder.findUsingReflectionInSingleClass(SubscriberMethodFinder.java:157)
E/AndroidRuntime(28537): at org.greenrobot.eventbus.SubscriberMethodFinder.findUsingInfo(SubscriberMethodFinder.java:88)
E/AndroidRuntime(28537): at org.greenrobot.eventbus.SubscriberMethodFinder.findSubscriberMethods(SubscriberMethodFinder.java:64)
E/AndroidRuntime(28537): at org.greenrobot.eventbus.EventBus.register(EventBus.java:140)
在注册EventBus的Activity或Fragment里重写了带有PersistableBundle参数的方法,比如:
@Override
public void onCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
super.onCreate(savedInstanceState, persistentState);
}
@Override
public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
super.onSaveInstanceState(outState, outPersistentState);
}
换成不带PersistableBundle参数的方法,比如:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
添加android.permission.PROCESS_OUTGOING_CALLS
权限会默认添加android.hardware.telephony
feature
(<uses-feature android:name="android.hardware.telephony" android:required="true"/>
), 即要求设备上具有telephony硬件功能支持, 而Google play 会利用在应用清单中声明的 元素从不符合应用的硬件和软件功能要求的设备上将应用滤除, 所以就导致了应用支持的设备数减少。
在AndroidManifest.xml中显示声明不需要设备具有telephony硬件功能(<uses-feature android:name="android.hardware.telephony" android:required="false"/>
)。 当然还是视需求而定,比如我某个功能就需要设备上具有所需的硬件或软件功能,否则就不能正常工作, 这时需要根据减少的设备数,以及对应用功能的影响综合评估下是否需要设备具有相应的硬件或软件功能支持。
使用aapt dump badging命令:
$ aapt dump badging <path_to_exported_.apk>
aapt存在于<SDK>/platform-tools/ 目录下。
https://developer.android.google.cn/guide/topics/manifest/uses-feature-element
Android8.0系统不允许后台应用创建后台服务。 Android 8.0 引入了一种全新的方法,即 Context.startForegroundService()
, 启动前台服务。在系统创建服务后,应用有五秒的时间来调用该服务的 startForeground() 方法以显示新服务的用户可见通知。
如果应用在此时间限制内未调用 startForeground()
,则系统将停止服务并抛出 ANR。
什么是后台应用?
满足以下任意条件的应用被视为处于前台:
1. 具有可见的Activity
2. 具有前台服务
3. 另一个前台应用关联到该应用,如:
- IME
- Wallpaper service
- Notification listener
- Voice or text service
如果以上条件均不满足,应用将被视为后台应用。
当应用程序位于前台时,它可以自由地创建和运行前台和后台服务。 当应用程序进入后台时,它有一个几分钟时间仍然可以创建和使用服务。 过了这段时间,该应用程序被视为空闲。 此时,系统会停止应用程序的后台服务,就像应用程序已调用服务的Service.stopSelf()
方法一样。
在某些情况下,后台应用程序会被放置在临时白名单上几分钟。 当应用程序位于白名单中,它可以无限制地启动服务,并允许其后台服务运行。 当应用程序处理用户可见的任务时,应用程序将放置在白名单中,例如:
- Handling a high-priority Firebase Cloud Messaging (FCM) message.
- Receiving a broadcast, such as an SMS/MMS message.
- Executing a PendingIntent from a notification.
- Starting a VpnService before the VPN app promotes itself to the foreground.
注意: IntentService是Service,因此受到后台Service的新限制。 因此,许多依赖IntentService的应用在Android 8.0或更高版本时无法正 常运行。 出于这个原因,Android支持库26.0.0引入了一个新的JobIntentService类,它提供与IntentService相同的功能,但在Android 8.0或更高 版本上运行时使用jobs而不是services。
启动service的时候按版本判断
if (Build.VERSION.SDK_INT >= 26) {
context.startForegroundService(intent);
} else {
// Pre-O behavior.
context.startService(intent);
}
在Service的onCreate()方法或onStartCommand()方法里调用以下代码
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH);
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(channel);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelId);
startForeground(id, builder.build());
}
-
针对Android 8.0 的应用无法继续在AndroidManifest中为隐式广播注册广播接收器。 隐式广播是一种不专门针对该应用的广播。 例如,
ACTION_PACKAGE_REPLACED
就是一种隐式广播,因为它将发送到所有注册该广播的监听者,让后者知道设备上的某些软件包已被替换。 不过,ACTION_MY_PACKAGE_REPLACED
不是隐式广播,因为不管已为该广播注册侦听器的其他应用有多少,它都会只发送到软件包已被替换的应用。 -
应用可以继续在它们的manifest中注册显式广播。
-
应用可以在运行时使用
Context.registerReceiver()
为任意广播(不管是隐式还是显式)注册接收器。 -
需要签名权限的广播不受此限制所限,因为这些广播只会发送到使用相同证书签名的应用,而不是发送到设备上的所有应用。
注: 很多隐式广播当前均已不受此限制。 应用可以继续在其清单中为这些广播注册接收器,不管应用针对哪个API级别。 有关已豁免广播的列表,请参阅隐式广播例外。
如果应用有监听ACTION_PACKAGE_REPLACED
,ACTION_PACKAGE_ADDED
,ACTION_PACKAGE_REMOVED
广播,并且是在manifest中注册的话,在Android8.0上注册这些广播的receiver是监听不到这些广播的,解决方式是在应用的常驻service里去动态注册这些广播。
使用 SYSTEM_ALERT_WINDOW 权限的应用无法再使用以下窗口类型来在其他应用和系统窗口上方显示提醒窗口:
相反,应用必须使用名为 TYPE_APPLICATION_OVERLAY 的新窗口类型。
使用 TYPE_APPLICATION_OVERLAY 窗口类型显示应用的提醒窗口时,请记住新窗口类型的以下特性:
- 应用的提醒窗口始终显示在状态栏和输入法等关键系统窗口的下面。
- 系统可以移动使用 TYPE_APPLICATION_OVERLAY 窗口类型的窗口或调整其大小,以改善屏幕显示效果。
- 通过打开通知栏,用户可以访问设置来阻止应用显示使用 TYPE_APPLICATION_OVERLAY 窗口类型显示的提醒窗口。
Android8.0上notification必须设置channel,否则不会显示。
创建notification示例:
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH);
notificationManager.createNotificationChannel(channel);
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelId);
notificationManager.notify(notificationId, builder.build());
Android7.0不识别uri以file://
开头,要将其转换为content://
才能识别uri,否则会报FileUriExposedException
错误。
要在应用间共享文件,您应发送一项 content://
URI,并授予URI临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。如需了解有关权限和共享文件的详细信息,请参阅共享文件。
例子:
1. xml的创建:
file_paths.xml中编写该Provider对外提供文件的目录:文件放置在res/xml/下。
文件内容:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path path="." name="external_storage_root" />
</paths>
内部的element可以是files-path,cache-path,external-path,external-files-path,external-cache-path,分别对应Context.getFilesDir(),Context.getCacheDir(),Environment.getExternalStorageDirectory(),Context.getExternalFilesDir(),Context.getExternalCacheDir()等几个方法。后来翻看源码发现还有一个没有写进文档的,但是也可以使用的element,是root-path,直接对应文件系统根目录。不过既然没有写进文档中,其实还是有将来移除的可能的。使用的话需要注意一下风险。
2. Manifests.xml中配置
<manifest>
...
<application>
...
<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>
...
</application>
</manifest>
为了避免和其它app冲突,authorities最好带上自己app的包名。
3. Uri使用
1)getUriForFile方法转换:
public static Uri getUriForFile(Context context, File file) {
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file);
}
//第二个参数是manifest中定义的`authorities`
2)intent加Flags
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
运行时权限
在旧的权限管理系统中,权限仅仅在App安装时询问用户一次,用户同意了这些权限App才能被安装(某些深度定制系统另说)。
以 Android 6.0(API 级别 23)或更高版本为目标平台的应用可以直接安装, 但是在运行时需要检查和请求权限,系统会弹出一个对话框让用户选择是否授权某个权限给App(这个Dialog不能由开发者定制),当App需要用户授予不恰当的权限的时候,用户可以拒绝,用户也可以在设置页面对每个App的权限进行管理。
哪些权限需要动态申请
新的权限策略讲权限分为两类,第一类是不涉及用户隐私的,只需要在Manifest中声明即可,比如网络、蓝牙、NFC等;第二类是涉及到用户隐私信息的,需要用户授权后才可使用,比如SD卡读写、联系人、短信读写等。
此类权限都是正常保护的权限,只需要在AndroidManifest.xml中简单声明这些权限即可,安装即授权,不需要每次使用时都检查权限,而且用户不能取消以上授权,除非用户卸载App。
ACCESS_LOCATION_EXTRA_COMMANDS
ACCESS_NETWORK_STATE
ACCESS_NOTIFICATION_POLICY
ACCESS_WIFI_STATE
BLUETOOTH
BLUETOOTH_ADMIN
BROADCAST_STICKY
CHANGE_NETWORK_STATE
CHANGE_WIFI_MULTICAST_STATE
CHANGE_WIFI_STATE
DISABLE_KEYGUARD
EXPAND_STATUS_BAR
GET_PACKAGE_SIZE
INSTALL_SHORTCUT
INTERNET
KILL_BACKGROUND_PROCESSES
MODIFY_AUDIO_SETTINGS
NFC
READ_SYNC_SETTINGS
READ_SYNC_STATS
RECEIVE_BOOT_COMPLETED
REORDER_TASKS
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
REQUEST_INSTALL_PACKAGES
SET_ALARM
SET_TIME_ZONE
SET_WALLPAPER
SET_WALLPAPER_HINTS
TRANSMIT_IR
UNINSTALL_SHORTCUT
USE_FINGERPRINT
VIBRATE
WAKE_LOCK
WRITE_SYNC_SETTINGS
所有危险的Android系统权限属于权限组,如果APP运行在Android 6.0 (API level 23)或者更高级别的设备中,而且targetSdkVersion>=23时,系统将会自动采用动态权限管理策略,如果你在涉及到特殊权限操作时没有申请权限权限而直接调用了相关代码,你的App可能就崩溃了,综上所述你需要注意:
-
此类权限也必须在Manifest中申明,否则申请时不提示用户,直接回调开发者权限被拒绝。
-
同一个权限组的任何一个权限被授权了,这个权限组的其他权限也自动被授权(注意: 在>=Android O设备上,申请一个子权限,组内其他子权限不会自动获取)。例如一旦WRITE_CONTACTS被授权了,App也有READ_CONTACTS和GET_ACCOUNTS了。
-
申请某一个权限的时候系统弹出的Dialog是对整个权限组的说明,而不是单个权限。例如我申请READ_EXTERNAL_STORAGE,系统会提示"允许xxx访问设备上的照片、媒体内容和文件吗?"。
如果App运行在Android 5.1 (API level 22)或者更低级别的设备中,或者targetSdkVersion<=22时(此时设备可以是Android 6.0 (API level 23)或者更高),在所有系统中仍将采用旧的权限管理策略,系统会要求用户在安装的时候授予权限。其次,系统就告诉用户App需要什么权限组,而不是个别的某个权限。
CALENDAR(日历)
READ_CALENDAR
WRITE_CALENDAR
CAMERA(相机)
CAMERA
CONTACTS(联系人)
READ_CONTACTS
WRITE_CONTACTS
GET_ACCOUNTS
LOCATION(位置)
ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
MICROPHONE(麦克风)
RECORD_AUDIO
PHONE(手机)
READ_PHONE_STATE
CALL_PHONE
READ_CALL_LOG
WRITE_CALL_LOG
ADD_VOICEMAIL
USE_SIP
PROCESS_OUTGOING_CALLS
SENSORS(传感器)
BODY_SENSORS
SMS(短信)
SEND_SMS
RECEIVE_SMS
READ_SMS
RECEIVE_WAP_PUSH
RECEIVE_MMS
STORAGE(存储卡)
READ_EXTERNAL_STORAGE
WRITE_EXTERNAL_STORAGE
使用adb命令可以查看这些需要授权的权限组:
adb shell pm list permissions -d -g
使用adb命令同样可以授权/撤销某个权限:
adb shell pm [grant|revoke] <permission-name>...
关于运行时权限的一些建议
-
只请求你需要的权限,减少请求的次数,或用隐式Intent来让其他的应用来处理。
1.1 如果你使用Intent,你不需要设计界面,由第三方的应用来完成所有操作。比如打电话、选择图片等。
1.2 如果你请求权限,你可以完全控制用户体验,自己定义UI。但是用户也可以拒绝权限,就意味着你的应用不能执行这个特殊操作。
-
防止一次请求太多的权限或请求次数太多,用户可能对你的应用感到厌烦,在应用启动的时候,最好先请求应用必须的一些权限,非必须权限在使用的时候才请求,建议整理并按照上述分类管理自己的权限:
2.1 普通权限(Normal PNermissions):只需要在Androidmanifest.xml中声明相应的权限,安装即许可。
2.2 需要运行时申请的权限(Dangerous Permissions):
- 必要权限:最好在应用启动的时候,进行请求许可的一些权限(主要是应用中主要功能需要的权限)。
- 附带权限:不是应用主要功能需要的权限(如:选择图片时,需要读取SD卡权限)。
-
解释你的应用为什么需要这些权限:在你调用requestPermissions()之前,你为什么需要这个权限。
例如,一个摄影的App可能需要使用定位服务,因为它需要用位置标记照片。一般的用户可能会不理解,他们会困惑为什么他们的App想要知道他的位置。所以在这种情况下,所以你需要在requestpermissions()之前告诉用户你为什么需要这个权限。
-
使用兼容库support-v4中的方法
ContextCompat.checkSelfPermission()
ActivityCompat.requestPermissions()
ActivityCompat.shouldShowRequestPermissionRationale()
References
- https://blog.csdn.net/yanzhenjie1003/article/details/52503533
- https://github.com/permissions-dispatcher/PermissionsDispatcher
- https://github.com/tbruyelle/RxPermissions
- https://github.com/googlesamples/easypermissions
- https://github.com/yanzhenjie/AndPermission
5.7 设置静音模式
注意7.0 以下,7.0~8.0(不包括8.0),8.0以上(包括8.0)的区分
完整方案:
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
File apkFile = new File(filePath);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
startActivity(intent);
} else {
boolean canRequest = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canRequest = activity.getPackageManager().canRequestPackageInstalls();
}
if (canRequest) {
Uri apkUri = FileProvider.getUriForFile(CleanApplication.getApp(), BuildConfig.APPLICATION_ID + ".fileprovider", apkFile);
//Granting Temporary Permissions to a URI
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
startActivity(intent);
} else {
//请求安装未知应用来源的权限
ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.REQUEST_INSTALL_PACKAGES}, INSTALL_PACKAGES_REQUESTCODE);
}
}
} catch (Exception e) {
e.printStackTrace();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == INSTALL_PACKAGES_REQUESTCODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
installApk();
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
startActivityForResult(intent, GET_UNKNOWN_APP_SOURCES);
}
}
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == GET_UNKNOWN_APP_SOURCES) {
installApk();
}
}
在Manifest文件里添加权限:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
具体实现见: 6.2 获取来电号码
添加权限,在AndroidManifest文件里加上:
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
监听Intent.ACTION_NEW_OUTGOING_CALL广播,可以动态注册也可以静态注册
动态注册:
在Activity或Service的onCreate()方法里
registerPhoneStateReceiver()
在Activity或Service的onDestroy()方法里
unregisterPhoneStateReceiver()
private void registerPhoneStateReceiver() {
IntentFilter intentFilter = new IntentFilter();
// 监听去电广播
intentFilter.addAction(Intent.ACTION_NEW_OUTGOING_CALL);
mPhoneStateReceiver = new MyPhoneStateReceiver();
registerReceiver(mPhoneStateReceiver, intentFilter);
}
private void unregisterPhoneStateReceiver() {
if (mPhoneStateReceiver != null) {
unregisterReceiver(mPhoneStateReceiver);
}
}
静态注册
在AndroidManifest里添加
<receiver android:name=".MyPhoneStateReceiver">
<intent-filter>
<action android:name="android.intent.action.NEW_OUTGOING_CALL" />
</intent-filter>
</receiver>
MyPhoneStateReceiver实现:
private class MyPhoneStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null) {
String action = intent.getAction();
if (Intent.ACTION_NEW_OUTGOING_CALL.equals(action)) {
// 获取去电号码
mOutgoingNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
}
}
}
}
添加权限:
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
如果target<23:
- 在Service或Activity的onCreate()方法里监听PhoneStateListener
TelephonyManager tm = (TelephonyManager) getSystemService(Service.TELEPHONY_SERVICE);
if (tm != null) {
tm.listen(mMyPhoneState, PhoneStateListener.LISTEN_CALL_STATE);
}
- MyPhoneState实现:
private class MyPhoneState extends PhoneStateListener {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
super.onCallStateChanged(state, incomingNumber);
}
}
如果target>=23, 则不能采取以上方式, 否则大部分设备上获取不到来电号码, 具体实现如下:
- 在AndroidManifest里添加CALL_PHONE权限, 否则获取不到来电号码
<uses-permission android:name="android.permission.CALL_PHONE"/>
- 监听TelephonyManager.ACTION_PHONE_STATE_CHANGED广播, 可以静态注册也可以动态注册
动态注册:
在Activity或Service的onCreate()方法里
registerPhoneStateReceiver()
在Activity或Service的onDestroy()方法里
unregisterPhoneStateReceiver()
private void registerPhoneStateReceiver() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
mPhoneStateReceiver = new MyPhoneStateReceiver();
registerReceiver(mPhoneStateReceiver, intentFilter);
}
private void unregisterPhoneStateReceiver() {
if (mPhoneStateReceiver != null) {
unregisterReceiver(mPhoneStateReceiver);
}
}
静态注册:
在AndroidManifest里添加
<receiver android:name=".MyPhoneStateReceiver">
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE" />
</intent-filter>
</receiver>
MyPhoneStateReceiver实现:
private class MyPhoneStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null) {
if (TelephonyManager.ACTION_PHONE_STATE_CHANGED.equals(intent.getAction())) {
// 在此处不能监听PhoneStateListener, 有两个原因:
// 1. 不管MyPhoneStateReceiver是动态注册还是静态注册, 同一个状态会监听到多次, 而且随着打电话次数或来电次数增多而增多,容易造成难以//预估的问题。
// 2. 如果MyPhoneStateReceiver是动态注册, 还会在某些设备上导致状态不对, 比如有的设备上在拨号过程中会回调OFFHOOK状态又会回调IDLE状态,正常情况是
// 拨号过程只应该回调OFFHOOK状态, 在挂断电话后才会回调IDLE状态
// TelephonyManager tm = (TelephonyManager) getSystemService(Service.TELEPHONY_SERVICE);
// if (tm != null) {
// tm.listen(mMyPhoneState, PhoneStateListener.LISTEN_CALL_STATE);
// }
String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
String incomingNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
if (TelephonyManager.EXTRA_STATE_IDLE.equals(state)) {
} else if (TelephonyManager.EXTRA_STATE_OFFHOOK.equals(state)) {
} else if (TelephonyManager.EXTRA_STATE_RINGING.equals(state)) {
}
}
}
}
}
- 在android>=6.0上运行时请求所需权限
7.1 如果不给notification布局设置固定高度,会出现适配问题,比如在s4上显示的高度是刚好包裹内容的高度,另外根部局是LinearLayout的话,就算设置了固定高度也有适配问题,有的设备上也是刚好包裹内容的高度(比如Nexus6上), 改成RelativeLayout就没问题了。
7.2 在Notification自定义布局里,如果给根布局设置高度是引用dimens(比如android:layout_height="@dimen/notification_height_that_64"),会报Exception:
android.app.RemoteServiceException: Bad notification posted from package antivirus.anti.virus.cleaner.security.booster: Couldn't expand RemoteViews for: StatusBarNotification
解决方式: 直接将高度值hard code写死在根布局,比如android:layout_height="64dp"
VerifyError
官方定义:
Thrown when the "verifier" detects that a class file, though well formed, contains some sort of internal inconsistency or security problem.
大概的意思就是说如果某个类文件里包含某种内部不一致或安全问题就会抛出VerifyError。
这解释还是很模糊,stackoverflow有比较好的解释: 在类文件里有使用到受系统版本限制的方法, 如果在低于某个系统版本上调用了该系统开始才有的方法就会抛出VerifyError异常。 解决方式是加系统版本判断。(https://stackoverflow.com/questions/668788/android-java-lang-verifyerror)
我遇到的VerifyError问题只有在android4.1和android4.2的设备上出现(应用最低支持android4.1),检查抛出VerifyError的类文件,里面用到了android4.3才开始加入的方法,并且已经加了系统版本判断,然后只能一个个排查,最终查到问题所在,原来是NotificationListenerService对象转成Context对象的问题,NotificationListenerService是android4.3开始加入的,所以只能在>=android4.3上才能转成Context,但是这个地方也是加了版本判断的,在低于4.3的设备上根本调用不到,也不知道为啥,最后还是在NotificationListenerService对象转成Context对象的地方改用service.getApplicationContext()就没问题了。
双重校验单例写法:
public class Singleton {
private volatile static Singleton singleton = null;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
需要注意的是singleton需要加volatile修饰,否则会存在多线程问题,具体原因见:
错误
android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@c17c6b8 is not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:679)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
at android.widget.Toast$TN.handleShow(Toast.java:434)
at android.widget.Toast$TN$2.handleMessage(Toast.java:345)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6119)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
原因
这个问题由于targetSDKVersion升到26之后,在7.1.1机型上概率性出现。稳定复现的步骤是,在Toast.show()之后,UI线程做了耗时的操作阻塞了Handler message的处理,如使用Thread.sleep(5000),然后这个崩溃就出现了。原因是7.1.1系统对TYPE_TOAST的Window类型做了超时限制,绑定了Window Token,最长超时时间是3.5s,如果UI在这段时间内没有执行完,Toast.show()内部的handler message得不到执行,NotificationManageService那端会把这个Toast取消掉,同时把Toast对于的window token置为无效。等App端真正需要显示Toast时,因为window token已经失效,ViewRootImpl就抛出了上面的异常。Android 8.0上面,google意识到这个bug,在Toast的内部加了try-catch保护。目前只有7.1.1上面的Toast存在这个问题,崩溃在系统源码里。
推荐解决方案
APP层可以通过自定义Toast类,反射替换TN的内部成员变量mHandler,从而添加try-catch做到workaround,所有使用Toast的地方都使用这个自定义的,不要直接使用系统原生的。推荐github上一开源项目:https://github.com/drakeet/ToastCompat
参考
有时候在运行的时候会出现Manifest merger failed with multiple errors,see logs
的错误,但是除了这个log,没有什么其他的有用信息.
处理方式:进入命令行,输入命令./gradlew processDebugManifest --stacktrace
, 回车后就能看到更多详细信息了(包含具体的错误原因)
Lottie版本:v2.5.4
原因: 经排查发现该问题产生的原因是将facebook-android-sdk版本升级到>=v5.0.0,具体为什么会影响到Lottie animation还没搞清楚.
解决办法:
- 将facebook-android-sdk版本降回到v5.0.0以下
- 如果不降级facebook-android-sdk版本的话,将Lottie版本升级到v2.7.0
原因: 小米手机上增加了“后台弹出界面”权限,该权限可以控制应用是否可以在后台启动页面。该权限开关在应用详情页的权限页面里,默认为拒绝的,既为应用默认不允许在后台弹出页面,针对特殊应用会提供白名单,例如音乐(歌词显示)、运动、VOIP(来电)等. 原文链接
可恨的是完全没有进行对用户通知,默默得就给小米手机系统增加了这么一个权限,而且居然不升级系统情况下也加了这么个权限
最近更新(2020-01-02):
在小米手机上可以通过以下方法来检测是否开启了后台启动页面权限
public static boolean canBackgroundStart(Context context) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
AppOpsManager ops = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
if (ops == null) {
return false;
}
try {
int op = 10021; // >= 23
// ops.checkOpNoThrow(op, uid, packageName)
Method method = ops.getClass().getMethod("checkOpNoThrow",
int.class, int.class, String.class);
Integer result = (Integer) method.invoke(ops, op, Process.myUid(), context.getPackageName());
return result == AppOpsManager.MODE_ALLOWED;
} catch (Exception e) {
Log.e("xxxxx", "not support", e);
}
}
return false;
}
通过以下方法跳到start in background权限的授予页面(注意: 虽然在小米上start in background和floatwindow权限是在一个页面,但不能直接通过overlay的intent去启动start in background权限页面,因为在红米手机上,通过overlay intent去启动会打开一个只有ovlay权限的页面,而不会打开既有start in background又有floatwindow权限的页面)
public static void openBackgroundSettingsInXiaomi(Context context) {
try {
Intent intent = new Intent();
intent.setAction("miui.intent.action.APP_PERM_EDITOR");
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.putExtra("extra_pkgname", BuildConfig.APPLICATION_ID);
context.startActivity(intent);
} catch (Exception e) {
try {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" +
BuildConfig.APPLICATION_ID));
context.startActivity(intent);
} catch (Exception e2) {
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null);
intent.setData(uri);
context.startActivity(intent);
}
}
}
原因: CoordinatorLayout only works with RecyclerView or NestedScrollView.
解决方法:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mExpandableListView.setNestedScrollingEnabled(true);
} else {
mExpandableListView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem > mPreviousVisibleItem) {
mAppBarLayout.setExpanded(false, true);
} else if (firstVisibleItem < mPreviousVisibleItem) {
mAppBarLayout.setExpanded(true, true);
}
mPreviousVisibleItem = firstVisibleItem;
}
});
}
解决方法: 给RecyclerView设置marginBottom,值为AppBarLayout的高度: android:layout_marginBottom="@dimen/height_of_app_bar"
24. 优化android.app.RemoteServiceException: Context.startForegroundService() did not then call Service.startForeground()问题
Android8.0开始不允许后台应用创建后台服务。 Android 8.0 引入了一种全新的方法,即 Context.startForegroundService(), 启动前台服务。在系统创建服务后,应用有五秒的时间来调用该服务的 startForeground() 方法以显示新服务的用户可见通知。 如果应用在此时间限制内未调用 startForeground(),则系统将停止服务并抛出ANR。
事实上即使通过Context.startForegroundService()去启动一个前台service,并且调用了startForeground()方法,而且代码也没有什么耗时操作,按道理不应该在五秒内没有调用到该服务的startForeground()方法, 但是仍然出现很多android.app.RemoteServiceException: Context.startForegroundService() did not then call Service.startForeground()
问题.有一种比较好的方式去减少该问题,如果应用处于前台,是可以直接调用startService()方法去启动后台服务,这样做的话,就会减少在前台因使用Context.startForegroundService()去启动前台服务引起的该问题,所以解决方法如下:
Intent intent = new Intent(context, ExampleService.class);
try {
context.startService(intent);
} catch (Exception e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
}
}
该问题从umeng后台错误来看,只出现在三星的机型上,产生的原因是因为在取消alarm的时候使用了PendingIntent.FLAG_CANCEL_CURRENT参数
如下代码所示:
Intent cancelIntent = new Intent(context, cls);
cancelIntent.setAction(action);
PendingIntent cancelPendingIntent = PendingIntent.getBroadcast(context, requestCode, cancelIntent, PendingIntent.FLAG_CANCEL_CURRENT);
cancel(context, cancelPendingIntent);
解决办法是用0替换PendingIntent.FLAG_CANCEL_CURRENT
26. java.lang.RuntimeException: Using WebView from more than one process at once with the same data directory is not supported
官方给出的说明是:Android P 以及之后版本不支持同时从多个进程使用具有相同数据目录的WebView.
如果在多个进程中使用具有相同目录的WebView就会报以下错误:
Caused by: java.lang.RuntimeException: Using WebView from more than one process at once with the same data directory is not supported
针对这个问题,Google也给出了解决方案:在初始化的时候,需要为其它进程webView设置目录
public void setWebViewPath() {
//java.lang.RuntimeException: Using WebView from more than one process at once with the same data directory is not supported
//android 9.0开始不支持从多个进程同时使用同一数据目录的WebView, 针对这个问题,谷歌也给出了解决方案:在初始化的时候,需要为其它进程webView设置不同目录
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
String processName = getProcessName();
if (!getPackageName().equals(processName)) {//判断不等于默认进程名称
WebView.setDataDirectorySuffix(processName);
}
}
}
遗憾的是,经过上面的代码优化后,仍然还有该问题,最终尝试按掘金上的一篇文章(有效解决WebView多进程崩溃(续))提到的解决方案做了处理,经过一段时间观察,统计平台里没有再出现该WebView错误。
注意事项
如果项目引入一些第三方sdk(比如google广告sdk), 那么这些第三方sdk可能会引入该WebView问题,尤其需要注意对不同进程下的WebView设置不同数据目录的操作需要放到所有sdk初始化前,这里建议放到Application attach时(attachBaseContext(Context base)方法里,因为attachBaseContext()方法调用时机早于ContentProvider onCreate(),ContentProvider onCreate()早于Application onCreate(), 而一些第三方的sdk初始化可能在ContentProvider里)。
参考:
- https://stackoverflow.com/questions/62079558/fatal-exception-java-lang-runtimeexceptionusing-webview-from-more-than-one-pro
- https://stackoverflow.com/questions/51843546/android-pie-9-0-webview-in-multi-process
- https://www.codenong.com/jsf3ccc065f632/
AndroidStudio 编译时如果出现 SSL peer shut down incorrectly 问题或者某些jar包下载不下来,一般是因为墙的原因导致的。这时候需要配置镜像来解决这个问题。(为了提高jar包的下载速度也可以配置)配置的方法就是在根build.gradle中添加镜像仓库,一般我们选择阿里的 http://maven.aliyun.com/nexus/content/groups/public/ 完整的如下所示:
buildscript {
repositories {
google()
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
}
}
allprojects {
repositories {
google()
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
注意: 要将jcenter放到最后一个,因为它就是那个下载慢,或者报错的罪魁祸首
https://blog.csdn.net/lhg1714538808/article/details/97886283
调用Vibrator的vibrate()方法在SamSung(android 8.1.0)上会出现空指针异常, 具体log如下所示:
java.lang.NullPointerException: Attempt to read from field 'android.os.VibrationEffect com.android.server.VibratorService$Vibration.mEffect' on a null object reference
at android.os.Parcel.readException(Parcel.java:2035)
at android.os.Parcel.readException(Parcel.java:1975)
at android.os.IVibratorService$Stub$Proxy.vibrate(IVibratorService.java:292)
at android.os.SystemVibrator.vibrate(SystemVibrator.java:81)
at android.os.Vibrator.vibrate(Vibrator.java:191)
at android.os.Vibrator.vibrate(Vibrator.java:110)
at android.os.Vibrator.vibrate(Vibrator.java:89)
解决方法: 使用try catch
https://www.badlogicgames.com/forum/viewtopic.php?f=11&t=28507
RecyclerView调用 notifyDataSetChanged()刷新列表会导致item上的图片闪烁(图片重新加载导致),为了使 url 没变的 ImageView 不重新加载,我们可以用
setHasStableIds(true);
使用这个,相当于给ImageView加了一个tag,tag不变的话,不用重新加载图片。但是加了这句话,会使得列表的数据项重复!!我们需要在我们的Adapter里面重写 getItemId就好了。
@Override
public long getItemId(int position) {
return position;
}
解决方法:用<br/>替换\n,然后再用Html.fromHtml处理。如下所示:
Spanned str = Html.fromHtml(message.replace("\n","<br />"));
textView.setText(str);
android\frameworks\opt\telephony中
com.android.internal.telephony.gsm.SmsMessage
编码——>ENCODING_7BIT
/** The maximum number of payload septets per message */
public static final int MAX_USER_DATA_SEPTETS = 160; //单条信息160个字节(ASCII码对应的纯英文字符)
/**
* The maximum number of payload septets per message if a user data header
* is present. This assumes the header only contains the
* CONCATENATED_8_BIT_REFERENCE element.
*/
public static final int MAX_USER_DATA_SEPTETS_WITH_HEADER = 153;//对于ASCII码对应的纯英文字符,长短信中每条短信的长度为153字节,7字节标记分页信息等协议
对于纯英文,1字符占用1字节
所以相当于第一条160个字符
第二条160-7-7=146个字符(第一条信息的协议信息也占用了第二条的长度)
第三条及以上,160-7=153个字符。
-----------------------------------------------------------------------
编码——>ENCODING_16BIT (处理包含中文等非ASSICC码字符的短信)
/** The maximum number of payload bytes per message */
public static final int MAX_USER_DATA_BYTES = 140;//单条信息140个字节(包含中文等非ASCII码字符的信息)
/**
* The maximum number of payload bytes per message if a user data header
* is present. This assumes the header only contains the
* CONCATENATED_8_BIT_REFERENCE element.
*/
public static final int MAX_USER_DATA_BYTES_WITH_HEADER = 134;//包含中文等非ASCII码字符的信息,长短信每条最大长度为140-6=134字节,6个字节用于标记分页信息
对于包含中文字符的信息,1字符占用2字节,140/2=70, 134/2=67, 6/2=3
所以相当于第一条70个字符
第二条70-3-3=64字符(第一条信息的分页信息也占用了第二条的长度)
第三条及以上70-3=67字符。
参考文档:
长短信组织结构。UDH(user data header)
http://wenku.baidu.com/link?url=ksdB3TkIqKdm7tASZWqmZ8BJcUDbTbVw5FDWButs8pDlEM0ZuiJ7z36Z6lQDqZBA9vI-_XE1UWBvkRzQdeCY6wfK8labQsqIkZgQAZ7sZnK
RecyclerView 通过adapter.notifyItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload)
或者adapter.notifyItemChanged(int position, @Nullable Object payload)
,可以实现局部刷新列表中某个条目中的一小部分ui, adapter需重写下面的方法,并在方法里实现局部刷新:
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
if (payloads != null && !payloads.isEmpty()) {
//局部刷新, 根据payloads里的数据做相应处理
} else {
super.onBindViewHolder(holder, position, payloads);
}
}
注意:需设置recyclerView.setItemAnimator(null);如果不将ItemAnimator设置为null,局部刷新操作虽然生效,但会同时刷新整个列表。
解决方式:设置属性app:contentInsetStartWithNavigation="0dp"
https://blog.csdn.net/jingbin_/article/details/79146189
解决方式:设置里 appearance 勾选 use custom font。未升级前默认是勾选的,升级后默认不勾选,所以重新勾选上即可。
使用终端 gradlew 命令就报错:
找不到或无法加载主类 org.gradle.wrapper.GradleWrapperMain
解决办法:查看项目的\gradle\wrapper目录下是否缺失gradle-wrapper.jar文件,如果是,则到别的工程拷贝一份放在该目录下,即可。