• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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