001/* 002 * Copyright 2015 The AppAuth for Android Authors. All Rights Reserved. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 005 * in compliance with the License. You may obtain a copy of the License at 006 * 007 * http://www.apache.org/licenses/LICENSE-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, software distributed under the 010 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 011 * express or implied. See the License for the specific language governing permissions and 012 * limitations under the License. 013 */ 014 015package net.openid.appauth.browser; 016 017import android.annotation.SuppressLint; 018import android.content.Context; 019import android.content.Intent; 020import android.content.pm.PackageInfo; 021import android.content.pm.PackageManager; 022import android.content.pm.PackageManager.NameNotFoundException; 023import android.content.pm.ResolveInfo; 024import android.net.Uri; 025import android.os.Build.VERSION; 026import android.os.Build.VERSION_CODES; 027import androidx.annotation.NonNull; 028import androidx.annotation.Nullable; 029import androidx.annotation.VisibleForTesting; 030import androidx.browser.customtabs.CustomTabsService; 031 032import java.util.ArrayList; 033import java.util.Iterator; 034import java.util.List; 035 036/** 037 * Utility class to obtain the browser package name to be used for 038 * {@link net.openid.appauth.AuthorizationService#performAuthorizationRequest( 039 * net.openid.appauth.AuthorizationRequest, 040 * android.app.PendingIntent)} calls. It prioritizes browsers which support 041 * [custom tabs](https://developer.chrome.com/multidevice/android/customtabs). To mitigate 042 * man-in-the-middle attacks by malicious apps pretending to be browsers for the specific URI we 043 * query, only those which are registered as a handler for _all_ HTTP and HTTPS URIs will be 044 * used. 045 */ 046public final class BrowserSelector { 047 048 private static final String SCHEME_HTTP = "http"; 049 private static final String SCHEME_HTTPS = "https"; 050 051 /** 052 * The service we expect to find on a web browser that indicates it supports custom tabs. 053 */ 054 @VisibleForTesting 055 static final String ACTION_CUSTOM_TABS_CONNECTION = 056 CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION; 057 058 /** 059 * Intent for querying installed web browsers as seen at 060 * https://cs.android.com/android/platform/superproject/+/master:packages/modules/Permission/PermissionController/src/com/android/permissioncontroller/role/model/BrowserRoleBehavior.java 061 */ 062 @VisibleForTesting 063 static final Intent BROWSER_INTENT = new Intent() 064 .setAction(Intent.ACTION_VIEW) 065 .addCategory(Intent.CATEGORY_BROWSABLE) 066 .setData(Uri.fromParts("http", "", null)); 067 068 /** 069 * Retrieves the full list of browsers installed on the device. Two entries will exist 070 * for each browser that supports custom tabs, with the {@link BrowserDescriptor#useCustomTab} 071 * flag set to `true` in one and `false` in the other. The list is in the 072 * order returned by the package manager, so indirectly reflects the user's preferences 073 * (i.e. their default browser, if set, should be the first entry in the list). 074 */ 075 @SuppressLint("PackageManagerGetSignatures") 076 @NonNull 077 public static List<BrowserDescriptor> getAllBrowsers(Context context) { 078 PackageManager pm = context.getPackageManager(); 079 List<BrowserDescriptor> browsers = new ArrayList<>(); 080 String defaultBrowserPackage = null; 081 082 int queryFlag = PackageManager.GET_RESOLVED_FILTER; 083 if (VERSION.SDK_INT >= VERSION_CODES.M) { 084 queryFlag |= PackageManager.MATCH_ALL; 085 } 086 // When requesting all matching activities for an intent from the package manager, 087 // the user's preferred browser is not guaranteed to be at the head of this list. 088 // Therefore, the preferred browser must be separately determined and the resultant 089 // list of browsers reordered to restored this desired property. 090 ResolveInfo resolvedDefaultActivity = 091 pm.resolveActivity(BROWSER_INTENT, 0); 092 if (resolvedDefaultActivity != null) { 093 defaultBrowserPackage = resolvedDefaultActivity.activityInfo.packageName; 094 } 095 List<ResolveInfo> resolvedActivityList = 096 pm.queryIntentActivities(BROWSER_INTENT, queryFlag); 097 098 for (ResolveInfo info : resolvedActivityList) { 099 // ignore handlers which are not browsers 100 if (!isFullBrowser(info)) { 101 continue; 102 } 103 104 try { 105 int defaultBrowserIndex = 0; 106 PackageInfo packageInfo = pm.getPackageInfo( 107 info.activityInfo.packageName, 108 PackageManager.GET_SIGNATURES); 109 110 if (hasWarmupService(pm, info.activityInfo.packageName)) { 111 BrowserDescriptor customTabBrowserDescriptor = 112 new BrowserDescriptor(packageInfo, true); 113 if (info.activityInfo.packageName.equals(defaultBrowserPackage)) { 114 // If the default browser is having a WarmupService, 115 // will it be added to the beginning of the list. 116 browsers.add(defaultBrowserIndex, customTabBrowserDescriptor); 117 defaultBrowserIndex++; 118 } else { 119 browsers.add(customTabBrowserDescriptor); 120 } 121 } 122 123 BrowserDescriptor fullBrowserDescriptor = 124 new BrowserDescriptor(packageInfo, false); 125 if (info.activityInfo.packageName.equals(defaultBrowserPackage)) { 126 // The default browser is added to the beginning of the list. 127 // If there is support for Custom Tabs, will the one disabling Custom Tabs 128 // be added as the second entry. 129 browsers.add(defaultBrowserIndex, fullBrowserDescriptor); 130 } else { 131 browsers.add(fullBrowserDescriptor); 132 } 133 } catch (NameNotFoundException e) { 134 // a descriptor cannot be generated without the package info 135 } 136 } 137 138 return browsers; 139 } 140 141 /** 142 * Searches through all browsers for the best match based on the supplied browser matcher. 143 * Custom tab supporting browsers are preferred, if the matcher permits them, and browsers 144 * are evaluated in the order returned by the package manager, which should indirectly match 145 * the user's preferences. 146 * 147 * @param context {@link Context} to use for accessing {@link PackageManager}. 148 * @return The package name recommended to use for connecting to custom tabs related components. 149 */ 150 @SuppressLint("PackageManagerGetSignatures") 151 @Nullable 152 public static BrowserDescriptor select(Context context, BrowserMatcher browserMatcher) { 153 List<BrowserDescriptor> allBrowsers = getAllBrowsers(context); 154 BrowserDescriptor bestMatch = null; 155 for (BrowserDescriptor browser : allBrowsers) { 156 if (!browserMatcher.matches(browser)) { 157 continue; 158 } 159 160 if (browser.useCustomTab) { 161 // directly return the first custom tab supporting browser that is matched 162 return browser; 163 } 164 165 if (bestMatch == null) { 166 // store this as the best match for use if we don't find any matching 167 // custom tab supporting browsers 168 bestMatch = browser; 169 } 170 } 171 172 return bestMatch; 173 } 174 175 private static boolean hasWarmupService(PackageManager pm, String packageName) { 176 Intent serviceIntent = new Intent(); 177 serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION); 178 serviceIntent.setPackage(packageName); 179 return (pm.resolveService(serviceIntent, 0) != null); 180 } 181 182 private static boolean isFullBrowser(ResolveInfo resolveInfo) { 183 // The filter must match ACTION_VIEW, CATEGORY_BROWSEABLE, and at least one scheme, 184 if (resolveInfo.filter == null 185 || !resolveInfo.filter.hasAction(Intent.ACTION_VIEW) 186 || !resolveInfo.filter.hasCategory(Intent.CATEGORY_BROWSABLE) 187 || resolveInfo.filter.schemesIterator() == null) { 188 return false; 189 } 190 191 // The filter must not be restricted to any particular set of authorities 192 if (resolveInfo.filter.authoritiesIterator() != null) { 193 return false; 194 } 195 196 // The filter must support both HTTP and HTTPS. 197 boolean supportsHttp = false; 198 boolean supportsHttps = false; 199 Iterator<String> schemeIter = resolveInfo.filter.schemesIterator(); 200 while (schemeIter.hasNext()) { 201 String scheme = schemeIter.next(); 202 supportsHttp |= SCHEME_HTTP.equals(scheme); 203 supportsHttps |= SCHEME_HTTPS.equals(scheme); 204 205 if (supportsHttp && supportsHttps) { 206 return true; 207 } 208 } 209 210 // at least one of HTTP or HTTPS is not supported 211 return false; 212 } 213}