安卓手机移动端 - Android OS应用开发、Android Studio IDE编程技术 | 通知、保活(闹钟/WorkManager)、音频、截图录屏
工具
Android Studio开发IDE官方下载(安装后空间占用10GiB+)
Android Debug Bridge(ADB/调试桥)官方下载
Android Gradle Plugin(AGP)版本列表
JADX - Android apk文件反编译工具(请合法使用)
Android官方apk/aab文件反编译GUI工具和命令行apkanalyzer
Java Keystores (.jks or .keystore):
说明 - 证书生成的参数相同,不代表生成后的证书,在全世界都是相同的,而是依照证书指纹来决定。
生成 debug.keystore 命令(命令参考/可选参数:-sigalg SHA256withRSA -keysize 2048 / -alias值大小写均可 )
keytool -genkey -alias AndroidDebugKey -keyalg RSA -dname "CN=Android Debug,O=Android,C=US" -validity 10950 -keypass android -keystore debug.keystore -storepass android
查看证书命令(输入密钥库口令: android /有效期默认为30年*365日=10950天)
C:\Progra~1\Java\jdk-22\bin\keytool -list -v -keystore C:\Users\person\.android\debug.keystore
或gradle查看signingConfigs配置块儿签名信息、证书指纹: ./gradlew signingReport
为Android程序apk文件签名(默认并存v1至当前SDK支持的所有版本签名,安卓OS校验时忽略不识别的,之后选用版本高者,可主动禁用;首选v4及支持轮替的v3 / --v4-signing-enabled only )
apksigner sign --ks debug.keystore --v1-signing-enabled false --v2-signing-enabled false app.apk
查看签名版本
apksigner verify --verbose app.apk
aab转apks并部署:参数--connected-device和--adb均为可选
java -jar E:\bundletool-all-1.15.5.jar build-apks --bundle=D:\projects\NewTileMatching.aab --output=D:\projects\NewTileMatching.aab.apks --connected-device
java -jar E:\bundletool-all-1.15.5.jar install-apks --apks=D:\projects\NewTileMatching.aab.apks --adb=C:\Users\aw\AppData\Local\Android\Sdk\platform-tools\adb.exe
综合
Android Studio在启用了Hyper-V组件时会走Windows Hypervisor Platform (WHPX),未启用则走Android Emulator Hypervisor Driver (AEHD)。
Android Activity默认intent-filter:
<intent-filter>
<!-- 未指定 Activity.class 时参与主入口启动候选; 多个则启动首个 -->
<action android:name="android.intent.action.MAIN"/>
<!-- 在多个Activity中定义则显示多个名为android:lable="程序名"的图标 -->
<category android:name="android.intent.category.LAUNCHER"/>
<!-- 是否启用隐式调用,未写action.MAIN也会参与隐式列表;action.MAIN已包含该项 -->
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
显式Intent:
var explicitIntent = new Intent("com.example.action.APP_ACTION")
explicitIntent.setPackage(context.getPackageName()); // 并不会填充 setComponent 的包名。
context.startActivity(explicitIntent);
隐式Intent:
导出属性未写则默认为 android:exported="false",若不设为true,无论外部还是自身的隐式调用,均报ActivityNotFoundException
context.startActivity(new Intent("com.example.action.APP_ACTION")); // 隐式调用
导出后,还可限制为同签名的外部应用 - android:prottectionLevel="signature",如果信任操作系统,可再放开点 signatureOrSystem
待定Intent:
PendingIntent - 即达到特定条件才执行的Intent,通知、闹钟等。
必须携带 PendingIntent.FLAG_IMMUTABLE(防修改) 或 FLAG_MUTABLE 标志。
var intent = this.getPackageManager().getLaunchIntentForPackage(getPackageName());
var pi = PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE);
感知应用:
查看所有应用安装包 - ./adb shell pm list packages
注意 - 首选queryIntentActivities;resolveActivity 若入参 intent.getComponent().getClassName()!=null,则不管应用存不存在,都会返回非null的原始intent,故只能用于隐式Intent。
var r = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
if (!r.isEmpty()) { startActivity(fIntent); } // resolveActivity
// 找到后会填充包名和类名,否则返回null。必须结合
if (intent.resolveActivity(c.getPackageManager()) != null) { c.startActivity(intent); }
// 或 异常捕获方式:
if (fIntent != null) {
try { startActivity(fIntent); } catch (ActivityNotFoundException ex) { ex.printStackTrace(System.err); }
}
服务:
bindService(Intent)会跟随上下文消亡(用于AIDL),而startService(Intent)则会持续运行,可同时调用。
Toast:
最多显示2行
应用处于前台时正常弹Toast;但处于后台时则无法弹Toast:
getApplicationContext().getMainExecutor().execute(() -> {
Toast.makeText(getApplicationContext(), "若处于后台则报(不影响继续执行) - Suppressing toast from package com.openle.v1app.visiontech by user request.", Toast.LENGTH_LONG).show();
});
通知:
后台Service虽然不能发Toast,但能发送通知,通过areNotificationsEnabled()判断是否已授权。
var intent = getApplicationContext().getPackageManager().getLaunchIntentForPackage(getApplicationContext().getPackageName());
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); // 该FLAG用于触发onNewIntent
var n = new Notification.Builder(getApplicationContext(), nc.getId())
// setSmallIcon未设置则报"Invalid notification";若为前台通知则会重写setContentIntent为App Info窗口。
.setSmallIcon(android.R.drawable.stat_notify_chat)
.setContentText("All the best.") // .setContentTitle("通知标题")
.setContentIntent(PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE))
.setAutoCancel(true) // 点击后自动移除;若为前台通知则不许移除,或构造个 PendingIntent.getService(...) 来 stopSelf(startId)。
.build();
NotificationManagerCompat.from(this).notify(1, n);
//startForeground(CommonIdOrCodes.DEFAULT_ID, n);
保活:
“UI不可见”后会处于后台,但并不代表“移出Recent”;“移出Recent”也并不会执行“强行停止”;重启也会恢复“Recent”状态。
“强行停止”应用后,WorkManager、闹钟和广播均会全部失效。
WorkManager - 设备重启依然存在;适合用途:定期发送已收集的日志、按活动更换图标角标。
WorkManager:
static PeriodicWorkRequest req = new PeriodicWorkRequest.Builder(ScheduleWorker.class,
7, TimeUnit.DAYS) // 周期执行间隔,执行时间会优化合并,但会保持在15分钟内。
.setInitialDelay(16, TimeUnit.MINUTES) // 若为KEEP则只在首次入队时延迟执行,不设则立即执行。
// 延迟值小于15分钟时,应用若被“移出Recent”,则积压至下次进入Recent后触发,跟Activity可见不可见无关。
.build(); // 一次性任务则用 OneTimeWorkRequest.from(NewWorker.class);
WorkManager.getInstance(c.getApplicationContext()).enqueueUniquePeriodicWork("work", ExistingPeriodicWorkPolicy.KEEP, req);
查看进程ID: ./adb shell dumpsys meminfo com.openle.v1app.visiontech.fullchannel | findstr pid
或 查看IDE的Logcat中4910-5025横杠前数字。
闹钟:
非精确AlarmManager可以做到固定日子触发(最迟1小时),而WorkManager无法精确到日子,间隔过段又太耗电。
Android 13+ 不上市场用默认就授权的 ,上Play市场则用需审核的 SCHEDULE_EXACT_ALARM。
Android 11(API Level 31)、12 才开始支持声明 SCHEDULE_EXACT_ALARM 权限,低版本则免权限使用精确闹钟。
查看已设置闹钟列表(重点看pending alarms段/为了隐私未提供查看闹钟列表的API):./adb shell dumpsys alarm
说明 - 闹钟必须通过广播或Service来间接触发通知;PendingIntent的requestCode相同则覆盖;设备重启则会清空闹钟,故应通过WorkManager等措施来保活下。
var c = Calendar.getInstance(); c.setTimeInMillis(System.currentTimeMillis()); c.set(Calendar.HOUR_OF_DAY, 14);
var intent = new Intent(context, AlarmReceiver.class);
var pi = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE); // 必须静态注册!
var am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
am.setInexactRepeating(AlarmManager.RTC_WAKEUP, c.getTimeInMillis(), AlarmManager.INTERVAL_DAY, pi);
换图标:
pm.setComponentEnabledSetting(new ComponentName("pkg","pkg.ActivityAlias"), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
全屏Intent:
if (notificationManager.canUseFullScreenIntent()) {
notificationManager.notify(5, notificationBuilder.build())
} else {
// Android 14+ 打开全屏Intent设置页
val intent = Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT)
intent.data = Uri.fromParts("package", requireActivity().packageName, null)
startActivity(intent)
}
传参:intent.putExtra("intentObj", intentAsParcelable); // 会隐式转换为Parcelable类型。
首次触发用onCreate,回发则用:
@Override // 用了 intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) 方触发!?
protected void onNewIntent(@NonNull Intent intent) {
super.onNewIntent(intent);
setIntent(intent); // 不set则仍为onCreate传入intent。
System.out.println("onNewIntent");
}
Android 13+ 支持每个应用有自己的语言设置窗口和API配置。
键盘:
隐藏键盘 - edit.setInputType(InputType.TYPE_NULL);
键盘遮盖输入框解决(仅WebView存在?):在AndroidManifest.xml中的activity添加android:windowSoftInputMode=”adjustPan”
切换键盘显示状态:
android:windowSoftInputMode="stateAlwaysHidden"
【or编程方式】onCreate中使用最好做500毫秒延迟
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);
WebView:
wv.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
wv.loadUrl(url); return true; // 避免跳至外部浏览器。
}
});
动态添加至约束布局:
var lp = new ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.WRAP_CONTENT);
lp.topToBottom = findViewById(R.id.textViewAppInfo).getId();
lp.startToStart = ConstraintLayout.LayoutParams.MATCH_PARENT;
lp.endToEnd = ConstraintLayout.LayoutParams.MATCH_PARENT;
var wv = new WebView(this); wv.setLayoutParams(lp);
var vg = (ViewGroup) getWindow().getDecorView().getRootView().findViewById(R.id.main);
System.out.println(vg); vg.addView(wv);
网络:
Cronet - Android Chromium HttpClient 库
前台应用 2 分钟内只能使用 4 次startScan(),关闭节流限制,Android 9+ -> 开发者选项 > 网络 > WLAN 扫描调节:❎(默认✔即受限)。
Wi-Fi Aware 感知 - WIFI设备间服务发现;测试APP https://play.google.com/store/apps/details?id=com.google.android.apps.location.rtt.wifinanscan
Wi-Fi RTT -
WIFI设备间测距(Fine Time Measurement, FTM);Android 9+ RTT(Round-Trip-Time);Android 15(API 35)引入了对 IEEE 802.11az 非触发器 (NTB) 测距的支持。
必须开启位置定位: Permission violation - startScan not allowed for ... SecurityException: Location mode is disabled for the device
对方WIFI或热点必须支持802.11mc - scanResult.is80211mcResponder()
测试APP - https://play.google.com/store/apps/details?id=com.google.android.apps.location.rtt.wifirttscan
上传房型图WIFI测距 - https://play.google.com/store/apps/details?id=com.google.android.apps.location.rtt.wifirttlocator
WIFI连接adb:
扫码字样为 WIFI:T:ADB;S:studio-UTHN-m^l5P;P:*r!2Sr>-Mq>!;;
组件:
Android Activity 任务栈启动模式(Launch Mode)
Android Studio IDEA Marketplace 插件:
列出任务栈和Activity顺序 - https://plugins.jetbrains.com/plugin/22717-android-activity-back-stack-viewer
AdMob for Android Plugin:
// AdMob task processFullDebugGoogleServices: No matching client found for package name
afterEvaluate {
project.tasks.forEach { task ->
if (task.name.startsWith("processFull")
&& task.name.endsWith("GoogleServices")
) {
println("Skip " + task.name + " task.")
task.onlyIf { return@onlyIf false }
}
}
}
音频
播放系统声音:
var r = RingtoneManager.getRingtone(this, RingtoneManager.getActualDefaultRingtoneUri(this,RingtoneManager.TYPE_NOTIFICATION));
r.setLooping(false); r.play(); // getActualDefaultRingtoneUri 已取代 getDefaultUri
播放音频文件:
// 常量FLAG_AUDIBILITY_ENFORCED使声音高分贝;不支持进度
var aa = new AudioAttributes.Builder().setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED).build();
// Android 12 以前为 "/system/media/audio/ui/camera_click.ogg"
new AsyncPlayer(null).play(this, Uri.parse("/product/media/audio/ui/camera_click.ogg"), false, aa);
截图录屏投影
Android 15+ 不再允许通过BOOT_COMPLETED调起。
Android 14+ 起每次必须弹框,且不能重用mp实例再次调createVirtualDisplay(...):
// MediaProjection 对象未调用 stop() 则录像图标就会一直显示在系统状态栏。
var mp = mpm.getMediaProjection(Activity.RESULT_OK, (Intent) data);
// 接图用“有变动才触发”的 ir.setOnImageAvailableListener(...) + ir.acquireLatestImage(),或单张接图的 mHandler.postDelayed(延迟时间) + ir.acquireLatestImage()
// 其他 - 首选只取最后一帧的 acquireLatestImage(),若想依序处理方用 acquireNextImage(),但消费慢了会丢弃新帧;maxImages 指从 ImageReader 队列中同时获取但未调用 Image.close() 方法对象的最大数量。
var surface = imageReader.getSurface(); // 录屏则换用 mediaRecorder.getSurface();
var wh = wm.getMaximumWindowMetrics().getBounds();
mp.createVirtualDisplay("ScreenShot", wh.width(), wh.height(), // 宽高必须大于0。
getResources().getDisplayMetrics().densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, surface, null, null);
// Android 14+ 必须注册该回调:
mp.registerCallback(new MediaProjection.Callback() {
@Override public void onStop() { System.out.println("MP onStop"); }
}, null);
录屏+系统内置录音 - AudioRecord.Builder().setAudioPlaybackCaptureConfig(config)AudioPlaybackCaptureConfiguration.Builder(mediaProjection) .addMatchingUsage(AudioAttributes.USAGE_MEDIA).build()
其他
android.os.NetworkOnMainThreadException解决:new Thread(Runnable).start();
Only the original thread that created a view hierarchy can touch its views.解决:handler.post(Runnable);
Android Library 库项目发布 build.gradle.kts 写法 - https://developer.android.com/build/publish-library/upload-library?hl=zh-cn
部分文字加超链接:
var ss = new SpannableString("为文字设置超链接");
var us = new URLSpan("http://example.com/?x"); // 图片用ImageSpan
ss.setSpan(urlSpan, 5, ss.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setHighlightColor(Color.parseColor("#36969696"));
textView.setText(ss);
单文件+多文件分享:
<intent-filter>
<action android:name="android.intent.action.SEND" /> intent.getParcelableExtra(Intent.EXTRA_STREAM);
<action android:name="android.intent.action.SEND_MULTIPLE" /> intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
Android衍生:
鸿蒙(HarmonyOS/OpenHarmony)应用开发工具 - HUAWEI DevEco Studio
疑虑app列表:
MIUI优化、反诈组件?(com.miui.guardprovider)
https://www.v2ex.com/t/982068#reply9
卸载重装 adb shell cmd package install-existing com.miui.guardprovider
小米、红米手机设备解锁:https://www.miui.com/unlock/
首先下载PC版“解锁工具”,然后连电脑USB,接着登录小米账号,最后按提示操作。