зеркало из https://github.com/mozilla/gecko-dev.git
Bug 793053 - Part 3: product announcements service. r=nalexander
This commit is contained in:
Родитель
fdcce4984a
Коммит
ca1a363d61
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -0,0 +1,20 @@
|
|||
/* 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.background;
|
||||
|
||||
/**
|
||||
* Constants that are not specific to any individual background service.
|
||||
*/
|
||||
public class BackgroundConstants {
|
||||
|
||||
public static final int SHARED_PREFERENCES_MODE = 0;
|
||||
// These are used to ask Fennec (via reflection) to send
|
||||
// us a pref notification. This avoids us having to guess
|
||||
// Fennec's prefs branch and pref name.
|
||||
// Eventually Fennec might listen to startup notifications and
|
||||
// do this automatically, but this will do for now. See Bug 800244.
|
||||
public static String GECKO_PREFERENCES_CLASS = "org.mozilla.gecko.GeckoPreferences";
|
||||
public static String GECKO_BROADCAST_METHOD = "broadcastAnnouncementsPref";
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/* 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.background.announcements;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
|
||||
/**
|
||||
* Represents a retrieved product announcement.
|
||||
*
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
public class Announcement {
|
||||
private static final String LOG_TAG = "Announcement";
|
||||
|
||||
private static final String KEY_ID = "id";
|
||||
private static final String KEY_TITLE = "title";
|
||||
private static final String KEY_URL = "url";
|
||||
private static final String KEY_TEXT = "text";
|
||||
|
||||
private final int id;
|
||||
private final String title;
|
||||
private final URI uri;
|
||||
private final String text;
|
||||
|
||||
public Announcement(int id, String title, String text, URI uri) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.uri = uri;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public static Announcement parseAnnouncement(ExtendedJSONObject body) throws URISyntaxException, IllegalArgumentException {
|
||||
final Integer id = body.getIntegerSafely(KEY_ID);
|
||||
if (id == null) {
|
||||
throw new IllegalArgumentException("No id provided in JSON.");
|
||||
}
|
||||
final String title = body.getString(KEY_TITLE);
|
||||
if (title == null || title.trim().length() == 0) {
|
||||
throw new IllegalArgumentException("Missing or empty announcement title.");
|
||||
}
|
||||
final String uri = body.getString(KEY_URL);
|
||||
if (uri == null) {
|
||||
// Empty or otherwise unhappy URI will throw a URISyntaxException.
|
||||
throw new IllegalArgumentException("Missing announcement URI.");
|
||||
}
|
||||
|
||||
final String text = body.getString(KEY_TEXT);
|
||||
if (text == null) {
|
||||
throw new IllegalArgumentException("Missing announcement body.");
|
||||
}
|
||||
|
||||
return new Announcement(id, title, text, new URI(uri));
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public URI getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public ExtendedJSONObject asJSON() {
|
||||
ExtendedJSONObject out = new ExtendedJSONObject();
|
||||
out.put(KEY_ID, id);
|
||||
out.put(KEY_TITLE, title);
|
||||
out.put(KEY_URL, uri.toASCIIString());
|
||||
out.put(KEY_TEXT, text);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return false if the provided Announcement is in some way invalid,
|
||||
* regardless of being well-formed.
|
||||
*/
|
||||
public static boolean isValidAnnouncement(final Announcement an) {
|
||||
final URI uri = an.getUri();
|
||||
if (uri == null) {
|
||||
Logger.warn(LOG_TAG, "No URI: announcement not valid.");
|
||||
return false;
|
||||
}
|
||||
|
||||
final String scheme = uri.getScheme();
|
||||
if (scheme == null) {
|
||||
Logger.warn(LOG_TAG, "Null scheme: announcement not valid.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only allow HTTP and HTTPS URLs.
|
||||
if (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) {
|
||||
Logger.warn(LOG_TAG, "Scheme '" + scheme + "' forbidden: announcement not valid.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/* 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.background.announcements;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.sync.GlobalConstants;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
/**
|
||||
* Handle requests to display a fetched announcement.
|
||||
*/
|
||||
public class AnnouncementPresenter {
|
||||
|
||||
/**
|
||||
* Display the provided snippet.
|
||||
* @param context
|
||||
* The context instance to use when obtaining the NotificationManager.
|
||||
* @param notificationID
|
||||
* A unique ID for this notification.
|
||||
* @param title
|
||||
* The *already localized* String title. Must not be null.
|
||||
* @param body
|
||||
* The *already localized* String body. Must not be null.
|
||||
* @param uri
|
||||
* The URL to open when the notification is tapped.
|
||||
*/
|
||||
public static void displayAnnouncement(final Context context,
|
||||
final int notificationID,
|
||||
final String title,
|
||||
final String body,
|
||||
final URI uri) {
|
||||
final String ns = Context.NOTIFICATION_SERVICE;
|
||||
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(ns);
|
||||
|
||||
// Set pending intent associated with the notification.
|
||||
Uri u = Uri.parse(uri.toASCIIString());
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, u);
|
||||
|
||||
// Always open the link with Fennec.
|
||||
intent.setClassName(GlobalConstants.BROWSER_INTENT_PACKAGE, GlobalConstants.BROWSER_INTENT_CLASS);
|
||||
PendingIntent contentIntent = PendingIntent.getActivity(context, 0, intent, 0);
|
||||
|
||||
final int icon = R.drawable.icon;
|
||||
|
||||
// Deprecated approach to building a notification.
|
||||
final long when = System.currentTimeMillis();
|
||||
Notification notification = new Notification(icon, title, when);
|
||||
notification.flags = Notification.FLAG_AUTO_CANCEL;
|
||||
notification.setLatestEventInfo(context, title, body, contentIntent);
|
||||
|
||||
// Notification.Builder since API 11.
|
||||
/*
|
||||
Notification notification = new Notification.Builder(context)
|
||||
.setContentTitle(title)
|
||||
.setContentText(body)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(contentIntent).getNotification();
|
||||
*/
|
||||
|
||||
// Send notification.
|
||||
notificationManager.notify(notificationID, notification);
|
||||
}
|
||||
|
||||
public static void displayAnnouncement(final Context context,
|
||||
final Announcement snippet) {
|
||||
final int notificationID = snippet.getId();
|
||||
final String title = snippet.getTitle();
|
||||
final String body = snippet.getText();
|
||||
final URI uri = snippet.getUri();
|
||||
AnnouncementPresenter.displayAnnouncement(context, notificationID, title, body, uri);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/* 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.background.announcements;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* Watch for notifications to start the announcements service.
|
||||
*
|
||||
* Some observations:
|
||||
*
|
||||
* "Also note that as of Android 3.0 the user needs to have started the
|
||||
* application at least once before your application can receive
|
||||
* android.intent.action.BOOT_COMPLETED events."
|
||||
*/
|
||||
public class AnnouncementsBroadcastReceiver extends BroadcastReceiver {
|
||||
|
||||
/**
|
||||
* Forward the intent to an IntentService to do background processing.
|
||||
*/
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Intent service = new Intent(context, AnnouncementsBroadcastService.class);
|
||||
service.putExtras(intent);
|
||||
service.setAction(intent.getAction());
|
||||
context.startService(service);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
/* 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.background.announcements;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.mozilla.gecko.background.BackgroundConstants;
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.IntentService;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
|
||||
/**
|
||||
* A service which listens to broadcast intents from the system and from the
|
||||
* browser, registering or unregistering the main
|
||||
* {@link AnnouncementsStartReceiver} with the {@link AlarmManager}.
|
||||
*/
|
||||
public class AnnouncementsBroadcastService extends IntentService {
|
||||
private static final String WORKER_THREAD_NAME = "AnnouncementsBroadcastServiceWorker";
|
||||
private static final String LOG_TAG = "AnnounceBrSvc";
|
||||
|
||||
public AnnouncementsBroadcastService() {
|
||||
super(WORKER_THREAD_NAME);
|
||||
}
|
||||
|
||||
private void toggleAlarm(final Context context, boolean enabled) {
|
||||
Logger.info(LOG_TAG, (enabled ? "R" : "Unr") + "egistering announcements broadcast receiver...");
|
||||
final AlarmManager alarm = getAlarmManager(context);
|
||||
|
||||
final Intent service = new Intent(context, AnnouncementsStartReceiver.class);
|
||||
final PendingIntent pending = PendingIntent.getBroadcast(context, 0, service, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
|
||||
if (!enabled) {
|
||||
alarm.cancel(pending);
|
||||
return;
|
||||
}
|
||||
|
||||
final long firstEvent = System.currentTimeMillis();
|
||||
final long pollInterval = getPollInterval(context);
|
||||
Logger.info(LOG_TAG, "Setting inexact repeating alarm for interval " + pollInterval);
|
||||
alarm.setInexactRepeating(AlarmManager.RTC, firstEvent, pollInterval, pending);
|
||||
}
|
||||
|
||||
private static AlarmManager getAlarmManager(Context context) {
|
||||
return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
}
|
||||
|
||||
private void recordLastLaunch(final Context context) {
|
||||
final SharedPreferences preferences = context.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, BackgroundConstants.SHARED_PREFERENCES_MODE);
|
||||
preferences.edit().putLong(AnnouncementsConstants.PREF_LAST_LAUNCH, System.currentTimeMillis()).commit();
|
||||
}
|
||||
|
||||
public static long getPollInterval(final Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, BackgroundConstants.SHARED_PREFERENCES_MODE);
|
||||
return preferences.getLong(AnnouncementsConstants.PREF_ANNOUNCE_FETCH_INTERVAL_MSEC, AnnouncementsConstants.DEFAULT_ANNOUNCE_FETCH_INTERVAL_MSEC);
|
||||
}
|
||||
|
||||
public static void setPollInterval(final Context context, long interval) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, BackgroundConstants.SHARED_PREFERENCES_MODE);
|
||||
preferences.edit().putLong(AnnouncementsConstants.PREF_ANNOUNCE_FETCH_INTERVAL_MSEC, interval).commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
Logger.setThreadLogTag(AnnouncementsConstants.GLOBAL_LOG_TAG);
|
||||
final String action = intent.getAction();
|
||||
Logger.debug(LOG_TAG, "Broadcast onReceive. Intent is " + action);
|
||||
|
||||
if (AnnouncementsConstants.ACTION_ANNOUNCEMENTS_PREF.equals(action)) {
|
||||
handlePrefIntent(intent);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Intent.ACTION_BOOT_COMPLETED.equals(action) ||
|
||||
Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
|
||||
handleSystemLifetimeIntent();
|
||||
return;
|
||||
}
|
||||
|
||||
// Failure case.
|
||||
Logger.warn(LOG_TAG, "Unknown intent " + action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle one of the system intents to which we listen to launch our service
|
||||
* without the browser being opened.
|
||||
*
|
||||
* To avoid tight coupling to Fennec, we use reflection to find
|
||||
* <code>GeckoPreferences</code>, invoking the same code path that
|
||||
* <code>GeckoApp</code> uses on startup to send the <i>other</i>
|
||||
* notification to which we listen.
|
||||
*
|
||||
* All of this is neatly wrapped in <code>try…catch</code>, so this code
|
||||
* will run safely without a Firefox build installed.
|
||||
*/
|
||||
protected void handleSystemLifetimeIntent() {
|
||||
// Ask the browser to tell us the current state of the preference.
|
||||
try {
|
||||
Class<?> geckoPreferences = Class.forName(BackgroundConstants.GECKO_PREFERENCES_CLASS);
|
||||
Method broadcastSnippetsPref = geckoPreferences.getMethod(BackgroundConstants.GECKO_BROADCAST_METHOD, Context.class);
|
||||
broadcastSnippetsPref.invoke(null, this);
|
||||
return;
|
||||
} catch (ClassNotFoundException e) {
|
||||
Logger.error(LOG_TAG, "Class " + BackgroundConstants.GECKO_PREFERENCES_CLASS + " not found!");
|
||||
return;
|
||||
} catch (NoSuchMethodException e) {
|
||||
Logger.error(LOG_TAG, "Method " + BackgroundConstants.GECKO_PREFERENCES_CLASS + "/" + BackgroundConstants.GECKO_BROADCAST_METHOD + " not found!");
|
||||
return;
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Fall through.
|
||||
} catch (IllegalAccessException e) {
|
||||
// Fall through.
|
||||
} catch (InvocationTargetException e) {
|
||||
// Fall through.
|
||||
}
|
||||
Logger.error(LOG_TAG, "Got exception invoking " + BackgroundConstants.GECKO_BROADCAST_METHOD + ".");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the intent sent by the browser when it wishes to notify us
|
||||
* of the value of the user preference. Look at the value and toggle the
|
||||
* alarm service accordingly.
|
||||
*/
|
||||
protected void handlePrefIntent(Intent intent) {
|
||||
recordLastLaunch(this); // TODO: wrong place!
|
||||
|
||||
if (!intent.hasExtra("enabled")) {
|
||||
Logger.warn(LOG_TAG, "Got ANNOUNCEMENTS_PREF intent without enabled. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean enabled = intent.getBooleanExtra("enabled", true);
|
||||
Logger.debug(LOG_TAG, intent.getStringExtra("branch") + "/" +
|
||||
intent.getStringExtra("pref") + " = " +
|
||||
(intent.hasExtra("enabled") ? enabled : ""));
|
||||
|
||||
toggleAlarm(this, enabled);
|
||||
|
||||
// Primarily intended for debugging and testing, but this doesn't do any harm.
|
||||
if (!enabled) {
|
||||
Logger.info(LOG_TAG, "!enabled: clearing last fetch.");
|
||||
final SharedPreferences sharedPreferences = this.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH,
|
||||
BackgroundConstants.SHARED_PREFERENCES_MODE);
|
||||
final Editor editor = sharedPreferences.edit();
|
||||
editor.remove(AnnouncementsConstants.PREF_LAST_FETCH);
|
||||
editor.remove(AnnouncementsConstants.PREF_EARLIEST_NEXT_ANNOUNCE_FETCH);
|
||||
editor.commit();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/* 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.background.announcements;
|
||||
|
||||
import org.mozilla.gecko.sync.GlobalConstants;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
|
||||
public class AnnouncementsConstants {
|
||||
public static final String GLOBAL_LOG_TAG = "GeckoAnnounce";
|
||||
public static final String ACTION_ANNOUNCEMENTS_PREF = "org.mozilla.gecko.ANNOUNCEMENTS_PREF";
|
||||
|
||||
static final String PREFS_BRANCH = "background";
|
||||
static final String PREF_LAST_FETCH = "last_fetch";
|
||||
static final String PREF_LAST_LAUNCH = "last_firefox_launch";
|
||||
static final String PREF_ANNOUNCE_SERVER_BASE_URL = "announce_server_base_url";
|
||||
static final String PREF_EARLIEST_NEXT_ANNOUNCE_FETCH = "earliest_next_announce_fetch";
|
||||
static final String PREF_ANNOUNCE_FETCH_INTERVAL_MSEC = "announce_fetch_interval_msec";
|
||||
|
||||
static final String DEFAULT_ANNOUNCE_SERVER_BASE_URL = "https://campaigns.services.mozilla.com/announce/";
|
||||
public static final String ANNOUNCE_PROTOCOL_VERSION = "1";
|
||||
public static final String ANNOUNCE_APPLICATION = "android";
|
||||
public static final String ANNOUNCE_PATH_SUFFIX = AnnouncementsConstants.ANNOUNCE_PROTOCOL_VERSION + "/" +
|
||||
AnnouncementsConstants.ANNOUNCE_APPLICATION + "/";
|
||||
|
||||
public static final long DEFAULT_ANNOUNCE_FETCH_INTERVAL_MSEC = AlarmManager.INTERVAL_HALF_DAY;
|
||||
public static final long DEFAULT_BACKOFF_MSEC = 2 * 24 * 60 * 60 * 1000; // Two days. Used if no Retry-After header.
|
||||
|
||||
public static final String ANNOUNCE_USER_AGENT = "Firefox Announcements " + GlobalConstants.MOZ_APP_VERSION;
|
||||
public static final String ANNOUNCE_CHANNEL = GlobalConstants.MOZ_UPDATE_CHANNEL.replace("default", GlobalConstants.MOZ_OFFICIAL_BRANDING ? "release" : "dev");
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/* 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.background.announcements;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public interface AnnouncementsFetchDelegate {
|
||||
/**
|
||||
* @return the timestamp of the last fetch in milliseconds.
|
||||
*/
|
||||
public long getLastFetch();
|
||||
|
||||
/**
|
||||
* @return the current system locale (e.g., en_us).
|
||||
*/
|
||||
public Locale getLocale();
|
||||
|
||||
/**
|
||||
* @return the User-Agent header to use for the request.
|
||||
*/
|
||||
public String getUserAgent();
|
||||
|
||||
/**
|
||||
* @return the server URL to interrogate, including path.
|
||||
*/
|
||||
public String getServiceURL();
|
||||
|
||||
/*
|
||||
* Callback methods.
|
||||
*/
|
||||
public void onNoNewAnnouncements(long fetched);
|
||||
public void onNewAnnouncements(List<Announcement> snippets, long fetched);
|
||||
public void onLocalError(Exception e);
|
||||
public void onRemoteError(Exception e);
|
||||
public void onRemoteFailure(int status);
|
||||
public void onBackoff(int retryAfterInSeconds);
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
/* 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.background.announcements;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
import org.mozilla.gecko.sync.NonArrayJSONException;
|
||||
import org.mozilla.gecko.sync.net.BaseResource;
|
||||
import org.mozilla.gecko.sync.net.Resource;
|
||||
import org.mozilla.gecko.sync.net.SyncResourceDelegate;
|
||||
import org.mozilla.gecko.sync.net.SyncResponse;
|
||||
|
||||
import ch.boye.httpclientandroidlib.HttpResponse;
|
||||
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
|
||||
import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
|
||||
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
|
||||
import ch.boye.httpclientandroidlib.impl.cookie.DateUtils;
|
||||
|
||||
/**
|
||||
* Converts HTTP resource callbacks into AnnouncementsFetchDelegate callbacks.
|
||||
*/
|
||||
public class AnnouncementsFetchResourceDelegate extends SyncResourceDelegate {
|
||||
private static final String ACCEPT_HEADER = "application/json;charset=utf-8";
|
||||
|
||||
private static final String LOG_TAG = "AnnounceFetchRD";
|
||||
|
||||
protected final long startTime;
|
||||
protected AnnouncementsFetchDelegate delegate;
|
||||
|
||||
public AnnouncementsFetchResourceDelegate(Resource resource, AnnouncementsFetchDelegate delegate) {
|
||||
super(resource);
|
||||
this.startTime = System.currentTimeMillis();
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
|
||||
super.addHeaders(request, client);
|
||||
|
||||
// The basics.
|
||||
request.addHeader("User-Agent", delegate.getUserAgent());
|
||||
request.addHeader("Accept-Language", delegate.getLocale().toString());
|
||||
request.addHeader("Accept", ACCEPT_HEADER);
|
||||
|
||||
// Set If-Modified-Since to avoid re-fetching content.
|
||||
final long ifModifiedSince = delegate.getLastFetch();
|
||||
if (ifModifiedSince > 0) {
|
||||
final String imsHeader = DateUtils.formatDate(new Date(ifModifiedSince));
|
||||
Logger.info(LOG_TAG, "If-Modified-Since: " + imsHeader);
|
||||
request.addHeader("If-Modified-Since", imsHeader);
|
||||
}
|
||||
|
||||
// Just in case.
|
||||
request.removeHeaders("Cookie");
|
||||
}
|
||||
|
||||
private List<Announcement> parseBody(ExtendedJSONObject body) throws NonArrayJSONException {
|
||||
List<Announcement> out = new ArrayList<Announcement>(1);
|
||||
JSONArray snippets = body.getArray("announcements");
|
||||
if (snippets == null) {
|
||||
Logger.warn(LOG_TAG, "Missing announcements body. Returning empty.");
|
||||
return out;
|
||||
}
|
||||
|
||||
for (Object s : snippets) {
|
||||
try {
|
||||
out.add(Announcement.parseAnnouncement(new ExtendedJSONObject((JSONObject) s)));
|
||||
} catch (Exception e) {
|
||||
Logger.warn(LOG_TAG, "Malformed announcement or display failed. Skipping.", e);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpResponse(HttpResponse response) {
|
||||
SyncResponse r = new SyncResponse(response); // For convenience.
|
||||
|
||||
try {
|
||||
final int statusCode = r.getStatusCode();
|
||||
Logger.debug(LOG_TAG, "Got announcements response: " + statusCode);
|
||||
|
||||
if (statusCode == 204 || statusCode == 304) {
|
||||
BaseResource.consumeEntity(response);
|
||||
delegate.onNoNewAnnouncements(startTime);
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusCode == 200) {
|
||||
final List<Announcement> snippets;
|
||||
try {
|
||||
snippets = parseBody(r.jsonObjectBody());
|
||||
} catch (Exception e) {
|
||||
delegate.onRemoteError(e);
|
||||
return;
|
||||
}
|
||||
delegate.onNewAnnouncements(snippets, startTime);
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusCode == 400 || statusCode == 405) {
|
||||
// We did something wrong.
|
||||
Logger.warn(LOG_TAG, "We did something wrong. Oh dear.");
|
||||
// Fall through.
|
||||
}
|
||||
|
||||
if (statusCode == 503 || statusCode == 500) {
|
||||
Logger.warn(LOG_TAG, "Server issue: " + r.body());
|
||||
delegate.onBackoff(r.retryAfterInSeconds());
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, clean up.
|
||||
delegate.onRemoteFailure(statusCode);
|
||||
|
||||
} catch (Exception e) {
|
||||
Logger.warn(LOG_TAG, "Failed to extract body.", e);
|
||||
delegate.onRemoteError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpProtocolException(ClientProtocolException e) {
|
||||
Logger.warn(LOG_TAG, "Protocol exception.", e);
|
||||
delegate.onLocalError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpIOException(IOException e) {
|
||||
Logger.warn(LOG_TAG, "IO exception.", e);
|
||||
delegate.onLocalError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportException(GeneralSecurityException e) {
|
||||
Logger.warn(LOG_TAG, "Transport exception.", e);
|
||||
// Class this as a remote error, because it's probably something odd
|
||||
// with SSL negotiation.
|
||||
delegate.onRemoteError(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Be very thorough in case the superclass implementation changes.
|
||||
* We never want this to be an authenticated request.
|
||||
*/
|
||||
@Override
|
||||
public String getCredentials() {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/* 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.background.announcements;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
|
||||
import org.mozilla.gecko.sync.GlobalConstants;
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
import org.mozilla.gecko.sync.net.BaseResource;
|
||||
|
||||
public class AnnouncementsFetcher {
|
||||
private static final String LOG_TAG = "AnnounceFetch";
|
||||
private static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
public static URI getSnippetURI(String base, String channel,
|
||||
String version, String platform,
|
||||
int idleDays)
|
||||
throws URISyntaxException {
|
||||
try {
|
||||
final String c = URLEncoder.encode(channel, "UTF-8");
|
||||
final String v = URLEncoder.encode(version, "UTF-8");
|
||||
final String p = URLEncoder.encode(platform, "UTF-8");
|
||||
final String s = base + c + "/" + v + "/" + p + ((idleDays == -1) ? "" : ("?idle=" + idleDays));
|
||||
return new URI(s);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// Nonsense.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static URI getAnnounceURI(final String baseURL, final long lastLaunch) throws URISyntaxException {
|
||||
final String channel = getChannel();
|
||||
final String version = getVersion();
|
||||
final String platform = getPlatform();
|
||||
final int idleDays = getIdleDays(lastLaunch);
|
||||
|
||||
Logger.debug(LOG_TAG, "Fetch URI: idle for " + idleDays + " days.");
|
||||
return getSnippetURI(baseURL, channel, version, platform, idleDays);
|
||||
}
|
||||
|
||||
protected static String getChannel() {
|
||||
return AnnouncementsConstants.ANNOUNCE_CHANNEL;
|
||||
}
|
||||
|
||||
protected static String getVersion() {
|
||||
return GlobalConstants.MOZ_APP_VERSION;
|
||||
}
|
||||
|
||||
protected static String getPlatform() {
|
||||
return GlobalConstants.ANDROID_CPU_ARCH;
|
||||
}
|
||||
|
||||
protected static int getIdleDays(final long lastLaunch) {
|
||||
if (lastLaunch == 0) {
|
||||
return -1;
|
||||
}
|
||||
final long idleMillis = System.currentTimeMillis() - lastLaunch;
|
||||
return (int) (idleMillis / MILLISECONDS_PER_DAY);
|
||||
}
|
||||
|
||||
public static void fetchAnnouncements(URI uri, AnnouncementsFetchDelegate delegate) {
|
||||
BaseResource r = new BaseResource(uri);
|
||||
r.delegate = new AnnouncementsFetchResourceDelegate(r, delegate);
|
||||
r.getBlocking();
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous.
|
||||
*/
|
||||
public static void fetchAndProcessAnnouncements(long lastLaunch,
|
||||
AnnouncementsFetchDelegate delegate) {
|
||||
final long now = System.currentTimeMillis();
|
||||
Logger.debug(LOG_TAG, "Fetching announcements. Last launch: " + lastLaunch + "; now: " + now);
|
||||
try {
|
||||
final String base = delegate.getServiceURL();
|
||||
final URI uri = getAnnounceURI(base, lastLaunch);
|
||||
Logger.info(LOG_TAG, "Fetching announcements from " + uri.toASCIIString());
|
||||
fetchAnnouncements(uri, delegate);
|
||||
} catch (URISyntaxException e) {
|
||||
Logger.warn(LOG_TAG, "Couldn't create URL.", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,281 @@
|
|||
/* 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.background.announcements;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.mozilla.gecko.background.BackgroundConstants;
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
/**
|
||||
* A Service to periodically check for new published announcements,
|
||||
* presenting them to the user if local conditions permit.
|
||||
*
|
||||
* We extend IntentService, rather than just Service, because this gives us
|
||||
* a worker thread to avoid main-thread networking.
|
||||
*
|
||||
* Yes, even though we're in an alarm-triggered service, it still counts
|
||||
* as main-thread.
|
||||
*
|
||||
* The operation of this service is as follows:
|
||||
*
|
||||
* 0. Decide if a request should be made.
|
||||
* 1. Compute the arguments to the request. This includes enough
|
||||
* pertinent details to allow the server to pre-filter a message
|
||||
* set, recording enough tracking details to compute statistics.
|
||||
* 2. Issue the request. If this succeeds with a 200 or 204, great;
|
||||
* track that timestamp for the next run through Step 0.
|
||||
* 3. Process any received messages.
|
||||
*
|
||||
* Message processing is as follows:
|
||||
*
|
||||
* 0. Decide if message display should occur. This might involve
|
||||
* user preference or other kinds of environmental factors.
|
||||
* 1. Use the AnnouncementPresenter to open the announcement.
|
||||
*
|
||||
* Future:
|
||||
* * Persisting of multiple announcements.
|
||||
* * Prioritization.
|
||||
*/
|
||||
public class AnnouncementsService extends IntentService implements AnnouncementsFetchDelegate {
|
||||
private static final String WORKER_THREAD_NAME = "AnnouncementsServiceWorker";
|
||||
private static final String LOG_TAG = "AnnounceService";
|
||||
|
||||
private static final long MINIMUM_FETCH_INTERVAL_MSEC = 60 * 60 * 1000; // 1 hour.
|
||||
|
||||
public AnnouncementsService() {
|
||||
super(WORKER_THREAD_NAME);
|
||||
Logger.setThreadLogTag(AnnouncementsConstants.GLOBAL_LOG_TAG);
|
||||
Logger.debug(LOG_TAG, "Creating AnnouncementsService.");
|
||||
}
|
||||
|
||||
public boolean shouldFetchAnnouncements() {
|
||||
final long now = System.currentTimeMillis();
|
||||
|
||||
if (!backgroundDataIsEnabled()) {
|
||||
Logger.debug(LOG_TAG, "Background data not possible. Skipping.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't fetch if we were told to back off.
|
||||
if (getEarliestNextFetch() > now) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't do anything if we haven't waited long enough.
|
||||
final long lastFetch = getLastFetch();
|
||||
|
||||
// Just in case the alarm manager schedules us more frequently, or something
|
||||
// goes awry with relaunches.
|
||||
if ((now - lastFetch) < MINIMUM_FETCH_INTERVAL_MSEC) {
|
||||
Logger.debug(LOG_TAG, "Returning: minimum fetch interval of " + MINIMUM_FETCH_INTERVAL_MSEC + "ms not met.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the first valid announcement in the list.
|
||||
*/
|
||||
protected void processAnnouncements(final List<Announcement> announcements) {
|
||||
if (announcements == null) {
|
||||
Logger.warn(LOG_TAG, "No announcements to present.");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean presented = false;
|
||||
for (Announcement an : announcements) {
|
||||
// Do this so we at least log, rather than just returning.
|
||||
if (presented) {
|
||||
Logger.warn(LOG_TAG, "Skipping announcement \"" + an.getTitle() + "\": one already shown.");
|
||||
continue;
|
||||
}
|
||||
if (Announcement.isValidAnnouncement(an)) {
|
||||
presented = true;
|
||||
AnnouncementPresenter.displayAnnouncement(this, an);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If it's time to do a fetch -- we've waited long enough,
|
||||
* we're allowed to use background data, etc. -- then issue
|
||||
* a fetch. The subsequent background check is handled implicitly
|
||||
* by the AlarmManager.
|
||||
*/
|
||||
@Override
|
||||
public void onHandleIntent(Intent intent) {
|
||||
Logger.setThreadLogTag(AnnouncementsConstants.GLOBAL_LOG_TAG);
|
||||
Logger.debug(LOG_TAG, "Running AnnouncementsService.");
|
||||
|
||||
if (!shouldFetchAnnouncements()) {
|
||||
Logger.debug(LOG_TAG, "Not fetching.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, grab our announcements URL and process the contents.
|
||||
AnnouncementsFetcher.fetchAndProcessAnnouncements(getLastLaunch(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the OS will allow us to perform background
|
||||
* data operations. This logic varies by OS version.
|
||||
*/
|
||||
protected boolean backgroundDataIsEnabled() {
|
||||
ConnectivityManager connectivity = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||
return connectivity.getBackgroundDataSetting();
|
||||
}
|
||||
NetworkInfo networkInfo = connectivity.getActiveNetworkInfo();
|
||||
if (networkInfo == null) {
|
||||
return false;
|
||||
}
|
||||
return networkInfo.isAvailable();
|
||||
}
|
||||
|
||||
protected long getLastLaunch() {
|
||||
return getSharedPreferences().getLong(AnnouncementsConstants.PREF_LAST_LAUNCH, 0);
|
||||
}
|
||||
|
||||
private SharedPreferences getSharedPreferences() {
|
||||
return this.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, BackgroundConstants.SHARED_PREFERENCES_MODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
|
||||
super.dump(fd, writer, args);
|
||||
|
||||
final long lastFetch = getLastFetch();
|
||||
final long lastLaunch = getLastLaunch();
|
||||
writer.write("AnnouncementsService: last fetch " + lastFetch +
|
||||
", last Firefox activity: " + lastLaunch + "\n");
|
||||
}
|
||||
|
||||
protected void setEarliestNextFetch(final long earliestInMsec) {
|
||||
this.getSharedPreferences().edit().putLong(AnnouncementsConstants.PREF_EARLIEST_NEXT_ANNOUNCE_FETCH, earliestInMsec).commit();
|
||||
}
|
||||
|
||||
protected long getEarliestNextFetch() {
|
||||
return this.getSharedPreferences().getLong(AnnouncementsConstants.PREF_EARLIEST_NEXT_ANNOUNCE_FETCH, 0L);
|
||||
}
|
||||
|
||||
protected void setLastFetch(final long fetch) {
|
||||
this.getSharedPreferences().edit().putLong(AnnouncementsConstants.PREF_LAST_FETCH, fetch).commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLastFetch() {
|
||||
return getSharedPreferences().getLong(AnnouncementsConstants.PREF_LAST_FETCH, 0L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to write the persisted server URL, overriding
|
||||
* the default value.
|
||||
* @param url a URI identifying the full request path, e.g.,
|
||||
* "http://foo.com:1234/announce/"
|
||||
*/
|
||||
public void setAnnouncementsServerBaseURL(final URI url) {
|
||||
if (url == null) {
|
||||
throw new IllegalArgumentException("url cannot be null.");
|
||||
}
|
||||
final String scheme = url.getScheme();
|
||||
if (scheme == null) {
|
||||
throw new IllegalArgumentException("url must have a scheme.");
|
||||
}
|
||||
if (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) {
|
||||
throw new IllegalArgumentException("url must be http or https.");
|
||||
}
|
||||
SharedPreferences p = this.getSharedPreferences();
|
||||
p.edit().putString(AnnouncementsConstants.PREF_ANNOUNCE_SERVER_BASE_URL, url.toASCIIString()).commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the service URL, including protocol version and application identifier. E.g.,
|
||||
*
|
||||
* "https://campaigns.services.mozilla.com/announce/1/android/"
|
||||
*/
|
||||
@Override
|
||||
public String getServiceURL() {
|
||||
SharedPreferences p = this.getSharedPreferences();
|
||||
String base = p.getString(AnnouncementsConstants.PREF_ANNOUNCE_SERVER_BASE_URL, AnnouncementsConstants.DEFAULT_ANNOUNCE_SERVER_BASE_URL);
|
||||
return base + AnnouncementsConstants.ANNOUNCE_PATH_SUFFIX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Locale getLocale() {
|
||||
return Locale.getDefault();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserAgent() {
|
||||
return AnnouncementsConstants.ANNOUNCE_USER_AGENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNoNewAnnouncements(long fetched) {
|
||||
Logger.info(LOG_TAG, "No new announcements to display.");
|
||||
setLastFetch(fetched);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewAnnouncements(List<Announcement> announcements, long fetched) {
|
||||
Logger.info(LOG_TAG, "Processing announcements: " + announcements.size());
|
||||
setLastFetch(fetched);
|
||||
processAnnouncements(announcements);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoteFailure(int status) {
|
||||
// Bump our fetch timestamp.
|
||||
Logger.warn(LOG_TAG, "Got remote fetch status " + status + "; bumping fetch time.");
|
||||
setLastFetch(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoteError(Exception e) {
|
||||
// Bump our fetch timestamp.
|
||||
Logger.warn(LOG_TAG, "Error processing response.", e);
|
||||
setLastFetch(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocalError(Exception e) {
|
||||
Logger.error(LOG_TAG, "Got exception in fetch.", e);
|
||||
// Do nothing yet, so we'll retry.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackoff(int retryAfterInSeconds) {
|
||||
Logger.info(LOG_TAG, "Got retry after: " + retryAfterInSeconds);
|
||||
final long delayInMsec = Math.max(retryAfterInSeconds * 1000, AnnouncementsConstants.DEFAULT_BACKOFF_MSEC);
|
||||
final long fuzzedBackoffInMsec = delayInMsec + Math.round(((double) delayInMsec * 0.25d * Math.random()));
|
||||
Logger.debug(LOG_TAG, "Fuzzed backoff: " + fuzzedBackoffInMsec + "ms.");
|
||||
setEarliestNextFetch(fuzzedBackoffInMsec + System.currentTimeMillis());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/* 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.background.announcements;
|
||||
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* Start the announcements service when instructed by the {@link AlarmManager}.
|
||||
*/
|
||||
public class AnnouncementsStartReceiver extends BroadcastReceiver {
|
||||
|
||||
private static final String LOG_TAG = "AnnounceStartRec";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Logger.debug(LOG_TAG, "AnnouncementsStartReceiver.onReceive().");
|
||||
Intent service = new Intent(context, AnnouncementsService.class);
|
||||
context.startService(service);
|
||||
}
|
||||
}
|
|
@ -311,6 +311,16 @@ public class BaseResource implements Resource {
|
|||
this.go(new HttpGet(this.uri));
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform an HTTP GET as with {@link BaseResource#get()}, returning only
|
||||
* after callbacks have been invoked.
|
||||
*/
|
||||
public void getBlocking() {
|
||||
// Until we use the asynchronous Apache HttpClient, we can simply call
|
||||
// through.
|
||||
this.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() {
|
||||
Logger.debug(LOG_TAG, "HTTP DELETE " + this.uri.toASCIIString());
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
background/announcements/Announcement.java
|
||||
background/announcements/AnnouncementPresenter.java
|
||||
background/announcements/AnnouncementsBroadcastReceiver.java
|
||||
background/announcements/AnnouncementsBroadcastService.java
|
||||
background/announcements/AnnouncementsConstants.java
|
||||
background/announcements/AnnouncementsFetchDelegate.java
|
||||
background/announcements/AnnouncementsFetcher.java
|
||||
background/announcements/AnnouncementsFetchResourceDelegate.java
|
||||
background/announcements/AnnouncementsService.java
|
||||
background/announcements/AnnouncementsStartReceiver.java
|
||||
background/BackgroundConstants.java
|
||||
sync/AlreadySyncingException.java
|
||||
sync/CollectionKeys.java
|
||||
sync/CommandProcessor.java
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<!--
|
||||
As well as these system actions, we also listen for pref notifications
|
||||
sent by Fennec: org.mozilla.gecko.SNIPPETS_PREF.
|
||||
-->
|
||||
<receiver android:name="org.mozilla.gecko.background.announcements.AnnouncementsBroadcastReceiver" >
|
||||
<intent-filter>
|
||||
<!-- Startup. -->
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<!-- SD card remounted. -->
|
||||
<action android:name="android.intent.action.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE" />
|
||||
</intent-filter>
|
||||
<intent-filter >
|
||||
<action android:name="org.mozilla.gecko.ANNOUNCEMENTS_PREF" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name="org.mozilla.gecko.background.announcements.AnnouncementsStartReceiver" >
|
||||
</receiver>
|
|
@ -0,0 +1,2 @@
|
|||
<!-- So we can start our service. -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
@ -0,0 +1,8 @@
|
|||
<service
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.background.announcements.AnnouncementsService" >
|
||||
</service>
|
||||
<service
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.background.announcements.AnnouncementsBroadcastService" >
|
||||
</service>
|
Загрузка…
Ссылка в новой задаче