原文:https://juejin.im/post/5c46db4ae51d4503834d8227
Android 学习笔记核心篇
基础知识
底层原理
Android 操作系统是一个多用户 Linux 操作系统,每个应用都是一个用户
操作系统一般会给每个应用分配一个唯一的 Linux 用户 ID,这个 ID 对应用是不可见的。但有些情况下两个应用可以共享同一个 Linux 用户 ID,此时他们可以访问彼此的文件,甚至还可以运行在同一个 Linux 进程中,共享同一个虚拟机。但两个应用的签名必须是一样的
每个进程都有自己的虚拟机,一般每个应用都运行在自己的 Linux 进程中
应用组件
应用没有唯一的入口,没有 main() 函数,因为应用是由多个组件拼凑在一起的,每个组件都是系统或者用户进入应用的入口,组件之间既可以是相互独立的,也可以是相互依赖的。系统和其它应用在被允许的情况下可以启动/激活一个应用的任意一个组件
组件有四种类型: Activity,Service,BroadcastReceiver 和 ContentProvider
Activity
Activity 表示一个新的用户界面,只能由系统进行创建和销毁,应用只能监听到一些生命周期回调,这些回调通常也被叫作生命周期方法
Activity 的名字一旦确定好就不要再更改了,否则可能会引发一系列问题
Service
Service 表示一个后台服务,Service 可以是独立的,可以在应用退出后继续运行。也可以绑定到其他进程或 Activity,表示其他进程想使用这个 Service,像输入法、动态壁纸、屏保等系统功能都是以 Service 的形式存在的,在需要运行的时候进行绑定
大部分情况下,更建议使用 JobScheduler,因为 JobScheduler 和 Doze API 配合下一般会比简单使用 Service 更省电
BroadcastReceiver
BroadcastReceiver 是一个事件传递的组件,通过它应用可以响应系统范围的广播通知。系统的包管理器会在安装应用时将应用中的静态广播接收器注册好,所以即使应用没在运行,系统也能把事件传递到该组件。
通过 BroadcastReceiver 可以实现进程间通信
ContentProvider
ContentProvider 是在多个应用间共享数据的组件,如果应用的一些数据想要被其它应用使用,必须通过 ContentPrivider 进行管理,不过应用的私有数据也可以通过 ContentProvider 进行管理,主要还是因为 ContentProvider 提供了共享数据的抽象,使用者不需要知道数据究竟是以文件形式还是数据库等其他形式存储的,只需要通过 ContentProvider 提供的 统一的 API 进行数据的增删改查即可。同时 ContentProvider 还提供了 安全 环境,可以根据需要方便地控制数据的访问权限,不需要手动控制文件权限或数据库权限
为了安全,也为了方便,一般需要通过 ContentResolver 操作 ContentProvider
通过 ContentProvider 可以实现进程间通信
激活组件
应用不能也不应该直接激活其它应用的任意一个组件,但是系统可以,所以要想激活一个组件,需要给系统发一个消息详细说明你的意图( Intent ),之后系统就会为你激活这个组件
Activity,Service,BroadcastReceiver 都需要通过被称为 Intent 的异步消息激活
被激活组件返回的结果也是 Intent 形式的
ContentProvider 只有在收到 ContentResolver 的请求时才会被激活
只有 BroadcastReceiver 可以不在 manifest 文件中注册,因为有些 BroadcastReceiver 需要在程序运行时动态地注册和注销。而其它组件必须在 manifest 文件中注册,否则无法被系统记录,也就无法被激活
如果 Intent 通过组件类名显式指明了唯一的目标组件,那么这个 Intent 就是显式的,否则就是隐式的。隐式 Intent 一般只描述要执行动作的类型,必要时可以携带数据,系统会根据这个隐式 Intent 的描述决定激活哪个组件,如果有多个组件符合激活条件,系统一般会弹出选择框让用户选择到底激活哪个组件
Service 必须使用显式 Intent 激活,不能声明 IntentFilter
启动指定的 Activity 使用显式 Intent,启动随便一个能完成指定工作的 Activity 使用隐式 Intent。能完成指定工作的那些想要被隐式 Intent 激活的 Activity 需要事先声明好 IntentFilter 表示自己有能力处理什么工作,IntentFilter 一般通过 能完成的动作 、意图类型 和 额外数据 来描述
要想被隐式 Intent 激活,意图类型至少要包含 android.intent.category.DEFAULT 的意图类型
在使用隐式 Intent 激活 Activity 之前一定要检查一下有没有 Activity 能处理这个 Intent :
if (sendIntent.resolveActivity(getPackageManager()) != null) {
startActivity(sendIntent);
}
复制代码
PackageManager packageManager = getPackageManager();
List activities = packageManager.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);
boolean isIntentSafe = activities.size() > 0;
复制代码
使用隐式 Intent 时每次都强制用户选择一个组件激活:
Intent intent = new Intent(Intent.ACTION_SEND);
String title = getResources().getString(R.string.chooser_title);
Intent chooser = Intent.createChooser(intent, title);
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(chooser);
}
复制代码
如果想要你的 Activity 能被隐式 Intent 激活,如果想要某个 链接 能直接跳转到你的 Activity,必须配置好 IntentFilter。这种链接分为两种: Deep links 和 Android App Links
Deep links 对链接的 scheme 没有要求,对系统版本也没有要求,也不会验证链接的安全性,不过需要一个 android.intent.action.VIEW 的 action 以便 Google Search 能直接打开,需要 android.intent.category.DEFAULT 的 category 才能响应隐式 Intent,需要 android.intent.category.BROWSABLE 的 category 浏览器打开链接时才能跳转到应用,所以经典用例如下。一个 intent filter 最好只声明一个 data 描述,否则你得考虑和测试所有变体的情况。系统处理这个链接的流程为: 如果用户之前指定了打开这个链接的默认应用就直接打开这个应用 → 如果只有一个应用可以处理这个链接就直接打开这个应用 → 弹窗让用户选择用哪个应用打开
复制代码
Android App Links 是一种特殊的 Deep links,要求链接必须是你自己网站的 HTTP URL 链接,系统版本至少是 Android 6.0 (API level 23),优点是安全且具体,其他应用不能使用你的链接,不过你得先 验证你的链接,由于链接和网站链接一致所以可以无缝地在应用和网站间切换,可以支持 Instant App,可以通过浏览器、谷歌搜索 APP、系统屏幕搜索、甚至 Google Assistant 的链接直接跳转到应用。验证链接的流程为: 将 标签的 android:autoVerify 设置为 true 以告诉系统自动验证你的应用属于这个 HTTP URL 域名 → 填写好网站域名和应用 ID 并使用签名文件生成 Digital Asset Links JSON 文件 → 将文件上传到服务器,访问路径为 https://domain.name/.well-known/assetlinks.json ,响应格式为 application/json,子域名也需要存在对应的文件,一个域名可以关联多个应用,一个应用也可以关联多个域名,且可以使用相同的签名 → 利用编辑器插件完成关联并验证
使用 Intent Scheme URL 需要做过滤。如果浏览器支持 Intent Scheme Uri 语法,如果过滤不当,那么恶意用户可能通过浏览器 js 代码进行一些恶意行为,比如盗取 cookie 等。所以如果使用了 Intent#parseUri() 方法,获取的 intent 必须严格过滤,intent 至少包含 addCategory(“android.intent.category.BROWSABLE”),setComponent(null),setSelector(null) 3 个策略
开放的 Activity/Service/BroadcastReceiver 等需要对传入的 intent 做合法性校验
应用资源
添加资源限定符的顺序为: SIM 卡所属的国家代码和移动网代码 → 语言区域代码 → 布局方向 → 最小宽度 → 可用宽度 → 可用高度 → 屏幕大不大 → 屏幕长不长 → 屏幕圆不圆 → 屏幕色域宽不宽 → 屏幕支持的动态范围高不高 → 屏幕方向 → 设备的 UI 模式 → 夜间模式 → 屏幕像素密度 → 触摸屏类型 → 键盘类型 → 主要的文字输入方式 → 导航键是否可用 → 主要的非触摸导航方式 → 支持的 API level
一个资源目录的每种资源限定符最多只能出现一次
必须提供缺省的资源文件
资源目录名是大小写不敏感的
drawable 资源取别名:
String replyLabel = getResources().getString(R.string.reply_label);
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
.setLabel(replyLabel)
.build();
PendingIntent replyPendingIntent =
PendingIntent.getBroadcast(getApplicationContext(),
conversation.getConversationId(),
getMessageReplyIntent(conversation.getConversationId()),
PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action action =
new NotificationCompat.Action.Builder(R.drawable.ic_reply_icon,
getString(R.string.label), replyPendingIntent)
.addRemoteInput(remoteInput)
.build();
Notification newMessageNotification = new Notification.Builder(mContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_message)
.setContentTitle(getString(R.string.title))
.setContentText(getString(R.string.content))
.addAction(action)
.build();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(notificationId, newMessageNotification);
复制代码
private CharSequence getMessageText(Intent intent) {
// 在 BroadcastReceiver 接收的 Intent 中可以根据之前的 KEY 拿到文本框的内容
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
return remoteInput.getCharSequence(KEY_TEXT_REPLY);
}
return null;
}
复制代码
// 在回复完成后更新通知表示已经处理这次回复,也可以调用 setRemoteInputHistory() 方法附加回复的内容
Notification repliedNotification = new Notification.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_message)
.setContentText(getString(R.string.replied))
.build();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(notificationId, repliedNotification);
复制代码
通过 setProgress(PROGRESS_MAX, PROGRESS_CURRENT, false) 可以给通知添加确定进度条,通过 setProgress(0, 0, true) 可以添加不确定进度条,通过 setProgress(0, 0, false) 可以在完成后移除进度条
setVisibility() 方法可以设置锁屏时的通知显示策略,VISIBILITY_PUBLIC(显示所有通知)表示完整地显示通知内容,VISIBILITY_SECRET(完全不显示内容)表示不显示通知的任何信息,VISIBILITY_PRIVATE(隐藏敏感通知内容)表示只显示图标标题等基本信息
NotificationManagerCompat#notify() 方法传递之前的通知 ID 可以更新之前的通知,调用 setOnlyAlertOnce() 方法以便只在第一次出现通知时提示用户
用户可以主动清除通知,创建通知时调用 setAutoCancel() 方法可以在用户点击通知后清除通知,创建通知时调用 setTimeoutAfter() 方法可以在超时后由系统自动清除通知,可以随时调用 cancel() 或 cancelAll() 方法清除之前的通知
点击通知后启动的 Activity 分为两种,一种是应用的正常用户体验流中的常规 Activity,拥有任务完整的返回栈。还有一种是仅仅用来展示通知的详细内容的特殊Activity,它不需要返回栈。
对于常规 Activity 需要先通过 android:parentActivityName 属性或者 android.support.PARENT_ACTIVITY 的 标签指定层级关系,然后
Intent resultIntent = new Intent(this, ResultActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addNextIntentWithParentStack(resultIntent);
PendingIntent resultPendingIntent =
stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
复制代码
对于特殊 Activity 需要先指定 android:taskAffinity="" 和 android:excludeFromRecents=“true” 以避免在之前的任务中启动,然后
Intent notifyIntent = new Intent(this, ResultActivity.class);
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent notifyPendingIntent = PendingIntent.getActivity(
this, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT
);
复制代码
Android 7.0 (API level 24) 开始,如果一个应用同时有 4 个及以上的通知,那么系统会自动将它们合并成一组,应用也可以自己定义和组织分组,用户点击后可以展开成一些单独的通知,老版本可以考虑使用 inbox 样式代替。每个通知可以通过 setGroup() 方法指定所属分组,通过 setSortKey() 方法排序,通过 setGroupAlertBehavior() 指定通知行为,默认是 GROUP_ALERT_ALL 表示组内所有的通知都可能产生声音和震动。系统默认会自动生成通知组的摘要,你也可以单独创建一个表示通知组摘要的通知
int SUMMARY_ID = 0;
String GROUP_KEY_WORK_EMAIL = “com.android.example.WORK_EMAIL”;
Notification newMessageNotification1 =
new NotificationCompat.Builder(MainActivity.this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify_email_status)
.setContentTitle(emailObject1.getSummary())
.setContentText(“You will not believe…”)
.setGroup(GROUP_KEY_WORK_EMAIL)
.build();
Notification newMessageNotification2 =
new NotificationCompat.Builder(MainActivity.this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify_email_status)
.setContentTitle(emailObject2.getSummary())
.setContentText(“Please join us to celebrate the…”)
.setGroup(GROUP_KEY_WORK_EMAIL)
.build();
Notification summaryNotification =
new NotificationCompat.Builder(MainActivity.this, CHANNEL_ID)
.setContentTitle(emailObject.getSummary())
//set content text to support devices running API level < 24
.setContentText(“Two new messages”)
.setSmallIcon(R.drawable.ic_notify_summary_status)
//build summary info into InboxStyle template
.setStyle(new NotificationCompat.InboxStyle()
.addLine(“Alex Faarborg Check this out”)
.addLine(“Jeff Chang Launch Party”)
.setBigContentTitle(“2 new messages”)
.setSummaryText(“janedoe@example.com”))
//specify which group this notification belongs to
.setGroup(GROUP_KEY_WORK_EMAIL)
//set this notification as the summary for the group
.setGroupSummary(true)
.build();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(emailNotificationId1, newMessageNotification1);
notificationManager.notify(emailNotificationId2, newMessageNotification2);
notificationManager.notify(SUMMARY_ID, summaryNotification);
复制代码
Android 8.0 (API level 26) 开始应用启动图标可以自动添加一个小圆点表示有新的通知,用户长按应用启动图标可以查看和处理通知,调用 mChannel.setShowBadge(false) 可以禁用小圆点标志,调用 setNumber(messageCount) 可以设置长按后显示给用户的消息数,调用 setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL) 可以设置长按后的图标样式,通过 setShortcutId() 可以隐藏重复的 shortcut
自定义通知内容的样式需要 setStyle(new NotificationCompat.DecoratedCustomViewStyle()) 样式,然后调用 setCustomContentView() 和 setCustomBigContentView() 方法指定自定义的折叠和展开布局(一般折叠布局限制高度为 64 dp,展开布局高度限制为 256 dp),布局中的控件要使用兼容库的样式,如 style="@style/TextAppearance.Compat.Notification.Title"。如果不想使用标准通知模板,不调用 setStyle() 只调用 setCustomBigContentView() 即可
Android 5.0 (API level 21) 开始支持顶部弹出的 heads-up 通知,可能触发 heads-up 通知的条件有: 用户的 Activity 正处于全屏模式并使用了 fullScreenIntent;Android 8.0 (API level 26) 及更高的设备上通知的重要程度为 IMPORTANCE_HIGH;Android 8.0 (API level 26) 以下的设备上通知的优先级为 PRIORITY_HIGH 或 PRIORITY_MAX 并且开启了铃声或震动
Service 相关
Service 运行在所在进程的主线程中,它不会自动创建线程,如果你不指定进程的话它甚至不会创建额外的进程,所以如果要执行耗时操作的话应该创建一个新的线程去执行
Service 有三种类型,一种是 Foreground service,用来执行用户能从通知栏感知到的后台操作,一种是 Background service,是用户感知不到的,但是 Android 8.0 (API level 26) 开始系统会对这类 Service 进行各种限制,一种是 Bound service,是通过 bindService() 方法绑定到其他组件的服务,Bound service 基于 C/S 架构的思想,被绑定的组件可以向这个服务发请求、接收响应数据、甚至 IPC 交互,只要有一个组件绑定了它,他就会马上运行,多个组件可以同时绑定它,当所有都解绑时,它就会被销毁
使用 startService() 方法启动 Service 时会导致它收到 onStartCommand() 回调,服务会一直运行直到你主动调用它的 stopSelf() 方法或其它组件主动调用 stopService() 方法。如果它只是 Bound service 可以不是实现该方法
使用 bindService() 方法启动 Service 时不会回调 onStartCommand() 方法,而会回调 onBind() 方法,为了与它的 client 通信这个方法需要返回 IBinder,如果你不允许它绑定就返回空,解绑使用 unbindService() 方法
一个 Service 可以启动多次,但是只能停止一次,stopSelf(int) 方法可以在满足指定的 startId 时停止
Service 和 Activity 一样其所在的的进程可能随时被系统杀掉,同样需要做好销毁重建的工作
IntentService 作为 Service 的子类可以方便地在工作线程中完成多个任务,多个任务是一个接一个的执行,所以不会存在线程安全问题,内部是借助一个 HandlerThread 实现异步处理的,当所有请求都完成后会自动销毁,onBind() 方法返回了空,为了方便调试可以在构造器中指定工作线程的名字,如果想重写 onCreate()、onStartCommand()、onDestroy() 方法的实现必须调用父类的实现,IntentService 是在一个工作线程中完成多个任务,所以如果想在多个线程中完成多个任务可以直接继承 Service 并借助 HandlerThread 等线程技术实现
相对于 IntentService 更推荐使用 JobIntentService,JobIntentService 借助了 JobScheduler 和 AsyncTask 完成更灵活的任务调度和处理,只需要申请好 WAKE_LOCK 权限 JobScheduler 就可以完成 WakeLock 的管理,使用 enqueueWork(Context, Class, int, Intent) 静态方法提交任务就可以让 onHandleWork(Intent) 回调中的代码被更好地调度执行了
不能绑定的 Service 只能通过 PendingIntent 进行组件间的通信
Foreground service 的通知栏通知只能通过停止服务或者从前台移除来解除
Android 9 (API level 28) 开始 Foreground service 必须请求 FOREGROUND_SERVICE 权限
使用 startForeground() 方法可以向系统请求以 Foreground service 模式运行,stopForeground() 可以请求退出该模式
后台任务
每个进程都有一个主线程用来完成任务,一般主线程结束了那么意味着整个任务完成了,进程就会自动结束退出了
Android 应用的主线程用来进行测量绘制 UI、协调用户操作、接收生命周期事件等工作,是与用户的感知直接关联的,所以通常也被叫做 UI 线程,如果在这个线程中做太多工作,那么会导致这个线程挂起或者卡顿,导致糟糕的用户体验。所以像解码 bitmap、读写磁盘、执行网络请求等需要长时间计算和处理的操作都应该放到单独的后台线程中去做
后台线程虽然是用户感觉不到的,但通常却是最消耗系统资源的,有的线程大部分时间都在占用 CPU 完成复杂的计算,我们管这种称为 CPU 密集型操作,有的线程大部分时间都在进行 I/O 的读写操作,我们管这种叫做 I/O 密集型操作。我们可以根据不同的操作类型选择不同的策略来处理以便最大化系统的吞吐量同时最小化所需代价。同时长时间运行的后台线程也加剧了电量的消耗,所以不管是操作系统还是开发者都需要 对这些后台线程的行为进行限制
在创建一个后台任务之前,我们需要先要对它分析一下,它是要马上执行还是可以延迟执行?它需要系统满足指定条件才能执行吗?它需要在精确的时间点执行吗?
WorkManager
通过 WorkManager 可以优雅地执行 可延迟执行的 异步任务,当应用退出后仍然可以继续执行,当满足系统条件(联网、充电、重启)时仍然可以触发任务的执行
特别适合用来向后台发送日志或分析数据,或者用来周期性的与服务器同步数据
WorkManager 在 Android 6.0 (API level 23) 及以上系统上借助 JobScheduler 实现,在之前的系统上借助 BroadcastReceiver 和 AlarmManager 实现
WorkManager 可以对任务添加网络条件和充电状态等条件限制,可以调度一次性的或周期性的任务,可以监听和管理被调度的任务,可以将多个任务连在一起
一次性的任务可以使用 OneTimeWorkRequest,周期性的任务使用 PeriodicTimeWorkRequest
如果指定了多个限制,那么只有在所有限制都满足的情况下任务才会执行:
Constraints constraints = new Constraints.Builder()
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
.build();
OneTimeWorkRequest compressionWork =
new OneTimeWorkRequest.Builder(CompressWorker.class)
.setConstraints(constraints)
.build();
复制代码
任务交给系统后可能会马上被执行,可以通过 setInitialDelay(10, TimeUnit.MINUTES) 设置一个最小延时
如果需要重试任务可以在 Worker 中使用 Result.retry() 完成,采用的补偿策略默认是 EXPONENTIAL 指数级的,可以使用 setBackoffCriteria() 方法调整策略
可以通过 setInputData() 方法为任务设置输入数据,在 Worker 中可以通过 getInputData() 方法获取到输入数据,Result.success() 和 Result.failure() 可以携带输出数据。数据应该尽可能的简单,不能超过 10KB
addTag 方法可以给任务打 Tag,然后就可以使用 WorkManager.cancelAllWorkByTag(String) 和 WorkManager.getWorkInfosByTagLiveData(String) 等方法方便操作任务了
如果一个任务的先决任务没有完成那么会被认为是 BLOCKED 态
如果任务的限制和定时满足要求那么会被认为是 ENQUEUED 态
如果任务正在执行那么会被认为是 RUNNING 态
如果任务返回了 Result.success() 那么会被认为是 SUCCEEDED 态,这是最终态,只有 OneTimeWorkRequest 可能进入这个状态
如果任务返回了 Result.failure() 那么会被认为是 FAILED 态,这是最终态,只有 OneTimeWorkRequest 可能进入这个状态,所有相关的任务也会被标记为 FAILED 且不会被执行
显式取消一个没终止的 WorkRequest 会被认为是 CANCELLED 态,所有相关的任务也会被标记为 CANCELLED 且不会被执行
WorkManager.getWorkInfoById(UUID) 和 WorkManager.getWorkInfoByIdLiveData(UUID) 等方法可以定位想要的任务进行观察
可以将任务连在一起:
WorkManager.getInstance()
.beginWith(Arrays.asList(filter1, filter2, filter3))
.then(compress)
.then(upload)
.enqueue();
复制代码
Foreground service
对于用户触发的必须马上执行且必须执行完的后台任务,需要使用 Foreground services 实现,它既告诉系统这个应用正在执行重要的任务不能被杀掉,又通过通知栏告诉用户有后台工作正在执行
AlarmManager
如果任务需要在精确的时间点执行,可以使用 AlarmManager
DownloadManager
如果需要执行一个长时间的 HTTP 下载任务,可以使用 DownloadManager。DownloadManager 独立于应用之外,可以在下载失败、更改网络连接、系统重启后进行重试
public static long downloadApk(String url, String title, String desc) {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
.setTitle(title)
.setDescription(desc)
.setDestinationInExternalFilesDir(MyApplication.getInstance(), null, “apks”)
.allowScanningByMediaScanner();
DownloadManager downloadManager = (DownloadManager) MyApplication.getInstance().getSystemService(Context.DOWNLOAD_SERVICE);
return downloadManager.enqueue(request);
}
复制代码
小技巧
测试 Deep links:
adb shell am start
-W -a android.intent.action.VIEW
-d “example://gizmos” com.example.android
复制代码
测试 Android App Links:
adb shell am start -a android.intent.action.VIEW
-c android.intent.category.BROWSABLE
-d “http://domain.name:optional_port”
复制代码
应用安装完 20s 后获取所有应用的链接处理策略:
adb shell dumpsys package domain-preferred-apps
复制代码
模拟系统杀掉应用进程:
adb shell am kill com.some.package
复制代码
将文件导入手机:
adb push com.some.package /sdcard/
复制代码
.nomedia 文件会导致其所在目录不被 Media Scanner 扫描到
附录
系统栏适配
/**
-
华为手机刘海屏适配
-
@author frank
-
@see 《华为刘海屏手机安卓O版本适配指导》
*/
public class HwNotchSizeUtil {private static final int FLAG_NOTCH_SUPPORT = 0x00010000;
/**
- 是否是刘海屏手机
- @param context Context
- @return true:刘海屏 false:非刘海屏
*/
public static boolean hasNotchInScreen(Context context) {
boolean ret = false;
try {
ClassLoader cl = context.getClassLoader();
Class HwNotchSizeUtil = cl.loadClass(“com.huawei.android.util.HwNotchSizeUtil”);
Method get = HwNotchSizeUtil.getMethod(“hasNotchInScreen”);
ret = (boolean) get.invoke(HwNotchSizeUtil);
} catch (Exception e) {
e.printStackTrace();
}
return ret;
}
/**
- 获取刘海尺寸
- @param context Context
- @return int[0]值为刘海宽度 int[1]值为刘海高度
*/
public static int[] getNotchSize(Context context) {
int[] ret = new int[]{0, 0};
try {
ClassLoader cl = context.getClassLoader();
Class HwNotchSizeUtil = cl.loadClass(“com.huawei.android.util.HwNotchSizeUtil”);
Method get = HwNotchSizeUtil.getMethod(“getNotchSize”);
ret = (int[]) get.invoke(HwNotchSizeUtil);
} catch (Exception e) {
e.printStackTrace();
}
return ret;
}
/**
- 设置应用窗口在华为刘海屏手机使用刘海区
- @param window Window
*/
public static void setFullScreenWindowLayoutInDisplayCutout(Window window) {
if (window == null) {
return;
}
WindowManager.LayoutParams layoutParams = window.getAttributes();
try {
Class layoutParamsExCls = Class.forName(“com.huawei.android.view.LayoutParamsEx”);
Constructor con = layoutParamsExCls.getConstructor(ViewGroup.LayoutParams.class);
Object layoutParamsExObj = con.newInstance(layoutParams);
Method method = layoutParamsExCls.getMethod(“addHwFlags”, int.class);
method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
- 设置应用窗口在华为刘海屏手机不使用刘海区显示
- @param window Window
*/
public static void setNotFullScreenWindowLayoutInDisplayCutout(Window window) {
if (window == null) {
return;
}
WindowManager.LayoutParams layoutParams = window.getAttributes();
try {
Class layoutParamsExCls = Class.forName(“com.huawei.android.view.LayoutParamsEx”);
Constructor con = layoutParamsExCls.getConstructor(ViewGroup.LayoutParams.class);
Object layoutParamsExObj = con.newInstance(layoutParams);
Method method = layoutParamsExCls.getMethod(“clearHwFlags”, int.class);
method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
/**
-
小米手机刘海屏适配
-
@author frank
-
@see 《小米刘海屏 Android O 适配》
-
@see 《小米刘海屏 Android P 适配》
*/
public class XiaomiNotchSizeUtil {private static final int FLAG_NOTCH_OPEN = 0x00000100;
private static final int FLAG_NOTCH_PORTRAIT = 0x00000200;
private static final int FLAG_NOTCH_LANDSCAPE = 0x00000400;/**
- 是否是刘海屏手机
- @param context Context
- @return true:刘海屏 false:非刘海屏
*/
public static boolean hasNotchInScreen(Context context) {
boolean ret = false;
try {
ret = “1”.equals(getSystemProperty(“ro.miui.notch”));
} catch (Exception e) {
e.printStackTrace();
}
return ret;
}
/**
- 获取刘海尺寸
- @param context Context
- @return int[0]值为刘海宽度 int[1]值为刘海高度
*/
public static int[] getNotchSize(Context context) {
int[] ret = new int[]{0, 0};
try {
int widthResId = context.getResources().getIdentifier(“notch_width”, “dimen”, “android”);
if (widthResId > 0) {
ret[0] = context.getResources().getDimensionPixelSize(widthResId);
}
int heightResId = context.getResources().getIdentifier(“notch_height”, “dimen”, “android”);
if (heightResId > 0) {
ret[1] = context.getResources().getDimensionPixelSize(heightResId);
}
} catch (Exception e) {
e.printStackTrace();
}
return ret;
}
/**
- 横竖屏都绘制到耳朵区
- @param window Window
*/
public static void setFullScreenWindowLayoutInDisplayCutout(Window window) {
if (window == null) {
return;
}
try {
Method method = Window.class.getMethod(“addExtraFlags”,
int.class);
method.invoke(window, FLAG_NOTCH_OPEN | FLAG_NOTCH_PORTRAIT | FLAG_NOTCH_LANDSCAPE);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
- 横竖屏都不会绘制到耳朵区
- @param window Window
*/
public static void setNotFullScreenWindowLayoutInDisplayCutout(Window window) {
if (window == null) {
return;
}
try {
Method method = Window.class.getMethod(“clearExtraFlags”,
int.class);
method.invoke(window, FLAG_NOTCH_OPEN | FLAG_NOTCH_PORTRAIT | FLAG_NOTCH_LANDSCAPE);
} catch (Exception e) {
e.printStackTrace();
}
}
private static String getSystemProperty(String key) {
String ret = null;
BufferedReader bufferedReader = null;
try {
Process process = Runtime.getRuntime().exec("getprop " + key);
bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
StringBuilder stringBuilder = new StringBuilder();
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
}
ret = stringBuilder.toString();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return ret;
}
}
复制代码
/**
-
OPPO手机刘海屏适配
-
@author frank
-
@see 《OPPO凹形屏适配说明》
*/
public class OppoNotchSizeUtil {/**
- 是否是刘海屏手机
- @param context Context
- @return true:刘海屏 false:非刘海屏
*/
public static boolean hasNotchInScreen(Context context) {
return context.getPackageManager().hasSystemFeature(“com.oppo.feature.screen.heteromorphism”);
}
}
复制代码
/**
-
VIVO手机刘海屏适配
-
@author frank
-
@see 《异形屏应用适配指南》
*/
public class VivoNotchSizeUtil {private static final int MASK_NOTCH_IN_SCREEN = 0x00000020;
private static final int MASK_ROUNDED_IN_SCREEN = 0x00000008;/**
- 是否是刘海屏手机
- @param context Context
- @return true:刘海屏 false:非刘海屏
*/
public static boolean hasNotchInScreen(Context context) {
boolean ret = false;
try {
ClassLoader cl = context.getClassLoader();
Class FtFeature = cl.loadClass(“android.util.FtFeature”);
Method get = FtFeature.getMethod(“isFeatureSupport”, int.class);
ret = (boolean) get.invoke(FtFeature, MASK_NOTCH_IN_SCREEN);
} catch (Exception e) {
e.printStackTrace();
}
return ret;
}
}
复制代码
/**
-
锤子手机刘海屏适配
-
@author frank
-
@see 《Smartisan 开发者文档》
*/
public class SmartisanNotchSizeUtil {private static final int MASK_NOTCH_IN_SCREEN = 0x00000001;
/**
- 是否是刘海屏手机
- @param context Context
- @return true:异形屏 false:非异形屏
*/
public static boolean hasNotchInScreen(Context context) {
boolean ret = false;
try {
ClassLoader cl = context.getClassLoader();
Class DisplayUtilsSmt = cl.loadClass(“smartisanos.api.DisplayUtilsSmt”);
Method get = DisplayUtilsSmt.getMethod(“isFeatureSupport”, int.class);
ret = (boolean) get.invoke(DisplayUtilsSmt, MASK_NOTCH_IN_SCREEN);
} catch (Exception e) {
e.printStackTrace();
}
return ret;
}
}
复制代码
通过相机或相册选择一张图片
findViewById(R.id.chooseImg).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType(“image/*”);
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, REQUEST_IMAGE_GET);
}
}
});
findViewById(R.id.takePicture).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
File photoFile = null;
try {
photoFile = createImageFile();
} catch (IOException ex) {
ex.printStackTrace();
}
if (photoFile != null) {
Uri photoURI = FileProvider.getUriForFile(MainActivity.this,
“com.your.package.fileprovider”,
photoFile);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
}
}
}
});
…
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_IMAGE_GET && resultCode == RESULT_OK) {
Uri fullPhotoUri = data.getData();
ParcelFileDescriptor descriptor;
try {
descriptor = getContentResolver().openFileDescriptor(fullPhotoUri, “r”);
FileDescriptor fd = descriptor.getFileDescriptor();
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd);
imageView.setImageBitmap(bitmap);
processImage();
} catch (Exception e) {
e.printStackTrace();
}
} else if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
int targetW = imageView.getWidth();
int targetH = imageView.getHeight();
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(currentPhotoPath, bmOptions);
int photoW = bmOptions.outWidth;
int photoH = bmOptions.outHeight;
int scaleFactor = Math.min(photoW / targetW, photoH / targetH);
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = scaleFactor;
Bitmap bitmap = BitmapFactory.decodeFile(currentPhotoPath, bmOptions);
imageView.setImageBitmap(bitmap);
processImage();
}
}
private File createImageFile() throws IOException {
String timeStamp = new SimpleDateFormat(“yyyyMMdd_HHmmss”, Locale.US).format(new Date());
String imageFileName = “JPEG_” + timeStamp + “_”;
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(imageFileName, “.jpg”, storageDir);
currentPhotoPath = image.getAbsolutePath();
return image;
}
复制代码
来源:CSDN
作者:杀姐姐不是姐
链接:https://blog.csdn.net/zhanglei892721/article/details/103454910