1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.healthconnect.exportimport; 18 19 import android.content.ContentProviderClient; 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.PackageManager; 24 import android.content.pm.ProviderInfo; 25 import android.content.pm.ResolveInfo; 26 import android.database.Cursor; 27 import android.health.connect.exportimport.ExportImportDocumentProvider; 28 import android.net.Uri; 29 import android.os.DeadObjectException; 30 import android.provider.DocumentsContract; 31 32 import java.util.ArrayList; 33 import java.util.List; 34 35 /** 36 * Manages querying the document providers available to use for export/import. 37 * 38 * @hide 39 */ 40 public final class DocumentProvidersManager { 41 private static final String REQUIRED_MIME_TYPE = "application/zip"; 42 43 /** Returns the document providers available to be used for export/import. */ queryDocumentProviders(Context context)44 public static List<ExportImportDocumentProvider> queryDocumentProviders(Context context) { 45 ArrayList<ExportImportDocumentProvider> documentProviders = new ArrayList<>(); 46 47 PackageManager packageManager = context.getPackageManager(); 48 ContentResolver contentResolver = context.getContentResolver(); 49 50 Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE); 51 List<ResolveInfo> providers = 52 packageManager.queryIntentContentProviders(intent, /* flags= */ 0); 53 for (ResolveInfo info : providers) { 54 ProviderInfo providerInfo = info.providerInfo; 55 String authority = providerInfo.authority; 56 if (authority != null) { 57 readDocumentProviders( 58 documentProviders, contentResolver, authority, /* attempt= */ 0); 59 } 60 } 61 62 return documentProviders; 63 } 64 readDocumentProviders( ArrayList<ExportImportDocumentProvider> documentProviders, ContentResolver contentResolver, String authority, int attempt)65 private static void readDocumentProviders( 66 ArrayList<ExportImportDocumentProvider> documentProviders, 67 ContentResolver contentResolver, 68 String authority, 69 int attempt) { 70 try (ContentProviderClient contentProviderClient = 71 contentResolver.acquireUnstableContentProviderClient(authority)) { 72 if (contentProviderClient != null) { 73 Uri rootsUri = DocumentsContract.buildRootsUri(authority); 74 try (Cursor cursor = 75 contentProviderClient.query(rootsUri, null, null, null, null)) { 76 if (cursor != null) { 77 readDocumentProvidersFromCursor(documentProviders, cursor, authority); 78 } 79 } catch (DeadObjectException e) { 80 if (attempt == 0) { 81 // The system can return a content provider that's gone away. Acquiring the 82 // content provider after the DeadObjectException will try to restart the 83 // content provider. 84 readDocumentProviders( 85 documentProviders, contentResolver, authority, /* attempt= */ 1); 86 } 87 // Ignore exception on second attempt so successful document provider queries 88 // are returned. 89 } catch (Exception ignored) { 90 // Ignore exception so successful document provider queries are returned. 91 } 92 } 93 } 94 } 95 readDocumentProvidersFromCursor( ArrayList<ExportImportDocumentProvider> documentProviders, Cursor cursor, String authority)96 private static void readDocumentProvidersFromCursor( 97 ArrayList<ExportImportDocumentProvider> documentProviders, 98 Cursor cursor, 99 String authority) { 100 while (cursor.moveToNext()) { 101 if (!isDocumentProviderSupported(cursor)) { 102 continue; 103 } 104 105 int titleIndex = cursor.getColumnIndex(DocumentsContract.Root.COLUMN_TITLE); 106 int summaryIndex = cursor.getColumnIndex(DocumentsContract.Root.COLUMN_SUMMARY); 107 int iconResourceIndex = cursor.getColumnIndex(DocumentsContract.Root.COLUMN_ICON); 108 int rootDocumentIndex = cursor.getColumnIndex(DocumentsContract.Root.COLUMN_ROOT_ID); 109 110 if (titleIndex == -1 || iconResourceIndex == -1 || rootDocumentIndex == -1) { 111 // These columns are required but this isn't enforced. Skip document providers that 112 // don't follow these requirements. 113 continue; 114 } 115 116 String title = cursor.getString(titleIndex); 117 String summary = summaryIndex != -1 ? cursor.getString(summaryIndex) : ""; 118 // Returned summary can be null even though the column is present. 119 if (summary == null) { 120 summary = ""; 121 } 122 int iconResource = cursor.getInt(iconResourceIndex); 123 String rootDocument = cursor.getString(rootDocumentIndex); 124 Uri rootDocumentUri = DocumentsContract.buildRootUri(authority, rootDocument); 125 126 documentProviders.add( 127 new ExportImportDocumentProvider( 128 title, summary, iconResource, rootDocumentUri, authority)); 129 } 130 } 131 isDocumentProviderSupported(Cursor cursor)132 private static boolean isDocumentProviderSupported(Cursor cursor) { 133 int flagsIndex = cursor.getColumnIndex(DocumentsContract.Root.COLUMN_FLAGS); 134 int flags = flagsIndex != -1 ? cursor.getInt(flagsIndex) : 0; 135 136 if ((flags & DocumentsContract.Root.FLAG_LOCAL_ONLY) 137 == DocumentsContract.Root.FLAG_LOCAL_ONLY) { 138 return false; 139 } 140 141 if ((flags & DocumentsContract.Root.FLAG_SUPPORTS_CREATE) 142 != DocumentsContract.Root.FLAG_SUPPORTS_CREATE) { 143 return false; 144 } 145 146 int mimeTypesIndex = cursor.getColumnIndex(DocumentsContract.Root.COLUMN_MIME_TYPES); 147 String mimeTypes = mimeTypesIndex != -1 ? cursor.getString(mimeTypesIndex) : null; 148 if (mimeTypes != null) { 149 return mimeTypes.lines().anyMatch(s -> s.equalsIgnoreCase(REQUIRED_MIME_TYPE)); 150 } 151 152 return true; 153 } 154 } 155