从此
📄文章 #️⃣专题 🌐酷站 👨‍💻技术 📺 📱

安卓手机移动端 - Android OS应用开发、Android Studio IDE编程技术 | 通知、保活(闹钟/WorkManager)、音频

Android应用开发主专题 | 非技术性Android专题

工具

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

bundletool官方下载

  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,接着登录小米账号,最后按提示操作。