安卓手机移动端 - 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); var surface = imageReader.getSurface(); // 录屏则换用 mediaRecorder.getSurface(); var wh = wm.getMaximumWindowMetrics().getBounds(); mp.createVirtualDisplay("ScreenShot", wh.width(), wh.height(), 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("MediaProjection.Callback 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,接着登录小米账号,最后按提示操作。