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