29

I am trying to achieve Web Scraping through a background IntentService that periodically scrape a website without a view displaying on the users phone.

  • Since I have to do call some javascript on the loaded page I cannot use any HttpGet's etc.
  • I therefore have to use a WebView instance which can only run on an UI thread.
  • Any attempts to start an Activity that use a WebView results in a View coming into the phones foreground (as per Android's design of Activities)
  • Any attempts to use a WebView outside of an Activity context resulted in error pointing to the fact that you cannot use WebView on a non-UI thread.
  • For various complexity reasons I cannot consider using libraries such as Rhino for UI-less web scraping.

Is there any way of working around this problem?

Pierre
  • 1,607
  • 3
  • 21
  • 33

10 Answers10

44

You can display a webview from a service. Code below creates a window which your service has access to. The window isn't visible because the size is 0 by 0.

public class ServiceWithWebView extends Service {

    @Override
    public void onCreate() {
        super.onCreate();

        WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
        params = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT);
        params.gravity = Gravity.TOP | Gravity.LEFT;
        params.x = 0;
        params.y = 0;
        params.width = 0;
        params.height = 0;

        LinearLayout view = new LinearLayout(this);
        view.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT));

        WebView wv = new WebView(this);
        wv.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT));
        view.addView(wv);
        wv.loadUrl("http://google.com");

        windowManager.addView(view, params);
    }
}

Also this will require the android.permission.SYSTEM_ALERT_WINDOW permission.

Anm
  • 3,291
  • 2
  • 29
  • 40
Randy
  • 4,351
  • 2
  • 25
  • 46
  • You are still launching the service in a ACTIVITY class onCreate method though. I cannot execute code in an activity because an activity will be visible to the user. Creating an new instance of WebView in a class that is not inheriting the Activity section will result in a runtime exception. – Pierre Sep 25 '13 at 05:29
  • I'm not sure I understand the first part of your comment. Yes, you'll have to find a way to start your service but it could be an activity that simply launches then finishes. As far as the WebView goes, I absolutely know this will work. This "onCreate()" method is from the service itself. I've done this myself about a week ago, no runtime exceptions ever experienced. My guess is you tried to use the "onStartCommand()" function which might be called from a different thread. – Randy Sep 25 '13 at 11:27
  • 1
    If I start the service in a activity then some screen will be visible to a user for at least a fraction of a second not so? It needs to be invisible. I have successfully launched your ServiceWithWebView sample from an IntentService getting called every 20 seconds by an AlarmManager. Everything executes fine EXCEPT when adding any WebView code I get the exception "Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag". I believe its because I am launching from a non-UI thread. I have tried adding the FLAG_ACTIVITY_NEW_TASK on the intent. – Pierre Sep 25 '13 at 17:35
  • 2
    I know this is an old answer, but for future reference: this method does indeed work, even with the Chromium-based WebView in KitKat. I'm using it in a service that is started periodically using AlarmManager. You don't need to add the WebView to a LinearLayout, though. Adding it directly to the WindowManager seems to work fine. – David Brown Sep 13 '14 at 04:31
  • anyone tried this recently, on lollipop? I have to find something in the loaded html page, but the onPageFinished method is never called... – Giorgio Gelardi Nov 24 '14 at 17:41
  • Its giving runtime exception when we click on certain link that opens alertwindow pop up – user2436032 Sep 15 '15 at 06:18
  • Yup. The service context doesn't allow you to create alert windows. You'll need to set the `WebChromeClient` and override the `onJsAlert`, `onJsConfirm`, and `onJsPrompt` functions and display your own custom dialog (probably just a view inside your window and not a real alert dialog). – Randy Sep 15 '15 at 14:28
  • Replaced WindowManager.LayoutParams.TYPE_PHONE with WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY to fix back button behavior when the service is active. – Anm Sep 30 '15 at 16:27
  • I can't select text in webview if it created by service, how can i fix it ? – Hoa Le Jan 15 '16 at 05:01
  • @HoaLe, you'll have to use a different flag: `WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE`. That flag keeps you from interacting with the window. – Randy Jan 15 '16 at 05:15
  • @Randy, i dont use flag WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, and when i long click in text on webview, -> selector visible and then it gone, action mode no visible, i don't know why. can you help me ? – Hoa Le Jan 15 '16 at 07:16
  • Hello @Randy? can you help me :(. – Hoa Le Jan 15 '16 at 09:07
  • @HoaLe, I haven't touched this code in 2 years. I'd either do more research or post another Stackoverflow post. Good luck. – Randy Jan 15 '16 at 14:44
  • This is quite a hack-y way to do it.  Because of this, I've requested to be able to have a WebView without the need of such ways: https://issuetracker.google.com/issues/113346931 Please consider starring it – android developer Aug 30 '18 at 16:24
  • 1
    This doesn't work. The last line throws an exception saying "android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?". – Donald Duck Aug 28 '21 at 15:48
  • I have the same problem as Donald Duck, implementing this in Xamarin C# code I get the "unable to add window" error where it basically says that you can't add a window outside of the activity. Tried various things to fix it but no joy (and yes I have all the permissions). IWindowManager windowManager = MainActivity.Instance.GetSystemService(WindowService).JavaCast(); var layoutParams = new WindowManagerLayoutParams(1, 1, WindowManagerTypes.ApplicationPanel, WindowManagerFlags.WatchOutsideTouch, Format.Translucent); windowManager.AddView(view, layoutParams); – Simon Gymer Feb 24 '22 at 11:29
13

Correct me if I am wrong but the correct answer to this question is that there is NO possible way to use a WebView in the background while the user is doing other things on the phone without interrupting the user by means of an Activity.

I have applied both Randy and Code_Yoga's suggestions: Using an activity with "Theme.NoDisplay" to launch a background service with a WebView to do some work. However even though no view is visible the switching to that activity for that second to start the services interrupts the user (ex. like pausing a running game that was being played).

Totally disastrous news for my app so I am still hoping someone will give me a way to use a WebView that does not need an Activity (or a substitute for a WebView that can accomplish the same)

Pierre
  • 1,607
  • 3
  • 21
  • 33
  • 1
    This is true, there's NO PROPER WAY you can use a webview outside of an ActivityContext, Randy's answer may be a solution for this, but it is never recommended to create a window on top of all other apps. – Aaron Jul 18 '17 at 07:09
  • Hi Pierre. I don't this your comment on code_yoda's answer is valid today as it requires finish() to be performed before onResume() https://commonsware.com/blog/2015/11/02/psa-android-6p0-theme.nodisplay-regression.html . However, even the activity is destroyed, the context of activity is still present in web view client associated with activity with NoDisplay theme (I don't know how). Would you like to comment on this? I have also added an alternative answer as well, but if there is some feasibility based on code_yoda solution I think we can build a solution around it. – Shubham AgaRwal Jun 12 '19 at 12:10
8

You can use this to hide the Activity

         <activity android:name="MyActivity"
          android:label="@string/app_name"
          android:theme="@android:style/Theme.NoDisplay">

Doing this will prevent the app from showing any Activity. And then you can do your stuff in the Activity.

Code_Yoga
  • 2,968
  • 6
  • 30
  • 49
  • 1
    The activity is still "shown", just not really visible. Would closing it and have a reference to the WebView still allow to use the WebView later? – android developer Aug 30 '18 at 16:25
3

the solution was like this, but with Looper.getMainLooper() :

https://github.com/JonasCz/save-for-offline/blob/master/app/src/main/java/jonas/tool/saveForOffline/ScreenshotService.java

@Override
public void onCreate() {
    super.onCreate();
    //HandlerThread thread = new HandlerThread("ScreenshotService", Process.THREAD_PRIORITY_BACKGROUND);
    //thread.start();
    //mServiceHandler = new ServiceHandler(thread.getLooper()); // not working
    mServiceHandler = new ServiceHandler(Looper.getMainLooper()); // working
}

with help of @JonasCz : https://stackoverflow.com/a/28234761/466363

Community
  • 1
  • 1
Shimon Doodkin
  • 4,310
  • 34
  • 37
2

I used the following code to get round this problem:

Handler handler = new Handler(Looper.getMainLooper());
try
{
    handler.post(
        new Runnable()
        {
            @Override
            public void run()
            {
                ProcessRequest(); // Where this method runs the code you're needing
            }
        }
    );
} catch (Exception e)
{
    e.printStackTrace();
}
Graeme Campbell
  • 364
  • 2
  • 4
1

A WebView cannot exist outside of an Activity or Fragment due to it being a UI. However, this means that an Activity is only needed to create the WebView, not handle all its requests.

If you create the invisible WebView in your main activity and have it accessible from a static context, you should be able to perform tasks in the view in the background from anywhere, since I believe all of WebView's IO is done asynchronously.

To take away the ick of that global access, you could always launch a Service with a reference to the WebView to do the work you need.

0

or a substitute for a WebView that can accomplish the same <=== if you do not wish to show the loaded info on UI, maybe you can try to use HTTP to call the url directly, and process on the returned response from HTTP

Nartus Team
  • 170
  • 1
  • 7
0

Why don't you create a Backend Service that does the scraping for you?

And then you just poll results from a RESTful Webservice or even use a messaging middleware (e.g. ZeroMQ).

Maybe more elegant if it fits your use case: let the Scraping Service send your App Push Messages via GCM :)

McOzD
  • 181
  • 1
  • 1
  • 10
  • 1
    Reason for this is because the backend will get detected as a bot scraping from the same IP and get blocked (in addition to backend resources needed to do a lot of scraping on different pages). – Pierre Mar 08 '16 at 13:29
  • because not everything is happening while online – Benoit Mar 16 '17 at 04:02
0

I am not sure if this is a silver bullet to the given problem. As per @Pierre's accepted answer (sounds correct to me)

there is NO possible way to use a WebView in the background while the user is doing other things on the phone without interrupting the user by means of an Activity.

Thus, I believe there must be some architectural/flow/strategy changes that must be done in order to solve this problem.

Proposed Solution #1: Instead of getting a push notification from the server and run a background job and followed by running some JS code or WebView. Instead, Whenever user launch the application one should query the backend server to know whether there is any need to perform any scraping or not. And on the basis of backend input android client can run JS code or WebView and pass the result back to the server.

I haven't tried this solution. But hope it is feasible.


This will also solve the following problem stated in the comments:

Reason for this is because the backend will get detected as a bot scraping from the same IP and get blocked (in addition to backend resources needed to do a lot of scraping on different pages).

Data might be unavailable for some time (until some user scrape it for you). But surely we can provide a better user experience to the end users using this strategy.

Shubham AgaRwal
  • 4,355
  • 8
  • 41
  • 62
-1

I know it'a been a year and a half, but I'm now facing the same issue. I solved it eventually by running my Javascript code inside a Node engine that is running inside my Android App. It's called JXCore. You can take a look. Also, take a look at this sample that runs Javascript without a WebView. I really would like to know what did you end up using?

oriharel
  • 10,418
  • 14
  • 48
  • 57