diff options
author | Ying Wang <wangying@google.com> | 2010-06-15 13:54:16 -0700 |
---|---|---|
committer | Ying Wang <wangying@google.com> | 2010-06-15 13:58:21 -0700 |
commit | 823b6f3516076b92f78c3fc27037d24bb514e653 (patch) | |
tree | 580ce1a902841fdc55901cfc5b4ba29171e14cc6 | |
parent | b87362e0a6de2bf398fd4b135c66adcfbb42d8e7 (diff) | |
download | ex-froyo.tar.gz |
move android-common from framework/base to framework/exandroid-sdk-tools_r7android-sdk-tools_r6android-sdk-2.2_r1android-cts-2.2_r8android-cts-2.2_r7android-cts-2.2_r6android-cts-2.2_r5android-cts-2.2_r4android-cts-2.2_r3android-cts-2.2_r2android-cts-2.2_r1android-adt-0.9.9android-adt-0.9.8tools_r9tools_r8tools_r7froyo-plus-aospfroyo
Also change the LOCAL_SDK_VERSION from current to 8.
Change-Id: I68943b8b41622dab88c7b13d8c067b39205f028e
-rw-r--r-- | common/Android.mk | 35 | ||||
-rw-r--r-- | common/java/com/android/common/ArrayListCursor.java | 167 | ||||
-rw-r--r-- | common/java/com/android/common/GoogleLogTags.logtags | 100 | ||||
-rw-r--r-- | common/java/com/android/common/NetworkConnectivityListener.java | 224 | ||||
-rw-r--r-- | common/java/com/android/common/OperationScheduler.java | 348 | ||||
-rw-r--r-- | common/java/com/android/common/Rfc822InputFilter.java | 74 | ||||
-rw-r--r-- | common/java/com/android/common/Rfc822Validator.java | 134 | ||||
-rw-r--r-- | common/java/com/android/common/Search.java | 38 | ||||
-rw-r--r-- | common/java/com/android/common/speech/LoggingEvents.java | 136 | ||||
-rw-r--r-- | common/java/com/android/common/speech/Recognition.java | 61 | ||||
-rw-r--r-- | common/java/com/android/common/userhappiness/UserHappinessSignals.java | 45 | ||||
-rw-r--r-- | common/tests/Android.mk | 28 | ||||
-rw-r--r-- | common/tests/AndroidManifest.xml | 30 | ||||
-rw-r--r-- | common/tests/src/com/android/common/OperationSchedulerTest.java | 219 | ||||
-rwxr-xr-x | common/tools/make-iana-tld-pattern.py | 144 |
15 files changed, 1783 insertions, 0 deletions
diff --git a/common/Android.mk b/common/Android.mk new file mode 100644 index 00000000..c8dfffc8 --- /dev/null +++ b/common/Android.mk @@ -0,0 +1,35 @@ +# Copyright (C) 2009 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH := $(call my-dir) + +# Note: the source code is in java/, not src/, because this code is also part of +# the framework library, and build/core/pathmap.mk expects a java/ subdirectory. + +include $(CLEAR_VARS) +LOCAL_MODULE := android-common +LOCAL_SDK_VERSION := 8 +LOCAL_SRC_FILES := \ + $(call all-java-files-under, java) \ + $(call all-logtags-files-under, java) +include $(BUILD_STATIC_JAVA_LIBRARY) + +# Include this library in the build server's output directory +$(call dist-for-goals, droid, $(LOCAL_BUILT_MODULE):android-common.jar) + +# Build the test package +# we can't build the test for apps only build, because android.test.runner is not unbundled yet. +ifeq ($(TARGET_BUILD_APPS),) +include $(call all-makefiles-under, $(LOCAL_PATH)) +endif diff --git a/common/java/com/android/common/ArrayListCursor.java b/common/java/com/android/common/ArrayListCursor.java new file mode 100644 index 00000000..9ad5c364 --- /dev/null +++ b/common/java/com/android/common/ArrayListCursor.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.common; + +import android.database.AbstractCursor; +import android.database.CursorWindow; + +import java.lang.System; +import java.util.ArrayList; + +/** + * A convenience class that presents a two-dimensional ArrayList + * as a Cursor. + * @deprecated This is has been replaced by MatrixCursor. +*/ +public class ArrayListCursor extends AbstractCursor { + private String[] mColumnNames; + private ArrayList<Object>[] mRows; + + @SuppressWarnings({"unchecked"}) + public ArrayListCursor(String[] columnNames, ArrayList<ArrayList> rows) { + int colCount = columnNames.length; + boolean foundID = false; + // Add an _id column if not in columnNames + for (int i = 0; i < colCount; ++i) { + if (columnNames[i].compareToIgnoreCase("_id") == 0) { + mColumnNames = columnNames; + foundID = true; + break; + } + } + + if (!foundID) { + mColumnNames = new String[colCount + 1]; + System.arraycopy(columnNames, 0, mColumnNames, 0, columnNames.length); + mColumnNames[colCount] = "_id"; + } + + int rowCount = rows.size(); + mRows = new ArrayList[rowCount]; + + for (int i = 0; i < rowCount; ++i) { + mRows[i] = rows.get(i); + if (!foundID) { + mRows[i].add(i); + } + } + } + + @Override + public void fillWindow(int position, CursorWindow window) { + if (position < 0 || position > getCount()) { + return; + } + + window.acquireReference(); + try { + int oldpos = mPos; + mPos = position - 1; + window.clear(); + window.setStartPosition(position); + int columnNum = getColumnCount(); + window.setNumColumns(columnNum); + while (moveToNext() && window.allocRow()) { + for (int i = 0; i < columnNum; i++) { + final Object data = mRows[mPos].get(i); + if (data != null) { + if (data instanceof byte[]) { + byte[] field = (byte[]) data; + if (!window.putBlob(field, mPos, i)) { + window.freeLastRow(); + break; + } + } else { + String field = data.toString(); + if (!window.putString(field, mPos, i)) { + window.freeLastRow(); + break; + } + } + } else { + if (!window.putNull(mPos, i)) { + window.freeLastRow(); + break; + } + } + } + } + + mPos = oldpos; + } catch (IllegalStateException e){ + // simply ignore it + } finally { + window.releaseReference(); + } + } + + @Override + public int getCount() { + return mRows.length; + } + + @Override + public String[] getColumnNames() { + return mColumnNames; + } + + @Override + public byte[] getBlob(int columnIndex) { + return (byte[]) mRows[mPos].get(columnIndex); + } + + @Override + public String getString(int columnIndex) { + Object cell = mRows[mPos].get(columnIndex); + return (cell == null) ? null : cell.toString(); + } + + @Override + public short getShort(int columnIndex) { + Number num = (Number) mRows[mPos].get(columnIndex); + return num.shortValue(); + } + + @Override + public int getInt(int columnIndex) { + Number num = (Number) mRows[mPos].get(columnIndex); + return num.intValue(); + } + + @Override + public long getLong(int columnIndex) { + Number num = (Number) mRows[mPos].get(columnIndex); + return num.longValue(); + } + + @Override + public float getFloat(int columnIndex) { + Number num = (Number) mRows[mPos].get(columnIndex); + return num.floatValue(); + } + + @Override + public double getDouble(int columnIndex) { + Number num = (Number) mRows[mPos].get(columnIndex); + return num.doubleValue(); + } + + @Override + public boolean isNull(int columnIndex) { + return mRows[mPos].get(columnIndex) == null; + } +} diff --git a/common/java/com/android/common/GoogleLogTags.logtags b/common/java/com/android/common/GoogleLogTags.logtags new file mode 100644 index 00000000..f848ddfd --- /dev/null +++ b/common/java/com/android/common/GoogleLogTags.logtags @@ -0,0 +1,100 @@ +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +option java_package com.android.common + +##### +# This file contains definitions for event log (android.util.EventLog) tags +# used by Google Mobile Services applications. These definitions are part of +# the platform even when the Google applications are not. +# +# See system/core/logcat/event.logtags for a description of the file format. +# +# These event log tags must be assigned specific numbers (no "?") in the range +# 200000-300000. If new tags are added, be aware that older platforms will be +# missing the tag definitions, and may not be able to show them in their logs. + +##### +# System Update (OTA) + +# System update status bits +# [31- 9] Reserved for future use +# [ 8- 7] package verified (0=not attempted, 1=succeeded, 2=failed) +# [ 6] install approved +# [ 5] download approved +# [ 4- 0] status +201001 system_update (status|1|5),(download_result|1|5),(bytes|2|2),(url|3) +201002 system_update_user (action|3) + +##### +# Android Market + +# @param changes Number of changes made to database in reconstruct +202001 vending_reconstruct (changes|1) + +##### +# Google Services Framework + +203001 sync_details (authority|3),(send|1|2),(recv|1|2),(details|3) + +203002 google_http_request (elapsed|2|3),(status|1),(appname|3),(reused|1) + +##### +# Google Talk Service + +# This event is logged when GTalkService encounters important events +204001 gtalkservice (eventType|1) +# This event is logged for GTalk connection state changes. The status field is an int, but +# it really contains 4 separate values, each taking up a byte +# (eventType << 24) + (connection state << 16) + (connection error << 8) + network state +204002 gtalk_connection (status|1) + +# This event is logged when GTalk connection is closed. +# The status field is an int, but contains 2 different values, it's represented as +# +# (networkType << 8) + connection error +# +# the possible error values are +# +# no_error=0, no_network=1, connection_failed=2, unknown_host=3, auth_failed=4, +# auth_expired=5, heart_beat_timeout=6, server_error=7, server_reject_rate_limiting=8, unknown=10 +# +# duration is the connection duration. +204003 gtalk_conn_close (status|1),(duration|1) + +# This event is logged for GTalk heartbeat resets +# interval_and_nt contains both the heartbeat interval and the network type, It's represented as +# (networkType << 16) + interval +# interval is in seconds; network type can be 0 (mobile) or 1 (wifi); ip is the host ip addr. +204004 gtalk_heartbeat_reset (interval_and_nt|1),(ip|3) + +# This event is logged when an Rmq v2 packet is sent or received. +204005 c2dm (packet_type|1),(persistent_id|3),(stream_id|1),(last_stream_id|1) + +##### +# Google Login Service and Setup Wizard + +# This event is for when communicating to the server times out during account setup +205001 setup_server_timeout +205002 setup_required_captcha (action|3) +205003 setup_io_error (status|3) +205004 setup_server_error +205005 setup_retries_exhausted +205006 setup_no_data_network +205007 setup_completed + +205008 gls_account_tried (status|1) +205009 gls_account_saved (status|1) +205010 gls_authenticate (status|1),(service|3) +205011 google_mail_switch (direction|1) diff --git a/common/java/com/android/common/NetworkConnectivityListener.java b/common/java/com/android/common/NetworkConnectivityListener.java new file mode 100644 index 00000000..b49b80d6 --- /dev/null +++ b/common/java/com/android/common/NetworkConnectivityListener.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.common; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import java.util.HashMap; +import java.util.Iterator; + +/** + * A wrapper for a broadcast receiver that provides network connectivity + * state information, independent of network type (mobile, Wi-Fi, etc.). + * @deprecated Code tempted to use this class should simply listen for connectivity intents + * (or poll ConnectivityManager) directly. + * {@hide} + */ +public class NetworkConnectivityListener { + private static final String TAG = "NetworkConnectivityListener"; + private static final boolean DBG = false; + + private Context mContext; + private HashMap<Handler, Integer> mHandlers = new HashMap<Handler, Integer>(); + private State mState; + private boolean mListening; + private String mReason; + private boolean mIsFailover; + + /** Network connectivity information */ + private NetworkInfo mNetworkInfo; + + /** + * In case of a Disconnect, the connectivity manager may have + * already established, or may be attempting to establish, connectivity + * with another network. If so, {@code mOtherNetworkInfo} will be non-null. + */ + private NetworkInfo mOtherNetworkInfo; + + private ConnectivityBroadcastReceiver mReceiver; + + private class ConnectivityBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + if (!action.equals(ConnectivityManager.CONNECTIVITY_ACTION) || + mListening == false) { + Log.w(TAG, "onReceived() called with " + mState.toString() + " and " + intent); + return; + } + + boolean noConnectivity = + intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); + + if (noConnectivity) { + mState = State.NOT_CONNECTED; + } else { + mState = State.CONNECTED; + } + + mNetworkInfo = (NetworkInfo) + intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO); + mOtherNetworkInfo = (NetworkInfo) + intent.getParcelableExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO); + + mReason = intent.getStringExtra(ConnectivityManager.EXTRA_REASON); + mIsFailover = + intent.getBooleanExtra(ConnectivityManager.EXTRA_IS_FAILOVER, false); + + if (DBG) { + Log.d(TAG, "onReceive(): mNetworkInfo=" + mNetworkInfo + " mOtherNetworkInfo = " + + (mOtherNetworkInfo == null ? "[none]" : mOtherNetworkInfo + + " noConn=" + noConnectivity) + " mState=" + mState.toString()); + } + + // Notifiy any handlers. + Iterator<Handler> it = mHandlers.keySet().iterator(); + while (it.hasNext()) { + Handler target = it.next(); + Message message = Message.obtain(target, mHandlers.get(target)); + target.sendMessage(message); + } + } + }; + + public enum State { + UNKNOWN, + + /** This state is returned if there is connectivity to any network **/ + CONNECTED, + /** + * This state is returned if there is no connectivity to any network. This is set + * to true under two circumstances: + * <ul> + * <li>When connectivity is lost to one network, and there is no other available + * network to attempt to switch to.</li> + * <li>When connectivity is lost to one network, and the attempt to switch to + * another network fails.</li> + */ + NOT_CONNECTED + } + + /** + * Create a new NetworkConnectivityListener. + */ + public NetworkConnectivityListener() { + mState = State.UNKNOWN; + mReceiver = new ConnectivityBroadcastReceiver(); + } + + /** + * This method starts listening for network connectivity state changes. + * @param context + */ + public synchronized void startListening(Context context) { + if (!mListening) { + mContext = context; + + IntentFilter filter = new IntentFilter(); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + context.registerReceiver(mReceiver, filter); + mListening = true; + } + } + + /** + * This method stops this class from listening for network changes. + */ + public synchronized void stopListening() { + if (mListening) { + mContext.unregisterReceiver(mReceiver); + mContext = null; + mNetworkInfo = null; + mOtherNetworkInfo = null; + mIsFailover = false; + mReason = null; + mListening = false; + } + } + + /** + * This methods registers a Handler to be called back onto with the specified what code when + * the network connectivity state changes. + * + * @param target The target handler. + * @param what The what code to be used when posting a message to the handler. + */ + public void registerHandler(Handler target, int what) { + mHandlers.put(target, what); + } + + /** + * This methods unregisters the specified Handler. + * @param target + */ + public void unregisterHandler(Handler target) { + mHandlers.remove(target); + } + + public State getState() { + return mState; + } + + /** + * Return the NetworkInfo associated with the most recent connectivity event. + * @return {@code NetworkInfo} for the network that had the most recent connectivity event. + */ + public NetworkInfo getNetworkInfo() { + return mNetworkInfo; + } + + /** + * If the most recent connectivity event was a DISCONNECT, return + * any information supplied in the broadcast about an alternate + * network that might be available. If this returns a non-null + * value, then another broadcast should follow shortly indicating + * whether connection to the other network succeeded. + * + * @return NetworkInfo + */ + public NetworkInfo getOtherNetworkInfo() { + return mOtherNetworkInfo; + } + + /** + * Returns true if the most recent event was for an attempt to switch over to + * a new network following loss of connectivity on another network. + * @return {@code true} if this was a failover attempt, {@code false} otherwise. + */ + public boolean isFailover() { + return mIsFailover; + } + + /** + * An optional reason for the connectivity state change may have been supplied. + * This returns it. + * @return the reason for the state change, if available, or {@code null} + * otherwise. + */ + public String getReason() { + return mReason; + } +} diff --git a/common/java/com/android/common/OperationScheduler.java b/common/java/com/android/common/OperationScheduler.java new file mode 100644 index 00000000..17869578 --- /dev/null +++ b/common/java/com/android/common/OperationScheduler.java @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.common; + +import android.content.SharedPreferences; +import android.net.http.AndroidHttpClient; +import android.text.format.Time; + +import java.util.Map; +import java.util.TreeSet; + +/** + * Tracks the success/failure history of a particular network operation in + * persistent storage and computes retry strategy accordingly. Handles + * exponential backoff, periodic rescheduling, event-driven triggering, + * retry-after moratorium intervals, etc. based on caller-specified parameters. + * + * <p>This class does not directly perform or invoke any operations, + * it only keeps track of the schedule. Somebody else needs to call + * {@link #getNextTimeMillis()} as appropriate and do the actual work. + */ +public class OperationScheduler { + /** Tunable parameter options for {@link #getNextTimeMillis}. */ + public static class Options { + /** Wait this long after every error before retrying. */ + public long backoffFixedMillis = 0; + + /** Wait this long times the number of consecutive errors so far before retrying. */ + public long backoffIncrementalMillis = 5000; + + /** Maximum duration of moratorium to honor. Mostly an issue for clock rollbacks. */ + public long maxMoratoriumMillis = 24 * 3600 * 1000; + + /** Minimum duration after success to wait before allowing another trigger. */ + public long minTriggerMillis = 0; + + /** Automatically trigger this long after the last success. */ + public long periodicIntervalMillis = 0; + + @Override + public String toString() { + return String.format( + "OperationScheduler.Options[backoff=%.1f+%.1f max=%.1f min=%.1f period=%.1f]", + backoffFixedMillis / 1000.0, backoffIncrementalMillis / 1000.0, + maxMoratoriumMillis / 1000.0, minTriggerMillis / 1000.0, + periodicIntervalMillis / 1000.0); + } + } + + private static final String PREFIX = "OperationScheduler_"; + private final SharedPreferences mStorage; + + /** + * Initialize the scheduler state. + * @param storage to use for recording the state of operations across restarts/reboots + */ + public OperationScheduler(SharedPreferences storage) { + mStorage = storage; + } + + /** + * Parse scheduler options supplied in this string form: + * + * <pre> + * backoff=(fixed)+(incremental) max=(maxmoratorium) min=(mintrigger) [period=](interval) + * </pre> + * + * All values are times in (possibly fractional) <em>seconds</em> (not milliseconds). + * Omitted settings are left at whatever existing default value was passed in. + * + * <p> + * The default options: <code>backoff=0+5 max=86400 min=0 period=0</code><br> + * Fractions are OK: <code>backoff=+2.5 period=10.0</code><br> + * The "period=" can be omitted: <code>3600</code><br> + * + * @param spec describing some or all scheduler options. + * @param options to update with parsed values. + * @return the options passed in (for convenience) + * @throws IllegalArgumentException if the syntax is invalid + */ + public static Options parseOptions(String spec, Options options) + throws IllegalArgumentException { + for (String param : spec.split(" +")) { + if (param.length() == 0) continue; + if (param.startsWith("backoff=")) { + int plus = param.indexOf('+', 8); + if (plus < 0) { + options.backoffFixedMillis = parseSeconds(param.substring(8)); + } else { + if (plus > 8) { + options.backoffFixedMillis = parseSeconds(param.substring(8, plus)); + } + options.backoffIncrementalMillis = parseSeconds(param.substring(plus + 1)); + } + } else if (param.startsWith("max=")) { + options.maxMoratoriumMillis = parseSeconds(param.substring(4)); + } else if (param.startsWith("min=")) { + options.minTriggerMillis = parseSeconds(param.substring(4)); + } else if (param.startsWith("period=")) { + options.periodicIntervalMillis = parseSeconds(param.substring(7)); + } else { + options.periodicIntervalMillis = parseSeconds(param); + } + } + return options; + } + + private static long parseSeconds(String param) throws NumberFormatException { + return (long) (Float.parseFloat(param) * 1000); + } + + /** + * Compute the time of the next operation. Does not modify any state + * (unless the clock rolls backwards, in which case timers are reset). + * + * @param options to use for this computation. + * @return the wall clock time ({@link System#currentTimeMillis()}) when the + * next operation should be attempted -- immediately, if the return value is + * before the current time. + */ + public long getNextTimeMillis(Options options) { + boolean enabledState = mStorage.getBoolean(PREFIX + "enabledState", true); + if (!enabledState) return Long.MAX_VALUE; + + boolean permanentError = mStorage.getBoolean(PREFIX + "permanentError", false); + if (permanentError) return Long.MAX_VALUE; + + // We do quite a bit of limiting to prevent a clock rollback from totally + // hosing the scheduler. Times which are supposed to be in the past are + // clipped to the current time so we don't languish forever. + + int errorCount = mStorage.getInt(PREFIX + "errorCount", 0); + long now = currentTimeMillis(); + long lastSuccessTimeMillis = getTimeBefore(PREFIX + "lastSuccessTimeMillis", now); + long lastErrorTimeMillis = getTimeBefore(PREFIX + "lastErrorTimeMillis", now); + long triggerTimeMillis = mStorage.getLong(PREFIX + "triggerTimeMillis", Long.MAX_VALUE); + long moratoriumSetMillis = getTimeBefore(PREFIX + "moratoriumSetTimeMillis", now); + long moratoriumTimeMillis = getTimeBefore(PREFIX + "moratoriumTimeMillis", + moratoriumSetMillis + options.maxMoratoriumMillis); + + long time = triggerTimeMillis; + if (options.periodicIntervalMillis > 0) { + time = Math.min(time, lastSuccessTimeMillis + options.periodicIntervalMillis); + } + + time = Math.max(time, moratoriumTimeMillis); + time = Math.max(time, lastSuccessTimeMillis + options.minTriggerMillis); + if (errorCount > 0) { + time = Math.max(time, lastErrorTimeMillis + options.backoffFixedMillis + + options.backoffIncrementalMillis * errorCount); + } + return time; + } + + /** + * Return the last time the operation completed. Does not modify any state. + * + * @return the wall clock time when {@link #onSuccess()} was last called. + */ + public long getLastSuccessTimeMillis() { + return mStorage.getLong(PREFIX + "lastSuccessTimeMillis", 0); + } + + /** + * Return the last time the operation was attempted. Does not modify any state. + * + * @return the wall clock time when {@link #onSuccess()} or {@link + * #onTransientError()} was last called. + */ + public long getLastAttemptTimeMillis() { + return Math.max( + mStorage.getLong(PREFIX + "lastSuccessTimeMillis", 0), + mStorage.getLong(PREFIX + "lastErrorTimeMillis", 0)); + } + + /** + * Fetch a {@link SharedPreferences} property, but force it to be before + * a certain time, updating the value if necessary. This is to recover + * gracefully from clock rollbacks which could otherwise strand our timers. + * + * @param name of SharedPreferences key + * @param max time to allow in result + * @return current value attached to key (default 0), limited by max + */ + private long getTimeBefore(String name, long max) { + long time = mStorage.getLong(name, 0); + if (time > max) mStorage.edit().putLong(name, (time = max)).commit(); + return time; + } + + /** + * Request an operation to be performed at a certain time. The actual + * scheduled time may be affected by error backoff logic and defined + * minimum intervals. Use {@link Long#MAX_VALUE} to disable triggering. + * + * @param millis wall clock time ({@link System#currentTimeMillis()}) to + * trigger another operation; 0 to trigger immediately + */ + public void setTriggerTimeMillis(long millis) { + mStorage.edit().putLong(PREFIX + "triggerTimeMillis", millis).commit(); + } + + /** + * Forbid any operations until after a certain (absolute) time. + * Limited by {@link #Options.maxMoratoriumMillis}. + * + * @param millis wall clock time ({@link System#currentTimeMillis()}) + * when operations should be allowed again; 0 to remove moratorium + */ + public void setMoratoriumTimeMillis(long millis) { + mStorage.edit() + .putLong(PREFIX + "moratoriumTimeMillis", millis) + .putLong(PREFIX + "moratoriumSetTimeMillis", currentTimeMillis()) + .commit(); + } + + /** + * Forbid any operations until after a certain time, as specified in + * the format used by the HTTP "Retry-After" header. + * Limited by {@link #Options.maxMoratoriumMillis}. + * + * @param retryAfter moratorium time in HTTP format + * @return true if a time was successfully parsed + */ + public boolean setMoratoriumTimeHttp(String retryAfter) { + try { + long ms = Long.valueOf(retryAfter) * 1000; + setMoratoriumTimeMillis(ms + currentTimeMillis()); + return true; + } catch (NumberFormatException nfe) { + try { + setMoratoriumTimeMillis(AndroidHttpClient.parseDate(retryAfter)); + return true; + } catch (IllegalArgumentException iae) { + return false; + } + } + } + + /** + * Enable or disable all operations. When disabled, all calls to + * {@link #getNextTimeMillis()} return {@link Long#MAX_VALUE}. + * Commonly used when data network availability goes up and down. + * + * @param enabled if operations can be performed + */ + public void setEnabledState(boolean enabled) { + mStorage.edit().putBoolean(PREFIX + "enabledState", enabled).commit(); + } + + /** + * Report successful completion of an operation. Resets all error + * counters, clears any trigger directives, and records the success. + */ + public void onSuccess() { + resetTransientError(); + resetPermanentError(); + mStorage.edit() + .remove(PREFIX + "errorCount") + .remove(PREFIX + "lastErrorTimeMillis") + .remove(PREFIX + "permanentError") + .remove(PREFIX + "triggerTimeMillis") + .putLong(PREFIX + "lastSuccessTimeMillis", currentTimeMillis()).commit(); + } + + /** + * Report a transient error (usually a network failure). Increments + * the error count and records the time of the latest error for backoff + * purposes. + */ + public void onTransientError() { + mStorage.edit().putLong(PREFIX + "lastErrorTimeMillis", currentTimeMillis()).commit(); + mStorage.edit().putInt(PREFIX + "errorCount", + mStorage.getInt(PREFIX + "errorCount", 0) + 1).commit(); + } + + /** + * Reset all transient error counts, allowing the next operation to proceed + * immediately without backoff. Commonly used on network state changes, when + * partial progress occurs (some data received), and in other circumstances + * where there is reason to hope things might start working better. + */ + public void resetTransientError() { + mStorage.edit().remove(PREFIX + "errorCount").commit(); + } + + /** + * Report a permanent error that will not go away until further notice. + * No operation will be scheduled until {@link #resetPermanentError()} + * is called. Commonly used for authentication failures (which are reset + * when the accounts database is updated). + */ + public void onPermanentError() { + mStorage.edit().putBoolean(PREFIX + "permanentError", true).commit(); + } + + /** + * Reset any permanent error status set by {@link #onPermanentError}, + * allowing operations to be scheduled as normal. + */ + public void resetPermanentError() { + mStorage.edit().remove(PREFIX + "permanentError").commit(); + } + + /** + * Return a string description of the scheduler state for debugging. + */ + public String toString() { + StringBuilder out = new StringBuilder("[OperationScheduler:"); + for (String key : new TreeSet<String>(mStorage.getAll().keySet())) { // Sort keys + if (key.startsWith(PREFIX)) { + if (key.endsWith("TimeMillis")) { + Time time = new Time(); + time.set(mStorage.getLong(key, 0)); + out.append(" ").append(key.substring(PREFIX.length(), key.length() - 10)); + out.append("=").append(time.format("%Y-%m-%d/%H:%M:%S")); + } else { + out.append(" ").append(key.substring(PREFIX.length())); + out.append("=").append(mStorage.getAll().get(key).toString()); + } + } + } + return out.append("]").toString(); + } + + /** + * Gets the current time. Can be overridden for unit testing. + * + * @return {@link System#currentTimeMillis()} + */ + protected long currentTimeMillis() { + return System.currentTimeMillis(); + } +} diff --git a/common/java/com/android/common/Rfc822InputFilter.java b/common/java/com/android/common/Rfc822InputFilter.java new file mode 100644 index 00000000..6dfdc7bf --- /dev/null +++ b/common/java/com/android/common/Rfc822InputFilter.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.common; + +import android.text.InputFilter; +import android.text.Spanned; +import android.text.SpannableStringBuilder; + +/** + * Implements special address cleanup rules: + * The first space key entry following an "@" symbol that is followed by any combination + * of letters and symbols, including one+ dots and zero commas, should insert an extra + * comma (followed by the space). + * + * @hide + */ +public class Rfc822InputFilter implements InputFilter { + + public CharSequence filter(CharSequence source, int start, int end, Spanned dest, + int dstart, int dend) { + + // quick check - did they enter a single space? + if (end-start != 1 || source.charAt(start) != ' ') { + return null; + } + + // determine if the characters before the new space fit the pattern + // follow backwards and see if we find a comma, dot, or @ + int scanBack = dstart; + boolean dotFound = false; + while (scanBack > 0) { + char c = dest.charAt(--scanBack); + switch (c) { + case '.': + dotFound = true; // one or more dots are req'd + break; + case ',': + return null; + case '@': + if (!dotFound) { + return null; + } + // we have found a comma-insert case. now just do it + // in the least expensive way we can. + if (source instanceof Spanned) { + SpannableStringBuilder sb = new SpannableStringBuilder(","); + sb.append(source); + return sb; + } else { + return ", "; + } + default: + // just keep going + } + } + + // no termination cases were found, so don't edit the input + return null; + } +} diff --git a/common/java/com/android/common/Rfc822Validator.java b/common/java/com/android/common/Rfc822Validator.java new file mode 100644 index 00000000..087e4256 --- /dev/null +++ b/common/java/com/android/common/Rfc822Validator.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.common; + +import android.text.TextUtils; +import android.text.util.Rfc822Token; +import android.text.util.Rfc822Tokenizer; +import android.widget.AutoCompleteTextView; + +import java.util.regex.Pattern; + +/** + * This class works as a Validator for AutoCompleteTextView for + * email addresses. If a token does not appear to be a valid address, + * it is trimmed of characters that cannot legitimately appear in one + * and has the specified domain name added. It is meant for use with + * {@link Rfc822Token} and {@link Rfc822Tokenizer}. + * + * @deprecated In the future make sure we don't quietly alter the user's + * text in ways they did not intend. Meanwhile, hide this + * class from the public API because it does not even have + * a full understanding of the syntax it claims to correct. + * @hide + */ +public class Rfc822Validator implements AutoCompleteTextView.Validator { + /* + * Regex.EMAIL_ADDRESS_PATTERN hardcodes the TLD that we accept, but we + * want to make sure we will keep accepting email addresses with TLD's + * that don't exist at the time of this writing, so this regexp relaxes + * that constraint by accepting any kind of top level domain, not just + * ".com", ".fr", etc... + */ + private static final Pattern EMAIL_ADDRESS_PATTERN = + Pattern.compile("[^\\s@]+@[^\\s@]+\\.[a-zA-z][a-zA-Z][a-zA-Z]*"); + + private String mDomain; + + /** + * Constructs a new validator that uses the specified domain name as + * the default when none is specified. + */ + public Rfc822Validator(String domain) { + mDomain = domain; + } + + /** + * {@inheritDoc} + */ + public boolean isValid(CharSequence text) { + Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(text); + + return tokens.length == 1 && + EMAIL_ADDRESS_PATTERN. + matcher(tokens[0].getAddress()).matches(); + } + + /** + * @return a string in which all the characters that are illegal for the username + * or the domain name part of the email address have been removed. + */ + private String removeIllegalCharacters(String s) { + StringBuilder result = new StringBuilder(); + int length = s.length(); + for (int i = 0; i < length; i++) { + char c = s.charAt(i); + + /* + * An RFC822 atom can contain any ASCII printing character + * except for periods and any of the following punctuation. + * A local-part can contain multiple atoms, concatenated by + * periods, so do allow periods here. + */ + + if (c <= ' ' || c > '~') { + continue; + } + + if (c == '(' || c == ')' || c == '<' || c == '>' || + c == '@' || c == ',' || c == ';' || c == ':' || + c == '\\' || c == '"' || c == '[' || c == ']') { + continue; + } + + result.append(c); + } + return result.toString(); + } + + /** + * {@inheritDoc} + */ + public CharSequence fixText(CharSequence cs) { + // Return an empty string if the email address only contains spaces, \n or \t + if (TextUtils.getTrimmedLength(cs) == 0) return ""; + + Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(cs); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < tokens.length; i++) { + String text = tokens[i].getAddress(); + int index = text.indexOf('@'); + if (index < 0) { + // If there is no @, just append the domain of the account + tokens[i].setAddress(removeIllegalCharacters(text) + "@" + mDomain); + } else { + // Otherwise, remove the illegal characters on both sides of the '@' + String fix = removeIllegalCharacters(text.substring(0, index)); + String domain = removeIllegalCharacters(text.substring(index + 1)); + tokens[i].setAddress(fix + "@" + (domain.length() != 0 ? domain : mDomain)); + } + + sb.append(tokens[i].toString()); + if (i + 1 < tokens.length) { + sb.append(", "); + } + } + + return sb; + } +} diff --git a/common/java/com/android/common/Search.java b/common/java/com/android/common/Search.java new file mode 100644 index 00000000..55fa6f58 --- /dev/null +++ b/common/java/com/android/common/Search.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.common; + +/** + * Utilities for search implementations. + * + * @see android.app.SearchManager + */ +public class Search { + + /** + * Key for the source identifier set by the application that launched a search intent. + * The identifier is search-source specific string. It can be used + * by the search provider to keep statistics of where searches are started from. + * + * The source identifier is stored in the {@link android.app.SearchManager#APP_DATA} + * Bundle in {@link android.content.Intent#ACTION_SEARCH} and + * {@link android.content.Intent#ACTION_WEB_SEARCH} intents. + */ + public final static String SOURCE = "source"; + + private Search() { } // don't instantiate +} diff --git a/common/java/com/android/common/speech/LoggingEvents.java b/common/java/com/android/common/speech/LoggingEvents.java new file mode 100644 index 00000000..1f3c6ef8 --- /dev/null +++ b/common/java/com/android/common/speech/LoggingEvents.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.common.speech; + +/** + * Logging event constants used for Voice Search and VoiceIME. These are the + * keys and values of extras to be specified in logging broadcast intents. + * This class is used by clients of the android.speech APIs to log how the + * user interacts with the IME settings and speech recognition result. + */ +public class LoggingEvents { + // The name of the broadcast intent for logging. + public static final String ACTION_LOG_EVENT = "com.android.common.speech.LOG_EVENT"; + + // The extra key used for the name of the app being logged. + public static final String EXTRA_APP_NAME = "app_name"; + + // The extra key used for the name of the app issuing the VoiceSearch + // or VoiceIME request + public static final String EXTRA_CALLING_APP_NAME = ""; + + // The extra key used for the event value. The possible event values depend + // on the app being logged for, and are defined in the subclasses below. + public static final String EXTRA_EVENT = "extra_event"; + + // The extra key used to log the time in milliseconds at which the EXTRA_EVENT + // occurred in the client. + public static final String EXTRA_TIMESTAMP = "timestamp"; + + // The extra key used (with a boolean value of 'true') as a way to trigger a + // flush of the log events to the server. + public static final String EXTRA_FLUSH = "flush"; + + /** + * Logging event constants for voice search. Below are the extra values for + * {@link LoggingEvents#EXTRA_EVENT}, clustered with keys to additional + * extras for some events that need to be included as additional fields in + * the event. Note that this is not representative of *all* voice search + * events - only the ones that need to be reported from outside the voice + * search app, such as from Browser. + */ + public class VoiceSearch { + // The app name to be used for logging VoiceSearch events. + public static final String APP_NAME = "googlemobile"; + + public static final int RETRY = 0; + + public static final int N_BEST_REVEAL = 1; + + public static final int N_BEST_CHOOSE = 2; + public static final String EXTRA_N_BEST_CHOOSE_INDEX = "index"; // value should be int + + public static final int QUERY_UPDATED = 3; + public static final String EXTRA_QUERY_UPDATED_VALUE = "value"; // value should be String + } + + /** + * Logging event constants for VoiceIME. Below are the extra values for + * {@link LoggingEvents#EXTRA_EVENT}, clustered with keys to additional + * extras for some events that need to be included as additional fields in + * the event. + */ + public class VoiceIme { + // The app name to be used for logging VoiceIME events. + public static final String APP_NAME = "voiceime"; + + public static final int KEYBOARD_WARNING_DIALOG_SHOWN = 0; + + public static final int KEYBOARD_WARNING_DIALOG_DISMISSED = 1; + + public static final int KEYBOARD_WARNING_DIALOG_OK = 2; + + public static final int KEYBOARD_WARNING_DIALOG_CANCEL = 3; + + public static final int SETTINGS_WARNING_DIALOG_SHOWN = 4; + + public static final int SETTINGS_WARNING_DIALOG_DISMISSED = 5; + + public static final int SETTINGS_WARNING_DIALOG_OK = 6; + + public static final int SETTINGS_WARNING_DIALOG_CANCEL = 7; + + public static final int SWIPE_HINT_DISPLAYED = 8; + + public static final int PUNCTUATION_HINT_DISPLAYED = 9; + + public static final int CANCEL_DURING_LISTENING = 10; + + public static final int CANCEL_DURING_WORKING = 11; + + public static final int CANCEL_DURING_ERROR = 12; + + public static final int ERROR = 13; + public static final String EXTRA_ERROR_CODE = "code"; // value should be int + + public static final int START = 14; + public static final String EXTRA_START_LOCALE = "locale"; // value should be String + public static final String EXTRA_START_SWIPE = "swipe"; // value should be boolean + + public static final int VOICE_INPUT_DELIVERED = 15; + + public static final int N_BEST_CHOOSE = 16; + public static final String EXTRA_N_BEST_CHOOSE_INDEX = "index"; // value should be int + + public static final int TEXT_MODIFIED = 17; + public static final String EXTRA_TEXT_MODIFIED_LENGTH = "length"; // value should be int + public static final String EXTRA_TEXT_MODIFIED_TYPE = "type"; // value should be int below + public static final int TEXT_MODIFIED_TYPE_CHOOSE_SUGGESTION = 1; + public static final int TEXT_MODIFIED_TYPE_TYPING_DELETION = 2; + public static final int TEXT_MODIFIED_TYPE_TYPING_INSERTION = 3; + public static final int TEXT_MODIFIED_TYPE_TYPING_INSERTION_PUNCTUATION = 4; + + public static final int INPUT_ENDED = 18; + + public static final int VOICE_INPUT_SETTING_ENABLED = 19; + + public static final int VOICE_INPUT_SETTING_DISABLED = 20; + + public static final int IME_TEXT_ACCEPTED = 21; + } + +} diff --git a/common/java/com/android/common/speech/Recognition.java b/common/java/com/android/common/speech/Recognition.java new file mode 100644 index 00000000..1970179d --- /dev/null +++ b/common/java/com/android/common/speech/Recognition.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.common.speech; + +/** + * Utilities for voice recognition implementations. + * + * @see android.speech.RecognitionService + * @see android.speech.RecognizerIntent + */ +public class Recognition { + + /** + * The key to the extra in the Bundle returned by + * android.speech.RecognizerIntent#ACTION_GET_LANGUAGE_DETAILS + * which is an ArrayList of CharSequences which are hints that can be shown to + * the user for voice actions currently supported by voice search for the user's current + * language preference for voice search (i.e., the one defined in the extra + * android.speech.RecognizerIntent#EXTRA_LANGUAGE_PREFERENCE). + * + * If this is paired with EXTRA_HINT_CONTEXT, should return a set of hints that are + * appropriate for the provided context. + * + * The CharSequences are SpannedStrings and will contain segments wrapped in + * <annotation action="true"></annotation>. This is to indicate the section of the text + * which represents the voice action, to be highlighted in the UI if so desired. + */ + public static final String EXTRA_HINT_STRINGS = "android.speech.extra.HINT_STRINGS"; + + /** + * The key to an extra to be included in the request intent for + * android.speech.RecognizerIntent#ACTION_GET_LANGUAGE_DETAILS. + * Should be an int of one of the values defined below. If an + * unknown int value is provided, it should be ignored. + */ + public static final String EXTRA_HINT_CONTEXT = "android.speech.extra.HINT_CONTEXT"; + + /** + * A set of values for EXTRA_HINT_CONTEXT. + */ + public static final int HINT_CONTEXT_UNKNOWN = 0; + public static final int HINT_CONTEXT_VOICE_SEARCH_HELP = 1; + public static final int HINT_CONTEXT_CAR_HOME = 2; + public static final int HINT_CONTEXT_LAUNCHER = 3; + + private Recognition() { } // don't instantiate +} diff --git a/common/java/com/android/common/userhappiness/UserHappinessSignals.java b/common/java/com/android/common/userhappiness/UserHappinessSignals.java new file mode 100644 index 00000000..347bdaaf --- /dev/null +++ b/common/java/com/android/common/userhappiness/UserHappinessSignals.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.common.userhappiness; + +import android.content.Intent; +import android.content.Context; +import com.android.common.speech.LoggingEvents; + +/** + * Metrics for User Happiness are recorded here. Each app can define when to + * call these User Happiness metrics. + */ +public class UserHappinessSignals { + + /** + * Log when a user "accepted" IME text. Each application can define what + * it means to "accept" text. In the case of Gmail, pressing the "Send" + * button indicates text acceptance. We broadcast this information to + * VoiceSearch LoggingEvents and use it to aggregate VoiceIME Happiness Metrics + */ + public static void userAcceptedImeText(Context context) { + // Create a Voice IME Logging intent. + Intent i = new Intent(LoggingEvents.ACTION_LOG_EVENT); + i.putExtra(LoggingEvents.EXTRA_APP_NAME, LoggingEvents.VoiceIme.APP_NAME); + i.putExtra(LoggingEvents.EXTRA_EVENT, LoggingEvents.VoiceIme.IME_TEXT_ACCEPTED); + i.putExtra(LoggingEvents.EXTRA_CALLING_APP_NAME, context.getPackageName()); + i.putExtra(LoggingEvents.EXTRA_TIMESTAMP, System.currentTimeMillis()); + context.sendBroadcast(i); + } + +} diff --git a/common/tests/Android.mk b/common/tests/Android.mk new file mode 100644 index 00000000..74255521 --- /dev/null +++ b/common/tests/Android.mk @@ -0,0 +1,28 @@ +# Copyright (C) 2009 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_CERTIFICATE := platform +LOCAL_JAVA_LIBRARIES := android.test.runner +LOCAL_MODULE_TAGS := tests +LOCAL_PACKAGE_NAME := AndroidCommonTests +LOCAL_SDK_VERSION := current +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_STATIC_JAVA_LIBRARIES := android-common + +LOCAL_PROGUARD_ENABLED := disabled + +include $(BUILD_PACKAGE) diff --git a/common/tests/AndroidManifest.xml b/common/tests/AndroidManifest.xml new file mode 100644 index 00000000..151ec20e --- /dev/null +++ b/common/tests/AndroidManifest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2009 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.common.tests" + android:sharedUserId="com.android.uid.test"> + + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <!-- Run tests with "runtest common" --> + <instrumentation android:name="android.test.InstrumentationTestRunner" + android:targetPackage="com.android.common.tests" + android:label="Android Common Library Tests" /> + +</manifest> diff --git a/common/tests/src/com/android/common/OperationSchedulerTest.java b/common/tests/src/com/android/common/OperationSchedulerTest.java new file mode 100644 index 00000000..955508fa --- /dev/null +++ b/common/tests/src/com/android/common/OperationSchedulerTest.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.common; + +import android.content.SharedPreferences; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.MediumTest; +import android.test.suitebuilder.annotation.SmallTest; + +public class OperationSchedulerTest extends AndroidTestCase { + /** + * OperationScheduler subclass which uses an artificial time. + * Set {@link #timeMillis} to whatever value you like. + */ + private class TimeTravelScheduler extends OperationScheduler { + static final long DEFAULT_TIME = 1250146800000L; // 13-Aug-2009, 12:00:00 am + public long timeMillis = DEFAULT_TIME; + + @Override + protected long currentTimeMillis() { return timeMillis; } + public TimeTravelScheduler() { super(getFreshStorage()); } + } + + private SharedPreferences getFreshStorage() { + SharedPreferences sp = getContext().getSharedPreferences("OperationSchedulerTest", 0); + sp.edit().clear().commit(); + return sp; + } + + @MediumTest + public void testScheduler() throws Exception { + TimeTravelScheduler scheduler = new TimeTravelScheduler(); + OperationScheduler.Options options = new OperationScheduler.Options(); + assertEquals(Long.MAX_VALUE, scheduler.getNextTimeMillis(options)); + assertEquals(0, scheduler.getLastSuccessTimeMillis()); + assertEquals(0, scheduler.getLastAttemptTimeMillis()); + + long beforeTrigger = scheduler.timeMillis; + scheduler.setTriggerTimeMillis(beforeTrigger + 1000000); + assertEquals(beforeTrigger + 1000000, scheduler.getNextTimeMillis(options)); + + // It will schedule for the later of the trigger and the moratorium... + scheduler.setMoratoriumTimeMillis(beforeTrigger + 500000); + assertEquals(beforeTrigger + 1000000, scheduler.getNextTimeMillis(options)); + scheduler.setMoratoriumTimeMillis(beforeTrigger + 1500000); + assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options)); + + // Test enable/disable toggle + scheduler.setEnabledState(false); + assertEquals(Long.MAX_VALUE, scheduler.getNextTimeMillis(options)); + scheduler.setEnabledState(true); + assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options)); + + // Backoff interval after an error + long beforeError = (scheduler.timeMillis += 100); + scheduler.onTransientError(); + assertEquals(0, scheduler.getLastSuccessTimeMillis()); + assertEquals(beforeError, scheduler.getLastAttemptTimeMillis()); + assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options)); + options.backoffFixedMillis = 1000000; + options.backoffIncrementalMillis = 500000; + assertEquals(beforeError + 1500000, scheduler.getNextTimeMillis(options)); + + // Two errors: backoff interval increases + beforeError = (scheduler.timeMillis += 100); + scheduler.onTransientError(); + assertEquals(beforeError, scheduler.getLastAttemptTimeMillis()); + assertEquals(beforeError + 2000000, scheduler.getNextTimeMillis(options)); + + // Reset transient error: no backoff interval + scheduler.resetTransientError(); + assertEquals(0, scheduler.getLastSuccessTimeMillis()); + assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options)); + assertEquals(beforeError, scheduler.getLastAttemptTimeMillis()); + + // Permanent error holds true even if transient errors are reset + // However, we remember that the transient error was reset... + scheduler.onPermanentError(); + assertEquals(Long.MAX_VALUE, scheduler.getNextTimeMillis(options)); + scheduler.resetTransientError(); + assertEquals(Long.MAX_VALUE, scheduler.getNextTimeMillis(options)); + scheduler.resetPermanentError(); + assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options)); + + // Success resets the trigger + long beforeSuccess = (scheduler.timeMillis += 100); + scheduler.onSuccess(); + assertEquals(beforeSuccess, scheduler.getLastAttemptTimeMillis()); + assertEquals(beforeSuccess, scheduler.getLastSuccessTimeMillis()); + assertEquals(Long.MAX_VALUE, scheduler.getNextTimeMillis(options)); + + // The moratorium is not reset by success! + scheduler.setTriggerTimeMillis(0); + assertEquals(beforeTrigger + 1500000, scheduler.getNextTimeMillis(options)); + scheduler.setMoratoriumTimeMillis(0); + assertEquals(beforeSuccess, scheduler.getNextTimeMillis(options)); + + // Periodic interval after success + options.periodicIntervalMillis = 250000; + scheduler.setTriggerTimeMillis(Long.MAX_VALUE); + assertEquals(beforeSuccess + 250000, scheduler.getNextTimeMillis(options)); + + // Trigger minimum is also since the last success + options.minTriggerMillis = 1000000; + assertEquals(beforeSuccess + 1000000, scheduler.getNextTimeMillis(options)); + } + + @SmallTest + public void testParseOptions() throws Exception { + OperationScheduler.Options options = new OperationScheduler.Options(); + assertEquals( + "OperationScheduler.Options[backoff=0.0+5.0 max=86400.0 min=0.0 period=3600.0]", + OperationScheduler.parseOptions("3600", options).toString()); + + assertEquals( + "OperationScheduler.Options[backoff=0.0+2.5 max=86400.0 min=0.0 period=3700.0]", + OperationScheduler.parseOptions("backoff=+2.5 3700", options).toString()); + + assertEquals( + "OperationScheduler.Options[backoff=10.0+2.5 max=12345.6 min=7.0 period=3800.0]", + OperationScheduler.parseOptions("max=12345.6 min=7 backoff=10 period=3800", + options).toString()); + + assertEquals( + "OperationScheduler.Options[backoff=10.0+2.5 max=12345.6 min=7.0 period=3800.0]", + OperationScheduler.parseOptions("", options).toString()); + } + + @SmallTest + public void testMoratoriumWithHttpDate() throws Exception { + TimeTravelScheduler scheduler = new TimeTravelScheduler(); + OperationScheduler.Options options = new OperationScheduler.Options(); + + long beforeTrigger = scheduler.timeMillis; + scheduler.setTriggerTimeMillis(beforeTrigger + 1000000); + assertEquals(beforeTrigger + 1000000, scheduler.getNextTimeMillis(options)); + + scheduler.setMoratoriumTimeMillis(beforeTrigger + 2000000); + assertEquals(beforeTrigger + 2000000, scheduler.getNextTimeMillis(options)); + + long beforeMoratorium = scheduler.timeMillis; + assertTrue(scheduler.setMoratoriumTimeHttp("3000")); + long afterMoratorium = scheduler.timeMillis; + assertTrue(beforeMoratorium + 3000000 <= scheduler.getNextTimeMillis(options)); + assertTrue(afterMoratorium + 3000000 >= scheduler.getNextTimeMillis(options)); + + options.maxMoratoriumMillis = Long.MAX_VALUE / 2; + assertTrue(scheduler.setMoratoriumTimeHttp("Fri, 31 Dec 2030 23:59:59 GMT")); + assertEquals(1924991999000L, scheduler.getNextTimeMillis(options)); + + assertFalse(scheduler.setMoratoriumTimeHttp("not actually a date")); + } + + @SmallTest + public void testClockRollbackScenario() throws Exception { + TimeTravelScheduler scheduler = new TimeTravelScheduler(); + OperationScheduler.Options options = new OperationScheduler.Options(); + options.minTriggerMillis = 2000; + + // First, set up a scheduler with reasons to wait: a transient + // error with backoff and a moratorium for a few minutes. + + long beforeTrigger = scheduler.timeMillis; + long triggerTime = beforeTrigger - 10000000; + scheduler.setTriggerTimeMillis(triggerTime); + assertEquals(triggerTime, scheduler.getNextTimeMillis(options)); + assertEquals(0, scheduler.getLastAttemptTimeMillis()); + + long beforeSuccess = (scheduler.timeMillis += 100); + scheduler.onSuccess(); + scheduler.setTriggerTimeMillis(triggerTime); + assertEquals(beforeSuccess, scheduler.getLastAttemptTimeMillis()); + assertEquals(beforeSuccess + 2000, scheduler.getNextTimeMillis(options)); + + long beforeError = (scheduler.timeMillis += 100); + scheduler.onTransientError(); + assertEquals(beforeError, scheduler.getLastAttemptTimeMillis()); + assertEquals(beforeError + 5000, scheduler.getNextTimeMillis(options)); + + long beforeMoratorium = (scheduler.timeMillis += 100); + scheduler.setMoratoriumTimeMillis(beforeTrigger + 1000000); + assertEquals(beforeTrigger + 1000000, scheduler.getNextTimeMillis(options)); + + // Now set the time back a few seconds. + // The moratorium time should still be honored. + long beforeRollback = (scheduler.timeMillis = beforeTrigger - 10000); + assertEquals(beforeTrigger + 1000000, scheduler.getNextTimeMillis(options)); + + // The rollback also moved the last-attempt clock back to the rollback time. + assertEquals(scheduler.timeMillis, scheduler.getLastAttemptTimeMillis()); + + // But if we set the time back more than a day, the moratorium + // resets to the maximum moratorium (a day, by default), exposing + // the original trigger time. + beforeRollback = (scheduler.timeMillis = beforeTrigger - 100000000); + assertEquals(triggerTime, scheduler.getNextTimeMillis(options)); + assertEquals(beforeRollback, scheduler.getLastAttemptTimeMillis()); + + // If we roll forward until after the re-set moratorium, then it expires. + scheduler.timeMillis = triggerTime + 5000000; + assertEquals(triggerTime, scheduler.getNextTimeMillis(options)); + assertEquals(beforeRollback, scheduler.getLastAttemptTimeMillis()); + assertEquals(beforeRollback, scheduler.getLastSuccessTimeMillis()); + } +} diff --git a/common/tools/make-iana-tld-pattern.py b/common/tools/make-iana-tld-pattern.py new file mode 100755 index 00000000..de81c587 --- /dev/null +++ b/common/tools/make-iana-tld-pattern.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python + +from urllib2 import urlopen + +TLD_PREFIX = r""" + /** + * Regular expression to match all IANA top-level domains. + * List accurate as of 2010/02/05. List taken from: + * http://data.iana.org/TLD/tlds-alpha-by-domain.txt + * This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py + */ + public static final String TOP_LEVEL_DOMAIN_STR = +""" +TLD_SUFFIX = '";' + +URL_PREFIX = r""" + /** + * Regular expression to match all IANA top-level domains for WEB_URL. + * List accurate as of 2010/02/05. List taken from: + * http://data.iana.org/TLD/tlds-alpha-by-domain.txt + * This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py + */ + public static final String TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL = + "(?:" +""" + +URL_SUFFIX = ';' + +class Bucket: + def __init__(self, baseLetter): + self.base=baseLetter + self.words=[] + self.letters=[] + + def dump(self, isWebUrl=False, isFirst=False, isLast=False): + if (len(self.words) == 0) and (len(self.letters) == 0): + return '' + + self.words.sort() + self.letters.sort() + + output = ' '; + + if isFirst: + if isWebUrl: + output += '+ "' + else: + output += '"(' + else: + output += '+ "|' + + if len(self.words) != 0: + output += '(' + + if isWebUrl: + output += '?:' + + firstWord = 1 + for word in self.words: + if firstWord == 0: + output += '|' + firstWord = 0 + for letter in word: + if letter == '-': + output += '\\\\' # escape the '-' character. + output += letter + + if len(self.words) > 0 and len(self.letters) > 0: + output += '|' + + if len(self.letters) == 1: + output += '%c%c' % (self.base, self.letters[0]) + elif len(self.letters) > 0: + output += '%c[' % self.base + + for letter in self.letters: + output += letter + + output += ']' + + if len(self.words) != 0: + output += ')' + + if not isLast: + output += '"' + output += '\n' + + return output; + + def add(self, line): + length = len(line) + + if line.startswith('#') or (length == 0): + return; + + if length == 2: + self.letters.append(line[1:2]) + else: + self.words.append(line) + +def getBucket(buckets, line): + letter = line[0] + bucket = buckets.get(letter) + + if bucket is None: + bucket = Bucket(letter) + buckets[letter] = bucket + + return bucket + +def makePattern(prefix, suffix, buckets, isWebUrl=False): + output = prefix + + output += getBucket(buckets, 'a').dump(isFirst=True, isWebUrl=isWebUrl) + + for letter in range(ord('b'), ord('z')): + output += getBucket(buckets, chr(letter)).dump(isWebUrl=isWebUrl) + + output += getBucket(buckets, 'z').dump(isLast=True, isWebUrl=isWebUrl) + + if isWebUrl: + output += '))"' + else: + output += ')' + + output += suffix + + print output + +if __name__ == "__main__": + f = urlopen('http://data.iana.org/TLD/tlds-alpha-by-domain.txt') + domains = f.readlines() + f.close() + + buckets = {} + + for domain in domains: + domain = domain.lower() + + if len(domain) > 0: + getBucket(buckets, domain[0]).add(domain.strip()) + + makePattern(TLD_PREFIX, TLD_SUFFIX, buckets, isWebUrl=False) + makePattern(URL_PREFIX, URL_SUFFIX, buckets, isWebUrl=True) |