Bug 793053 - Part 3: product announcements service. r=nalexander

This commit is contained in:
Richard Newman 2012-10-26 17:37:49 -07:00
Родитель fdcce4984a
Коммит ca1a363d61
17 изменённых файлов: 1085 добавлений и 1 удалений

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -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>trycatch</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>