I have something to add here and how we can improve your solution. It could be better if you accept the following limitations:
- The application has an embedded list of supported browsers. Any other browser won't be supported.
- The accessibility service will look for an id of an address bar text field and try to intercept the URL from there. It is not possible to catch directly the URL which is going to be loaded. To find this id we should perform a bit of reverse engineering of the target browser: collect all fields by accessibility service and compare their ids and values with user input.
- From the previous point, the next limitation is that we are going to support only the current version of the browser. The id could be changed in the future by 3rd-party browser developers and we will have to update our interceptor to continue support. This could be done either by updating an app or by providing the browser package to id mapping by the remote server
- We detect either manual user input or redirect on link pressing (because in this case, the new URL will be also visible in an address bar). BTW it is not clear what you have mean when said
I want to get the URL only after it is hit in the address bar.
- The last limitation. Despite we are able to intercept the URL and redirect user to another page we can't prevent the site from being loaded or being started to load due to delays of asynchronous parsing a browser app screen. E.g. it is not really safe to protect a user from any access to a fraud site
The implementation:
public class UrlInterceptorService extends AccessibilityService {
private HashMap<String, Long> previousUrlDetections = new HashMap<>();
@Override
protected void onServiceConnected() {
AccessibilityServiceInfo info = getServiceInfo();
info.eventTypes = AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
info.packageNames = packageNames();
info.feedbackType = AccessibilityServiceInfo.FEEDBACK_VISUAL;
//throttling of accessibility event notification
info.notificationTimeout = 300;
//support ids interception
info.flags = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS |
AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
this.setServiceInfo(info);
}
private String captureUrl(AccessibilityNodeInfo info, SupportedBrowserConfig config) {
List<AccessibilityNodeInfo> nodes = info.findAccessibilityNodeInfosByViewId(config.addressBarId);
if (nodes == null || nodes.size() <= 0) {
return null;
}
AccessibilityNodeInfo addressBarNodeInfo = nodes.get(0);
String url = null;
if (addressBarNodeInfo.getText() != null) {
url = addressBarNodeInfo.getText().toString();
}
addressBarNodeInfo.recycle();
return url;
}
@Override
public void onAccessibilityEvent(@NonNull AccessibilityEvent event) {
AccessibilityNodeInfo parentNodeInfo = event.getSource();
if (parentNodeInfo == null) {
return;
}
String packageName = event.getPackageName().toString();
SupportedBrowserConfig browserConfig = null;
for (SupportedBrowserConfig supportedConfig: getSupportedBrowsers()) {
if (supportedConfig.packageName.equals(packageName)) {
browserConfig = supportedConfig;
}
}
//this is not supported browser, so exit
if (browserConfig == null) {
return;
}
String capturedUrl = captureUrl(parentNodeInfo, browserConfig);
parentNodeInfo.recycle();
//we can't find a url. Browser either was updated or opened page without url text field
if (capturedUrl == null) {
return;
}
long eventTime = event.getEventTime();
String detectionId = packageName + ", and url " + capturedUrl;
//noinspection ConstantConditions
long lastRecordedTime = previousUrlDetections.containsKey(detectionId) ? previousUrlDetections.get(detectionId) : 0;
//some kind of redirect throttling
if (eventTime - lastRecordedTime > 2000) {
previousUrlDetections.put(detectionId, eventTime);
analyzeCapturedUrl(capturedUrl, browserConfig.packageName);
}
}
private void analyzeCapturedUrl(@NonNull String capturedUrl, @NonNull String browserPackage) {
String redirectUrl = "your redirect url is here";
if (capturedUrl.contains("facebook.com")) {
performRedirect(redirectUrl, browserPackage);
}
}
/** we just reopen the browser app with our redirect url using service context
* We may use more complicated solution with invisible activity to send a simple intent to open the url */
private void performRedirect(@NonNull String redirectUrl, @NonNull String browserPackage) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(redirectUrl));
intent.setPackage(browserPackage);
intent.putExtra(Browser.EXTRA_APPLICATION_ID, browserPackage);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);
}
catch(ActivityNotFoundException e) {
// the expected browser is not installed
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(redirectUrl));
startActivity(i);
}
}
@Override
public void onInterrupt() { }
@NonNull
private static String[] packageNames() {
List<String> packageNames = new ArrayList<>();
for (SupportedBrowserConfig config: getSupportedBrowsers()) {
packageNames.add(config.packageName);
}
return packageNames.toArray(new String[0]);
}
private static class SupportedBrowserConfig {
public String packageName, addressBarId;
public SupportedBrowserConfig(String packageName, String addressBarId) {
this.packageName = packageName;
this.addressBarId = addressBarId;
}
}
/** @return a list of supported browser configs
* This list could be instead obtained from remote server to support future browser updates without updating an app */
@NonNull
private static List<SupportedBrowserConfig> getSupportedBrowsers() {
List<SupportedBrowserConfig> browsers = new ArrayList<>();
browsers.add( new SupportedBrowserConfig("com.android.chrome", "com.android.chrome:id/url_bar"));
browsers.add( new SupportedBrowserConfig("org.mozilla.firefox", "org.mozilla.firefox:id/url_bar_title"));
return browsers;
}
}
and accessibility service config:
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/accessibility_service_description"
android:canRetrieveWindowContent="true"
android:settingsActivity=".ServiceSettingsActivity" />
Feel free to ask any questions and I'll try to help
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…