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

安卓手机移动端 - Android OS应用开发、Android Studio IDE编程技术


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)。

显式Intent:
  var explicitIntent = new Intent("com.example.action.APP_ACTION")
  explicitIntent.setPackage(context.getPackageName());
  context.startActivity(explicitIntent);

隐式Intent: 即使是内部组件,也要导出android:exported="true",否则报ActivityNotFoundException
  context.startActivity(new Intent("com.example.action.APP_ACTION"));

待定Intent:
  PendingIntent - 即达到特定条件才执行的Intent,通知、闹钟等。
  必须携带 PendingIntent.FLAG_IMMUTABLE(防修改) 或 FLAG_MUTABLE 标志。

服务:
  bindService(Intent)会跟随上下文消亡(用于AIDL),而startService(Intent)则会持续运行,可同时调用。

权限:
  普通权限直接xml声明即可;危险权限需要代码动态申请
  特殊权限则需要Intent请求 - SYSTEM_ALERT_WINDOW或WRITE_SETTINGS
  手动开启应用权限:设置》应用》权限》勾上麦克风等
  依赖库权限合并情况 - build\intermediates\manifest_merge_blame_file\
  删除第三方权限 - <uses-permission android:name="android.permission.X" tools:node="remove" />;谷歌gms和firebase库依赖的datatransport均引入了网络访问权限ACCESS_NETWORK_STATE。
  获取xml声明的权限列表(即<uses-permission />) - pm.getPackageInfo(pkg, PackageManager.GET_PERMISSIONS).requestedPermissions;
  授权框 - com.google.android.permissioncontroller/com.android.permissioncontroller.permission.ui.GrantPermissionsActivity

  重置app权限状态(首次和二次拒绝) - ./adb shell pm clear-permission-flags com.example.myapplication android.permission.CAMERA  user-set user-fixed

  说明 - AS IDE权限检查的抑制注解:@SuppressLint("MissingPermission")
    改RequestPermission为RequestMultiplePermissions可同时请求多个权限
    运行时权限 - Protection level: dangerous 标识了normal的安装时即获取
        https://developer.android.com/reference/android/Manifest.permission
    注意 - 需要动态授权的项目,比如TakePicturePreview不能直接launch(null),必须外层用RequestPermission包裹下。

  实例:
    说明 - Android 12、15 授权框必须后退才会dismiss(),Android 14 则失去焦点即触发dismiss()。
    ActivityResultCallback<Boolean> arc = isGranted -> {
        System.out.println("isGranted - " + isGranted);
        if (isGranted) { /* 做业务 */ } else {
            System.out.println("拒绝"); // dismiss()也会进入。
        }
    };
    ActivityResultLauncher<String> arl = registerForActivityResult(
            new ActivityResultContracts.RequestPermission(), arc);

    // [弹框检测方案]预热下授权框,避免设备重启后首次载入较慢;将未定义权限名请求加入onCreate:
    ActivityCompat.requestPermissions(this, new String[]{UUID.randomUUID().toString()}, Integer.MAX_VALUE);

    // 上方代码声明为本类全局,下方则放调用处。
    var p = android.Manifest.permission.CAMERA;
    if (ContextCompat.checkSelfPermission(this, p) == PackageManager.PERMISSION_GRANTED) {
        arc.onActivityResult(true); // 授权后代码归并至一处
    } else if (this.shouldShowRequestPermissionRationale(p)) {
        arl.launch(p); // First Time Deny
    } else {
        System.out.println("First Time Launch and Second Time Deny - else");
        arl.launch(android.Manifest.permission.CAMERA);
        new Handler().postDelayed(() -> { // 为true则指已拒绝2次不再弹框,为false则由授权框dismiss()触发。
            // or 回调快慢方案 time = System.currentTimeMillis(); (System.currentTimeMillis() - time)<400;
            if (notShownGrantPermissionsActivity(MainActivity.this)) {
              new AlertDialog.Builder(this).setMessage("[扫码]功能需要摄像头拍照"+"\n\uD83D\uDE4F[CAMERA]")
                .setPositiveButton("✅ 开始授权", (di, i) -> PermissionCommon.toAppSettings(MainActivity.this))
                .setNegativeButton("\uD83D\uDEAB 拒绝授权(功能受限)", (di, i) ->
                  Toast.makeText(this, "仅基本功能", Toast.LENGTH_SHORT).show()).show();
            }
        }, 250);
    }

    public static boolean notShownGrantPermissionsActivity(Activity activity) {
        boolean dialogIsHide = false;
        var activityManager = (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
        var infos = activityManager.getRunningTasks(Integer.MAX_VALUE);
        for (var info : infos) {
            if (info.id == activity.getTaskId()) {
                Log.d("TaskInfo", "Found task: " + info.topActivity);
                dialogIsHide = activity.getComponentName().equals(info.topActivity);
            }
        }
        return dialogIsHide;
    }

    try {
        var packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_PERMISSIONS);
        var ps = packageInfo.requestedPermissions; // 静态权限列表,非runtime权限。
        System.out.println("permissions - " + ps);
        if (ps != null) {
            for (var permission : ps) {
                var permissionInfo = pm.getPermissionInfo(permission, PackageManager.GET_META_DATA);
                System.out.println("permissionInfo - " + permissionInfo);
                // 获取 flag 值,可能需要反射
                int flags = permissionInfo.flags;
                // 根据 flags 值判断权限状态
                // PackageManager.java
                final int FLAG_PERMISSION_USER_SET = 1 << 0;
                final int FLAG_PERMISSION_USER_FIXED = 1 << 1;
                System.out.println("flags - " + flags);
                if ((flags & FLAG_PERMISSION_USER_FIXED) != 0) {
                    // 用户已明确拒绝权限
                    System.out.println("用户已明确拒绝权限");
                } else if ((flags & FLAG_PERMISSION_USER_SET) != 0) {
                    // 用户曾手动设置过该权限
                    System.out.println("用户曾手动设置过该权限");
                }
            }
        }
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace(System.err);
    }


全屏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
    protected void onNewIntent(@NonNull Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent); // 不set则仍为onCreate传入intent。
        System.out.println("onNewIntent");
    }

Android 13+ 支持每个应用有自己的语言设置窗口和API配置。

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 14+ 起每次必须弹框,且不能重用mp实例再次调createVirtualDisplay(...):
  // MediaProjection 对象未调用 stop() 则录像图标就会一直显示在系统状态栏。
  var mp = mpm.getMediaProjection(Activity.RESULT_OK, (Intent) data);
  mp.createVirtualDisplay("ScreenShot", ir.getWidth(), ir.getHeight(), getResources().getDisplayMetrics().densityDpi,
                        DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, ir.getSurface(), null, null);


                //  Android 14+ 必须注册该回调:
                mp.registerCallback(new MediaProjection.Callback() {
                    @Override
                    public void onStop() {
                        System.out.println("MediaProjection.Callback onStop");
                    }
                }, null);

其他

  android.os.NetworkOnMainThreadException解决:new Thread(Runnable).start();
  Only the original thread that created a view hierarchy can touch its views.解决:handler.post(Runnable);

  单文件+多文件分享:
            <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,接着登录小米账号,最后按提示操作。