207 lines
8.0 KiB
Java
207 lines
8.0 KiB
Java
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
package org.mozilla.gecko.tabqueue;
|
|
|
|
import android.app.Service;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.res.Resources;
|
|
import android.graphics.PixelFormat;
|
|
import android.os.Handler;
|
|
import android.os.HandlerThread;
|
|
import android.os.IBinder;
|
|
import android.util.Log;
|
|
import android.view.Gravity;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.WindowManager;
|
|
import android.widget.Button;
|
|
import android.widget.TextView;
|
|
import org.mozilla.gecko.BrowserApp;
|
|
import org.mozilla.gecko.GeckoProfile;
|
|
import org.mozilla.gecko.R;
|
|
import org.mozilla.gecko.mozglue.ContextUtils;
|
|
|
|
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.Executors;
|
|
|
|
|
|
/**
|
|
* On launch this Service displays a View over the currently running process with an action to open the url in Fennec
|
|
* immediately. If the user takes no action, allowing the runnable to be processed after the specified
|
|
* timeout (TOAST_TIMEOUT), the url is added to a file which is then read in Fennec on next launch, this allows the
|
|
* user to quickly queue urls to open without having to open Fennec each time. If the Service receives an Intent whilst
|
|
* the created View is still active, the old url is immediately processed and the View is re-purposed with the new
|
|
* Intent data.
|
|
* <p/>
|
|
* The SYSTEM_ALERT_WINDOW permission is used to allow us to insert a View from this Service which responds to user
|
|
* interaction, whilst still allowing whatever is in the background to be seen and interacted with.
|
|
* <p/>
|
|
* Using an Activity to do this doesn't seem to work as there's an issue to do with the native android intent resolver
|
|
* dialog not being hidden when the toast is shown. Using an IntentService instead of a Service doesn't work as
|
|
* each new Intent received kicks off the IntentService lifecycle anew which means that a new View is created each time,
|
|
* meaning that we can't quickly queue the current data and re-purpose the View. The asynchronous nature of the
|
|
* IntentService is another prohibitive factor.
|
|
* <p/>
|
|
* General approach taken is similar to the FB chat heads functionality:
|
|
* http://stackoverflow.com/questions/15975988/what-apis-in-android-is-facebook-using-to-create-chat-heads
|
|
*/
|
|
public class TabQueueService extends Service {
|
|
private static final String LOGTAG = "Gecko" + TabQueueService.class.getSimpleName();
|
|
|
|
private static final long TOAST_TIMEOUT = 3000;
|
|
|
|
private WindowManager windowManager;
|
|
private View toastLayout;
|
|
private Button openNowButton;
|
|
private Handler tabQueueHandler;
|
|
private WindowManager.LayoutParams toastLayoutParams;
|
|
private volatile StopServiceRunnable stopServiceRunnable;
|
|
private HandlerThread handlerThread;
|
|
private ExecutorService executorService;
|
|
|
|
@Override
|
|
public IBinder onBind(Intent intent) {
|
|
// Not used
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public void onCreate() {
|
|
super.onCreate();
|
|
executorService = Executors.newSingleThreadExecutor();
|
|
|
|
handlerThread = new HandlerThread("TabQueueHandlerThread");
|
|
handlerThread.start();
|
|
tabQueueHandler = new Handler(handlerThread.getLooper());
|
|
|
|
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
|
|
|
|
LayoutInflater layoutInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
|
|
toastLayout = layoutInflater.inflate(R.layout.button_toast, null);
|
|
|
|
final Resources resources = getResources();
|
|
|
|
TextView messageView = (TextView) toastLayout.findViewById(R.id.toast_message);
|
|
messageView.setText(resources.getText(R.string.tab_queue_toast_message));
|
|
|
|
openNowButton = (Button) toastLayout.findViewById(R.id.toast_button);
|
|
openNowButton.setText(resources.getText(R.string.tab_queue_toast_action));
|
|
|
|
toastLayoutParams = new WindowManager.LayoutParams(
|
|
WindowManager.LayoutParams.WRAP_CONTENT,
|
|
WindowManager.LayoutParams.WRAP_CONTENT,
|
|
WindowManager.LayoutParams.TYPE_PHONE,
|
|
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
|
|
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
|
|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
|
PixelFormat.TRANSLUCENT);
|
|
|
|
toastLayoutParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
|
|
}
|
|
|
|
@Override
|
|
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
|
if (stopServiceRunnable != null) {
|
|
// If we're already displaying a toast, keep displaying it but store the previous url.
|
|
// The open button will refer to the most recently opened link.
|
|
tabQueueHandler.removeCallbacks(stopServiceRunnable);
|
|
stopServiceRunnable.run(false);
|
|
} else {
|
|
windowManager.addView(toastLayout, toastLayoutParams);
|
|
}
|
|
|
|
stopServiceRunnable = new StopServiceRunnable(startId) {
|
|
@Override
|
|
public void onRun() {
|
|
addURLToTabQueue(intent, TabQueueHelper.FILE_NAME);
|
|
stopServiceRunnable = null;
|
|
}
|
|
};
|
|
|
|
openNowButton.setOnClickListener(new View.OnClickListener() {
|
|
@Override
|
|
public void onClick(final View view) {
|
|
tabQueueHandler.removeCallbacks(stopServiceRunnable);
|
|
stopServiceRunnable = null;
|
|
|
|
|
|
Intent forwardIntent = new Intent(intent);
|
|
forwardIntent.setClass(getApplicationContext(), BrowserApp.class);
|
|
forwardIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
startActivity(forwardIntent);
|
|
|
|
removeView();
|
|
}
|
|
});
|
|
|
|
tabQueueHandler.postDelayed(stopServiceRunnable, TOAST_TIMEOUT);
|
|
|
|
return START_FLAG_REDELIVERY;
|
|
}
|
|
|
|
private void removeView() {
|
|
windowManager.removeView(toastLayout);
|
|
}
|
|
|
|
private void addURLToTabQueue(final Intent intent, final String filename) {
|
|
if (intent == null) {
|
|
// This should never happen, but let's return silently instead of crashing if it does.
|
|
Log.w(LOGTAG, "Error adding URL to tab queue - invalid intent passed in.");
|
|
return;
|
|
}
|
|
final ContextUtils.SafeIntent safeIntent = new ContextUtils.SafeIntent(intent);
|
|
final String intentData = safeIntent.getDataString();
|
|
|
|
// As we're doing disk IO, let's run this stuff in a separate thread.
|
|
executorService.submit(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
Context applicationContext = getApplicationContext();
|
|
final GeckoProfile profile = GeckoProfile.get(applicationContext);
|
|
int tabsQueued = TabQueueHelper.queueURL(profile, intentData, filename);
|
|
TabQueueHelper.showNotification(applicationContext, tabsQueued);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
super.onDestroy();
|
|
tabQueueHandler = null;
|
|
handlerThread.quit();
|
|
}
|
|
|
|
/**
|
|
* A modified Runnable which additionally removes the view from the window view hierarchy and stops the service
|
|
* when run, unless explicitly instructed not to.
|
|
*/
|
|
private abstract class StopServiceRunnable implements Runnable {
|
|
|
|
private final int startId;
|
|
|
|
public StopServiceRunnable(final int startId) {
|
|
this.startId = startId;
|
|
}
|
|
|
|
public void run(final boolean shouldStopService) {
|
|
onRun();
|
|
|
|
if (shouldStopService) {
|
|
removeView();
|
|
}
|
|
|
|
stopSelfResult(startId);
|
|
}
|
|
|
|
public void run() {
|
|
run(true);
|
|
}
|
|
|
|
public abstract void onRun();
|
|
}
|
|
} |