1 /* 2 * Copyright (C) 2014 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.intentresolver; 18 19 import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL; 20 import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK; 21 import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; 22 23 import static com.android.intentresolver.ui.viewmodel.ResolverRequestReaderKt.EXTRA_CALLING_USER; 24 import static com.android.intentresolver.ui.viewmodel.ResolverRequestReaderKt.EXTRA_SELECTED_PROFILE; 25 26 import android.app.Activity; 27 import android.app.ActivityThread; 28 import android.app.AppGlobals; 29 import android.app.admin.DevicePolicyManager; 30 import android.content.ComponentName; 31 import android.content.ContentResolver; 32 import android.content.Intent; 33 import android.content.pm.ActivityInfo; 34 import android.content.pm.IPackageManager; 35 import android.content.pm.PackageManager; 36 import android.content.pm.ResolveInfo; 37 import android.content.pm.UserInfo; 38 import android.metrics.LogMaker; 39 import android.os.Bundle; 40 import android.os.RemoteException; 41 import android.os.UserHandle; 42 import android.os.UserManager; 43 import android.provider.Settings; 44 import android.util.Slog; 45 import android.widget.Toast; 46 47 import androidx.annotation.Nullable; 48 49 import com.android.intentresolver.profiles.MultiProfilePagerAdapter; 50 import com.android.internal.annotations.VisibleForTesting; 51 import com.android.internal.logging.MetricsLogger; 52 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 53 54 import java.util.Arrays; 55 import java.util.HashSet; 56 import java.util.List; 57 import java.util.Set; 58 import java.util.concurrent.CompletableFuture; 59 import java.util.concurrent.ExecutorService; 60 import java.util.concurrent.Executors; 61 62 /** 63 * This is used in conjunction with 64 * {@link DevicePolicyManager#addCrossProfileIntentFilter} to enable intents to 65 * be passed in and out of a managed profile. 66 */ 67 public class IntentForwarderActivity extends Activity { 68 public static String TAG = "IntentForwarderActivity"; 69 70 public static String FORWARD_INTENT_TO_PARENT 71 = "com.android.internal.app.ForwardIntentToParent"; 72 73 public static String FORWARD_INTENT_TO_MANAGED_PROFILE 74 = "com.android.internal.app.ForwardIntentToManagedProfile"; 75 76 private static final Set<String> ALLOWED_TEXT_MESSAGE_SCHEMES 77 = new HashSet<>(Arrays.asList("sms", "smsto", "mms", "mmsto")); 78 79 private static final String TEL_SCHEME = "tel"; 80 81 private static final ComponentName RESOLVER_COMPONENT_NAME = 82 new ComponentName("android", ResolverActivity.class.getName()); 83 84 private Injector mInjector; 85 86 private MetricsLogger mMetricsLogger; 87 protected ExecutorService mExecutorService; 88 89 @Override onDestroy()90 protected void onDestroy() { 91 super.onDestroy(); 92 mExecutorService.shutdown(); 93 } 94 95 @Override onCreate(Bundle savedInstanceState)96 protected void onCreate(Bundle savedInstanceState) { 97 super.onCreate(savedInstanceState); 98 mInjector = createInjector(); 99 mExecutorService = Executors.newSingleThreadExecutor(); 100 101 Intent intentReceived = getIntent(); 102 String className = intentReceived.getComponent().getClassName(); 103 final int targetUserId; 104 final String userMessage; 105 if (className.equals(FORWARD_INTENT_TO_PARENT)) { 106 userMessage = getForwardToPersonalMessage(); 107 targetUserId = getProfileParent(); 108 109 getMetricsLogger().write( 110 new LogMaker(MetricsEvent.ACTION_SWITCH_SHARE_PROFILE) 111 .setSubtype(MetricsEvent.PARENT_PROFILE)); 112 } else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) { 113 userMessage = getForwardToWorkMessage(); 114 targetUserId = getManagedProfile(); 115 116 getMetricsLogger().write( 117 new LogMaker(MetricsEvent.ACTION_SWITCH_SHARE_PROFILE) 118 .setSubtype(MetricsEvent.MANAGED_PROFILE)); 119 } else { 120 Slog.wtf(TAG, IntentForwarderActivity.class.getName() + " cannot be called directly"); 121 userMessage = null; 122 targetUserId = UserHandle.USER_NULL; 123 } 124 if (targetUserId == UserHandle.USER_NULL) { 125 // This covers the case where there is no parent / managed profile. 126 finish(); 127 return; 128 } 129 if (Intent.ACTION_CHOOSER.equals(intentReceived.getAction())) { 130 launchChooserActivityWithCorrectTab(intentReceived, className); 131 return; 132 } 133 134 final int callingUserId = getUserId(); 135 final Intent newIntent = canForward(intentReceived, getUserId(), targetUserId, 136 mInjector.getIPackageManager(), getContentResolver()); 137 138 if (newIntent == null) { 139 Slog.wtf(TAG, "the intent: " + intentReceived + " cannot be forwarded from user " 140 + callingUserId + " to user " + targetUserId); 141 finish(); 142 return; 143 } 144 145 newIntent.prepareToLeaveUser(callingUserId); 146 final CompletableFuture<ResolveInfo> targetResolveInfoFuture = 147 mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, targetUserId); 148 targetResolveInfoFuture 149 .thenApplyAsync(targetResolveInfo -> { 150 if (isResolverActivityResolveInfo(targetResolveInfo)) { 151 launchResolverActivityWithCorrectTab(intentReceived, className, newIntent, 152 callingUserId, targetUserId); 153 return targetResolveInfo; 154 } 155 startActivityAsCaller(newIntent, targetUserId); 156 return targetResolveInfo; 157 }, mExecutorService) 158 .thenAcceptAsync(result -> { 159 maybeShowDisclosure(intentReceived, result, userMessage); 160 finish(); 161 }, getApplicationContext().getMainExecutor()); 162 } 163 getForwardToPersonalMessage()164 private String getForwardToPersonalMessage() { 165 return getSystemService(DevicePolicyManager.class).getResources().getString( 166 FORWARD_INTENT_TO_PERSONAL, 167 () -> getString(R.string.forward_intent_to_owner)); 168 } 169 getForwardToWorkMessage()170 private String getForwardToWorkMessage() { 171 return getSystemService(DevicePolicyManager.class).getResources().getString( 172 FORWARD_INTENT_TO_WORK, 173 () -> getString(R.string.forward_intent_to_work)); 174 } 175 isIntentForwarderResolveInfo(ResolveInfo resolveInfo)176 private boolean isIntentForwarderResolveInfo(ResolveInfo resolveInfo) { 177 if (resolveInfo == null) { 178 return false; 179 } 180 ActivityInfo activityInfo = resolveInfo.activityInfo; 181 if (activityInfo == null) { 182 return false; 183 } 184 if (!"android".equals(activityInfo.packageName)) { 185 return false; 186 } 187 return activityInfo.name.equals(FORWARD_INTENT_TO_PARENT) 188 || activityInfo.name.equals(FORWARD_INTENT_TO_MANAGED_PROFILE); 189 } 190 isResolverActivityResolveInfo(@ullable ResolveInfo resolveInfo)191 private boolean isResolverActivityResolveInfo(@Nullable ResolveInfo resolveInfo) { 192 return resolveInfo != null 193 && resolveInfo.activityInfo != null 194 && RESOLVER_COMPONENT_NAME.equals(resolveInfo.activityInfo.getComponentName()); 195 } 196 maybeShowDisclosure( Intent intentReceived, ResolveInfo resolveInfo, @Nullable String message)197 private void maybeShowDisclosure( 198 Intent intentReceived, ResolveInfo resolveInfo, @Nullable String message) { 199 if (shouldShowDisclosure(resolveInfo, intentReceived) && message != null) { 200 mInjector.showToast(message, Toast.LENGTH_LONG); 201 } 202 } 203 startActivityAsCaller(Intent newIntent, int userId)204 private void startActivityAsCaller(Intent newIntent, int userId) { 205 try { 206 startActivityAsCaller( 207 newIntent, 208 /* options= */ null, 209 /* ignoreTargetSecurity= */ false, 210 userId); 211 } catch (RuntimeException e) { 212 Slog.wtf(TAG, "Unable to launch as UID " + getLaunchedFromUid() + " package " 213 + getLaunchedFromPackage() + ", while running in " 214 + ActivityThread.currentProcessName(), e); 215 } 216 } 217 launchChooserActivityWithCorrectTab(Intent intentReceived, String className)218 private void launchChooserActivityWithCorrectTab(Intent intentReceived, String className) { 219 // When showing the sharesheet, instead of forwarding to the other profile, 220 // we launch the sharesheet in the current user and select the other tab. 221 // This fixes b/152866292 where the user can not go back to the original profile 222 // when cross-profile intents are disabled. 223 int selectedProfile = findSelectedProfile(className); 224 sanitizeIntent(intentReceived); 225 intentReceived.putExtra(EXTRA_SELECTED_PROFILE, selectedProfile); 226 Intent innerIntent = intentReceived.getParcelableExtra(Intent.EXTRA_INTENT); 227 if (innerIntent == null) { 228 Slog.wtf(TAG, "Cannot start a chooser intent with no extra " + Intent.EXTRA_INTENT); 229 return; 230 } 231 sanitizeIntent(innerIntent); 232 startActivityAsCaller(intentReceived, null, false, getUserId()); 233 finish(); 234 } 235 launchResolverActivityWithCorrectTab(Intent intentReceived, String className, Intent newIntent, int callingUserId, int targetUserId)236 private void launchResolverActivityWithCorrectTab(Intent intentReceived, String className, 237 Intent newIntent, int callingUserId, int targetUserId) { 238 // When showing the intent resolver, instead of forwarding to the other profile, 239 // we launch it in the current user and select the other tab. This fixes b/155874820. 240 // 241 // In the case when there are 0 targets in the current profile and >1 apps in the other 242 // profile, the package manager launches the intent resolver in the other profile. 243 // If that's the case, we launch the resolver in the target user instead (other profile). 244 ResolveInfo callingResolveInfo = mInjector.resolveActivityAsUser( 245 newIntent, MATCH_DEFAULT_ONLY, callingUserId).join(); 246 int userId = isIntentForwarderResolveInfo(callingResolveInfo) 247 ? targetUserId : callingUserId; 248 int selectedProfile = findSelectedProfile(className); 249 sanitizeIntent(intentReceived); 250 intentReceived.putExtra(EXTRA_SELECTED_PROFILE, selectedProfile); 251 intentReceived.putExtra(EXTRA_CALLING_USER, UserHandle.of(callingUserId)); 252 startActivityAsCaller(intentReceived, null, false, userId); 253 finish(); 254 } 255 findSelectedProfile(String className)256 private int findSelectedProfile(String className) { 257 if (className.equals(FORWARD_INTENT_TO_PARENT)) { 258 return MultiProfilePagerAdapter.PROFILE_PERSONAL; 259 } else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) { 260 return MultiProfilePagerAdapter.PROFILE_WORK; 261 } 262 return -1; 263 } 264 shouldShowDisclosure(@ullable ResolveInfo ri, Intent intent)265 private boolean shouldShowDisclosure(@Nullable ResolveInfo ri, Intent intent) { 266 if (!isDeviceProvisioned()) { 267 return false; 268 } 269 if (ri == null || ri.activityInfo == null) { 270 return true; 271 } 272 if (ri.activityInfo.applicationInfo.isSystemApp() 273 && (isDialerIntent(intent) || isTextMessageIntent(intent))) { 274 return false; 275 } 276 return !isTargetResolverOrChooserActivity(ri.activityInfo); 277 } 278 isDeviceProvisioned()279 private boolean isDeviceProvisioned() { 280 return Settings.Global.getInt(getContentResolver(), 281 Settings.Global.DEVICE_PROVISIONED, /* def= */ 0) != 0; 282 } 283 isTextMessageIntent(Intent intent)284 private boolean isTextMessageIntent(Intent intent) { 285 return (Intent.ACTION_SENDTO.equals(intent.getAction()) || isViewActionIntent(intent)) 286 && ALLOWED_TEXT_MESSAGE_SCHEMES.contains(intent.getScheme()); 287 } 288 isDialerIntent(Intent intent)289 private boolean isDialerIntent(Intent intent) { 290 return Intent.ACTION_DIAL.equals(intent.getAction()) 291 || Intent.ACTION_CALL.equals(intent.getAction()) 292 || Intent.ACTION_CALL_PRIVILEGED.equals(intent.getAction()) 293 || Intent.ACTION_CALL_EMERGENCY.equals(intent.getAction()) 294 || (isViewActionIntent(intent) && TEL_SCHEME.equals(intent.getScheme())); 295 } 296 isViewActionIntent(Intent intent)297 private boolean isViewActionIntent(Intent intent) { 298 return Intent.ACTION_VIEW.equals(intent.getAction()) 299 && intent.hasCategory(Intent.CATEGORY_BROWSABLE); 300 } 301 isTargetResolverOrChooserActivity(ActivityInfo activityInfo)302 private boolean isTargetResolverOrChooserActivity(ActivityInfo activityInfo) { 303 if (!"android".equals(activityInfo.packageName)) { 304 return false; 305 } 306 return ResolverActivity.class.getName().equals(activityInfo.name) 307 || ChooserActivity.class.getName().equals(activityInfo.name); 308 } 309 310 /** 311 * Check whether the intent can be forwarded to target user. Return the intent used for 312 * forwarding if it can be forwarded, {@code null} otherwise. 313 */ canForward(Intent incomingIntent, int sourceUserId, int targetUserId, IPackageManager packageManager, ContentResolver contentResolver)314 public static Intent canForward(Intent incomingIntent, int sourceUserId, int targetUserId, 315 IPackageManager packageManager, ContentResolver contentResolver) { 316 Intent forwardIntent = new Intent(incomingIntent); 317 forwardIntent.addFlags( 318 Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); 319 sanitizeIntent(forwardIntent); 320 321 Intent intentToCheck = forwardIntent; 322 if (Intent.ACTION_CHOOSER.equals(forwardIntent.getAction())) { 323 return null; 324 } 325 if (forwardIntent.getSelector() != null) { 326 intentToCheck = forwardIntent.getSelector(); 327 } 328 String resolvedType = intentToCheck.resolveTypeIfNeeded(contentResolver); 329 sanitizeIntent(intentToCheck); 330 try { 331 if (packageManager.canForwardTo( 332 intentToCheck, resolvedType, sourceUserId, targetUserId)) { 333 return forwardIntent; 334 } 335 } catch (RemoteException e) { 336 Slog.e(TAG, "PackageManagerService is dead?"); 337 } 338 return null; 339 } 340 341 /** 342 * Returns the userId of the managed profile for this device or UserHandle.USER_NULL if there is 343 * no managed profile. 344 * 345 * TODO: Remove the assumption that there is only one managed profile 346 * on the device. 347 */ getManagedProfile()348 private int getManagedProfile() { 349 List<UserInfo> relatedUsers = mInjector.getUserManager().getProfiles(UserHandle.myUserId()); 350 for (UserInfo userInfo : relatedUsers) { 351 if (userInfo.isManagedProfile()) return userInfo.id; 352 } 353 Slog.wtf(TAG, FORWARD_INTENT_TO_MANAGED_PROFILE 354 + " has been called, but there is no managed profile"); 355 return UserHandle.USER_NULL; 356 } 357 358 /** 359 * Returns the userId of the profile parent or UserHandle.USER_NULL if there is 360 * no parent. 361 */ getProfileParent()362 private int getProfileParent() { 363 UserInfo parent = mInjector.getUserManager().getProfileParent(UserHandle.myUserId()); 364 if (parent == null) { 365 Slog.wtf(TAG, FORWARD_INTENT_TO_PARENT 366 + " has been called, but there is no parent"); 367 return UserHandle.USER_NULL; 368 } 369 return parent.id; 370 } 371 372 /** 373 * Sanitize the intent in place. 374 */ sanitizeIntent(Intent intent)375 private static void sanitizeIntent(Intent intent) { 376 // Apps should not be allowed to target a specific package/ component in the target user. 377 intent.setPackage(null); 378 intent.setComponent(null); 379 } 380 getMetricsLogger()381 protected MetricsLogger getMetricsLogger() { 382 if (mMetricsLogger == null) { 383 mMetricsLogger = new MetricsLogger(); 384 } 385 return mMetricsLogger; 386 } 387 388 @VisibleForTesting createInjector()389 protected Injector createInjector() { 390 return new InjectorImpl(); 391 } 392 393 private class InjectorImpl implements Injector { 394 395 @Override getIPackageManager()396 public IPackageManager getIPackageManager() { 397 return AppGlobals.getPackageManager(); 398 } 399 400 @Override getUserManager()401 public UserManager getUserManager() { 402 return getSystemService(UserManager.class); 403 } 404 405 @Override getPackageManager()406 public PackageManager getPackageManager() { 407 return IntentForwarderActivity.this.getPackageManager(); 408 } 409 410 @Override 411 @Nullable resolveActivityAsUser( Intent intent, int flags, int userId)412 public CompletableFuture<ResolveInfo> resolveActivityAsUser( 413 Intent intent, int flags, int userId) { 414 return CompletableFuture.supplyAsync( 415 () -> getPackageManager().resolveActivityAsUser(intent, flags, userId)); 416 } 417 418 @Override showToast(String message, int duration)419 public void showToast(String message, int duration) { 420 Toast.makeText(IntentForwarderActivity.this, message, duration).show(); 421 } 422 } 423 424 public interface Injector { getIPackageManager()425 IPackageManager getIPackageManager(); 426 getUserManager()427 UserManager getUserManager(); 428 getPackageManager()429 PackageManager getPackageManager(); 430 resolveActivityAsUser(Intent intent, int flags, int userId)431 CompletableFuture<ResolveInfo> resolveActivityAsUser(Intent intent, int flags, int userId); 432 showToast(String message, int duration)433 void showToast(String message, int duration); 434 } 435 } 436