android Q launcher 数据加载流程

本文详细介绍了AndroidQ和P版本中Launcher的结构变化,LoaderTask如何运行加载工作空间,包括加载默认最爱、不同场景下的默认布局加载路径以及图标和应用列表的加载过程。同时,文章还涉及数据库操作,如默认配置的加载条件、数据库的插入时机以及数据库中的数据组织和排序。

时间:2020/08/24
之前公司不允许csdn,笔记写在其它地方。最近整理过来
下一篇:launcher数据加载(二)

前言

androidQ和androidP上Launcher结构有很大区别。

LoaderTask从LauncherModel中分离出来
LoaderTask也没有直接取调用LauncherProvider,而是通过LauncherSetting.call
LauncherSetting:工具类,保存Launcher中用到的一些静态值,类似于key值。

加载流程

LoaderTask->run()

1、loadworkspace

//loading default favorites
通过call方法调用到LauncherProvider对应的方法(和之前的流程一样,调用方式稍微有点区别)

Log.d(TAG, "loadWorkspace: loading default favorites");
LauncherSettings.Settings.call(contentResolver,
        LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES);
//LauncherProvider中重写call方法,根据传入的string调用到loadDefaultFavoritesIfNecessary(P上是launchermodel直接调用该方法,区别不是很大)
2、loadDefaultFavoritesIfNecessary
//loadDefaultFavoritesIfNecessary里根据条件,从不同的路径加载默认布局文件
//执行条件
getFlagEmptyDbCreated
1AutoInstallsLayout 
launcher3.layout.provider    根据key获取系统provider
2AutoInstallsLayout
android.autoinstalls.config.action.PLAY_AUTO_INSTALL
//Google PAI的在线布局就是这里获取的布局资源文件。
3、partner_default_layout
com.android.launcher3.action.PARTNER_CUSTOMIZATION    根据action获取应用资源,加载资源
//默认是指向gms包里的vendor/partner_gms/apps/GmsSampleIntegration
4、device_profiles.xml中读取配置,defaultLayoutId的值,例如default_workspace_3x3    apk本地资源
//创建数据库
mOpenHelper.createEmptyDB
//解析加载布局文件,默认配置插入数据库
mOpenHelper.loadFavorites-->parseLayout-->parseAndAddNode-->addShortcut-->insertAndCheck-->dbInsertAndCheck(launcher.db   favorites table )
//最后
clearFlagEmptyDbCreated
2.1、加载默认配置的条件
//执行条件的标志位初始化
LoaderTask.loadWorkspace-->GridSizeMigrationTask.migrateGridIfNeeded-->Settings.call(
        context.getContentResolver(), Settings.METHOD_NEW_TRANSACTION)-->LauncherProvider.createDbIfNotExists-->DatabaseHelper.<init>-->getReadableDatabase()
//getReadableDatabase时,底层调用到DatabaseHelper.onCreate
DatabaseHelper.onCreate-->DatabaseHelper.onEmptyDbCreated-->LauncherProvider.setFlagEmptyDbCreated
2.2、默认配置路径说明
ED6109G项目默认配置:
2AutoInstallsLayout
android.autoinstalls.config.action.PLAY_AUTO_INSTALL
//Google PAI的在线布局就是这里获取的布局资源文件。
需要内置PlayAutoInstallStub底包,刷机oobe界面联网登录Google账号,会根据Android Device Config Portal (ADCP)后台配置下载完整的包PlayAutoInstallConfig,并自动下载配置的应用,使用布局文件。
ADCP账号一般掌握在甲方爸爸手里,我们提供完整的apk包和配置说明,让甲方爸爸帮忙配置。

3、partner_default_layout.xml
按照代码定位到配置在:com.google.android.gmsintegration(代码打印包名)
apk位置:/product/app/GmsSampleIntegration/GmsSampleIntegration.apk
代码位置:vendor\partner_gms\apps\GmsSampleIntegration\res_dhs_full\xml

修改默认配置后,需要重新生成GmsSampleIntegration.apk安装验证默认布局

之后回到loadworkspace方法中,查询数据库–>加载数据库里的布局/加载默认布局(首次启动数据库只有默认布局的数据,所以效果看上去只加载了默认布局)

3、图标加载

step 1.1: loading workspace

//loadworkspace中,上面读取到数据库内容后,对内容进行解析,并获取app info,如title 和icon
else if (c.itemType ==
        LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {//item类型
    info = c.getAppShortcutInfo(//构造iteminfo的入口
            intent, allowMissingTarget, useLowResIcon);
//默认配置,获取图标的调用堆栈
at com.android.launcher3.IconProvider.getIcon(IconProvider.java:27)
at com.sprd.ext.dynamicicon.DynamicIconProvider.getIcon(DynamicIconProvider.java:36)
at com.android.launcher3.icons.IconCache.getFullResIcon(IconCache.java:246)
at com.android.launcher3.icons.IconCache.getFullResIcon(IconCache.java:242)
at com.android.launcher3.icons.LauncherActivtiyCachingLogic.loadIcon(LauncherActivtiyCachingLogic.java:52)
at com.android.launcher3.icons.LauncherActivtiyCachingLogic.loadIcon(LauncherActivtiyCachingLogic.java:25)
at com.android.launcher3.icons.cache.BaseIconCache.cacheLocked(BaseIconCache.java:340)
at com.android.launcher3.icons.cache.BaseIconCache.cacheLocked(BaseIconCache.java:312)
at com.android.launcher3.icons.IconCache.getTitleAndIcon(IconCache.java:211)
at com.android.launcher3.icons.IconCache.getTitleAndIcon(IconCache.java:175)
at com.android.launcher3.model.LoaderCursor.getAppShortcutInfo(LoaderCursor.java:275)
at com.android.launcher3.model.LoaderTask.loadWorkspace(LoaderTask.java:478)
at com.android.launcher3.model.LoaderTask.run(LoaderTask.java:179)

//info是LauncherActivityInfo,调用这个接口获取图标
info.getIcon(iconDpi)//这里获取到的就是已经变形的图了
//launcher中另外一种获取图标的方式,上面的方式其实底层也是调用的这个方式
resources.getDrawableForDensity(id, mFillResIconDpi),

step 2.1: loading all apps

//loadallapps
//查询手机安装的所有应用
// Query for the set of apps
final List<LauncherActivityInfo> apps = mLauncherApps.getActivityList(null, user);
//最终调用到底层,查询main launcher的activitys
com/android/server/pm/LauncherAppsService.java
ParceledListSlice<ResolveInfo> launcherActivities = queryActivitiesForUser(
        callingPackage,
        new Intent(Intent.ACTION_MAIN)
                .addCategory(Intent.CATEGORY_LAUNCHER)
                .setPackage(packageName),
        user);
com/android/server/pm/PackageManagerService.java
queryIntentActivitiesInternal();

//图标获取和上面加载默认配置图标差不多,只是触发的地方不同而已
at com.android.launcher3.IconProvider.getIcon(IconProvider.java:27)
at com.sprd.ext.dynamicicon.DynamicIconProvider.getIcon(DynamicIconProvider.java:36)
at com.android.launcher3.icons.IconCache.getFullResIcon(IconCache.java:246)
at com.android.launcher3.icons.IconCache.getFullResIcon(IconCache.java:242)
at com.android.launcher3.icons.LauncherActivtiyCachingLogic.loadIcon(LauncherActivtiyCachingLogic.java:52)
at com.android.launcher3.icons.LauncherActivtiyCachingLogic.loadIcon(LauncherActivtiyCachingLogic.java:25)
at com.android.launcher3.icons.cache.BaseIconCache.cacheLocked(BaseIconCache.java:340)
at com.android.launcher3.icons.cache.BaseIconCache.cacheLocked(BaseIconCache.java:312)
at com.android.launcher3.icons.IconCache.getTitleAndIcon(IconCache.java:211)
at com.android.launcher3.icons.IconCache.getTitleAndIcon(IconCache.java:175)
at com.android.launcher3.AllAppsList.add(AllAppsList.java:83)
at com.android.launcher3.model.LoaderTask.loadAllApps(LoaderTask.java:834)
at com.android.launcher3.model.LoaderTask.run(LoaderTask.java:196)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.os.HandlerThread.run(HandlerThread.java:67)

step 3.1: loading deep shortcuts

ShortcutManager.queryForAllShortcuts(user);
//根据不同的mk导入不同的DeepShortcutManager.java
public List<ShortcutInfo> queryForAllShortcuts(UserHandle user) {
    return query(FLAG_GET_ALL, null, null, null, user);
}
//调用到getShortcuts
LauncherApps.getShortcuts(q, user);
↓
mService.getShortcuts(mContext.getPackageName(),
        query.mChangedSince, query.mPackage, query.mShortcutIds, query.mActivity,
        query.mQueryFlags, user)
...
//具体查询方式后面跟一下
...

step 4.1: loading widgets

List<ComponentWithLabel> allWidgetsList = mBgDataModel.widgetsModel.update(mApp, null);//这里会调用到插入数据库,见下方 插入时机2

//根据不同的mk走到不同的WidgetsModel.java
//具体的数据加载方法
// Widgets
AppWidgetManagerCompat widgetManager = AppWidgetManagerCompat.getInstance(context);
for (AppWidgetProviderInfo widgetInfo : widgetManager.getAllProviders(packageUser)) {
    LauncherAppWidgetProviderInfo launcherWidgetInfo =
            LauncherAppWidgetProviderInfo.fromProviderInfo(context, widgetInfo);
    widgetsAndShortcuts.add(new WidgetItem(
            launcherWidgetInfo, idp, app.getIconCache()));
    updatedItems.add(launcherWidgetInfo);
}
// Shortcuts
for (ShortcutConfigActivityInfo info : LauncherAppsCompat.getInstance(context)
        .getCustomShortcutActivityList(packageUser)) {
    widgetsAndShortcuts.add(new WidgetItem(info, app.getIconCache(), pm));
    updatedItems.add(info);
}
setWidgetsAndShortcuts(widgetsAndShortcuts, app, packageUser);

//插入数据库的时机
插入时机2、
//插入数据包类名都是包名,表示拥有widgets的应用的数据,数据如下:
rowid componentName profileId lastUpdated version icon icon_color label system_state
12 com.google.android.calendar/com.google.android.calendar. 0 1599004063000 2016416004 [BLOB_DATA] -13475377 日历 zh-Hans-CN,en-US,29

at com.android.launcher3.icons.cache.BaseIconCache.addIconToDB(BaseIconCache.java:286)
at com.android.launcher3.icons.cache.BaseIconCache.getEntryForPackageLocked(BaseIconCache.java:450)
at com.android.launcher3.icons.IconCache.getTitleAndIconForApp(IconCache.java:219)
at com.android.launcher3.model.WidgetsModel.setWidgetsAndShortcuts(WidgetsModel.java:216)
at com.android.launcher3.model.WidgetsModel.update(WidgetsModel.java:111)
at com.android.launcher3.model.LoaderTask.run(LoaderTask.java:227)

step 4.3: Update icon cache
插入时机1、3、
//插入时机1、
//插入时机3、

at com.android.launcher3.icons.cache.BaseIconCache.addIconToDB(BaseIconCache.java:286)
at com.android.launcher3.icons.cache.BaseIconCache.addIconToDBAndMemCache(BaseIconCache.java:273)
at com.android.launcher3.icons.cache.IconCacheUpdateHandler$SerializedIconUpdateTask.run(IconCacheUpdateHandler.java:283)
IconCacheUpdateHandler.updateIconsPerUser
IconCacheUpdateHandler.updateIcons

//触发点LoaderTask.run中updateIcons
updateHandler.updateIcons(allActivityList,
        new LauncherActivtiyCachingLogic(mApp.getIconCache()),
        mApp.getModel()::onPackageIconsUpdated);//插入所有安装应用的数据

updateHandler.updateIcons(allWidgetsList, new ComponentCachingLogic(mApp.getContext()),
        mApp.getModel()::onWidgetLabelsUpdated);//插入数据的是widgets
4、数据库信息解读

launcher.db插入的是workspace和host上的数据(图标、文件夹、widget )
app_icons.db插入的是应用安装信息和widgets列表信息
widgetpreviews.db插入的widgets列表里的widgets信息,包含预览图

app_icons.db中
//deepshortcut没有数据插入数据库

//为啥时机2的数据在数据库前面呢?

//因为是直接操作数据库进行插入,而时机1、3调用updateHandler.updateIcons,其核心是起一个Task,mIconCache.mWorkerHandler.postAtTime
//和LoaderTask共用一个looper,加载流程走完,才会真正的run起来

//插入时,采用pop的方式一次写入一个数据,如果还有数据则把自己post到队列。
//所以数据库的数据格式,应用、widget、应用、widget...依次循环,直到某一方完全没有数据,另一方一直写到数据结束
//最终数据库的数据从上往下

//支持widget的app
//安装APP信息
//widget信息
//安装APP信息
//widget信息
5、allapps列表界面排序
mResults.bindAllApps();-->launcher.bindAllApplications-->AllAppsStore.setApps-->AllAppsStore.addOrUpdateApps-->AllAppsStore.notifyUpdate-->AlphabeticalAppsList.onAppsUpdated
onAppsUpdated这里面处理排序
<think> 我们已经在代码中动态请求了相机和存储权限,但根据用户反馈,在AndroidManifest.xml中已经声明了权限的情况下,仍然提示需要权限。这通常是因为Android 6.0(API级别23)及以上版本需要动态请求危险权限。不过,我们已经在MainActivity中实现了动态权限请求。如果仍然出现问题,可能是以下原因: 1. 在AndroidManifest.xml中声明的权限不正确或缺失。 2. 动态权限请求的代码没有在适当的时机执行。 3. 用户拒绝了权限,但应用没有正确处理拒绝情况。 4. 设备或模拟器的权限设置问题。 根据提供的AndroidManifest.xml片段,我们发现已经声明了相机和存储权限吗?实际上,在提供的AndroidManifest.xml片段中,并没有看到权限声明。因此,我们需要在<manifest>标签下添加权限声明。 ### 解决方案 #### 步骤1:在AndroidManifest.xml中添加权限声明 请确保在AndroidManifest.xml的<manifest>标签内添加以下权限声明: ```xml <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <!-- 相机权限 --> <uses-permission android:name="android.permission.CAMERA" /> <!-- 存储权限(对于Android 10以下) --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- 如果应用需要读取存储,则添加读取权限 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <application ...> ... </application> </manifest> ``` #### 步骤2:动态请求权限 在MainActivity中,我们已经实现了动态权限请求。但是,我们需要确保: - 在`onCreate`中调用`checkPermissions()`方法。 - 权限数组`REQUIRED_PERMISSIONS`包含所有需要动态请求的权限。 对于Android 10(API 29)及以上,如果应用的目标SDK版本>=29,则默认使用分区存储(Scoped Storage)。在这种情况下,我们不需要`WRITE_EXTERNAL_STORAGE`权限来保存图片到公共目录(如Pictures目录),因为我们可以使用MediaStore API。但是,为了兼容旧版本,我们仍然请求存储权限。 #### 步骤3:处理权限拒绝的情况 在`onRequestPermissionsResult`中,如果用户拒绝了权限,我们应该提示用户并可能再次请求权限,或者关闭应用。 ### 修改后的MainActivity权限处理部分(补充) 如果用户拒绝了权限,我们可以考虑显示一个说明,告诉用户为什么需要这些权限,并在用户确认后再次请求权限。以下是改进的代码: ```java private void checkPermissions() { if (!allPermissionsGranted()) { // 如果用户之前拒绝过权限,我们可以显示一个提示 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA) || ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { new AlertDialog.Builder(this) .setTitle("需要权限") .setMessage("相机和存储权限用于拍照和保存照片") .setPositiveButton("确定", (dialog, which) -> { ActivityCompat.requestPermissions( MainActivity.this, REQUIRED_PERMISSIONS, PERMISSION_REQUEST_CODE); }) .create() .show(); } else { // 直接请求权限 ActivityCompat.requestPermissions( this, REQUIRED_PERMISSIONS, PERMISSION_REQUEST_CODE); } } else { startCamera(); } } ``` ### 完整AndroidManifest.xml示例 下面是一个完整的AndroidManifest.xml示例,包括权限声明和必要的特性声明: ```xml <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.android.example.cameraappxjava"> <!-- 相机权限 --> <uses-permission android:name="android.permission.CAMERA" /> <!-- 存储权限(对于Android 10以下) --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- 对于Android 10及以上,如果目标版本>=29,可能需要添加READ_EXTERNAL_STORAGE来读取图片 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- 声明相机特性(可选) --> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" /> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.CameraAppXJava" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> ``` ### 注意事项 1. 对于Android 10及以上,如果应用的目标SDK版本>=29,并且希望保存文件到公共目录,建议使用MediaStore API,这样就不需要请求存储权限。但是,为了兼容旧版本,我们仍然请求存储权限。 2. 如果用户永久拒绝了权限(选择“不再询问”),那么再次调用`ActivityCompat.requestPermissions`将不会显示权限对话框。此时,我们应该引导用户到设置中手动开启权限。 ### 引导用户到设置 我们可以修改权限请求结果处理: ```java @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSION_REQUEST_CODE) { if (allPermissionsGranted()) { startCamera(); } else { // 检查是否有权限被永久拒绝 boolean anyPermissionPermanentlyDenied = false; for (String permission : REQUIRED_PERMISSIONS) { if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) { anyPermissionPermanentlyDenied = true; break; } } if (anyPermissionPermanentlyDenied) { // 显示一个对话框引导用户去设置 new AlertDialog.Builder(this) .setTitle("需要权限") .setMessage("您已永久拒绝相机或存储权限,请到设置中手动启用") .setPositiveButton("去设置", (dialog, which) -> { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Uri uri = Uri.fromParts("package", getPackageName(), null); intent.setData(uri); startActivity(intent); }) .setNegativeButton("取消", (dialog, which) -> finish()) .create() .show(); } else { Toast.makeText(this, "权限被拒绝", Toast.LENGTH_SHORT).show(); finish(); } } } } ``` ### 总结 请确保: 1. 在AndroidManifest.xml中正确声明权限。 2. 在MainActivity中实现动态权限请求,并处理用户拒绝的情况,包括引导用户到设置。 如果按照以上步骤,应该可以解决权限问题。 §§相关问题§§ 1. 如何在Android 11及以上版本中适配分区存储(Scoped Storage)? 2. 如何检测用户是否永久拒绝了权限并引导用户到应用设置? 3. 在Camera2 API中,如何处理多摄像头设备的切换? 4. 如何为相机应用添加闪光灯控制功能? 5. 在Camera2中如何实现连拍或视频录制功能? </think> 您的 `AndroidManifest.xml` 文件中缺失了必要的权限声明和特性声明,这是导致权限提示的根本原因。以下是完整的修复方案: ### 完整修复的 AndroidManifest.xml ```xml <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.android.example.cameraappxjava"> <!-- 必需权限声明 --> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Android 10+ 存储权限适配 --> <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" tools:ignore="OverridePermission" /> <!-- 硬件特性声明 --> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" /> <!-- 解决 Android 11+ 分区存储问题 --> <application android:requestLegacyExternalStorage="true" android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.CameraAppXJava" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true" android:screenOrientation="portrait"> <!-- 固定为竖屏简化处理 --> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <!-- Android 13+ 照片选择器支持 --> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> </application> </manifest> ``` ### 需要添加的配置文件 1. **创建 `file_paths.xml`** (在 `res/xml/file_paths.xml`): ```xml <?xml version="1.0" encoding="utf-8"?> <paths> <!-- Android 10+ 适配 --> <external-path name="external_files" path="." /> <external-files-path name="photos" path="Pictures" /> </paths> ``` 2. **创建 `backup_rules.xml`** (在 `res/xml/backup_rules.xml`): ```xml <?xml version="1.0" encoding="utf-8"?> <full-backup-content> <exclude domain="sharedpref" path="device_id.xml"/> </full-backup-content> ``` ### MainActivity 必需的权限处理增强 在您的 `MainActivity` 中添加以下关键修复: ```java // 在类顶部添加 private static final int PERMISSION_REQUEST_CODE = 101; // 在 onCreate 中添加权限检查 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textureView = findViewById(R.id.texture_view); // 权限检查 if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { requestPermissions( new String[]{ Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE }, PERMISSION_REQUEST_CODE ); } else { startCamera(); } } // 添加权限请求结果处理 @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSION_REQUEST_CODE) { boolean allGranted = true; for (int result : grantResults) { if (result != PackageManager.PERMISSION_GRANTED) { allGranted = false; break; } } if (allGranted) { startCamera(); } else { // 优雅处理权限拒绝 new AlertDialog.Builder(this) .setTitle("权限被拒绝") .setMessage("需要相机和存储权限才能使用拍照功能") .setPositiveButton("重新请求", (dialog, which) -> { requestPermissions(permissions, PERMISSION_REQUEST_CODE); }) .setNegativeButton("取消", (dialog, which) -> finish()) .show(); } } } ``` ### Android 10+ 存储适配关键代码 在 `saveImageToGallery` 方法中添加适配代码: ```java private void saveImageToGallery(Image image) { // ... [获取字节数据代码不变] ... // Android 10+ 兼容保存方式 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ContentResolver resolver = getContentResolver(); ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName); values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES); Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); if (uri != null) { try (OutputStream output = resolver.openOutputStream(uri)) { output.write(bytes); showToast("照片已保存到相册"); } catch (IOException e) { Log.e(TAG, "保存照片失败", e); showToast("保存失败: " + e.getMessage()); } } } else { // Android 9 及以下保存方式 File storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); // ... [原有保存代码] ... } } ``` ### 常见问题解决方案 1. **权限请求不弹出问题**: - 确保在 `AndroidManifest.xml` 中正确声明权限 - 检查设备是否启用了权限请求限制(某些厂商系统设置) - 在首次启动时请求权限,不要延迟 2. **Android 11+ 存储权限问题**: ```java // 在 Application 类中添加 public void onCreate() { super.onCreate(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { StorageManager sm = (StorageManager) getSystemService(STORAGE_SERVICE); if (!sm.isStorageEnabled()) { Intent intent = new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS); startActivity(intent); } } } ``` 3. **相机启动失败问题**: ```java private void openCamera() { try { // 添加超时处理 Handler mainHandler = new Handler(Looper.getMainLooper()); mainHandler.postDelayed(() -> { if (cameraDevice == null) { Toast.makeText(this, "相机启动超时", Toast.LENGTH_SHORT).show(); } }, 3000); // ... [原有打开相机代码] ... } ``` ### 最终检查清单 1. 确认 `AndroidManifest.xml` 包含所有必需权限 2. 添加 `file_paths.xml` 和 `backup_rules.xml` 3. 实现权限请求结果处理逻辑 4. 添加 Android 10+ 存储适配代码 5. 在设备上重置应用权限后测试完整流程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值