1 /*
2  * Copyright (C) 2023 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.migration;
18 
19 import static com.android.server.healthconnect.migration.MigrationConstants.HC_PACKAGE_NAME_CONFIG_NAME;
20 import static com.android.server.healthconnect.migration.MigrationUtils.filterIntent;
21 import static com.android.server.healthconnect.migration.MigrationUtils.filterPermissions;
22 
23 import android.annotation.NonNull;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.pm.PackageManager;
28 import android.content.pm.ResolveInfo;
29 import android.content.res.Resources;
30 import android.health.connect.Constants;
31 import android.health.connect.HealthConnectManager;
32 import android.os.UserHandle;
33 import android.os.UserManager;
34 import android.util.Slog;
35 
36 import java.util.List;
37 import java.util.Objects;
38 
39 /**
40  * This class contains methods to:
41  *
42  * <ul>
43  *   <li>Filter and get the package names of migration aware apps present on the device. Migration
44  *       aware apps are those that both hold {@link
45  *       android.Manifest.permission#MIGRATE_HEALTH_CONNECT_DATA} and handle {@link
46  *       android.health.connect.HealthConnectManager#ACTION_SHOW_MIGRATION_INFO}.
47  *   <li>Send an explicit broadcast with action {@link
48  *       android.health.connect.HealthConnectManager#ACTION_HEALTH_CONNECT_MIGRATION_READY} to
49  *       migration aware apps to prompt them to start/continue HC data migration.
50  * </ul>
51  *
52  * @hide
53  */
54 public class MigrationBroadcast {
55 
56     private static final String TAG = "HealthConnectMigrationBroadcast";
57     private final Context mContext;
58     private final UserHandle mUser;
59 
60     /**
61      * Constructs a {@link MigrationBroadcast} object.
62      *
63      * @param context the service context.
64      * @param user the user to send the broadcasts to.
65      */
MigrationBroadcast(@onNull Context context, UserHandle user)66     public MigrationBroadcast(@NonNull Context context, UserHandle user) {
67         mContext = context;
68         mUser = user;
69     }
70 
71     /**
72      * Sends a broadcast with action {@link
73      * android.health.connect.HealthConnectManager#ACTION_HEALTH_CONNECT_MIGRATION_READY} to
74      * applications which hold {@link android.Manifest.permission#MIGRATE_HEALTH_CONNECT_DATA} and
75      * handle {@link android.health.connect.HealthConnectManager#ACTION_SHOW_MIGRATION_INFO}.
76      */
sendInvocationBroadcast()77     public void sendInvocationBroadcast() throws Exception {
78         Slog.i(TAG, "Calling sendInvocationBroadcast()");
79 
80         String hcMigratorPackage =
81                 mContext.getResources()
82                         .getString(
83                                 Resources.getSystem()
84                                         .getIdentifier(HC_PACKAGE_NAME_CONFIG_NAME, null, null));
85         String migrationAwarePackage;
86 
87         List<String> permissionFilteredPackages = filterPermissions(mContext);
88         List<String> filteredPackages = filterIntent(mContext, permissionFilteredPackages);
89 
90         int numPackages = filteredPackages.size();
91 
92         if (numPackages == 0) {
93             if (Constants.DEBUG) {
94                 Slog.d(TAG, "There are no migration aware apps");
95             }
96             return;
97         } else if (numPackages == 1) {
98             if (Objects.equals(hcMigratorPackage, filteredPackages.get(0))) {
99                 migrationAwarePackage = filteredPackages.get(0);
100             } else {
101                 throw new Exception("Migration aware app is not Health Connect");
102             }
103         } else {
104             if (filteredPackages.contains(hcMigratorPackage)) {
105                 migrationAwarePackage = hcMigratorPackage;
106             } else {
107                 throw new Exception("Multiple packages are migration aware");
108             }
109         }
110 
111         if (Constants.DEBUG) {
112             Slog.d(TAG, "Checking if migration aware package is installed on user");
113         }
114 
115         Context userContext = mContext.createContextAsUser(mUser, 0);
116         if (isPackageInstalled(migrationAwarePackage, userContext)) {
117             UserManager userManager =
118                     Objects.requireNonNull(userContext.getSystemService(UserManager.class));
119             if (userManager.isUserForeground()) {
120                 Intent intent =
121                         new Intent(HealthConnectManager.ACTION_HEALTH_CONNECT_MIGRATION_READY)
122                                 .setPackage(migrationAwarePackage);
123 
124                 queryAndSetComponentForIntent(intent);
125 
126                 mContext.sendBroadcastAsUser(intent, mUser);
127                 if (Constants.DEBUG) {
128                     Slog.d(TAG, "Sent broadcast to migration aware application.");
129                 }
130             } else if (Constants.DEBUG) {
131                 Slog.d(TAG, "User " + mUser + " is not currently active");
132             }
133         } else if (Constants.DEBUG) {
134             Slog.d(TAG, "Migration aware app is not installed on the current user");
135         }
136     }
137 
138     /** Checks if the package is installed on the given user. */
isPackageInstalled(String packageName, Context userContext)139     private boolean isPackageInstalled(String packageName, Context userContext) {
140         try {
141             PackageManager packageManager = userContext.getPackageManager();
142             packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
143             return true;
144         } catch (PackageManager.NameNotFoundException e) {
145             return false;
146         }
147     }
148 
149     /**
150      * Sets the component to send the migration ready intent to, if only one such receiver is found.
151      *
152      * <p>This is needed to send an explicit broadcast containing an explicit intent which has the
153      * target component specified.
154      *
155      * @param intent Intent which has the package set, for which the broadcast receiver is to be
156      *     queried.
157      * @throws Exception if multiple broadcast receivers are found for the migration ready intent.
158      */
queryAndSetComponentForIntent(Intent intent)159     private void queryAndSetComponentForIntent(Intent intent) throws Exception {
160         List<ResolveInfo> queryResults =
161                 mContext.getPackageManager()
162                         .queryBroadcastReceiversAsUser(
163                                 intent,
164                                 PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_ALL),
165                                 mUser);
166 
167         int numReceivers = queryResults.size();
168 
169         if (numReceivers == 0 && Constants.DEBUG) {
170             Slog.d(TAG, "Found no broadcast receivers for the migration broadcast intent");
171         } else if (numReceivers == 1) {
172             ResolveInfo queryResult = queryResults.get(0);
173             if (queryResult.activityInfo != null) {
174                 ComponentName componentName =
175                         new ComponentName(
176                                 queryResult.activityInfo.packageName,
177                                 queryResult.activityInfo.name);
178                 intent.setComponent(componentName);
179             } else if (Constants.DEBUG) {
180                 Slog.d(TAG, "Found no corresponding broadcast receiver for intent resolution");
181             }
182         } else if (numReceivers > 1 && Constants.DEBUG) {
183             Slog.d(TAG, "Found multiple broadcast receivers for migration broadcast intent");
184         }
185     }
186 }
187