summaryrefslogtreecommitdiffstats
path: root/core/java
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-01-15 16:12:10 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-01-15 16:12:10 -0800
commit9266c558bf1d21ff647525ff99f7dadbca417309 (patch)
tree1630b1ba80f4793caf39d865528e662bdb1037fe /core/java
parentb798689749c64baba81f02e10cf2157c747d6b46 (diff)
downloadframeworks_base-9266c558bf1d21ff647525ff99f7dadbca417309.zip
frameworks_base-9266c558bf1d21ff647525ff99f7dadbca417309.tar.gz
frameworks_base-9266c558bf1d21ff647525ff99f7dadbca417309.tar.bz2
auto import from //branches/cupcake/...@126645
Diffstat (limited to 'core/java')
-rw-r--r--core/java/android/app/ActivityThread.java31
-rw-r--r--core/java/android/app/AlertDialog.java10
-rw-r--r--core/java/android/app/ApplicationThreadNative.java12
-rw-r--r--core/java/android/app/Dialog.java23
-rw-r--r--core/java/android/app/IApplicationThread.java4
-rw-r--r--core/java/android/app/SearchDialog.java129
-rw-r--r--core/java/android/content/AbstractSyncableContentProvider.java601
-rw-r--r--core/java/android/content/AbstractTableMerger.java108
-rw-r--r--core/java/android/content/SyncableContentProvider.java446
-rw-r--r--core/java/android/inputmethodservice/IInputMethodWrapper.java8
-rw-r--r--core/java/android/inputmethodservice/InputMethodService.java34
-rwxr-xr-xcore/java/android/inputmethodservice/KeyboardView.java131
-rw-r--r--core/java/android/os/Debug.java6
-rw-r--r--core/java/android/provider/Calendar.java49
-rw-r--r--core/java/android/provider/Gmail.java5
-rw-r--r--core/java/android/provider/MediaStore.java10
-rw-r--r--core/java/android/provider/Settings.java105
-rw-r--r--core/java/android/server/BluetoothA2dpService.java19
-rw-r--r--core/java/android/speech/srec/WaveHeader.java267
-rw-r--r--core/java/android/text/format/DateFormat.java2
-rw-r--r--core/java/android/util/DayOfMonthCursor.java14
-rw-r--r--core/java/android/util/Log.java24
-rw-r--r--core/java/android/view/IWindowManager.aidl1
-rw-r--r--core/java/android/view/RawInputEvent.java5
-rw-r--r--core/java/android/view/View.java30
-rw-r--r--core/java/android/view/ViewRoot.java33
-rw-r--r--core/java/android/view/WindowManager.java53
-rw-r--r--core/java/android/view/animation/Animation.java46
-rw-r--r--core/java/android/view/inputmethod/DefaultInputMethod.java4
-rw-r--r--core/java/android/view/inputmethod/InputMethod.java13
-rw-r--r--core/java/android/view/inputmethod/InputMethodManager.java40
-rw-r--r--core/java/android/webkit/MimeTypeMap.java5
-rw-r--r--core/java/android/webkit/WebView.java9
-rw-r--r--core/java/android/webkit/gears/ApacheHttpRequestAndroid.java1122
-rw-r--r--core/java/android/widget/AbsListView.java1
-rw-r--r--core/java/android/widget/AutoCompleteTextView.java40
-rw-r--r--core/java/android/widget/CursorAdapter.java3
-rw-r--r--core/java/android/widget/Filter.java17
-rw-r--r--core/java/android/widget/Gallery.java3
-rw-r--r--core/java/android/widget/PopupWindow.java18
-rw-r--r--core/java/android/widget/ScrollView.java14
-rw-r--r--core/java/android/widget/TextView.java15
-rw-r--r--core/java/com/android/internal/app/AlertController.java25
-rw-r--r--core/java/com/android/internal/os/HandlerCaller.java6
-rw-r--r--core/java/com/android/internal/view/IInputMethod.aidl2
-rw-r--r--core/java/com/android/internal/view/IInputMethodManager.aidl10
-rw-r--r--core/java/com/google/android/gdata/client/AndroidGDataClient.java47
47 files changed, 2876 insertions, 724 deletions
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 3d448a6..a98e295 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -1072,6 +1072,7 @@ public final class ActivityThread {
List<Intent> pendingIntents;
boolean startsNotResumed;
+ boolean isForward;
ActivityRecord() {
parent = null;
@@ -1225,8 +1226,8 @@ public final class ActivityThread {
token);
}
- public final void scheduleResumeActivity(IBinder token) {
- queueOrSendMessage(H.RESUME_ACTIVITY, token);
+ public final void scheduleResumeActivity(IBinder token, boolean isForward) {
+ queueOrSendMessage(H.RESUME_ACTIVITY, token, isForward ? 1 : 0);
}
public final void scheduleSendResult(IBinder token, List<ResultInfo> results) {
@@ -1240,7 +1241,7 @@ public final class ActivityThread {
// activity itself back to the activity manager. (matters more with ipc)
public final void scheduleLaunchActivity(Intent intent, IBinder token,
ActivityInfo info, Bundle state, List<ResultInfo> pendingResults,
- List<Intent> pendingNewIntents, boolean notResumed) {
+ List<Intent> pendingNewIntents, boolean notResumed, boolean isForward) {
ActivityRecord r = new ActivityRecord();
r.token = token;
@@ -1252,6 +1253,7 @@ public final class ActivityThread {
r.pendingIntents = pendingNewIntents;
r.startsNotResumed = notResumed;
+ r.isForward = isForward;
queueOrSendMessage(H.LAUNCH_ACTIVITY, r);
}
@@ -1604,7 +1606,8 @@ public final class ActivityThread {
handleWindowVisibility((IBinder)msg.obj, false);
break;
case RESUME_ACTIVITY:
- handleResumeActivity((IBinder)msg.obj, true);
+ handleResumeActivity((IBinder)msg.obj, true,
+ msg.arg1 != 0);
break;
case SEND_RESULT:
handleSendResult((ResultData)msg.obj);
@@ -2167,7 +2170,7 @@ public final class ActivityThread {
Activity a = performLaunchActivity(r);
if (a != null) {
- handleResumeActivity(r.token, false);
+ handleResumeActivity(r.token, false, r.isForward);
if (!r.activity.mFinished && r.startsNotResumed) {
// The activity manager actually wants this one to start out
@@ -2522,7 +2525,7 @@ public final class ActivityThread {
return r;
}
- final void handleResumeActivity(IBinder token, boolean clearHide) {
+ final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
@@ -2537,6 +2540,9 @@ public final class ActivityThread {
a.mStartedActivity + ", hideForNow: " + r.hideForNow
+ ", finished: " + a.mFinished);
+ final int forwardBit = isForward ?
+ WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
+
// If the window hasn't yet been added to the window manager,
// and this guy didn't finish itself or start another activity,
// then go ahead and add the window.
@@ -2548,6 +2554,7 @@ public final class ActivityThread {
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+ l.softInputMode |= forwardBit;
wm.addView(decor, l);
// If the window has already been added, but during resume
@@ -2567,6 +2574,18 @@ public final class ActivityThread {
performConfigurationChanged(r.activity, r.newConfig);
r.newConfig = null;
}
+ Log.v(TAG, "Resuming " + r + " with isForward=" + isForward);
+ WindowManager.LayoutParams l = r.window.getAttributes();
+ if ((l.softInputMode
+ & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
+ != forwardBit) {
+ l.softInputMode = (l.softInputMode
+ & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
+ | forwardBit;
+ ViewManager wm = a.getWindowManager();
+ View decor = r.window.getDecorView();
+ wm.updateViewLayout(decor, l);
+ }
r.activity.mDecor.setVisibility(View.VISIBLE);
mNumVisibleActivities++;
}
diff --git a/core/java/android/app/AlertDialog.java b/core/java/android/app/AlertDialog.java
index a6981a5..f2b89c3 100644
--- a/core/java/android/app/AlertDialog.java
+++ b/core/java/android/app/AlertDialog.java
@@ -24,6 +24,7 @@ import android.os.Bundle;
import android.os.Message;
import android.view.KeyEvent;
import android.view.View;
+import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.ListAdapter;
@@ -41,6 +42,15 @@ import com.android.internal.app.AlertController;
* FrameLayout fl = (FrameLayout) findViewById(R.id.body);
* fl.add(myView, new LayoutParams(FILL_PARENT, WRAP_CONTENT));
* </pre>
+ *
+ * <p>The AlertDialog class takes care of automatically setting
+ * {@link WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM
+ * WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM} for you based on whether
+ * any views in the dialog return true from {@link View#onCheckIsTextEditor()
+ * View.onCheckIsTextEditor()}. Generally you want this set for a Dialog
+ * without text editors, so that it will be placed on top of the current
+ * input method UI. You can modify this behavior by forcing the flag to your
+ * desired mode after calling {@link #onCreate}.
*/
public class AlertDialog extends Dialog implements DialogInterface {
private AlertController mAlert;
diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java
index 6a70329..54237ae 100644
--- a/core/java/android/app/ApplicationThreadNative.java
+++ b/core/java/android/app/ApplicationThreadNative.java
@@ -97,7 +97,8 @@ public abstract class ApplicationThreadNative extends Binder
{
data.enforceInterface(IApplicationThread.descriptor);
IBinder b = data.readStrongBinder();
- scheduleResumeActivity(b);
+ boolean isForward = data.readInt() != 0;
+ scheduleResumeActivity(b, isForward);
return true;
}
@@ -120,7 +121,8 @@ public abstract class ApplicationThreadNative extends Binder
List<ResultInfo> ri = data.createTypedArrayList(ResultInfo.CREATOR);
List<Intent> pi = data.createTypedArrayList(Intent.CREATOR);
boolean notResumed = data.readInt() != 0;
- scheduleLaunchActivity(intent, b, info, state, ri, pi, notResumed);
+ boolean isForward = data.readInt() != 0;
+ scheduleLaunchActivity(intent, b, info, state, ri, pi, notResumed, isForward);
return true;
}
@@ -376,11 +378,12 @@ class ApplicationThreadProxy implements IApplicationThread {
data.recycle();
}
- public final void scheduleResumeActivity(IBinder token)
+ public final void scheduleResumeActivity(IBinder token, boolean isForward)
throws RemoteException {
Parcel data = Parcel.obtain();
data.writeInterfaceToken(IApplicationThread.descriptor);
data.writeStrongBinder(token);
+ data.writeInt(isForward ? 1 : 0);
mRemote.transact(SCHEDULE_RESUME_ACTIVITY_TRANSACTION, data, null,
IBinder.FLAG_ONEWAY);
data.recycle();
@@ -399,7 +402,7 @@ class ApplicationThreadProxy implements IApplicationThread {
public final void scheduleLaunchActivity(Intent intent, IBinder token,
ActivityInfo info, Bundle state, List<ResultInfo> pendingResults,
- List<Intent> pendingNewIntents, boolean notResumed)
+ List<Intent> pendingNewIntents, boolean notResumed, boolean isForward)
throws RemoteException {
Parcel data = Parcel.obtain();
data.writeInterfaceToken(IApplicationThread.descriptor);
@@ -410,6 +413,7 @@ class ApplicationThreadProxy implements IApplicationThread {
data.writeTypedList(pendingResults);
data.writeTypedList(pendingNewIntents);
data.writeInt(notResumed ? 1 : 0);
+ data.writeInt(isForward ? 1 : 0);
mRemote.transact(SCHEDULE_LAUNCH_ACTIVITY_TRANSACTION, data, null,
IBinder.FLAG_ONEWAY);
data.recycle();
diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java
index f1d2e65..951b48d 100644
--- a/core/java/android/app/Dialog.java
+++ b/core/java/android/app/Dialog.java
@@ -48,13 +48,23 @@ import java.lang.ref.WeakReference;
/**
* Base class for Dialogs.
*
- * Note: Activities provide a facility to manage the creation, saving and
+ * <p>Note: Activities provide a facility to manage the creation, saving and
* restoring of dialogs. See {@link Activity#onCreateDialog(int)},
* {@link Activity#onPrepareDialog(int, Dialog)},
* {@link Activity#showDialog(int)}, and {@link Activity#dismissDialog(int)}. If
* these methods are used, {@link #getOwnerActivity()} will return the Activity
* that managed this dialog.
*
+ * <p>Often you will want to have a Dialog display on top of the current
+ * input method, because there is no reason for it to accept text. You can
+ * do this by setting the {@link WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM
+ * WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM} window flag (assuming
+ * your Dialog takes input focus, as it the default) with the following code:
+ *
+ * <pre>
+ * getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
+ * WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+ * </pre>
*/
public class Dialog implements DialogInterface, Window.Callback,
KeyEvent.Callback, OnCreateContextMenuListener {
@@ -209,7 +219,16 @@ public class Dialog implements DialogInterface, Window.Callback,
onStart();
mDecor = mWindow.getDecorView();
- mWindowManager.addView(mDecor, mWindow.getAttributes());
+ WindowManager.LayoutParams l = mWindow.getAttributes();
+ if ((l.softInputMode
+ & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
+ WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
+ nl.copyFrom(l);
+ nl.softInputMode |=
+ WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
+ l = nl;
+ }
+ mWindowManager.addView(mDecor, l);
mShowing = true;
}
diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java
index ecd993a..a351581 100644
--- a/core/java/android/app/IApplicationThread.java
+++ b/core/java/android/app/IApplicationThread.java
@@ -45,11 +45,11 @@ public interface IApplicationThread extends IInterface {
void scheduleStopActivity(IBinder token, boolean showWindow,
int configChanges) throws RemoteException;
void scheduleWindowVisibility(IBinder token, boolean showWindow) throws RemoteException;
- void scheduleResumeActivity(IBinder token) throws RemoteException;
+ void scheduleResumeActivity(IBinder token, boolean isForward) throws RemoteException;
void scheduleSendResult(IBinder token, List<ResultInfo> results) throws RemoteException;
void scheduleLaunchActivity(Intent intent, IBinder token,
ActivityInfo info, Bundle state, List<ResultInfo> pendingResults,
- List<Intent> pendingNewIntents, boolean notResumed)
+ List<Intent> pendingNewIntents, boolean notResumed, boolean isForward)
throws RemoteException;
void scheduleRelaunchActivity(IBinder token, List<ResultInfo> pendingResults,
List<Intent> pendingNewIntents, int configChanges,
diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java
index f1c604c..495156e 100644
--- a/core/java/android/app/SearchDialog.java
+++ b/core/java/android/app/SearchDialog.java
@@ -38,6 +38,7 @@ import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
+import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
@@ -538,7 +539,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
}
updateWidgetState();
// Only do suggestions if actually typed by user
- if (mSuggestionsAdapter.getNonUserQuery()) {
+ if (!mSuggestionsAdapter.getNonUserQuery()) {
mPreviousSuggestionQuery = s.toString();
mUserQuery = mSearchTextField.getText().toString();
mUserQuerySelStart = mSearchTextField.getSelectionStart();
@@ -640,6 +641,12 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
if (DBG_LOG_TIMING == 1) {
dbgLogTiming("doTextKey()");
}
+ // dispatch "typing in the list" first
+ if (mSearchTextField.isPopupShowing() &&
+ mSearchTextField.getListSelection() != ListView.INVALID_POSITION) {
+ return onSuggestionsKey(v, keyCode, event);
+ }
+ // otherwise, dispatch an "edit view" key
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_DPAD_CENTER:
@@ -649,6 +656,13 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
return true;
}
break;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ // capture the EditText state, so we can restore the user entry later
+ mUserQuery = mSearchTextField.getText().toString();
+ mUserQuerySelStart = mSearchTextField.getSelectionStart();
+ mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
+ // pass through - we're just watching here
+ break;
default:
if (event.getAction() == KeyEvent.ACTION_DOWN) {
SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
@@ -668,24 +682,18 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
* React to the user typing while the suggestions are focused. First, check for action
* keys. If not handled, try refocusing regular characters into the EditText. In this case,
* replace the query text (start typing fresh text).
- *
- * TODO: Move this code into mTextKeyListener, testing for a list entry being hilited
*/
- /*
- View.OnKeyListener mSuggestionsKeyListener = new View.OnKeyListener() {
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- boolean handled = false;
- // also guard against possible race conditions (late arrival after dismiss)
- if (mSearchable != null) {
- handled = doSuggestionsKey(v, keyCode, event);
- if (!handled) {
- handled = refocusingKeyListener(v, keyCode, event);
- }
+ private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
+ boolean handled = false;
+ // also guard against possible race conditions (late arrival after dismiss)
+ if (mSearchable != null) {
+ handled = doSuggestionsKey(v, keyCode, event);
+ if (!handled) {
+ handled = refocusingKeyListener(v, keyCode, event);
}
- return handled;
}
- };
- */
+ return handled;
+ }
/**
* Per UI design, we're going to "steer" any typed keystrokes back into the EditText
@@ -821,26 +829,26 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
// First, check for enter or search (both of which we'll treat as a "click")
if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
- AdapterView<?> av = (AdapterView<?>) v;
- int position = av.getSelectedItemPosition();
- return launchSuggestion(av, position);
+ int position = mSearchTextField.getListSelection();
+ return launchSuggestion(mSuggestionsAdapter, position);
}
- // Next, check for left/right moves while we'll manually grab and shift focus
+ // Next, check for left/right moves, which we use to "return" the user to the edit view
if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
- // give focus to text editor
- // but don't restore the user's original query
- mLeaveJammedQueryOnRefocus = true;
- if (mSearchTextField.requestFocus()) {
- mLeaveJammedQueryOnRefocus = false;
- if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
- mSearchTextField.setSelection(0);
- } else {
- mSearchTextField.setSelection(mSearchTextField.length());
- }
- return true;
- }
- mLeaveJammedQueryOnRefocus = false;
+ // give "focus" to text editor, but don't restore the user's original query
+ int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ?
+ 0 : mSearchTextField.length();
+ mSearchTextField.setSelection(selPoint);
+ mSearchTextField.setListSelection(0);
+ mSearchTextField.clearListSelection();
+ return true;
+ }
+
+ // Next, check for an "up and out" move
+ if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchTextField.getListSelection()) {
+ jamSuggestionQuery(false, null, -1);
+ // let ACTV complete the move
+ return false;
}
// Next, check for an "action key"
@@ -849,11 +857,9 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
((actionKey.mSuggestActionMsg != null) ||
(actionKey.mSuggestActionMsgColumn != null))) {
// launch suggestion using action key column
- ListView lv = (ListView) v;
- int position = lv.getSelectedItemPosition();
+ int position = mSearchTextField.getListSelection();
if (position >= 0) {
- CursorAdapter ca = getSuggestionsAdapter(lv);
- Cursor c = ca.getCursor();
+ Cursor c = mSuggestionsAdapter.getCursor();
if (c.moveToPosition(position)) {
final String actionMsg = getActionKeyMessage(c, actionKey);
if (actionMsg != null && (actionMsg.length() > 0)) {
@@ -977,19 +983,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
/**
* Shared code for launching a query from a suggestion.
- *
- * @param av The AdapterView (really a ListView) containing the suggestions
- * @param position The suggestion we'll be launching from
- *
- * @return Returns true if a successful launch, false if could not (e.g. bad position)
- */
- private boolean launchSuggestion(AdapterView<?> av, int position) {
- CursorAdapter ca = getSuggestionsAdapter(av);
- return launchSuggestion(ca, position);
- }
-
- /**
- * Shared code for launching a query from a suggestion.
* @param ca The cursor adapter containing the suggestions
* @param position The suggestion we'll be launching from
* @return true if a successful launch, false if could not (e.g. bad position)
@@ -1116,6 +1109,36 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
}
/**
+ * Local subclass for AutoCompleteTextView
+ *
+ * This exists entirely to override the threshold method. Otherwise we just use the class
+ * as-is.
+ */
+ public static class SearchAutoComplete extends AutoCompleteTextView {
+
+ public SearchAutoComplete(Context context) {
+ super(null);
+ }
+
+ public SearchAutoComplete(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ /**
+ * We always return true, so that the effective threshold is "zero". This allows us
+ * to provide "null" suggestions such as "just show me some recent entries".
+ */
+ @Override
+ public boolean enoughToFilter() {
+ return true;
+ }
+ }
+
+ /**
* Support for AutoCompleteTextView-based suggestions
*/
/**
@@ -1391,7 +1414,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
* Implements OnItemClickListener
*/
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-// Log.d(LOG_TAG, "onItemClick() position " + position);
+ // Log.d(LOG_TAG, "onItemClick() position " + position);
launchSuggestion(mSuggestionsAdapter, position);
}
@@ -1399,7 +1422,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
* Implements OnItemSelectedListener
*/
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
-// Log.d(LOG_TAG, "onItemSelected() position " + position);
+ // Log.d(LOG_TAG, "onItemSelected() position " + position);
jamSuggestionQuery(true, parent, position);
}
@@ -1407,7 +1430,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
* Implements OnItemSelectedListener
*/
public void onNothingSelected(AdapterView<?> parent) {
-// Log.d(LOG_TAG, "onNothingSelected()");
+ // Log.d(LOG_TAG, "onNothingSelected()");
}
/**
diff --git a/core/java/android/content/AbstractSyncableContentProvider.java b/core/java/android/content/AbstractSyncableContentProvider.java
new file mode 100644
index 0000000..ce6501c
--- /dev/null
+++ b/core/java/android/content/AbstractSyncableContentProvider.java
@@ -0,0 +1,601 @@
+package android.content;
+
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.Cursor;
+import android.net.Uri;
+import android.accounts.AccountMonitor;
+import android.accounts.AccountMonitorListener;
+import android.provider.SyncConstValue;
+import android.util.Config;
+import android.util.Log;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Vector;
+import java.util.ArrayList;
+
+/**
+ * A specialization of the ContentProvider that centralizes functionality
+ * used by ContentProviders that are syncable. It also wraps calls to the ContentProvider
+ * inside of database transactions.
+ *
+ * @hide
+ */
+public abstract class AbstractSyncableContentProvider extends SyncableContentProvider {
+ private static final String TAG = "SyncableContentProvider";
+ protected SQLiteOpenHelper mOpenHelper;
+ protected SQLiteDatabase mDb;
+ private final String mDatabaseName;
+ private final int mDatabaseVersion;
+ private final Uri mContentUri;
+ private AccountMonitor mAccountMonitor;
+
+ /** the account set in the last call to onSyncStart() */
+ private String mSyncingAccount;
+
+ private SyncStateContentProviderHelper mSyncState = null;
+
+ private static final String[] sAccountProjection = new String[] {SyncConstValue._SYNC_ACCOUNT};
+
+ private boolean mIsTemporary;
+
+ private AbstractTableMerger mCurrentMerger = null;
+ private boolean mIsMergeCancelled = false;
+
+ private static final String SYNC_ACCOUNT_WHERE_CLAUSE = SyncConstValue._SYNC_ACCOUNT + "=?";
+
+ protected boolean isTemporary() {
+ return mIsTemporary;
+ }
+
+ /**
+ * Indicates whether or not this ContentProvider contains a full
+ * set of data or just diffs. This knowledge comes in handy when
+ * determining how to incorporate the contents of a temporary
+ * provider into a real provider.
+ */
+ private boolean mContainsDiffs;
+
+ /**
+ * Initializes the AbstractSyncableContentProvider
+ * @param dbName the filename of the database
+ * @param dbVersion the current version of the database schema
+ * @param contentUri The base Uri of the syncable content in this provider
+ */
+ public AbstractSyncableContentProvider(String dbName, int dbVersion, Uri contentUri) {
+ super();
+
+ mDatabaseName = dbName;
+ mDatabaseVersion = dbVersion;
+ mContentUri = contentUri;
+ mIsTemporary = false;
+ setContainsDiffs(false);
+ if (Config.LOGV) {
+ Log.v(TAG, "created SyncableContentProvider " + this);
+ }
+ }
+
+ /**
+ * Close resources that must be closed. You must call this to properly release
+ * the resources used by the AbstractSyncableContentProvider.
+ */
+ public void close() {
+ if (mOpenHelper != null) {
+ mOpenHelper.close(); // OK to call .close() repeatedly.
+ }
+ }
+
+ /**
+ * Override to create your schema and do anything else you need to do with a new database.
+ * This is run inside a transaction (so you don't need to use one).
+ * This method may not use getDatabase(), or call content provider methods, it must only
+ * use the database handle passed to it.
+ */
+ protected void bootstrapDatabase(SQLiteDatabase db) {}
+
+ /**
+ * Override to upgrade your database from an old version to the version you specified.
+ * Don't set the DB version; this will automatically be done after the method returns.
+ * This method may not use getDatabase(), or call content provider methods, it must only
+ * use the database handle passed to it.
+ *
+ * @param oldVersion version of the existing database
+ * @param newVersion current version to upgrade to
+ * @return true if the upgrade was lossless, false if it was lossy
+ */
+ protected abstract boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion);
+
+ /**
+ * Override to do anything (like cleanups or checks) you need to do after opening a database.
+ * Does nothing by default. This is run inside a transaction (so you don't need to use one).
+ * This method may not use getDatabase(), or call content provider methods, it must only
+ * use the database handle passed to it.
+ */
+ protected void onDatabaseOpened(SQLiteDatabase db) {}
+
+ private class DatabaseHelper extends SQLiteOpenHelper {
+ DatabaseHelper(Context context, String name) {
+ // Note: context and name may be null for temp providers
+ super(context, name, null, mDatabaseVersion);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ bootstrapDatabase(db);
+ mSyncState.createDatabase(db);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (!upgradeDatabase(db, oldVersion, newVersion)) {
+ mSyncState.discardSyncData(db, null /* all accounts */);
+ getContext().getContentResolver().startSync(mContentUri, new Bundle());
+ }
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ onDatabaseOpened(db);
+ mSyncState.onDatabaseOpened(db);
+ }
+ }
+
+ @Override
+ public boolean onCreate() {
+ if (isTemporary()) throw new IllegalStateException("onCreate() called for temp provider");
+ mOpenHelper = new AbstractSyncableContentProvider.DatabaseHelper(getContext(), mDatabaseName);
+ mSyncState = new SyncStateContentProviderHelper(mOpenHelper);
+
+ AccountMonitorListener listener = new AccountMonitorListener() {
+ public void onAccountsUpdated(String[] accounts) {
+ // Some providers override onAccountsChanged(); give them a database to work with.
+ mDb = mOpenHelper.getWritableDatabase();
+ onAccountsChanged(accounts);
+ TempProviderSyncAdapter syncAdapter = (TempProviderSyncAdapter)getSyncAdapter();
+ if (syncAdapter != null) {
+ syncAdapter.onAccountsChanged(accounts);
+ }
+ }
+ };
+ mAccountMonitor = new AccountMonitor(getContext(), listener);
+
+ return true;
+ }
+
+ /**
+ * Get a non-persistent instance of this content provider.
+ * You must call {@link #close} on the returned
+ * SyncableContentProvider when you are done with it.
+ *
+ * @return a non-persistent content provider with the same layout as this
+ * provider.
+ */
+ public AbstractSyncableContentProvider getTemporaryInstance() {
+ AbstractSyncableContentProvider temp;
+ try {
+ temp = getClass().newInstance();
+ } catch (InstantiationException e) {
+ throw new RuntimeException("unable to instantiate class, "
+ + "this should never happen", e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(
+ "IllegalAccess while instantiating class, "
+ + "this should never happen", e);
+ }
+
+ // Note: onCreate() isn't run for the temp provider, and it has no Context.
+ temp.mIsTemporary = true;
+ temp.setContainsDiffs(true);
+ temp.mOpenHelper = temp.new DatabaseHelper(null, null);
+ temp.mSyncState = new SyncStateContentProviderHelper(temp.mOpenHelper);
+ if (!isTemporary()) {
+ mSyncState.copySyncState(
+ mOpenHelper.getReadableDatabase(),
+ temp.mOpenHelper.getWritableDatabase(),
+ getSyncingAccount());
+ }
+ return temp;
+ }
+
+ public SQLiteDatabase getDatabase() {
+ if (mDb == null) mDb = mOpenHelper.getWritableDatabase();
+ return mDb;
+ }
+
+ public boolean getContainsDiffs() {
+ return mContainsDiffs;
+ }
+
+ public void setContainsDiffs(boolean containsDiffs) {
+ if (containsDiffs && !isTemporary()) {
+ throw new IllegalStateException(
+ "only a temporary provider can contain diffs");
+ }
+ mContainsDiffs = containsDiffs;
+ }
+
+ /**
+ * Each subclass of this class should define a subclass of {@link
+ * android.content.AbstractTableMerger} for each table they wish to merge. It
+ * should then override this method and return one instance of
+ * each merger, in sequence. Their {@link
+ * android.content.AbstractTableMerger#merge merge} methods will be called, one at a
+ * time, in the order supplied.
+ *
+ * <p>The default implementation returns an empty list, so that no
+ * merging will occur.
+ * @return A sequence of subclasses of {@link
+ * android.content.AbstractTableMerger}, one for each table that should be merged.
+ */
+ protected Iterable<? extends AbstractTableMerger> getMergers() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public final int update(final Uri url, final ContentValues values,
+ final String selection, final String[] selectionArgs) {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransaction();
+ try {
+ if (isTemporary() && mSyncState.matches(url)) {
+ int numRows = mSyncState.asContentProvider().update(
+ url, values, selection, selectionArgs);
+ mDb.setTransactionSuccessful();
+ return numRows;
+ }
+
+ int result = updateInternal(url, values, selection, selectionArgs);
+ mDb.setTransactionSuccessful();
+
+ if (!isTemporary() && result > 0) {
+ getContext().getContentResolver().notifyChange(url, null /* observer */,
+ changeRequiresLocalSync(url));
+ }
+
+ return result;
+ } finally {
+ mDb.endTransaction();
+ }
+ }
+
+ @Override
+ public final int delete(final Uri url, final String selection,
+ final String[] selectionArgs) {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransaction();
+ try {
+ if (isTemporary() && mSyncState.matches(url)) {
+ int numRows = mSyncState.asContentProvider().delete(url, selection, selectionArgs);
+ mDb.setTransactionSuccessful();
+ return numRows;
+ }
+ int result = deleteInternal(url, selection, selectionArgs);
+ mDb.setTransactionSuccessful();
+ if (!isTemporary() && result > 0) {
+ getContext().getContentResolver().notifyChange(url, null /* observer */,
+ changeRequiresLocalSync(url));
+ }
+ return result;
+ } finally {
+ mDb.endTransaction();
+ }
+ }
+
+ @Override
+ public final Uri insert(final Uri url, final ContentValues values) {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransaction();
+ try {
+ if (isTemporary() && mSyncState.matches(url)) {
+ Uri result = mSyncState.asContentProvider().insert(url, values);
+ mDb.setTransactionSuccessful();
+ return result;
+ }
+ Uri result = insertInternal(url, values);
+ mDb.setTransactionSuccessful();
+ if (!isTemporary() && result != null) {
+ getContext().getContentResolver().notifyChange(url, null /* observer */,
+ changeRequiresLocalSync(url));
+ }
+ return result;
+ } finally {
+ mDb.endTransaction();
+ }
+ }
+
+ @Override
+ public final int bulkInsert(final Uri uri, final ContentValues[] values) {
+ int size = values.length;
+ int completed = 0;
+ final boolean isSyncStateUri = mSyncState.matches(uri);
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransaction();
+ try {
+ for (int i = 0; i < size; i++) {
+ Uri result;
+ if (isTemporary() && isSyncStateUri) {
+ result = mSyncState.asContentProvider().insert(uri, values[i]);
+ } else {
+ result = insertInternal(uri, values[i]);
+ mDb.yieldIfContended();
+ }
+ if (result != null) {
+ completed++;
+ }
+ }
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+ if (!isTemporary() && completed == size) {
+ getContext().getContentResolver().notifyChange(uri, null /* observer */,
+ changeRequiresLocalSync(uri));
+ }
+ return completed;
+ }
+
+ /**
+ * Check if changes to this URI can be syncable changes.
+ * @param uri the URI of the resource that was changed
+ * @return true if changes to this URI can be syncable changes, false otherwise
+ */
+ public boolean changeRequiresLocalSync(Uri uri) {
+ return true;
+ }
+
+ @Override
+ public final Cursor query(final Uri url, final String[] projection,
+ final String selection, final String[] selectionArgs,
+ final String sortOrder) {
+ mDb = mOpenHelper.getReadableDatabase();
+ if (isTemporary() && mSyncState.matches(url)) {
+ return mSyncState.asContentProvider().query(
+ url, projection, selection, selectionArgs, sortOrder);
+ }
+ return queryInternal(url, projection, selection, selectionArgs, sortOrder);
+ }
+
+ /**
+ * Called right before a sync is started.
+ *
+ * @param context the sync context for the operation
+ * @param account
+ */
+ public void onSyncStart(SyncContext context, String account) {
+ if (TextUtils.isEmpty(account)) {
+ throw new IllegalArgumentException("you passed in an empty account");
+ }
+ mSyncingAccount = account;
+ }
+
+ /**
+ * Called right after a sync is completed
+ *
+ * @param context the sync context for the operation
+ * @param success true if the sync succeeded, false if an error occurred
+ */
+ public void onSyncStop(SyncContext context, boolean success) {
+ }
+
+ /**
+ * The account of the most recent call to onSyncStart()
+ * @return the account
+ */
+ public String getSyncingAccount() {
+ return mSyncingAccount;
+ }
+
+ /**
+ * Merge diffs from a sync source with this content provider.
+ *
+ * @param context the SyncContext within which this merge is taking place
+ * @param diffs A temporary content provider containing diffs from a sync
+ * source.
+ * @param result a MergeResult that contains information about the merge, including
+ * a temporary content provider with the same layout as this provider containing
+ * @param syncResult
+ */
+ public void merge(SyncContext context, SyncableContentProvider diffs,
+ TempProviderSyncResult result, SyncResult syncResult) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ synchronized(this) {
+ mIsMergeCancelled = false;
+ }
+ Iterable<? extends AbstractTableMerger> mergers = getMergers();
+ try {
+ for (AbstractTableMerger merger : mergers) {
+ synchronized(this) {
+ if (mIsMergeCancelled) break;
+ mCurrentMerger = merger;
+ }
+ merger.merge(context, getSyncingAccount(), diffs, result, syncResult, this);
+ }
+ if (mIsMergeCancelled) return;
+ if (diffs != null) {
+ mSyncState.copySyncState(
+ ((AbstractSyncableContentProvider)diffs).mOpenHelper.getReadableDatabase(),
+ mOpenHelper.getWritableDatabase(),
+ getSyncingAccount());
+ }
+ } finally {
+ synchronized (this) {
+ mCurrentMerger = null;
+ }
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+
+ /**
+ * Invoked when the active sync has been canceled. Sets the sync state of this provider and
+ * its merger to canceled.
+ */
+ public void onSyncCanceled() {
+ synchronized (this) {
+ mIsMergeCancelled = true;
+ if (mCurrentMerger != null) {
+ mCurrentMerger.onMergeCancelled();
+ }
+ }
+ }
+
+
+ public boolean isMergeCancelled() {
+ return mIsMergeCancelled;
+ }
+
+ /**
+ * Subclasses should override this instead of update(). See update()
+ * for details.
+ *
+ * <p> This method is called within a acquireDbLock()/releaseDbLock() block,
+ * which means a database transaction will be active during the call;
+ */
+ protected abstract int updateInternal(Uri url, ContentValues values,
+ String selection, String[] selectionArgs);
+
+ /**
+ * Subclasses should override this instead of delete(). See delete()
+ * for details.
+ *
+ * <p> This method is called within a acquireDbLock()/releaseDbLock() block,
+ * which means a database transaction will be active during the call;
+ */
+ protected abstract int deleteInternal(Uri url, String selection, String[] selectionArgs);
+
+ /**
+ * Subclasses should override this instead of insert(). See insert()
+ * for details.
+ *
+ * <p> This method is called within a acquireDbLock()/releaseDbLock() block,
+ * which means a database transaction will be active during the call;
+ */
+ protected abstract Uri insertInternal(Uri url, ContentValues values);
+
+ /**
+ * Subclasses should override this instead of query(). See query()
+ * for details.
+ *
+ * <p> This method is *not* called within a acquireDbLock()/releaseDbLock()
+ * block for performance reasons. If an implementation needs atomic access
+ * to the database the lock can be acquired then.
+ */
+ protected abstract Cursor queryInternal(Uri url, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder);
+
+ /**
+ * Make sure that there are no entries for accounts that no longer exist
+ * @param accountsArray the array of currently-existing accounts
+ */
+ protected void onAccountsChanged(String[] accountsArray) {
+ Map<String, Boolean> accounts = new HashMap<String, Boolean>();
+ for (String account : accountsArray) {
+ accounts.put(account, false);
+ }
+ accounts.put(SyncConstValue.NON_SYNCABLE_ACCOUNT, false);
+
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ Map<String, String> tableMap = db.getSyncedTables();
+ Vector<String> tables = new Vector<String>();
+ tables.addAll(tableMap.keySet());
+ tables.addAll(tableMap.values());
+
+ db.beginTransaction();
+ try {
+ mSyncState.onAccountsChanged(accountsArray);
+ for (String table : tables) {
+ deleteRowsForRemovedAccounts(accounts, table,
+ SyncConstValue._SYNC_ACCOUNT);
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * A helper method to delete all rows whose account is not in the accounts
+ * map. The accountColumnName is the name of the column that is expected
+ * to hold the account. If a row has an empty account it is never deleted.
+ *
+ * @param accounts a map of existing accounts
+ * @param table the table to delete from
+ * @param accountColumnName the name of the column that is expected
+ * to hold the account.
+ */
+ protected void deleteRowsForRemovedAccounts(Map<String, Boolean> accounts,
+ String table, String accountColumnName) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ Cursor c = db.query(table, sAccountProjection, null, null,
+ accountColumnName, null, null);
+ try {
+ while (c.moveToNext()) {
+ String account = c.getString(0);
+ if (TextUtils.isEmpty(account)) {
+ continue;
+ }
+ if (!accounts.containsKey(account)) {
+ int numDeleted;
+ numDeleted = db.delete(table, accountColumnName + "=?", new String[]{account});
+ if (Config.LOGV) {
+ Log.v(TAG, "deleted " + numDeleted
+ + " records from table " + table
+ + " for account " + account);
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Called when the sync system determines that this provider should no longer
+ * contain records for the specified account.
+ */
+ public void wipeAccount(String account) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ Map<String, String> tableMap = db.getSyncedTables();
+ ArrayList<String> tables = new ArrayList<String>();
+ tables.addAll(tableMap.keySet());
+ tables.addAll(tableMap.values());
+
+ db.beginTransaction();
+
+ try {
+ // remove the SyncState data
+ mSyncState.discardSyncData(db, account);
+
+ // remove the data in the synced tables
+ for (String table : tables) {
+ db.delete(table, SYNC_ACCOUNT_WHERE_CLAUSE, new String[]{account});
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Retrieves the SyncData bytes for the given account. The byte array returned may be null.
+ */
+ public byte[] readSyncDataBytes(String account) {
+ return mSyncState.readSyncDataBytes(mOpenHelper.getReadableDatabase(), account);
+ }
+
+ /**
+ * Sets the SyncData bytes for the given account. The byte array may be null.
+ */
+ public void writeSyncDataBytes(String account, byte[] data) {
+ mSyncState.writeSyncDataBytes(mOpenHelper.getWritableDatabase(), account, data);
+ }
+}
diff --git a/core/java/android/content/AbstractTableMerger.java b/core/java/android/content/AbstractTableMerger.java
index 5511ff6..700f1d8 100644
--- a/core/java/android/content/AbstractTableMerger.java
+++ b/core/java/android/content/AbstractTableMerger.java
@@ -21,12 +21,8 @@ import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Debug;
-import static android.provider.SyncConstValue._SYNC_ACCOUNT;
-import static android.provider.SyncConstValue._SYNC_DIRTY;
-import static android.provider.SyncConstValue._SYNC_ID;
-import static android.provider.SyncConstValue._SYNC_LOCAL_ID;
-import static android.provider.SyncConstValue._SYNC_MARK;
-import static android.provider.SyncConstValue._SYNC_VERSION;
+import android.provider.BaseColumns;
+import static android.provider.SyncConstValue.*;
import android.text.TextUtils;
import android.util.Log;
@@ -53,7 +49,7 @@ public abstract class AbstractTableMerger
private static final String TAG = "AbstractTableMerger";
private static final String[] syncDirtyProjection =
- new String[] {_SYNC_DIRTY, "_id", _SYNC_ID, _SYNC_VERSION};
+ new String[] {_SYNC_DIRTY, BaseColumns._ID, _SYNC_ID, _SYNC_VERSION};
private static final String[] syncIdAndVersionProjection =
new String[] {_SYNC_ID, _SYNC_VERSION};
@@ -61,8 +57,9 @@ public abstract class AbstractTableMerger
private static final String SELECT_MARKED = _SYNC_MARK + "> 0 and " + _SYNC_ACCOUNT + "=?";
- private static final String SELECT_BY_ID_AND_ACCOUNT =
+ private static final String SELECT_BY_SYNC_ID_AND_ACCOUNT =
_SYNC_ID +"=? and " + _SYNC_ACCOUNT + "=?";
+ private static final String SELECT_BY_ID = BaseColumns._ID +"=?";
private static final String SELECT_UNSYNCED = ""
+ _SYNC_DIRTY + " > 0 and (" + _SYNC_ACCOUNT + "=? or " + _SYNC_ACCOUNT + " is null)";
@@ -90,7 +87,8 @@ public abstract class AbstractTableMerger
* This is called when it is determined that a row should be deleted from the
* ContentProvider. The localCursor is on a table from the local ContentProvider
* and its current position is of the row that should be deleted. The localCursor
- * contains the complete projection of the table.
+ * is only guaranteed to contain the BaseColumns.ID column so the implementation
+ * of deleteRow() must query the database directly if other columns are needed.
* <p>
* It is the responsibility of the implementation of this method to ensure that the cursor
* points to the next row when this method returns, either by calling Cursor.deleteRow() or
@@ -153,7 +151,10 @@ public abstract class AbstractTableMerger
if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "merge complete");
}
- private void mergeServerDiffs(SyncContext context,
+ /**
+ * @hide this is public for testing purposes only
+ */
+ public void mergeServerDiffs(SyncContext context,
String account, SyncableContentProvider serverDiffs, SyncResult syncResult) {
boolean diffsArePartial = serverDiffs.getContainsDiffs();
// mark the current rows so that we can distinguish these from new
@@ -202,7 +203,7 @@ public abstract class AbstractTableMerger
mDb.yieldIfContended();
String serverSyncId = diffsCursor.getString(serverSyncIDColumn);
String serverSyncVersion = diffsCursor.getString(serverSyncVersionColumn);
- long localPersonID = 0;
+ long localRowId = 0;
String localSyncVersion = null;
diffsCount++;
@@ -316,7 +317,7 @@ public abstract class AbstractTableMerger
" that matches the server _sync_id");
}
localSyncDirty = localCursor.getInt(0) != 0;
- localPersonID = localCursor.getLong(1);
+ localRowId = localCursor.getLong(1);
localSyncVersion = localCursor.getString(3);
localCursor.moveToNext();
}
@@ -345,23 +346,20 @@ public abstract class AbstractTableMerger
continue;
}
- // If the _sync_local_id is set and > -1 in the diffsCursor
+ // If the _sync_local_id is present in the diffsCursor
// then this record corresponds to a local record that was just
// inserted into the server and the _sync_local_id is the row id
// of the local record. Set these fields so that the next check
// treats this record as an update, which will allow the
// merger to update the record with the server's sync id
- long serverLocalSyncId =
- diffsCursor.isNull(serverSyncLocalIdColumn)
- ? -1
- : diffsCursor.getLong(serverSyncLocalIdColumn);
- if (serverLocalSyncId > -1) {
- if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "the remote record with sync id "
- + serverSyncId + " has a local sync id, "
- + serverLocalSyncId);
+ if (!diffsCursor.isNull(serverSyncLocalIdColumn)) {
+ localRowId = diffsCursor.getLong(serverSyncLocalIdColumn);
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "the remote record with sync id " + serverSyncId
+ + " has a local sync id, " + localRowId);
+ }
localSyncID = serverSyncId;
localSyncDirty = false;
- localPersonID = serverLocalSyncId;
localSyncVersion = null;
}
@@ -372,12 +370,9 @@ public abstract class AbstractTableMerger
if (recordChanged) {
if (localSyncDirty) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG,
- "remote record " +
- serverSyncId +
- " conflicts with local _sync_id " +
- localSyncID + ", local _id " +
- localPersonID);
+ Log.v(TAG, "remote record " + serverSyncId
+ + " conflicts with local _sync_id " + localSyncID
+ + ", local _id " + localRowId);
}
conflict = true;
} else {
@@ -387,7 +382,7 @@ public abstract class AbstractTableMerger
serverSyncId +
" updates local _sync_id " +
localSyncID + ", local _id " +
- localPersonID);
+ localRowId);
}
update = true;
}
@@ -395,18 +390,16 @@ public abstract class AbstractTableMerger
} else {
// the local db doesn't know about this record so add it
if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "remote record "
- + serverSyncId + " is new, inserting");
+ Log.v(TAG, "remote record " + serverSyncId + " is new, inserting");
}
insert = true;
}
if (update) {
- updateRow(localPersonID, serverDiffs, diffsCursor);
+ updateRow(localRowId, serverDiffs, diffsCursor);
syncResult.stats.numUpdates++;
} else if (conflict) {
- resolveRow(localPersonID, serverSyncId, serverDiffs,
- diffsCursor);
+ resolveRow(localRowId, serverSyncId, serverDiffs, diffsCursor);
syncResult.stats.numUpdates++;
} else if (insert) {
insertRow(serverDiffs, diffsCursor);
@@ -414,16 +407,16 @@ public abstract class AbstractTableMerger
}
}
- if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "processed " + diffsCount +
- " server entries");
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "processed " + diffsCount + " server entries");
+ }
// If tombstones aren't in use delete any remaining local rows that
// don't have corresponding server rows. Keep the rows that don't
// have a sync id since those were created locally and haven't been
// synced to the server yet.
if (!diffsArePartial) {
- while (!localCursor.isAfterLast() &&
- !TextUtils.isEmpty(localCursor.getString(2))) {
+ while (!localCursor.isAfterLast() && !TextUtils.isEmpty(localCursor.getString(2))) {
if (mIsMergeCancelled) {
localCursor.deactivate();
deletedCursor.deactivate();
@@ -458,7 +451,6 @@ public abstract class AbstractTableMerger
// Apply deletions from the server
if (mDeletedTableURL != null) {
diffsCursor = serverDiffs.query(mDeletedTableURL, null, null, null, null);
- serverSyncIDColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID);
while (diffsCursor.moveToNext()) {
if (mIsMergeCancelled) {
@@ -466,19 +458,31 @@ public abstract class AbstractTableMerger
return;
}
// delete all rows that match each element in the diffsCursor
- fullyDeleteRowsWithSyncId(diffsCursor.getString(serverSyncIDColumn), account,
- syncResult);
+ fullyDeleteMatchingRows(diffsCursor, account, syncResult);
mDb.yieldIfContended();
}
diffsCursor.deactivate();
}
}
- private void fullyDeleteRowsWithSyncId(String syncId, String account, SyncResult syncResult) {
- final String[] selectionArgs = new String[]{syncId, account};
+ private void fullyDeleteMatchingRows(Cursor diffsCursor, String account,
+ SyncResult syncResult) {
+ int serverSyncIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID);
+ final boolean deleteBySyncId = !diffsCursor.isNull(serverSyncIdColumn);
+
// delete the rows explicitly so that the delete operation can be overridden
- Cursor c = mDb.query(mTable, getDeleteRowProjection(), SELECT_BY_ID_AND_ACCOUNT,
- selectionArgs, null, null, null);
+ final Cursor c;
+ final String[] selectionArgs;
+ if (deleteBySyncId) {
+ selectionArgs = new String[]{diffsCursor.getString(serverSyncIdColumn), account};
+ c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_SYNC_ID_AND_ACCOUNT,
+ selectionArgs, null, null, null);
+ } else {
+ int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID);
+ selectionArgs = new String[]{diffsCursor.getString(serverSyncLocalIdColumn)};
+ c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_ID, selectionArgs,
+ null, null, null);
+ }
try {
c.moveToFirst();
while (!c.isAfterLast()) {
@@ -488,22 +492,12 @@ public abstract class AbstractTableMerger
} finally {
c.deactivate();
}
- if (mDeletedTable != null) {
- mDb.delete(mDeletedTable, SELECT_BY_ID_AND_ACCOUNT, selectionArgs);
+ if (deleteBySyncId && mDeletedTable != null) {
+ mDb.delete(mDeletedTable, SELECT_BY_SYNC_ID_AND_ACCOUNT, selectionArgs);
}
}
/**
- * Provides the projection used by
- * {@link AbstractTableMerger#deleteRow(android.database.Cursor)}.
- * This should be overridden if the deleteRow implementation requires
- * additional columns.
- */
- protected String[] getDeleteRowProjection() {
- return new String[]{"_id"};
- }
-
- /**
* Converts cursor into a Map, using the correct types for the values.
*/
protected void cursorRowToContentValues(Cursor cursor, ContentValues map) {
diff --git a/core/java/android/content/SyncableContentProvider.java b/core/java/android/content/SyncableContentProvider.java
index 1e55e27..e0cd786 100644
--- a/core/java/android/content/SyncableContentProvider.java
+++ b/core/java/android/content/SyncableContentProvider.java
@@ -16,23 +16,11 @@
package android.content;
-import android.accounts.AccountMonitor;
-import android.accounts.AccountMonitorListener;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
-import android.provider.SyncConstValue;
-import android.text.TextUtils;
-import android.util.Config;
-import android.util.Log;
-import android.os.Bundle;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
+
import java.util.Map;
-import java.util.Vector;
/**
* A specialization of the ContentProvider that centralizes functionality
@@ -42,68 +30,13 @@ import java.util.Vector;
* @hide
*/
public abstract class SyncableContentProvider extends ContentProvider {
- private static final String TAG = "SyncableContentProvider";
- protected SQLiteOpenHelper mOpenHelper;
- protected SQLiteDatabase mDb;
- private final String mDatabaseName;
- private final int mDatabaseVersion;
- private final Uri mContentUri;
- private AccountMonitor mAccountMonitor;
-
- /** the account set in the last call to onSyncStart() */
- private String mSyncingAccount;
-
- private SyncStateContentProviderHelper mSyncState = null;
-
- private static final String[] sAccountProjection = new String[] {SyncConstValue._SYNC_ACCOUNT};
-
- private boolean mIsTemporary;
-
- private AbstractTableMerger mCurrentMerger = null;
- private boolean mIsMergeCancelled = false;
-
- private static final String SYNC_ACCOUNT_WHERE_CLAUSE = SyncConstValue._SYNC_ACCOUNT + "=?";
-
- protected boolean isTemporary() {
- return mIsTemporary;
- }
-
- /**
- * Indicates whether or not this ContentProvider contains a full
- * set of data or just diffs. This knowledge comes in handy when
- * determining how to incorporate the contents of a temporary
- * provider into a real provider.
- */
- private boolean mContainsDiffs;
-
- /**
- * Initializes the SyncableContentProvider
- * @param dbName the filename of the database
- * @param dbVersion the current version of the database schema
- * @param contentUri The base Uri of the syncable content in this provider
- */
- public SyncableContentProvider(String dbName, int dbVersion, Uri contentUri) {
- super();
-
- mDatabaseName = dbName;
- mDatabaseVersion = dbVersion;
- mContentUri = contentUri;
- mIsTemporary = false;
- setContainsDiffs(false);
- if (Config.LOGV) {
- Log.v(TAG, "created SyncableContentProvider " + this);
- }
- }
+ protected abstract boolean isTemporary();
/**
* Close resources that must be closed. You must call this to properly release
* the resources used by the SyncableContentProvider.
*/
- public void close() {
- if (mOpenHelper != null) {
- mOpenHelper.close(); // OK to call .close() repeatedly.
- }
- }
+ public abstract void close();
/**
* Override to create your schema and do anything else you need to do with a new database.
@@ -111,7 +44,7 @@ public abstract class SyncableContentProvider extends ContentProvider {
* This method may not use getDatabase(), or call content provider methods, it must only
* use the database handle passed to it.
*/
- protected void bootstrapDatabase(SQLiteDatabase db) {}
+ protected abstract void bootstrapDatabase(SQLiteDatabase db);
/**
* Override to upgrade your database from an old version to the version you specified.
@@ -131,56 +64,7 @@ public abstract class SyncableContentProvider extends ContentProvider {
* This method may not use getDatabase(), or call content provider methods, it must only
* use the database handle passed to it.
*/
- protected void onDatabaseOpened(SQLiteDatabase db) {}
-
- private class DatabaseHelper extends SQLiteOpenHelper {
- DatabaseHelper(Context context, String name) {
- // Note: context and name may be null for temp providers
- super(context, name, null, mDatabaseVersion);
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- bootstrapDatabase(db);
- mSyncState.createDatabase(db);
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- if (!upgradeDatabase(db, oldVersion, newVersion)) {
- mSyncState.discardSyncData(db, null /* all accounts */);
- getContext().getContentResolver().startSync(mContentUri, new Bundle());
- }
- }
-
- @Override
- public void onOpen(SQLiteDatabase db) {
- onDatabaseOpened(db);
- mSyncState.onDatabaseOpened(db);
- }
- }
-
- @Override
- public boolean onCreate() {
- if (isTemporary()) throw new IllegalStateException("onCreate() called for temp provider");
- mOpenHelper = new DatabaseHelper(getContext(), mDatabaseName);
- mSyncState = new SyncStateContentProviderHelper(mOpenHelper);
-
- AccountMonitorListener listener = new AccountMonitorListener() {
- public void onAccountsUpdated(String[] accounts) {
- // Some providers override onAccountsChanged(); give them a database to work with.
- mDb = mOpenHelper.getWritableDatabase();
- onAccountsChanged(accounts);
- TempProviderSyncAdapter syncAdapter = (TempProviderSyncAdapter)getSyncAdapter();
- if (syncAdapter != null) {
- syncAdapter.onAccountsChanged(accounts);
- }
- }
- };
- mAccountMonitor = new AccountMonitor(getContext(), listener);
-
- return true;
- }
+ protected abstract void onDatabaseOpened(SQLiteDatabase db);
/**
* Get a non-persistent instance of this content provider.
@@ -190,49 +74,13 @@ public abstract class SyncableContentProvider extends ContentProvider {
* @return a non-persistent content provider with the same layout as this
* provider.
*/
- public SyncableContentProvider getTemporaryInstance() {
- SyncableContentProvider temp;
- try {
- temp = getClass().newInstance();
- } catch (InstantiationException e) {
- throw new RuntimeException("unable to instantiate class, "
- + "this should never happen", e);
- } catch (IllegalAccessException e) {
- throw new RuntimeException(
- "IllegalAccess while instantiating class, "
- + "this should never happen", e);
- }
-
- // Note: onCreate() isn't run for the temp provider, and it has no Context.
- temp.mIsTemporary = true;
- temp.setContainsDiffs(true);
- temp.mOpenHelper = temp.new DatabaseHelper(null, null);
- temp.mSyncState = new SyncStateContentProviderHelper(temp.mOpenHelper);
- if (!isTemporary()) {
- mSyncState.copySyncState(
- mOpenHelper.getReadableDatabase(),
- temp.mOpenHelper.getWritableDatabase(),
- getSyncingAccount());
- }
- return temp;
- }
-
- public SQLiteDatabase getDatabase() {
- if (mDb == null) mDb = mOpenHelper.getWritableDatabase();
- return mDb;
- }
-
- public boolean getContainsDiffs() {
- return mContainsDiffs;
- }
-
- public void setContainsDiffs(boolean containsDiffs) {
- if (containsDiffs && !isTemporary()) {
- throw new IllegalStateException(
- "only a temporary provider can contain diffs");
- }
- mContainsDiffs = containsDiffs;
- }
+ public abstract SyncableContentProvider getTemporaryInstance();
+
+ public abstract SQLiteDatabase getDatabase();
+
+ public abstract boolean getContainsDiffs();
+
+ public abstract void setContainsDiffs(boolean containsDiffs);
/**
* Each subclass of this class should define a subclass of {@link
@@ -247,133 +95,14 @@ public abstract class SyncableContentProvider extends ContentProvider {
* @return A sequence of subclasses of {@link
* AbstractTableMerger}, one for each table that should be merged.
*/
- protected Iterable<? extends AbstractTableMerger> getMergers() {
- return Collections.emptyList();
- }
-
- @Override
- public final int update(final Uri url, final ContentValues values,
- final String selection, final String[] selectionArgs) {
- mDb = mOpenHelper.getWritableDatabase();
- mDb.beginTransaction();
- try {
- if (isTemporary() && mSyncState.matches(url)) {
- int numRows = mSyncState.asContentProvider().update(
- url, values, selection, selectionArgs);
- mDb.setTransactionSuccessful();
- return numRows;
- }
-
- int result = updateInternal(url, values, selection, selectionArgs);
- mDb.setTransactionSuccessful();
-
- if (!isTemporary() && result > 0) {
- getContext().getContentResolver().notifyChange(url, null /* observer */,
- changeRequiresLocalSync(url));
- }
-
- return result;
- } finally {
- mDb.endTransaction();
- }
- }
-
- @Override
- public final int delete(final Uri url, final String selection,
- final String[] selectionArgs) {
- mDb = mOpenHelper.getWritableDatabase();
- mDb.beginTransaction();
- try {
- if (isTemporary() && mSyncState.matches(url)) {
- int numRows = mSyncState.asContentProvider().delete(url, selection, selectionArgs);
- mDb.setTransactionSuccessful();
- return numRows;
- }
- int result = deleteInternal(url, selection, selectionArgs);
- mDb.setTransactionSuccessful();
- if (!isTemporary() && result > 0) {
- getContext().getContentResolver().notifyChange(url, null /* observer */,
- changeRequiresLocalSync(url));
- }
- return result;
- } finally {
- mDb.endTransaction();
- }
- }
-
- @Override
- public final Uri insert(final Uri url, final ContentValues values) {
- mDb = mOpenHelper.getWritableDatabase();
- mDb.beginTransaction();
- try {
- if (isTemporary() && mSyncState.matches(url)) {
- Uri result = mSyncState.asContentProvider().insert(url, values);
- mDb.setTransactionSuccessful();
- return result;
- }
- Uri result = insertInternal(url, values);
- mDb.setTransactionSuccessful();
- if (!isTemporary() && result != null) {
- getContext().getContentResolver().notifyChange(url, null /* observer */,
- changeRequiresLocalSync(url));
- }
- return result;
- } finally {
- mDb.endTransaction();
- }
- }
-
- @Override
- public final int bulkInsert(final Uri uri, final ContentValues[] values) {
- int size = values.length;
- int completed = 0;
- final boolean isSyncStateUri = mSyncState.matches(uri);
- mDb = mOpenHelper.getWritableDatabase();
- mDb.beginTransaction();
- try {
- for (int i = 0; i < size; i++) {
- Uri result;
- if (isTemporary() && isSyncStateUri) {
- result = mSyncState.asContentProvider().insert(uri, values[i]);
- } else {
- result = insertInternal(uri, values[i]);
- mDb.yieldIfContended();
- }
- if (result != null) {
- completed++;
- }
- }
- mDb.setTransactionSuccessful();
- } finally {
- mDb.endTransaction();
- }
- if (!isTemporary() && completed == values.length) {
- getContext().getContentResolver().notifyChange(uri, null /* observer */,
- changeRequiresLocalSync(uri));
- }
- return completed;
- }
+ protected abstract Iterable<? extends AbstractTableMerger> getMergers();
/**
* Check if changes to this URI can be syncable changes.
* @param uri the URI of the resource that was changed
* @return true if changes to this URI can be syncable changes, false otherwise
*/
- public boolean changeRequiresLocalSync(Uri uri) {
- return true;
- }
-
- @Override
- public final Cursor query(final Uri url, final String[] projection,
- final String selection, final String[] selectionArgs,
- final String sortOrder) {
- mDb = mOpenHelper.getReadableDatabase();
- if (isTemporary() && mSyncState.matches(url)) {
- return mSyncState.asContentProvider().query(
- url, projection, selection, selectionArgs, sortOrder);
- }
- return queryInternal(url, projection, selection, selectionArgs, sortOrder);
- }
+ public abstract boolean changeRequiresLocalSync(Uri uri);
/**
* Called right before a sync is started.
@@ -381,12 +110,7 @@ public abstract class SyncableContentProvider extends ContentProvider {
* @param context the sync context for the operation
* @param account
*/
- public void onSyncStart(SyncContext context, String account) {
- if (TextUtils.isEmpty(account)) {
- throw new IllegalArgumentException("you passed in an empty account");
- }
- mSyncingAccount = account;
- }
+ public abstract void onSyncStart(SyncContext context, String account);
/**
* Called right after a sync is completed
@@ -394,16 +118,13 @@ public abstract class SyncableContentProvider extends ContentProvider {
* @param context the sync context for the operation
* @param success true if the sync succeeded, false if an error occurred
*/
- public void onSyncStop(SyncContext context, boolean success) {
- }
+ public abstract void onSyncStop(SyncContext context, boolean success);
/**
* The account of the most recent call to onSyncStart()
* @return the account
*/
- public String getSyncingAccount() {
- return mSyncingAccount;
- }
+ public abstract String getSyncingAccount();
/**
* Merge diffs from a sync source with this content provider.
@@ -415,40 +136,8 @@ public abstract class SyncableContentProvider extends ContentProvider {
* a temporary content provider with the same layout as this provider containing
* @param syncResult
*/
- public void merge(SyncContext context, SyncableContentProvider diffs,
- TempProviderSyncResult result, SyncResult syncResult) {
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- db.beginTransaction();
- try {
- synchronized(this) {
- mIsMergeCancelled = false;
- }
- Iterable<? extends AbstractTableMerger> mergers = getMergers();
- try {
- for (AbstractTableMerger merger : mergers) {
- synchronized(this) {
- if (mIsMergeCancelled) break;
- mCurrentMerger = merger;
- }
- merger.merge(context, getSyncingAccount(), diffs, result, syncResult, this);
- }
- if (mIsMergeCancelled) return;
- if (diffs != null) {
- mSyncState.copySyncState(
- diffs.mOpenHelper.getReadableDatabase(),
- mOpenHelper.getWritableDatabase(),
- getSyncingAccount());
- }
- } finally {
- synchronized (this) {
- mCurrentMerger = null;
- }
- }
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
+ public abstract void merge(SyncContext context, SyncableContentProvider diffs,
+ TempProviderSyncResult result, SyncResult syncResult);
/**
@@ -457,19 +146,10 @@ public abstract class SyncableContentProvider extends ContentProvider {
* provider is syncable). Subclasses of ContentProvider
* that support canceling of sync should override this.
*/
- public void onSyncCanceled() {
- synchronized (this) {
- mIsMergeCancelled = true;
- if (mCurrentMerger != null) {
- mCurrentMerger.onMergeCancelled();
- }
- }
- }
+ public abstract void onSyncCanceled();
- public boolean isMergeCancelled() {
- return mIsMergeCancelled;
- }
+ public abstract boolean isMergeCancelled();
/**
* Subclasses should override this instead of update(). See update()
@@ -514,31 +194,7 @@ public abstract class SyncableContentProvider extends ContentProvider {
* Make sure that there are no entries for accounts that no longer exist
* @param accountsArray the array of currently-existing accounts
*/
- protected void onAccountsChanged(String[] accountsArray) {
- Map<String, Boolean> accounts = new HashMap<String, Boolean>();
- for (String account : accountsArray) {
- accounts.put(account, false);
- }
- accounts.put(SyncConstValue.NON_SYNCABLE_ACCOUNT, false);
-
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- Map<String, String> tableMap = db.getSyncedTables();
- Vector<String> tables = new Vector<String>();
- tables.addAll(tableMap.keySet());
- tables.addAll(tableMap.values());
-
- db.beginTransaction();
- try {
- mSyncState.onAccountsChanged(accountsArray);
- for (String table : tables) {
- deleteRowsForRemovedAccounts(accounts, table,
- SyncConstValue._SYNC_ACCOUNT);
- }
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
+ protected abstract void onAccountsChanged(String[] accountsArray);
/**
* A helper method to delete all rows whose account is not in the accounts
@@ -550,71 +206,23 @@ public abstract class SyncableContentProvider extends ContentProvider {
* @param accountColumnName the name of the column that is expected
* to hold the account.
*/
- protected void deleteRowsForRemovedAccounts(Map<String, Boolean> accounts,
- String table, String accountColumnName) {
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- Cursor c = db.query(table, sAccountProjection, null, null,
- accountColumnName, null, null);
- try {
- while (c.moveToNext()) {
- String account = c.getString(0);
- if (TextUtils.isEmpty(account)) {
- continue;
- }
- if (!accounts.containsKey(account)) {
- int numDeleted;
- numDeleted = db.delete(table, accountColumnName + "=?", new String[]{account});
- if (Config.LOGV) {
- Log.v(TAG, "deleted " + numDeleted
- + " records from table " + table
- + " for account " + account);
- }
- }
- }
- } finally {
- c.close();
- }
- }
+ protected abstract void deleteRowsForRemovedAccounts(Map<String, Boolean> accounts,
+ String table, String accountColumnName);
/**
* Called when the sync system determines that this provider should no longer
* contain records for the specified account.
*/
- public void wipeAccount(String account) {
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- Map<String, String> tableMap = db.getSyncedTables();
- ArrayList<String> tables = new ArrayList<String>();
- tables.addAll(tableMap.keySet());
- tables.addAll(tableMap.values());
-
- db.beginTransaction();
-
- try {
- // remote the SyncState data
- mSyncState.discardSyncData(db, account);
-
- // remove the data in the synced tables
- for (String table : tables) {
- db.delete(table, SYNC_ACCOUNT_WHERE_CLAUSE, new String[]{account});
- }
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
+ public abstract void wipeAccount(String account);
/**
* Retrieves the SyncData bytes for the given account. The byte array returned may be null.
*/
- public byte[] readSyncDataBytes(String account) {
- return mSyncState.readSyncDataBytes(mOpenHelper.getReadableDatabase(), account);
- }
+ public abstract byte[] readSyncDataBytes(String account);
/**
* Sets the SyncData bytes for the given account. The bytes array may be null.
*/
- public void writeSyncDataBytes(String account, byte[] data) {
- mSyncState.writeSyncDataBytes(mOpenHelper.getWritableDatabase(), account, data);
- }
+ public abstract void writeSyncDataBytes(String account, byte[] data);
}
diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java
index 4108bdd..9abc23b 100644
--- a/core/java/android/inputmethodservice/IInputMethodWrapper.java
+++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java
@@ -105,7 +105,8 @@ class IInputMethodWrapper extends IInputMethod.Stub
mInputMethod.revokeSession((InputMethodSession)msg.obj);
return;
case DO_SHOW_SOFT_INPUT:
- mInputMethod.showSoftInput();
+ mInputMethod.showSoftInput(
+ msg.arg1 != 0 ? InputMethod.SHOW_EXPLICIT : 0);
return;
case DO_HIDE_SOFT_INPUT:
mInputMethod.hideSoftInput();
@@ -162,8 +163,9 @@ class IInputMethodWrapper extends IInputMethod.Stub
}
}
- public void showSoftInput() {
- mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_SHOW_SOFT_INPUT));
+ public void showSoftInput(boolean explicit) {
+ mCaller.executeOrSendMessage(mCaller.obtainMessageI(DO_SHOW_SOFT_INPUT,
+ explicit ? 1 : 0));
}
public void hideSoftInput() {
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 0588bea..21bb38e 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -289,9 +289,9 @@ public class InputMethodService extends AbstractInputMethodService {
/**
* Handle a request by the system to show the soft input area.
*/
- public void showSoftInput() {
+ public void showSoftInput(int flags) {
if (DEBUG) Log.v(TAG, "showSoftInput()");
- showWindow(true);
+ onShowRequested(flags);
}
}
@@ -805,6 +805,27 @@ public class InputMethodService extends AbstractInputMethodService {
}
}
+ /**
+ * The system has decided that it may be time to show your input method.
+ * This is called due to a corresponding call to your
+ * {@link InputMethod#showSoftInput(int) InputMethod.showSoftInput(int)}
+ * method. The default implementation simply calls
+ * {@link #showWindow(boolean)}, except if the
+ * {@link InputMethod#SHOW_EXPLICIT InputMethod.SHOW_EXPLICIT} flag is
+ * not set and the input method is running in fullscreen mode.
+ *
+ * @param flags Provides additional information about the show request,
+ * as per {@link InputMethod#showSoftInput(int) InputMethod.showSoftInput(int)}.
+ */
+ public void onShowRequested(int flags) {
+ if ((flags&InputMethod.SHOW_EXPLICIT) == 0 && onEvaluateFullscreenMode()) {
+ // Don't show if this is not explicit requested by the user and
+ // the input method is fullscreen. That would be too disruptive.
+ return;
+ }
+ showWindow(true);
+ }
+
public void showWindow(boolean showInput) {
if (DEBUG) Log.v(TAG, "Showing window: showInput=" + showInput
+ " mShowInputRequested=" + mShowInputRequested
@@ -943,10 +964,13 @@ public class InputMethodService extends AbstractInputMethodService {
* Close this input method's soft input area, removing it from the display.
* The input method will continue running, but the user can no longer use
* it to generate input by touching the screen.
+ * @param flags Provides additional operating flags. Currently may be
+ * 0 or have the {@link InputMethodManager#HIDE_IMPLICIT_ONLY
+ * InputMethodManager.HIDE_IMPLICIT_ONLY} bit set.
*/
- public void dismissSoftInput() {
+ public void dismissSoftInput(int flags) {
((InputMethodManager)getSystemService(INPUT_METHOD_SERVICE))
- .hideSoftInputFromInputMethod(mToken);
+ .hideSoftInputFromInputMethod(mToken, flags);
}
public boolean onKeyDown(int keyCode, KeyEvent event) {
@@ -955,7 +979,7 @@ public class InputMethodService extends AbstractInputMethodService {
if (mShowInputRequested) {
// If the soft input area is shown, back closes it and we
// consume the back key.
- dismissSoftInput();
+ dismissSoftInput(0);
return true;
}
if (mShowCandidatesRequested) {
diff --git a/core/java/android/inputmethodservice/KeyboardView.java b/core/java/android/inputmethodservice/KeyboardView.java
index 3b5d741..6f044b6 100755
--- a/core/java/android/inputmethodservice/KeyboardView.java
+++ b/core/java/android/inputmethodservice/KeyboardView.java
@@ -68,6 +68,24 @@ public class KeyboardView extends View implements View.OnClickListener {
* Listener for virtual keyboard events.
*/
public interface OnKeyboardActionListener {
+
+ /**
+ * Called when the user presses a key. This is sent before the {@link #onKey} is called.
+ * For keys that repeat, this is only called once.
+ * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid
+ * key, the value will be zero.
+ * @hide Pending API Council approval
+ */
+ void onPress(int primaryCode);
+
+ /**
+ * Called when the user releases a key. This is sent after the {@link #onKey} is called.
+ * For keys that repeat, this is only called once.
+ * @param primaryCode the code of the key that was released
+ * @hide Pending API Council approval
+ */
+ void onRelease(int primaryCode);
+
/**
* Send a key press to the listener.
* @param primaryCode this is the key that was pressed
@@ -111,6 +129,9 @@ public class KeyboardView extends View implements View.OnClickListener {
private int mLabelTextSize;
private int mKeyTextSize;
private int mKeyTextColor;
+ private float mShadowRadius;
+ private int mShadowColor;
+ private float mBackgroundDimAmount;
private TextView mPreviewText;
private PopupWindow mPreviewPopup;
@@ -150,8 +171,6 @@ public class KeyboardView extends View implements View.OnClickListener {
private int mStartX;
private int mStartY;
- private boolean mVibrateOn;
- private boolean mSoundOn;
private boolean mProximityCorrectOn;
private Paint mPaint;
@@ -172,20 +191,15 @@ public class KeyboardView extends View implements View.OnClickListener {
private int mRepeatKeyIndex = NOT_A_KEY;
private int mPopupLayout;
private boolean mAbortKey;
+ private Key mInvalidatedKey;
+ private Rect mClipRegion = new Rect(0, 0, 0, 0);
private Drawable mKeyBackground;
-
- private static final String PREF_VIBRATE_ON = "vibrate_on";
- private static final String PREF_SOUND_ON = "sound_on";
- private static final String PREF_PROXIMITY_CORRECTION = "hit_correction";
private static final int REPEAT_INTERVAL = 50; // ~20 keys per second
private static final int REPEAT_START_DELAY = 400;
private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
- private Vibrator mVibrator;
- private long[] mVibratePattern = new long[] {1, 20};
-
private static int MAX_NEARBY_KEYS = 12;
private int[] mDistances = new int[MAX_NEARBY_KEYS];
@@ -269,15 +283,19 @@ public class KeyboardView extends View implements View.OnClickListener {
case com.android.internal.R.styleable.KeyboardView_popupLayout:
mPopupLayout = a.getResourceId(attr, 0);
break;
+ case com.android.internal.R.styleable.KeyboardView_shadowColor:
+ mShadowColor = a.getColor(attr, 0);
+ break;
+ case com.android.internal.R.styleable.KeyboardView_shadowRadius:
+ mShadowRadius = a.getFloat(attr, 0f);
+ break;
}
}
- // Get the settings preferences
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
- mVibrateOn = sp.getBoolean(PREF_VIBRATE_ON, mVibrateOn);
- mSoundOn = sp.getBoolean(PREF_SOUND_ON, mSoundOn);
- mProximityCorrectOn = sp.getBoolean(PREF_PROXIMITY_CORRECTION, true);
-
+ a = mContext.obtainStyledAttributes(
+ com.android.internal.R.styleable.Theme);
+ mBackgroundDimAmount = a.getFloat(android.R.styleable.Theme_backgroundDimAmount, 0.5f);
+
mPreviewPopup = new PopupWindow(context);
if (previewLayout != 0) {
mPreviewText = (TextView) inflate.inflate(previewLayout, null);
@@ -309,7 +327,7 @@ public class KeyboardView extends View implements View.OnClickListener {
resetMultiTap();
initGestureDetector();
}
-
+
private void initGestureDetector() {
mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
@Override
@@ -440,7 +458,26 @@ public class KeyboardView extends View implements View.OnClickListener {
mPreviewPopup.dismiss();
}
}
-
+
+ /**
+ * Enables or disables proximity correction. When enabled, {@link OnKeyboardActionListener#onKey}
+ * gets called with key codes for adjacent keys. Otherwise only the primary code is returned.
+ * @param enabled whether or not the proximity correction is enabled
+ * @hide Pending API Council approval
+ */
+ public void setProximityCorrectionEnabled(boolean enabled) {
+ mProximityCorrectOn = enabled;
+ }
+
+ /**
+ * Returns the enabled state of the proximity correction.
+ * @return true if proximity correction is enabled, false otherwise
+ * @hide Pending API Council approval
+ */
+ public boolean isProximityCorrectionEnabled() {
+ return mProximityCorrectOn;
+ }
+
/**
* Popup keyboard close button clicked.
* @hide
@@ -498,19 +535,37 @@ public class KeyboardView extends View implements View.OnClickListener {
if (mKeyboard == null) return;
final Paint paint = mPaint;
- //final int descent = (int) paint.descent();
final Drawable keyBackground = mKeyBackground;
+ final Rect clipRegion = mClipRegion;
final Rect padding = mPadding;
final int kbdPaddingLeft = mPaddingLeft;
final int kbdPaddingTop = mPaddingTop;
- List<Key> keys = mKeyboard.getKeys();
+ final List<Key> keys = mKeyboard.getKeys();
+ final Key invalidKey = mInvalidatedKey;
//canvas.translate(0, mKeyboardPaddingTop);
paint.setAlpha(255);
paint.setColor(mKeyTextColor);
-
+ boolean drawSingleKey = false;
+ if (invalidKey != null && canvas.getClipBounds(clipRegion)) {
+// System.out.println("Key bounds = " + (invalidKey.x + mPaddingLeft) + ","
+// + (invalidKey.y + mPaddingTop) + ","
+// + (invalidKey.x + invalidKey.width + mPaddingLeft) + ","
+// + (invalidKey.y + invalidKey.height + mPaddingTop));
+// System.out.println("Clip bounds =" + clipRegion.toShortString());
+ // Is clipRegion completely contained within the invalidated key?
+ if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left &&
+ invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top &&
+ invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right &&
+ invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) {
+ drawSingleKey = true;
+ }
+ }
final int keyCount = keys.size();
for (int i = 0; i < keyCount; i++) {
final Key key = keys.get(i);
+ if (drawSingleKey && invalidKey != key) {
+ continue;
+ }
int[] drawableState = key.getCurrentDrawableState();
keyBackground.setState(drawableState);
@@ -535,7 +590,7 @@ public class KeyboardView extends View implements View.OnClickListener {
paint.setTypeface(Typeface.DEFAULT);
}
// Draw a drop shadow for the text
- paint.setShadowLayer(3f, 0, 0, 0xCC000000);
+ paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
// Draw the text
canvas.drawText(label,
(key.width - padding.left - padding.right) / 2
@@ -558,10 +613,10 @@ public class KeyboardView extends View implements View.OnClickListener {
}
canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop);
}
-
+ mInvalidatedKey = null;
// Overlay a dark rectangle to dim the keyboard
if (mMiniKeyboardOnScreen) {
- paint.setColor(0xA0000000);
+ paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
}
@@ -577,22 +632,6 @@ public class KeyboardView extends View implements View.OnClickListener {
}
}
- private void playKeyClick() {
- if (mSoundOn) {
- playSoundEffect(0);
- }
- }
-
- private void vibrate() {
- if (!mVibrateOn) {
- return;
- }
- if (mVibrator == null) {
- mVibrator = new Vibrator();
- }
- mVibrator.vibrate(mVibratePattern, -1);
- }
-
private int getKeyIndices(int x, int y, int[] allKeys) {
final List<Key> keys = mKeyboard.getKeys();
final boolean shifted = mKeyboard.isShifted();
@@ -650,12 +689,12 @@ public class KeyboardView extends View implements View.OnClickListener {
private void detectAndSendKey(int x, int y, long eventTime) {
int index = mCurrentKey;
if (index != NOT_A_KEY) {
- vibrate();
final Key key = mKeyboard.getKeys().get(index);
if (key.text != null) {
for (int i = 0; i < key.text.length(); i++) {
mKeyboardActionListener.onKey(key.text.charAt(i), key.codes);
}
+ mKeyboardActionListener.onRelease(NOT_A_KEY);
} else {
int code = key.codes[0];
//TextEntryState.keyPressedAt(key, x, y);
@@ -672,6 +711,7 @@ public class KeyboardView extends View implements View.OnClickListener {
code = key.codes[mTapCount];
}
mKeyboardActionListener.onKey(code, codes);
+ mKeyboardActionListener.onRelease(code);
}
mLastSentIndex = index;
mLastTapTime = eventTime;
@@ -781,6 +821,7 @@ public class KeyboardView extends View implements View.OnClickListener {
return;
}
final Key key = mKeyboard.getKeys().get(keyIndex);
+ mInvalidatedKey = key;
invalidate(key.x + mPaddingLeft, key.y + mPaddingTop,
key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop);
}
@@ -834,6 +875,12 @@ public class KeyboardView extends View implements View.OnClickListener {
public void swipeRight() { }
public void swipeUp() { }
public void swipeDown() { }
+ public void onPress(int primaryCode) {
+ mKeyboardActionListener.onPress(primaryCode);
+ }
+ public void onRelease(int primaryCode) {
+ mKeyboardActionListener.onRelease(primaryCode);
+ }
});
//mInputView.setSuggest(mSuggest);
Keyboard keyboard;
@@ -913,6 +960,8 @@ public class KeyboardView extends View implements View.OnClickListener {
mDownTime = me.getEventTime();
mLastMoveTime = mDownTime;
checkMultiTap(eventTime, keyIndex);
+ mKeyboardActionListener.onPress(keyIndex != NOT_A_KEY ?
+ mKeyboard.getKeys().get(keyIndex).codes[0] : 0);
if (mCurrentKey >= 0 && mKeyboard.getKeys().get(mCurrentKey).repeatable) {
mRepeatKeyIndex = mCurrentKey;
repeatKey();
@@ -924,8 +973,6 @@ public class KeyboardView extends View implements View.OnClickListener {
mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
}
showPreview(keyIndex);
- playKeyClick();
- vibrate();
break;
case MotionEvent.ACTION_MOVE:
diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java
index c3bb967..5f7f91f 100644
--- a/core/java/android/os/Debug.java
+++ b/core/java/android/os/Debug.java
@@ -182,11 +182,11 @@ public final class Debug
}
/**
- * Change the JDWP port -- this is a temporary measure.
+ * Change the JDWP port.
*
- * If a debugger is currently attached the change may not happen
- * until after the debugger disconnects.
+ * @deprecated no longer needed or useful
*/
+ @Deprecated
public static void changeDebugPort(int port) {}
/**
diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java
index b137b34..d75a25f 100644
--- a/core/java/android/provider/Calendar.java
+++ b/core/java/android/provider/Calendar.java
@@ -16,19 +16,6 @@
package android.provider;
-import com.google.android.gdata.client.AndroidGDataClient;
-import com.google.android.gdata.client.AndroidXmlParserFactory;
-import com.google.wireless.gdata.calendar.client.CalendarClient;
-import com.google.wireless.gdata.calendar.data.EventEntry;
-import com.google.wireless.gdata.calendar.data.Who;
-import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
-import com.google.wireless.gdata.client.AuthenticationException;
-import com.google.wireless.gdata.client.AllDeletedUnavailableException;
-import com.google.wireless.gdata.data.Entry;
-import com.google.wireless.gdata.data.StringUtils;
-import com.google.wireless.gdata.parser.ParseException;
-import com.android.internal.database.ArrayListCursor;
-
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.ContentResolver;
@@ -45,8 +32,15 @@ import android.text.format.DateUtils;
import android.text.format.Time;
import android.util.Config;
import android.util.Log;
+import com.android.internal.database.ArrayListCursor;
+import com.google.android.gdata.client.AndroidGDataClient;
+import com.google.android.gdata.client.AndroidXmlParserFactory;
+import com.google.wireless.gdata.calendar.client.CalendarClient;
+import com.google.wireless.gdata.calendar.data.EventEntry;
+import com.google.wireless.gdata.calendar.data.Who;
+import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
+import com.google.wireless.gdata.data.StringUtils;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Vector;
@@ -922,9 +916,30 @@ public final class Calendar {
public static final String ALARM_TIME = "alarmTime";
/**
+ * The creation time of this database entry, in UTC.
+ * (Useful for debugging missed reminders.)
+ * <P>Type: INTEGER (long; millis since epoch)</P>
+ */
+ public static final String CREATION_TIME = "creationTime";
+
+ /**
+ * The time that the alarm broadcast was received by the Calendar app,
+ * in UTC. (Useful for debugging missed reminders.)
+ * <P>Type: INTEGER (long; millis since epoch)</P>
+ */
+ public static final String RECEIVED_TIME = "receivedTime";
+
+ /**
+ * The time that the notification was created by the Calendar app,
+ * in UTC. (Useful for debugging missed reminders.)
+ * <P>Type: INTEGER (long; millis since epoch)</P>
+ */
+ public static final String NOTIFY_TIME = "notifyTime";
+
+ /**
* The state of this alert. It starts out as SCHEDULED, then when
* the alarm goes off, it changes to FIRED, and then when the user
- * sees and dismisses the alarm it changes to DISMISSED.
+ * dismisses the alarm it changes to DISMISSED.
* <P>Type: INTEGER</P>
*/
public static final String STATE = "state";
@@ -966,6 +981,10 @@ public final class Calendar {
values.put(CalendarAlerts.BEGIN, begin);
values.put(CalendarAlerts.END, end);
values.put(CalendarAlerts.ALARM_TIME, alarmTime);
+ long currentTime = System.currentTimeMillis();
+ values.put(CalendarAlerts.CREATION_TIME, currentTime);
+ values.put(CalendarAlerts.RECEIVED_TIME, 0);
+ values.put(CalendarAlerts.NOTIFY_TIME, 0);
values.put(CalendarAlerts.STATE, SCHEDULED);
values.put(CalendarAlerts.MINUTES, minutes);
return cr.insert(CONTENT_URI, values);
diff --git a/core/java/android/provider/Gmail.java b/core/java/android/provider/Gmail.java
index 0b6a758..1bbcc33 100644
--- a/core/java/android/provider/Gmail.java
+++ b/core/java/android/provider/Gmail.java
@@ -1433,6 +1433,11 @@ public final class Gmail {
return SORTED_USER_MEANINGFUL_SYSTEM_LABELS;
}
+ /**
+ * If you are ever tempted to remove outbox or draft from this set make sure you have a
+ * way to stop draft and outbox messages from getting purged before they are sent to the
+ * server.
+ */
private static final Set<String> FORCED_INCLUDED_LABELS =
Sets.newHashSet(LABEL_OUTBOX, LABEL_DRAFT);
diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java
index 7b2f18c..0184db8 100644
--- a/core/java/android/provider/MediaStore.java
+++ b/core/java/android/provider/MediaStore.java
@@ -1303,6 +1303,16 @@ public final class MediaStore
* <P>Type: TEXT</P>
*/
public static final String BUCKET_DISPLAY_NAME = "bucket_display_name";
+
+ /**
+ * The bookmark for the video. Time in ms. Represents the location in the video that the
+ * video should start playing at the next time it is opened. If the value is null or
+ * out of the range 0..DURATION-1 then the video should start playing from the
+ * beginning.
+ * @hide
+ * <P>Type: INTEGER</P>
+ */
+ public static final String BOOKMARK = "bookmark";
}
public static final class Media implements VideoColumns {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 91624ad..7b64405 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -25,7 +25,10 @@ import android.annotation.SdkConstant.SdkConstantType;
import android.content.ContentQueryMap;
import android.content.ContentResolver;
import android.content.ContentValues;
+import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
@@ -37,6 +40,7 @@ import android.text.TextUtils;
import android.util.AndroidException;
import android.util.Log;
+import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
@@ -855,6 +859,43 @@ public final class Settings {
public static final String WIFI_IDLE_MS = "wifi_idle_ms";
/**
+ * The policy for deciding when Wi-Fi should go to sleep (which will in
+ * turn switch to using the mobile data as an Internet connection).
+ * <p>
+ * Set to one of {@link #WIFI_SLEEP_POLICY_DEFAULT},
+ * {@link #WIFI_SLEEP_POLICY_NEVER_WHILE_PLUGGED}, or
+ * {@link #WIFI_SLEEP_POLICY_NEVER}.
+ *
+ * @hide pending API council
+ */
+ public static final String WIFI_SLEEP_POLICY = "wifi_sleep_policy";
+
+ /**
+ * Value for {@link #WIFI_SLEEP_POLICY} to use the default Wi-Fi sleep
+ * policy, which is to sleep shortly after the turning off
+ * according to the {@link #STAY_ON_WHILE_PLUGGED_IN} setting.
+ *
+ * @hide pending API council
+ */
+ public static final int WIFI_SLEEP_POLICY_DEFAULT = 0;
+
+ /**
+ * Value for {@link #WIFI_SLEEP_POLICY} to use the default policy when
+ * the device is on battery, and never go to sleep when the device is
+ * plugged in.
+ *
+ * @hide pending API council
+ */
+ public static final int WIFI_SLEEP_POLICY_NEVER_WHILE_PLUGGED = 1;
+
+ /**
+ * Value for {@link #WIFI_SLEEP_POLICY} to never go to sleep.
+ *
+ * @hide pending API council
+ */
+ public static final int WIFI_SLEEP_POLICY_NEVER = 2;
+
+ /**
* Whether to use static IP and other static network attributes.
* <p>
* Set to 1 for true and 0 for false.
@@ -2674,7 +2715,12 @@ public final class Settings {
/**
* Descriptive name of the bookmark that can be displayed to the user.
- * <P>Type: TEXT</P>
+ * If this is empty, the title should be resolved at display time (use
+ * {@link #getTitle(Context, Cursor)} any time you want to display the
+ * title of a bookmark.)
+ * <P>
+ * Type: TEXT
+ * </P>
*/
public static final String TITLE = "title";
@@ -2754,17 +2800,16 @@ public final class Settings {
/**
* Add a new bookmark to the system.
- *
+ *
* @param cr The ContentResolver to query.
* @param intent The desired target of the bookmark.
- * @param title Bookmark title that is shown to the user; null if none.
+ * @param title Bookmark title that is shown to the user; null if none
+ * or it should be resolved to the intent's title.
* @param folder Folder in which to place the bookmark; null if none.
- * @param shortcut Shortcut that will invoke the bookmark; 0 if none.
- * If this is non-zero and there is an existing
- * bookmark entry with this same shortcut, then that
- * existing shortcut is cleared (the bookmark is not
- * removed).
- *
+ * @param shortcut Shortcut that will invoke the bookmark; 0 if none. If
+ * this is non-zero and there is an existing bookmark entry
+ * with this same shortcut, then that existing shortcut is
+ * cleared (the bookmark is not removed).
* @return The unique content URL for the new bookmark entry.
*/
public static Uri add(ContentResolver cr,
@@ -2813,9 +2858,49 @@ public final class Settings {
* @return CharSequence The label for this folder that should be shown
* to the user.
*/
- public static CharSequence labelForFolder(Resources r, String folder) {
+ public static CharSequence getLabelForFolder(Resources r, String folder) {
return folder;
}
+
+ /**
+ * Return the title as it should be displayed to the user. This takes
+ * care of localizing bookmarks that point to activities.
+ *
+ * @param context A context.
+ * @param cursor A cursor pointing to the row whose title should be
+ * returned. The cursor must contain at least the
+ * {@link #TITLE} and {@link #INTENT} columns.
+ * @return A title that is localized and can be displayed to the user.
+ */
+ public static CharSequence getTitle(Context context, Cursor cursor) {
+ int titleColumn = cursor.getColumnIndex(TITLE);
+ int intentColumn = cursor.getColumnIndex(INTENT);
+ if (titleColumn == -1 || intentColumn == -1) {
+ throw new IllegalArgumentException(
+ "The cursor must contain the TITLE and INTENT columns.");
+ }
+
+ String title = cursor.getString(titleColumn);
+ if (!TextUtils.isEmpty(title)) {
+ return title;
+ }
+
+ String intentUri = cursor.getString(intentColumn);
+ if (TextUtils.isEmpty(intentUri)) {
+ return "";
+ }
+
+ Intent intent;
+ try {
+ intent = Intent.getIntent(intentUri);
+ } catch (URISyntaxException e) {
+ return "";
+ }
+
+ PackageManager packageManager = context.getPackageManager();
+ ResolveInfo info = packageManager.resolveActivity(intent, 0);
+ return info.loadLabel(packageManager);
+ }
}
/**
diff --git a/core/java/android/server/BluetoothA2dpService.java b/core/java/android/server/BluetoothA2dpService.java
index ded50e4..90ef8f6 100644
--- a/core/java/android/server/BluetoothA2dpService.java
+++ b/core/java/android/server/BluetoothA2dpService.java
@@ -53,6 +53,8 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub {
private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN;
private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH;
+ private static final String A2DP_SINK_ADDRESS = "a2dp_sink_address";
+
private final Context mContext;
private final IntentFilter mIntentFilter;
private HashMap<String, SinkState> mAudioDevices;
@@ -251,6 +253,7 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub {
}
}
+ mAudioManager.setParameter(A2DP_SINK_ADDRESS, lookupAddress(path));
mAudioManager.setBluetoothA2dpOn(true);
updateState(path, BluetoothA2dp.STATE_CONNECTED);
}
@@ -302,13 +305,15 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub {
s.state = state;
}
- if (DBG) log("state " + address + " (" + path + ") " + prevState + "->" + state);
-
- Intent intent = new Intent(BluetoothA2dp.SINK_STATE_CHANGED_ACTION);
- intent.putExtra(BluetoothIntent.ADDRESS, address);
- intent.putExtra(BluetoothA2dp.SINK_PREVIOUS_STATE, prevState);
- intent.putExtra(BluetoothA2dp.SINK_STATE, state);
- mContext.sendBroadcast(intent, BLUETOOTH_PERM);
+ if (state != prevState) {
+ if (DBG) log("state " + address + " (" + path + ") " + prevState + "->" + state);
+
+ Intent intent = new Intent(BluetoothA2dp.SINK_STATE_CHANGED_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ intent.putExtra(BluetoothA2dp.SINK_PREVIOUS_STATE, prevState);
+ intent.putExtra(BluetoothA2dp.SINK_STATE, state);
+ mContext.sendBroadcast(intent, BLUETOOTH_PERM);
+ }
}
@Override
diff --git a/core/java/android/speech/srec/WaveHeader.java b/core/java/android/speech/srec/WaveHeader.java
new file mode 100644
index 0000000..0aa3cc2
--- /dev/null
+++ b/core/java/android/speech/srec/WaveHeader.java
@@ -0,0 +1,267 @@
+/*
+ * 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 android.speech.srec;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * This class represents the header of a WAVE format audio file, which usually
+ * have a .wav suffix. The following integer valued fields are contained:
+ * <ul>
+ * <li> format - usually PCM, ALAW or ULAW.
+ * <li> numChannels - 1 for mono, 2 for stereo.
+ * <li> sampleRate - usually 8000, 11025, 16000, 22050, or 44100 hz.
+ * <li> bitsPerSample - usually 16 for PCM, 8 for ALAW, or 8 for ULAW.
+ * <li> numBytes - size of audio data after this header, in bytes.
+ * </ul>
+ * @hide pending API council approval
+ */
+public class WaveHeader {
+
+ // follows WAVE format in http://ccrma.stanford.edu/courses/422/projects/WaveFormat
+
+ private static final String TAG = "WaveHeader";
+
+ private static final int HEADER_LENGTH = 44;
+
+ /** Indicates PCM format. */
+ public static final short FORMAT_PCM = 1;
+ /** Indicates ALAW format. */
+ public static final short FORMAT_ALAW = 6;
+ /** Indicates ULAW format. */
+ public static final short FORMAT_ULAW = 7;
+
+ private short mFormat;
+ private short mNumChannels;
+ private int mSampleRate;
+ private short mBitsPerSample;
+ private int mNumBytes;
+
+ /**
+ * Construct a WaveHeader, with all fields defaulting to zero.
+ */
+ public WaveHeader() {
+ }
+
+ /**
+ * Construct a WaveHeader, with fields initialized.
+ * @param format format of audio data,
+ * one of {@link #FORMAT_PCM}, {@link #FORMAT_ULAW}, or {@link #FORMAT_ALAW}.
+ * @param numChannels 1 for mono, 2 for stereo.
+ * @param sampleRate typically 8000, 11025, 16000, 22050, or 44100 hz.
+ * @param bitsPerSample usually 16 for PCM, 8 for ULAW or 8 for ALAW.
+ * @param numBytes size of audio data after this header, in bytes.
+ */
+ public WaveHeader(short format, short numChannels, int sampleRate, short bitsPerSample, int numBytes) {
+ mFormat = format;
+ mSampleRate = sampleRate;
+ mNumChannels = numChannels;
+ mBitsPerSample = bitsPerSample;
+ mNumBytes = numBytes;
+ }
+
+ /**
+ * Get the format field.
+ * @return format field,
+ * one of {@link #FORMAT_PCM}, {@link #FORMAT_ULAW}, or {@link #FORMAT_ALAW}.
+ */
+ public short getFormat() {
+ return mFormat;
+ }
+
+ /**
+ * Set the format field.
+ * @param format
+ * one of {@link #FORMAT_PCM}, {@link #FORMAT_ULAW}, or {@link #FORMAT_ALAW}.
+ * @return reference to this WaveHeader instance.
+ */
+ public WaveHeader setFormat(short format) {
+ mFormat = format;
+ return this;
+ }
+
+ /**
+ * Get the number of channels.
+ * @return number of channels, 1 for mono, 2 for stereo.
+ */
+ public short getNumChannels() {
+ return mNumChannels;
+ }
+
+ /**
+ * Set the number of channels.
+ * @param numChannels 1 for mono, 2 for stereo.
+ * @return reference to this WaveHeader instance.
+ */
+ public WaveHeader setNumChannels(short numChannels) {
+ mNumChannels = numChannels;
+ return this;
+ }
+
+ /**
+ * Get the sample rate.
+ * @return sample rate, typically 8000, 11025, 16000, 22050, or 44100 hz.
+ */
+ public int getSampleRate() {
+ return mSampleRate;
+ }
+
+ /**
+ * Set the sample rate.
+ * @param sampleRate sample rate, typically 8000, 11025, 16000, 22050, or 44100 hz.
+ * @return reference to this WaveHeader instance.
+ */
+ public WaveHeader setSampleRate(int sampleRate) {
+ mSampleRate = sampleRate;
+ return this;
+ }
+
+ /**
+ * Get the number of bits per sample.
+ * @return number of bits per sample,
+ * usually 16 for PCM, 8 for ULAW or 8 for ALAW.
+ */
+ public short getBitsPerSample() {
+ return mBitsPerSample;
+ }
+
+ /**
+ * Set the number of bits per sample.
+ * @param bitsPerSample number of bits per sample,
+ * usually 16 for PCM, 8 for ULAW or 8 for ALAW.
+ * @return reference to this WaveHeader instance.
+ */
+ public WaveHeader setBitsPerSample(short bitsPerSample) {
+ mBitsPerSample = bitsPerSample;
+ return this;
+ }
+
+ /**
+ * Get the size of audio data after this header, in bytes.
+ * @return size of audio data after this header, in bytes.
+ */
+ public int getNumBytes() {
+ return mNumBytes;
+ }
+
+ /**
+ * Set the size of audio data after this header, in bytes.
+ * @param numBytes size of audio data after this header, in bytes.
+ * @return reference to this WaveHeader instance.
+ */
+ public WaveHeader setNumBytes(int numBytes) {
+ mNumBytes = numBytes;
+ return this;
+ }
+
+ /**
+ * Read and initialize a WaveHeader.
+ * @param in {@link java.io.InputStream} to read from.
+ * @return number of bytes consumed.
+ * @throws IOException
+ */
+ public int read(InputStream in) throws IOException {
+ /* RIFF header */
+ readId(in, "RIFF");
+ int numBytes = readInt(in) - 36;
+ readId(in, "WAVE");
+
+ /* fmt chunk */
+ readId(in, "fmt ");
+ if (16 != readInt(in)) throw new IOException("fmt chunk length not 16");
+ mFormat = readShort(in);
+ mNumChannels = readShort(in);
+ mSampleRate = readInt(in);
+ int byteRate = readInt(in);
+ short blockAlign = readShort(in);
+ mBitsPerSample = readShort(in);
+ if (byteRate != mNumChannels * mSampleRate * mBitsPerSample / 8) {
+ throw new IOException("fmt.ByteRate field inconsistent");
+ }
+ if (blockAlign != mNumChannels * mBitsPerSample / 8) {
+ throw new IOException("fmt.BlockAlign field inconsistent");
+ }
+
+ /* data chunk */
+ readId(in, "data");
+ mNumBytes = readInt(in);
+
+ return HEADER_LENGTH;
+ }
+
+ private static void readId(InputStream in, String id) throws IOException {
+ for (int i = 0; i < id.length(); i++) {
+ if (id.charAt(i) != in.read()) throw new IOException( id + " tag not present");
+ }
+ }
+
+ private static int readInt(InputStream in) throws IOException {
+ return in.read() | (in.read() << 8) | (in.read() << 16) | (in.read() << 24);
+ }
+
+ private static short readShort(InputStream in) throws IOException {
+ return (short)(in.read() | (in.read() << 8));
+ }
+
+ /**
+ * Write a WAVE file header.
+ * @param out {@link java.io.OutputStream} to receive the header.
+ * @return number of bytes written.
+ * @throws IOException
+ */
+ public int write(OutputStream out) throws IOException {
+ /* RIFF header */
+ writeId(out, "RIFF");
+ writeInt(out, 36 + mNumBytes);
+ writeId(out, "WAVE");
+
+ /* fmt chunk */
+ writeId(out, "fmt ");
+ writeInt(out, 16);
+ writeShort(out, mFormat);
+ writeShort(out, mNumChannels);
+ writeInt(out, mSampleRate);
+ writeInt(out, mNumChannels * mSampleRate * mBitsPerSample / 8);
+ writeShort(out, (short)(mNumChannels * mBitsPerSample / 8));
+ writeShort(out, mBitsPerSample);
+
+ /* data chunk */
+ writeId(out, "data");
+ writeInt(out, mNumBytes);
+
+ return HEADER_LENGTH;
+ }
+
+ private static void writeId(OutputStream out, String id) throws IOException {
+ for (int i = 0; i < id.length(); i++) out.write(id.charAt(i));
+ }
+
+ private static void writeInt(OutputStream out, int val) throws IOException {
+ out.write(val >> 0);
+ out.write(val >> 8);
+ out.write(val >> 16);
+ out.write(val >> 24);
+ }
+
+ private static void writeShort(OutputStream out, short val) throws IOException {
+ out.write(val >> 0);
+ out.write(val >> 8);
+ }
+
+}
diff --git a/core/java/android/text/format/DateFormat.java b/core/java/android/text/format/DateFormat.java
index 3437978..73adedf 100644
--- a/core/java/android/text/format/DateFormat.java
+++ b/core/java/android/text/format/DateFormat.java
@@ -70,7 +70,7 @@ import java.text.SimpleDateFormat;
&quot;MMMM dd, yyyy h:mmaa&quot; -&gt; &quot;April 6, 1970 3:23am&quot<br/>
&quot;E, MMMM dd, yyyy h:mmaa&quot; -&gt; &quot;Mon, April 6, 1970 3:23am&<br/>
&quot;EEEE, MMMM dd, yyyy h:mmaa&quot; -&gt; &quot;Monday, April 6, 1970 3:23am&quot;<br/>
- &quot;&apos;Best day evar: &apos;M/d/yy&quot; -&gt; &quot;Best day evar: 4/6/70&quot;
+ &quot;&apos;Noteworthy day: &apos;M/d/yy&quot; -&gt; &quot;Noteworthy day: 4/6/70&quot;
*/
public class DateFormat {
diff --git a/core/java/android/util/DayOfMonthCursor.java b/core/java/android/util/DayOfMonthCursor.java
index 52ee00e..393b98e 100644
--- a/core/java/android/util/DayOfMonthCursor.java
+++ b/core/java/android/util/DayOfMonthCursor.java
@@ -68,6 +68,20 @@ public class DayOfMonthCursor extends MonthDisplayHelper {
public int getSelectedDayOfMonth() {
return getDayAt(mRow, mColumn);
}
+
+ /**
+ * @return 0 if the selection is in the current month, otherwise -1 or +1
+ * depending on whether the selection is in the first or last row.
+ */
+ public int getSelectedMonthOffset() {
+ if (isWithinCurrentMonth(mRow, mColumn)) {
+ return 0;
+ }
+ if (mRow == 0) {
+ return -1;
+ }
+ return 1;
+ }
public void setSelectedDayOfMonth(int dayOfMonth) {
mRow = getRowOf(dayOfMonth);
diff --git a/core/java/android/util/Log.java b/core/java/android/util/Log.java
index 24f67cd..2572679 100644
--- a/core/java/android/util/Log.java
+++ b/core/java/android/util/Log.java
@@ -86,7 +86,7 @@ public final class Log {
/**
* Send a {@link #VERBOSE} log message.
- * @param tag Used to identify the source of a log message. It usually identfies
+ * @param tag Used to identify the source of a log message. It usually identifies
* the class or activity where the log call occurs.
* @param msg The message you would like logged.
*/
@@ -96,7 +96,7 @@ public final class Log {
/**
* Send a {@link #VERBOSE} log message and log the exception.
- * @param tag Used to identify the source of a log message. It usually identfies
+ * @param tag Used to identify the source of a log message. It usually identifies
* the class or activity where the log call occurs.
* @param msg The message you would like logged.
* @param tr An exception to log
@@ -107,7 +107,7 @@ public final class Log {
/**
* Send a {@link #DEBUG} log message.
- * @param tag Used to identify the source of a log message. It usually identfies
+ * @param tag Used to identify the source of a log message. It usually identifies
* the class or activity where the log call occurs.
* @param msg The message you would like logged.
*/
@@ -117,7 +117,7 @@ public final class Log {
/**
* Send a {@link #DEBUG} log message and log the exception.
- * @param tag Used to identify the source of a log message. It usually identfies
+ * @param tag Used to identify the source of a log message. It usually identifies
* the class or activity where the log call occurs.
* @param msg The message you would like logged.
* @param tr An exception to log
@@ -128,7 +128,7 @@ public final class Log {
/**
* Send an {@link #INFO} log message.
- * @param tag Used to identify the source of a log message. It usually identfies
+ * @param tag Used to identify the source of a log message. It usually identifies
* the class or activity where the log call occurs.
* @param msg The message you would like logged.
*/
@@ -138,7 +138,7 @@ public final class Log {
/**
* Send a {@link #INFO} log message and log the exception.
- * @param tag Used to identify the source of a log message. It usually identfies
+ * @param tag Used to identify the source of a log message. It usually identifies
* the class or activity where the log call occurs.
* @param msg The message you would like logged.
* @param tr An exception to log
@@ -149,7 +149,7 @@ public final class Log {
/**
* Send a {@link #WARN} log message.
- * @param tag Used to identify the source of a log message. It usually identfies
+ * @param tag Used to identify the source of a log message. It usually identifies
* the class or activity where the log call occurs.
* @param msg The message you would like logged.
*/
@@ -159,7 +159,7 @@ public final class Log {
/**
* Send a {@link #WARN} log message and log the exception.
- * @param tag Used to identify the source of a log message. It usually identfies
+ * @param tag Used to identify the source of a log message. It usually identifies
* the class or activity where the log call occurs.
* @param msg The message you would like logged.
* @param tr An exception to log
@@ -190,7 +190,7 @@ public final class Log {
/*
* Send a {@link #WARN} log message and log the exception.
- * @param tag Used to identify the source of a log message. It usually identfies
+ * @param tag Used to identify the source of a log message. It usually identifies
* the class or activity where the log call occurs.
* @param tr An exception to log
*/
@@ -200,7 +200,7 @@ public final class Log {
/**
* Send an {@link #ERROR} log message.
- * @param tag Used to identify the source of a log message. It usually identfies
+ * @param tag Used to identify the source of a log message. It usually identifies
* the class or activity where the log call occurs.
* @param msg The message you would like logged.
*/
@@ -210,7 +210,7 @@ public final class Log {
/**
* Send a {@link #ERROR} log message and log the exception.
- * @param tag Used to identify the source of a log message. It usually identfies
+ * @param tag Used to identify the source of a log message. It usually identifies
* the class or activity where the log call occurs.
* @param msg The message you would like logged.
* @param tr An exception to log
@@ -238,7 +238,7 @@ public final class Log {
/**
* Low-level logging call.
* @param priority The priority/type of this log message
- * @param tag Used to identify the source of a log message. It usually identfies
+ * @param tag Used to identify the source of a log message. It usually identifies
* the class or activity where the log call occurs.
* @param msg The message you would like logged.
* @return The number of bytes written.
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index d89c7b4..40251db 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -67,6 +67,7 @@ interface IWindowManager
int getAppOrientation(IApplicationToken token);
void setFocusedApp(IBinder token, boolean moveFocusNow);
void prepareAppTransition(int transit);
+ int getPendingAppTransition();
void executeAppTransition();
void setAppStartingWindow(IBinder token, String pkg, int theme,
CharSequence nonLocalizedLabel, int labelRes,
diff --git a/core/java/android/view/RawInputEvent.java b/core/java/android/view/RawInputEvent.java
index 580a80d..30da83e 100644
--- a/core/java/android/view/RawInputEvent.java
+++ b/core/java/android/view/RawInputEvent.java
@@ -10,8 +10,9 @@ package android.view;
public class RawInputEvent {
// Event class as defined by EventHub.
public static final int CLASS_KEYBOARD = 0x00000001;
- public static final int CLASS_TOUCHSCREEN = 0x00000002;
- public static final int CLASS_TRACKBALL = 0x00000004;
+ public static final int CLASS_ALPHAKEY = 0x00000002;
+ public static final int CLASS_TOUCHSCREEN = 0x00000004;
+ public static final int CLASS_TRACKBALL = 0x00000008;
// More special classes for QueuedEvent below.
public static final int CLASS_CONFIGURATION_CHANGED = 0x10000000;
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 1cc7b60..85f482c 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -1670,6 +1670,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback {
int viewFlagValues = 0;
int viewFlagMasks = 0;
+ boolean setScrollContainer = false;
+
int x = 0;
int y = 0;
@@ -1796,6 +1798,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback {
viewFlagMasks |= SCROLLBARS_STYLE_MASK;
}
break;
+ case R.styleable.View_isScrollContainer:
+ setScrollContainer = true;
+ if (a.getBoolean(attr, false)) {
+ setScrollContainer(true);
+ }
+ break;
case com.android.internal.R.styleable.View_keepScreenOn:
if (a.getBoolean(attr, false)) {
viewFlagValues |= KEEP_SCREEN_ON;
@@ -1856,6 +1864,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback {
scrollTo(x, y);
}
+ if (!setScrollContainer && (viewFlagValues&SCROLLBARS_VERTICAL) != 0) {
+ setScrollContainer(true);
+ }
+
a.recycle();
}
@@ -3555,10 +3567,28 @@ public class View implements Drawable.Callback, KeyEvent.Callback {
}
/**
+ * Check whether the called view is a text editor, in which case it
+ * would make sense to automatically display a soft input window for
+ * it. Subclasses should override this if they implement
+ * {@link #onCreateInputConnection(EditorInfo)} to return true if
+ * a call on that method would return a non-null InputConnection. The
+ * default implementation always returns false.
+ *
+ * @return Returns true if this view is a text editor, else false.
+ */
+ public boolean onCheckIsTextEditor() {
+ return false;
+ }
+
+ /**
* Create a new InputConnection for an InputMethod to interact
* with the view. The default implementation returns null, since it doesn't
* support input methods. You can override this to implement such support.
* This is only needed for views that take focus and text input.
+ *
+ * <p>When implementing this, you probably also want to implement
+ * {@link #onCheckIsTextEditor()} to indicate you will return a
+ * non-null InputConnection.
*
* @param outAttrs Fill in with attribute information about the connection.
*/
diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java
index a254edb..9d7a124 100644
--- a/core/java/android/view/ViewRoot.java
+++ b/core/java/android/view/ViewRoot.java
@@ -1579,9 +1579,16 @@ public final class ViewRoot extends Handler implements ViewParent,
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null) {
imm.onWindowFocus(mView.findFocus(),
- mWindowAttributes.softInputMode, !mHasHadWindowFocus,
- mWindowAttributes.flags);
+ mWindowAttributes.softInputMode,
+ !mHasHadWindowFocus, mWindowAttributes.flags);
}
+ // Clear the forward bit. We can just do this directly, since
+ // the window manager doesn't care about it.
+ mWindowAttributes.softInputMode &=
+ ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
+ ((WindowManager.LayoutParams)mView.getLayoutParams())
+ .softInputMode &=
+ ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
mHasHadWindowFocus = true;
}
}
@@ -2030,14 +2037,20 @@ public final class ViewRoot extends Handler implements ViewParent,
}
return;
}
- InputMethodManager imm = InputMethodManager.peekInstance();
- if (imm != null && mView != null && imm.isActive()) {
- int seq = enqueuePendingEvent(event, sendDone);
- if (DEBUG_IMF) Log.v(TAG, "Sending key event to IME: seq="
- + seq + " event=" + event);
- imm.dispatchKeyEvent(mView.getContext(), seq, event,
- mInputMethodCallback);
- return;
+ // If it is possible for this window to interact with the input
+ // method window, then we want to first dispatch our key events
+ // to the input method.
+ if (WindowManager.LayoutParams.mayUseInputMethod(
+ mWindowAttributes.flags)) {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null && mView != null && imm.isActive()) {
+ int seq = enqueuePendingEvent(event, sendDone);
+ if (DEBUG_IMF) Log.v(TAG, "Sending key event to IME: seq="
+ + seq + " event=" + event);
+ imm.dispatchKeyEvent(mView.getContext(), seq, event,
+ mInputMethodCallback);
+ return;
+ }
}
deliverKeyEventToViewHierarchy(event, sendDone);
}
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 7d202aa..7e47ad1 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -466,12 +466,6 @@ public interface WindowManager extends ViewManager {
*/
public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;
- /** Window flag: set when this window was created from the restored
- * state of a previous window, indicating this is not the first time
- * the user has navigated to it.
- */
- public static final int FLAG_RESTORED_STATE = 0x00080000;
-
/** Window flag: a special option intended for system dialogs. When
* this flag is set, the window will demand focus unconditionally when
* it is created.
@@ -479,6 +473,29 @@ public interface WindowManager extends ViewManager {
public static final int FLAG_SYSTEM_ERROR = 0x40000000;
/**
+ * Given a particular set of window manager flags, determine whether
+ * such a window may be a target for an input method when it has
+ * focus. In particular, this checks the
+ * {@link #FLAG_NOT_FOCUSABLE} and {@link #FLAG_ALT_FOCUSABLE_IM}
+ * flags and returns true if the combination of the two corresponds
+ * to a window that needs to be behind the input method so that the
+ * user can type into it.
+ *
+ * @param flags The current window manager flags.
+ *
+ * @return Returns true if such a window should be behind/interact
+ * with an input method, false if not.
+ */
+ public static boolean mayUseInputMethod(int flags) {
+ switch (flags&(FLAG_NOT_FOCUSABLE|FLAG_ALT_FOCUSABLE_IM)) {
+ case 0:
+ case FLAG_NOT_FOCUSABLE|FLAG_ALT_FOCUSABLE_IM:
+ return true;
+ }
+ return false;
+ }
+
+ /**
* Mask for {@link #softInputMode} of the bits that determine the
* desired visibility state of the soft input area for this window.
*/
@@ -502,16 +519,17 @@ public interface WindowManager extends ViewManager {
public static final int SOFT_INPUT_STATE_HIDDEN = 2;
/**
- * Visibility state for {@link #softInputMode}: please show the soft input area
- * the first time the window is shown.
+ * Visibility state for {@link #softInputMode}: please show the soft
+ * input area when normally appropriate (when the user is navigating
+ * forward to your window).
*/
- public static final int SOFT_INPUT_STATE_FIRST_VISIBLE = 3;
+ public static final int SOFT_INPUT_STATE_VISIBLE = 3;
/**
- * Visibility state for {@link #softInputMode}: please always show the soft
- * input area.
+ * Visibility state for {@link #softInputMode}: please always make the
+ * soft input area visible when this window receives input focus.
*/
- public static final int SOFT_INPUT_STATE_VISIBLE = 4;
+ public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 4;
/**
* Mask for {@link #softInputMode} of the bits that determine the
@@ -547,13 +565,22 @@ public interface WindowManager extends ViewManager {
public static final int SOFT_INPUT_ADJUST_PAN = 0x20;
/**
+ * Bit for {@link #softInputMode}: set when the user has navigated
+ * forward to the window. This is normally set automatically for
+ * you by the system, though you may want to set it in certain cases
+ * when you are displaying a window yourself. This flag will always
+ * be cleared automatically after the window is displayed.
+ */
+ public static final int SOFT_INPUT_IS_FORWARD_NAVIGATION = 0x100;
+
+ /**
* Desired operating mode for any soft input area. May any combination
* of:
*
* <ul>
* <li> One of the visibility states
* {@link #SOFT_INPUT_STATE_UNSPECIFIED}, {@link #SOFT_INPUT_STATE_UNCHANGED},
- * {@link #SOFT_INPUT_STATE_HIDDEN}, {@link #SOFT_INPUT_STATE_FIRST_VISIBLE}, or
+ * {@link #SOFT_INPUT_STATE_HIDDEN}, {@link #SOFT_INPUT_STATE_ALWAYS_VISIBLE}, or
* {@link #SOFT_INPUT_STATE_VISIBLE}.
* <li> One of the adjustment options
* {@link #SOFT_INPUT_ADJUST_UNSPECIFIED},
diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java
index d1d549c..9264398 100644
--- a/core/java/android/view/animation/Animation.java
+++ b/core/java/android/view/animation/Animation.java
@@ -118,7 +118,12 @@ public abstract class Animation {
* Indicates whether the animation transformation should be applied after the
* animation ends.
*/
- boolean mFillAfter = true;
+ boolean mFillAfter = false;
+
+ /**
+ * Indicates whether fillAfter should be taken into account.
+ */
+ boolean mFillEnabled = false;
/**
* The time in milliseconds at which the animation must start;
@@ -193,6 +198,7 @@ public abstract class Animation {
setDuration((long) a.getInt(com.android.internal.R.styleable.Animation_duration, 0));
setStartOffset((long) a.getInt(com.android.internal.R.styleable.Animation_startOffset, 0));
+ setFillEnabled(a.getBoolean(com.android.internal.R.styleable.Animation_fillEnabled, mFillEnabled));
setFillBefore(a.getBoolean(com.android.internal.R.styleable.Animation_fillBefore, mFillBefore));
setFillAfter(a.getBoolean(com.android.internal.R.styleable.Animation_fillAfter, mFillAfter));
@@ -406,6 +412,31 @@ public abstract class Animation {
}
/**
+ * If fillEnabled is true, this animation will apply fillBefore and fillAfter.
+ *
+ * @return true if the animation will take fillBefore and fillAfter into account
+ * @attr ref android.R.styleable#Animation_fillEnabled
+ */
+ public boolean isFillEnabled() {
+ return mFillEnabled;
+ }
+
+ /**
+ * If fillEnabled is true, the animation will apply the value of fillBefore and
+ * fillAfter. Otherwise, fillBefore and fillAfter are ignored and the animation
+ * transformation is always applied.
+ *
+ * @param fillEnabled true if the animation should take fillBefore and fillAfter into account
+ * @attr ref android.R.styleable#Animation_fillEnabled
+ *
+ * @see #setFillBefore(boolean)
+ * @see #setFillAfter(boolean)
+ */
+ public void setFillEnabled(boolean fillEnabled) {
+ mFillEnabled = fillEnabled;
+ }
+
+ /**
* If fillBefore is true, this animation will apply its transformation
* before the start time of the animation. Defaults to true if not set.
* Note that this applies when using an {@link
@@ -415,6 +446,8 @@ public abstract class Animation {
*
* @param fillBefore true if the animation should apply its transformation before it starts
* @attr ref android.R.styleable#Animation_fillBefore
+ *
+ * @see #setFillEnabled(boolean)
*/
public void setFillBefore(boolean fillBefore) {
mFillBefore = fillBefore;
@@ -422,7 +455,7 @@ public abstract class Animation {
/**
* If fillAfter is true, the transformation that this animation performed
- * will persist when it is finished. Defaults to true if not set.
+ * will persist when it is finished. Defaults to false if not set.
* Note that this applies when using an {@link
* android.view.animation.AnimationSet AnimationSet} to chain
* animations. The transformation is not applied before the AnimationSet
@@ -430,6 +463,8 @@ public abstract class Animation {
*
* @param fillAfter true if the animation should apply its transformation after it ends
* @attr ref android.R.styleable#Animation_fillAfter
+ *
+ * @see #setFillEnabled(boolean)
*/
public void setFillAfter(boolean fillAfter) {
mFillAfter = fillAfter;
@@ -623,9 +658,11 @@ public abstract class Animation {
normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
}
- boolean expired = normalizedTime >= 1.0f;
+ final boolean expired = normalizedTime >= 1.0f;
mMore = !expired;
+ if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
+
if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
if (!mStarted) {
if (mListener != null) {
@@ -634,8 +671,7 @@ public abstract class Animation {
mStarted = true;
}
- // Pin time to 0.0 to 1.0 range
- normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
+ if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
if (mCycleFlip) {
normalizedTime = 1.0f - normalizedTime;
diff --git a/core/java/android/view/inputmethod/DefaultInputMethod.java b/core/java/android/view/inputmethod/DefaultInputMethod.java
index e92cbad..073b01c 100644
--- a/core/java/android/view/inputmethod/DefaultInputMethod.java
+++ b/core/java/android/view/inputmethod/DefaultInputMethod.java
@@ -98,7 +98,7 @@ public class DefaultInputMethod implements InputMethod, InputMethodSession {
public void hideSoftInput() {
}
- public void showSoftInput() {
+ public void showSoftInput(int flags) {
}
}
@@ -231,7 +231,7 @@ class SimpleInputMethod extends IInputMethod.Stub {
}
}
- public void showSoftInput() {
+ public void showSoftInput(boolean blah) {
}
public void hideSoftInput() {
diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java
index ad61f94..c0e6590 100644
--- a/core/java/android/view/inputmethod/InputMethod.java
+++ b/core/java/android/view/inputmethod/InputMethod.java
@@ -165,9 +165,20 @@ public interface InputMethod {
public void revokeSession(InputMethodSession session);
/**
+ * Flag for {@link #showSoftInput(int)}: this show has been explicitly
+ * requested by the user. If not set, the system has decided it may be
+ * a good idea to show the input method based on a navigation operation
+ * in the UI.
+ */
+ public static final int SHOW_EXPLICIT = 0x00001;
+
+ /**
* Request that any soft input part of the input method be shown to the user.
+ *
+ * @param flags Provide additional information about the show request.
+ * Currently may be 0 or have the bit {@link #SHOW_EXPLICIT} set.
*/
- public void showSoftInput();
+ public void showSoftInput(int flags);
/**
* Request that any soft input part of the input method be hidden from the user.
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index a9c46c3..a676234 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -645,6 +645,13 @@ public final class InputMethodManager {
}
/**
+ * Flag for {@link #showSoftInput} to indicate that the this is an implicit
+ * request to show the input window, not as the result of a direct request
+ * by the user. The window may not be shown in this case.
+ */
+ public static final int SHOW_IMPLICIT = 0x0001;
+
+ /**
* Explicitly request that the current input method's soft input area be
* shown to the user, if needed. Call this if the user interacts with
* your view in such a way that they have expressed they would like to
@@ -652,21 +659,30 @@ public final class InputMethodManager {
*
* @param view The currently focused view, which would like to receive
* soft keyboard input.
+ * @param flags Provides additional operating flags. Currently may be
+ * 0 or have the {@link #SHOW_IMPLICIT} bit set.
*/
- public void showSoftInput(View view) {
+ public void showSoftInput(View view, int flags) {
synchronized (mH) {
if (mServedView != view) {
return;
}
try {
- mService.showSoftInput(mClient);
+ mService.showSoftInput(mClient, flags);
} catch (RemoteException e) {
}
}
}
/**
+ * Flag for {@link #hideSoftInputFromWindow} to indicate that the soft
+ * input window should only be hidden if it was not explicitly shown
+ * by the user.
+ */
+ public static final int HIDE_IMPLICIT_ONLY = 0x0001;
+
+ /**
* Request to hide the soft input window from the context of the window
* that is currently accepting input. This should be called as a result
* of the user doing some actually than fairly explicitly requests to
@@ -674,15 +690,17 @@ public final class InputMethodManager {
*
* @param windowToken The token of the window that is making the request,
* as returned by {@link View#getWindowToken() View.getWindowToken()}.
+ * @param flags Provides additional operating flags. Currently may be
+ * 0 or have the {@link #HIDE_IMPLICIT_ONLY} bit set.
*/
- public void hideSoftInputFromWindow(IBinder windowToken) {
+ public void hideSoftInputFromWindow(IBinder windowToken, int flags) {
synchronized (mH) {
if (mServedView == null || mServedView.getWindowToken() != windowToken) {
return;
}
try {
- mService.hideSoftInput(mClient);
+ mService.hideSoftInput(mClient, flags);
} catch (RemoteException e) {
}
}
@@ -880,13 +898,14 @@ public final class InputMethodManager {
void closeCurrentInput() {
try {
- mService.hideSoftInput(mClient);
+ mService.hideSoftInput(mClient, 0);
} catch (RemoteException e) {
}
}
/**
* Called by ViewRoot the first time it gets window focus.
+ * @hide
*/
public void onWindowFocus(View focusedView, int softInputMode,
boolean first, int windowFlags) {
@@ -896,8 +915,10 @@ public final class InputMethodManager {
+ " first=" + first + " flags=#"
+ Integer.toHexString(windowFlags));
try {
+ final boolean isTextEditor = focusedView != null &&
+ focusedView.onCheckIsTextEditor();
mService.windowGainedFocus(mClient, focusedView != null,
- softInputMode, first, windowFlags);
+ isTextEditor, softInputMode, first, windowFlags);
} catch (RemoteException e) {
}
}
@@ -987,13 +1008,16 @@ public final class InputMethodManager {
* Close/hide the input method's soft input area, so the user no longer
* sees it or can interact with it. This can only be called
* from the currently active input method, as validated by the given token.
+ *
* @param token Supplies the identifying token given to an input method
* when it was started, which allows it to perform this operation on
* itself.
+ * @param flags Provides additional operating flags. Currently may be
+ * 0 or have the {@link #HIDE_IMPLICIT_ONLY} bit set.
*/
- public void hideSoftInputFromInputMethod(IBinder token) {
+ public void hideSoftInputFromInputMethod(IBinder token, int flags) {
try {
- mService.hideMySoftInput(token);
+ mService.hideMySoftInput(token, flags);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
diff --git a/core/java/android/webkit/MimeTypeMap.java b/core/java/android/webkit/MimeTypeMap.java
index 85cb8c0..685e352 100644
--- a/core/java/android/webkit/MimeTypeMap.java
+++ b/core/java/android/webkit/MimeTypeMap.java
@@ -430,8 +430,9 @@ public /* package */ class MimeTypeMap {
sMimeTypeMap.loadEntry("text/h323", "323", true);
sMimeTypeMap.loadEntry("text/iuls", "uls", true);
sMimeTypeMap.loadEntry("text/mathml", "mml", true);
- sMimeTypeMap.loadEntry("text/plain", "asc", true);
+ // add it first so it will be the default for ExtensionFromMimeType
sMimeTypeMap.loadEntry("text/plain", "txt", true);
+ sMimeTypeMap.loadEntry("text/plain", "asc", true);
sMimeTypeMap.loadEntry("text/plain", "text", true);
sMimeTypeMap.loadEntry("text/plain", "diff", true);
sMimeTypeMap.loadEntry("text/plain", "pot", true);
@@ -469,6 +470,8 @@ public /* package */ class MimeTypeMap {
sMimeTypeMap.loadEntry("text/x-tex", "cls", true);
sMimeTypeMap.loadEntry("text/x-vcalendar", "vcs", true);
sMimeTypeMap.loadEntry("text/x-vcard", "vcf", true);
+ sMimeTypeMap.loadEntry("video/3gpp", "3gp", false);
+ sMimeTypeMap.loadEntry("video/3gpp", "3g2", false);
sMimeTypeMap.loadEntry("video/dl", "dl", false);
sMimeTypeMap.loadEntry("video/dv", "dif", false);
sMimeTypeMap.loadEntry("video/dv", "dv", false);
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index bd910b5..f00238d 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -2694,7 +2694,7 @@ public class WebView extends AbsoluteLayout
private void displaySoftKeyboard() {
InputMethodManager imm = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.showSoftInput(mTextEntry);
+ imm.showSoftInput(mTextEntry, 0);
}
// Used to register the global focus change listener one time to avoid
@@ -3066,6 +3066,13 @@ public class WebView extends AbsoluteLayout
// Bubble up the key event as WebView doesn't handle it
return false;
}
+
+ /**
+ * @hide
+ */
+ public void emulateShiftHeld() {
+ mShiftIsPressed = true;
+ }
private boolean commitCopy() {
boolean copiedSomething = false;
diff --git a/core/java/android/webkit/gears/ApacheHttpRequestAndroid.java b/core/java/android/webkit/gears/ApacheHttpRequestAndroid.java
new file mode 100644
index 0000000..0569255
--- /dev/null
+++ b/core/java/android/webkit/gears/ApacheHttpRequestAndroid.java
@@ -0,0 +1,1122 @@
+// Copyright 2008, The Android Open Source Project
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+// 3. Neither the name of Google Inc. nor the names of its contributors may be
+// used to endorse or promote products derived from this software without
+// specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package android.webkit.gears;
+
+import android.net.http.Headers;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Config;
+import android.util.Log;
+import android.webkit.CacheManager;
+import android.webkit.CacheManager.CacheResult;
+import android.webkit.CookieManager;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.IOException;
+import java.lang.StringBuilder;
+import java.util.Date;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.params.HttpClientParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpProtocolParams;
+import org.apache.http.HttpResponse;
+import org.apache.http.entity.AbstractHttpEntity;
+import org.apache.http.client.*;
+import org.apache.http.client.methods.*;
+import org.apache.http.impl.client.AbstractHttpClient;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+import org.apache.http.impl.cookie.DateUtils;
+import org.apache.http.util.CharArrayBuffer;
+
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Performs the underlying HTTP/HTTPS GET, POST, HEAD, PUT, DELETE requests.
+ * <p> These are performed synchronously (blocking). The caller should
+ * ensure that it is in a background thread if asynchronous behavior
+ * is required. All data is pushed, so there is no need for JNI native
+ * callbacks.
+ * <p> This uses Apache's HttpClient framework to perform most
+ * of the underlying network activity. The Android brower's cache,
+ * android.webkit.CacheManager, is also used when caching is enabled,
+ * and updated with new data. The android.webkit.CookieManager is also
+ * queried and updated as necessary.
+ * <p> The public interface is designed to be called by native code
+ * through JNI, and to simplify coding none of the public methods will
+ * surface a checked exception. Unchecked exceptions may still be
+ * raised but only if the system is in an ill state, such as out of
+ * memory.
+ * <p> TODO: This isn't plumbed into LocalServer yet. Mutually
+ * dependent on LocalServer - will attach the two together once both
+ * are submitted.
+ */
+public final class ApacheHttpRequestAndroid {
+ /** Debug logging tag. */
+ private static final String LOG_TAG = "Gears-J";
+ /** HTTP response header line endings are CR-LF style. */
+ private static final String HTTP_LINE_ENDING = "\r\n";
+ /** Safe MIME type to use whenever it isn't specified. */
+ private static final String DEFAULT_MIME_TYPE = "text/plain";
+ /** Case-sensitive header keys */
+ public static final String KEY_CONTENT_LENGTH = "Content-Length";
+ public static final String KEY_EXPIRES = "Expires";
+ public static final String KEY_LAST_MODIFIED = "Last-Modified";
+ public static final String KEY_ETAG = "ETag";
+ public static final String KEY_LOCATION = "Location";
+ public static final String KEY_CONTENT_TYPE = "Content-Type";
+ /** Number of bytes to send and receive on the HTTP connection in
+ * one go. */
+ private static final int BUFFER_SIZE = 4096;
+
+ /** The first element of the String[] value in a headers map is the
+ * unmodified (case-sensitive) key. */
+ public static final int HEADERS_MAP_INDEX_KEY = 0;
+ /** The second element of the String[] value in a headers map is the
+ * associated value. */
+ public static final int HEADERS_MAP_INDEX_VALUE = 1;
+
+ /** Request headers, as key -> value map. */
+ // TODO: replace this design by a simpler one (the C++ side has to
+ // be modified too), where we do not store both the original header
+ // and the lowercase one.
+ private Map<String, String[]> mRequestHeaders =
+ new HashMap<String, String[]>();
+ /** Response headers, as a lowercase key -> value map. */
+ private Map<String, String[]> mResponseHeaders =
+ new HashMap<String, String[]>();
+ /** The URL used for createCacheResult() */
+ private String mCacheResultUrl;
+ /** CacheResult being saved into, if inserting a new cache entry. */
+ private CacheResult mCacheResult;
+ /** Initialized by initChildThread(). Used to target abort(). */
+ private Thread mBridgeThread;
+
+ /** Our HttpClient */
+ private AbstractHttpClient mClient;
+ /** The HttpMethod associated with this request */
+ private HttpRequestBase mMethod;
+ /** The complete response line e.g "HTTP/1.0 200 OK" */
+ private String mResponseLine;
+ /** HTTP body stream, setup after connection. */
+ private InputStream mBodyInputStream;
+
+ /** HTTP Response Entity */
+ private HttpResponse mResponse;
+
+ /** Post Entity, used to stream the request to the server */
+ private StreamEntity mPostEntity = null;
+ /** Content lenght, mandatory when using POST */
+ private long mContentLength;
+
+ /** The request executes in a parallel thread */
+ private Thread mHttpThread = null;
+ /** protect mHttpThread, if interrupt() is called concurrently */
+ private Lock mHttpThreadLock = new ReentrantLock();
+ /** Flag set to true when the request thread is joined */
+ private boolean mConnectionFinished = false;
+ /** Flag set to true by interrupt() and/or connection errors */
+ private boolean mConnectionFailed = false;
+ /** Lock protecting the access to mConnectionFailed */
+ private Lock mConnectionFailedLock = new ReentrantLock();
+
+ /** Lock on the loop in StreamEntity */
+ private Lock mStreamingReadyLock = new ReentrantLock();
+ /** Condition variable used to signal the loop is ready... */
+ private Condition mStreamingReady = mStreamingReadyLock.newCondition();
+
+ /** Used to pass around the block of data POSTed */
+ private Buffer mBuffer = new Buffer();
+ /** Used to signal that the block of data has been written */
+ private SignalConsumed mSignal = new SignalConsumed();
+
+ // inner classes
+
+ /**
+ * Implements the http request
+ */
+ class Connection implements Runnable {
+ public void run() {
+ boolean problem = false;
+ try {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "REQUEST : " + mMethod.getRequestLine());
+ }
+ mResponse = mClient.execute(mMethod);
+ if (mResponse != null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "response (status line): "
+ + mResponse.getStatusLine());
+ }
+ mResponseLine = "" + mResponse.getStatusLine();
+ } else {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "problem, response == null");
+ }
+ problem = true;
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Connection IO exception ", e);
+ problem = true;
+ } catch (RuntimeException e) {
+ Log.e(LOG_TAG, "Connection runtime exception ", e);
+ problem = true;
+ }
+
+ if (!problem) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Request complete ("
+ + mMethod.getRequestLine() + ")");
+ }
+ } else {
+ mConnectionFailedLock.lock();
+ mConnectionFailed = true;
+ mConnectionFailedLock.unlock();
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Request FAILED ("
+ + mMethod.getRequestLine() + ")");
+ }
+ // We abort the execution in order to shutdown and release
+ // the underlying connection
+ mMethod.abort();
+ if (mPostEntity != null) {
+ // If there is a post entity, we need to wake it up from
+ // a potential deadlock
+ mPostEntity.signalOutputStream();
+ }
+ }
+ }
+ }
+
+ /**
+ * simple buffer class implementing a producer/consumer model
+ */
+ class Buffer {
+ private DataPacket mPacket;
+ private boolean mEmpty = true;
+ public synchronized void put(DataPacket packet) {
+ while (!mEmpty) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "InterruptedException while putting " +
+ "a DataPacket in the Buffer: " + e);
+ }
+ }
+ }
+ mPacket = packet;
+ mEmpty = false;
+ notify();
+ }
+ public synchronized DataPacket get() {
+ while (mEmpty) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "InterruptedException while getting " +
+ "a DataPacket in the Buffer: " + e);
+ }
+ }
+ }
+ mEmpty = true;
+ notify();
+ return mPacket;
+ }
+ }
+
+ /**
+ * utility class used to block until the packet is signaled as being
+ * consumed
+ */
+ class SignalConsumed {
+ private boolean mConsumed = false;
+ public synchronized void waitUntilPacketConsumed() {
+ while (!mConsumed) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "InterruptedException while waiting " +
+ "until a DataPacket is consumed: " + e);
+ }
+ }
+ }
+ mConsumed = false;
+ notify();
+ }
+ public synchronized void packetConsumed() {
+ while (mConsumed) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "InterruptedException while indicating "
+ + "that the DataPacket has been consumed: " + e);
+ }
+ }
+ }
+ mConsumed = true;
+ notify();
+ }
+ }
+
+ /**
+ * Utility class encapsulating a packet of data
+ */
+ class DataPacket {
+ private byte[] mContent;
+ private int mLength;
+ public DataPacket(byte[] content, int length) {
+ mContent = content;
+ mLength = length;
+ }
+ public byte[] getBytes() {
+ return mContent;
+ }
+ public int getLength() {
+ return mLength;
+ }
+ }
+
+ /**
+ * HttpEntity class to write the bytes received by the C++ thread
+ * on the connection outputstream, in a streaming way.
+ * This entity is executed in the request thread.
+ * The writeTo() method is automatically called by the
+ * HttpPost execution; upon reception, we loop while receiving
+ * the data packets from the main thread, until completion
+ * or error. When done, we flush the outputstream.
+ * The main thread (sendPostData()) also blocks until the
+ * outputstream is made available (or an error happens)
+ */
+ class StreamEntity implements HttpEntity {
+ private OutputStream mOutputStream;
+
+ // HttpEntity interface methods
+
+ public boolean isRepeatable() {
+ return false;
+ }
+
+ public boolean isChunked() {
+ return false;
+ }
+
+ public long getContentLength() {
+ return mContentLength;
+ }
+
+ public Header getContentType() {
+ return null;
+ }
+
+ public Header getContentEncoding() {
+ return null;
+ }
+
+ public InputStream getContent() throws IOException {
+ return null;
+ }
+
+ public void writeTo(final OutputStream out) throws IOException {
+ // We signal that the outputstream is available
+ mStreamingReadyLock.lock();
+ mOutputStream = out;
+ mStreamingReady.signal();
+ mStreamingReadyLock.unlock();
+
+ // We then loop waiting on messages to process.
+ boolean finished = false;
+ while (!finished) {
+ DataPacket packet = mBuffer.get();
+ if (packet == null) {
+ finished = true;
+ } else {
+ write(packet);
+ }
+ mSignal.packetConsumed();
+ mConnectionFailedLock.lock();
+ if (mConnectionFailed) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "stopping loop on error");
+ }
+ finished = true;
+ }
+ mConnectionFailedLock.unlock();
+ }
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "flushing the outputstream...");
+ }
+ mOutputStream.flush();
+ }
+
+ public boolean isStreaming() {
+ return true;
+ }
+
+ public void consumeContent() throws IOException {
+ // Nothing to release
+ }
+
+ // local methods
+
+ private void write(DataPacket packet) {
+ try {
+ if (mOutputStream == null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "NO OUTPUT STREAM !!!");
+ }
+ return;
+ }
+ mOutputStream.write(packet.getBytes(), 0, packet.getLength());
+ mOutputStream.flush();
+ } catch (IOException e) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "exc: " + e);
+ }
+ mConnectionFailedLock.lock();
+ mConnectionFailed = true;
+ mConnectionFailedLock.unlock();
+ }
+ }
+
+ public boolean isReady() {
+ mStreamingReadyLock.lock();
+ try {
+ if (mOutputStream == null) {
+ mStreamingReady.await();
+ }
+ } catch (InterruptedException e) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "InterruptedException in "
+ + "StreamEntity::isReady() : ", e);
+ }
+ } finally {
+ mStreamingReadyLock.unlock();
+ }
+ if (mOutputStream == null) {
+ return false;
+ }
+ return true;
+ }
+
+ public void signalOutputStream() {
+ mStreamingReadyLock.lock();
+ mStreamingReady.signal();
+ mStreamingReadyLock.unlock();
+ }
+ }
+
+ /**
+ * Initialize mBridgeThread using the TLS value of
+ * Thread.currentThread(). Called on start up of the native child
+ * thread.
+ */
+ public synchronized void initChildThread() {
+ mBridgeThread = Thread.currentThread();
+ }
+
+ public void setContentLength(long length) {
+ mContentLength = length;
+ }
+
+ /**
+ * Analagous to the native-side HttpRequest::open() function. This
+ * initializes an underlying HttpClient method, but does
+ * not go to the wire. On success, this enables a call to send() to
+ * initiate the transaction.
+ *
+ * @param method The HTTP method, e.g GET or POST.
+ * @param url The URL to open.
+ * @return True on success with a complete HTTP response.
+ * False on failure.
+ */
+ public synchronized boolean open(String method, String url) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "open " + method + " " + url);
+ }
+ // Create the client
+ if (mConnectionFailed) {
+ // interrupt() could have been called even before open()
+ return false;
+ }
+ mClient = new DefaultHttpClient();
+ mClient.setHttpRequestRetryHandler(
+ new DefaultHttpRequestRetryHandler(0, false));
+ mBodyInputStream = null;
+ mResponseLine = null;
+ mResponseHeaders = null;
+ mPostEntity = null;
+ mHttpThread = null;
+ mConnectionFailed = false;
+ mConnectionFinished = false;
+
+ // Create the method. We support everything that
+ // Apache HttpClient supports, apart from TRACE.
+ if ("GET".equalsIgnoreCase(method)) {
+ mMethod = new HttpGet(url);
+ } else if ("POST".equalsIgnoreCase(method)) {
+ mMethod = new HttpPost(url);
+ mPostEntity = new StreamEntity();
+ ((HttpPost)mMethod).setEntity(mPostEntity);
+ } else if ("HEAD".equalsIgnoreCase(method)) {
+ mMethod = new HttpHead(url);
+ } else if ("PUT".equalsIgnoreCase(method)) {
+ mMethod = new HttpPut(url);
+ } else if ("DELETE".equalsIgnoreCase(method)) {
+ mMethod = new HttpDelete(url);
+ } else {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Method " + method + " not supported");
+ }
+ return false;
+ }
+ HttpParams params = mClient.getParams();
+ // We handle the redirections C++-side
+ HttpClientParams.setRedirecting(params, false);
+ HttpProtocolParams.setUseExpectContinue(params, false);
+ return true;
+ }
+
+ /**
+ * We use this to start the connection thread (doing the method execute).
+ * We usually always return true here, as the connection will run its
+ * course in the thread.
+ * We only return false if interrupted beforehand -- if a connection
+ * problem happens, we will thus fail in either sendPostData() or
+ * parseHeaders().
+ */
+ public synchronized boolean connectToRemote() {
+ boolean ret = false;
+ applyRequestHeaders();
+ mConnectionFailedLock.lock();
+ if (!mConnectionFailed) {
+ mHttpThread = new Thread(new Connection());
+ mHttpThread.start();
+ }
+ ret = mConnectionFailed;
+ mConnectionFailedLock.unlock();
+ return !ret;
+ }
+
+ /**
+ * Get the complete response line of the HTTP request. Only valid on
+ * completion of the transaction.
+ * @return The complete HTTP response line, e.g "HTTP/1.0 200 OK".
+ */
+ public synchronized String getResponseLine() {
+ return mResponseLine;
+ }
+
+ /**
+ * Wait for the request thread completion
+ * (unless already finished)
+ */
+ private void waitUntilConnectionFinished() {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "waitUntilConnectionFinished("
+ + mConnectionFinished + ")");
+ }
+ if (!mConnectionFinished) {
+ if (mHttpThread != null) {
+ try {
+ mHttpThread.join();
+ mConnectionFinished = true;
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "http thread joined");
+ }
+ } catch (InterruptedException e) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "interrupted: " + e);
+ }
+ }
+ } else {
+ Log.e(LOG_TAG, ">>> Trying to join on mHttpThread " +
+ "when it does not exist!");
+ }
+ }
+ }
+
+ // Headers handling
+
+ /**
+ * Receive all headers from the server and populate
+ * mResponseHeaders.
+ * @return True if headers are successfully received, False on
+ * connection error.
+ */
+ public synchronized boolean parseHeaders() {
+ mConnectionFailedLock.lock();
+ if (mConnectionFailed) {
+ mConnectionFailedLock.unlock();
+ return false;
+ }
+ mConnectionFailedLock.unlock();
+ waitUntilConnectionFinished();
+ mResponseHeaders = new HashMap<String, String[]>();
+ if (mResponse == null)
+ return false;
+
+ Header[] headers = mResponse.getAllHeaders();
+ for (int i = 0; i < headers.length; i++) {
+ Header header = headers[i];
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "header " + header.getName()
+ + " -> " + header.getValue());
+ }
+ setResponseHeader(header.getName(), header.getValue());
+ }
+
+ return true;
+ }
+
+ /**
+ * Set a header to send with the HTTP request. Will not take effect
+ * on a transaction already in progress. The key is associated
+ * case-insensitive, but stored case-sensitive.
+ * @param name The name of the header, e.g "Set-Cookie".
+ * @param value The value for this header, e.g "text/html".
+ */
+ public synchronized void setRequestHeader(String name, String value) {
+ String[] mapValue = { name, value };
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "setRequestHeader: " + name + " => " + value);
+ }
+ if (name.equalsIgnoreCase(KEY_CONTENT_LENGTH)) {
+ setContentLength(Long.parseLong(value));
+ } else {
+ mRequestHeaders.put(name.toLowerCase(), mapValue);
+ }
+ }
+
+ /**
+ * Returns the value associated with the given request header.
+ * @param name The name of the request header, non-null, case-insensitive.
+ * @return The value associated with the request header, or null if
+ * not set, or error.
+ */
+ public synchronized String getRequestHeader(String name) {
+ String[] value = mRequestHeaders.get(name.toLowerCase());
+ if (value != null) {
+ return value[HEADERS_MAP_INDEX_VALUE];
+ } else {
+ return null;
+ }
+ }
+
+ private void applyRequestHeaders() {
+ if (mMethod == null)
+ return;
+ Iterator<String[]> it = mRequestHeaders.values().iterator();
+ while (it.hasNext()) {
+ // Set the key case-sensitive.
+ String[] entry = it.next();
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "apply header " + entry[HEADERS_MAP_INDEX_KEY] +
+ " => " + entry[HEADERS_MAP_INDEX_VALUE]);
+ }
+ mMethod.setHeader(entry[HEADERS_MAP_INDEX_KEY],
+ entry[HEADERS_MAP_INDEX_VALUE]);
+ }
+ }
+
+ /**
+ * Returns the value associated with the given response header.
+ * @param name The name of the response header, non-null, case-insensitive.
+ * @return The value associated with the response header, or null if
+ * not set or error.
+ */
+ public synchronized String getResponseHeader(String name) {
+ if (mResponseHeaders != null) {
+ String[] value = mResponseHeaders.get(name.toLowerCase());
+ if (value != null) {
+ return value[HEADERS_MAP_INDEX_VALUE];
+ } else {
+ return null;
+ }
+ } else {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "getResponseHeader() called but "
+ + "response not received");
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Return all response headers, separated by CR-LF line endings, and
+ * ending with a trailing blank line. This mimics the format of the
+ * raw response header up to but not including the body.
+ * @return A string containing the entire response header.
+ */
+ public synchronized String getAllResponseHeaders() {
+ if (mResponseHeaders == null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "getAllResponseHeaders() called but "
+ + "response not received");
+ }
+ return null;
+ }
+ StringBuilder result = new StringBuilder();
+ Iterator<String[]> it = mResponseHeaders.values().iterator();
+ while (it.hasNext()) {
+ String[] entry = it.next();
+ // Output the "key: value" lines.
+ result.append(entry[HEADERS_MAP_INDEX_KEY]);
+ result.append(": ");
+ result.append(entry[HEADERS_MAP_INDEX_VALUE]);
+ result.append(HTTP_LINE_ENDING);
+ }
+ result.append(HTTP_LINE_ENDING);
+ return result.toString();
+ }
+
+
+ /**
+ * Set a response header and associated value. The key is associated
+ * case-insensitively, but stored case-sensitively.
+ * @param name Case sensitive request header key.
+ * @param value The associated value.
+ */
+ private void setResponseHeader(String name, String value) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Set response header " + name + ": " + value);
+ }
+ String mapValue[] = { name, value };
+ mResponseHeaders.put(name.toLowerCase(), mapValue);
+ }
+
+ // Cookie handling
+
+ /**
+ * Get the cookie for the given URL.
+ * @param url The fully qualified URL.
+ * @return A string containing the cookie for the URL if it exists,
+ * or null if not.
+ */
+ public static String getCookieForUrl(String url) {
+ // Get the cookie for this URL, set as a header
+ return CookieManager.getInstance().getCookie(url);
+ }
+
+ /**
+ * Set the cookie for the given URL.
+ * @param url The fully qualified URL.
+ * @param cookie The new cookie value.
+ * @return A string containing the cookie for the URL if it exists,
+ * or null if not.
+ */
+ public static void setCookieForUrl(String url, String cookie) {
+ // Get the cookie for this URL, set as a header
+ CookieManager.getInstance().setCookie(url, cookie);
+ }
+
+ // Cache handling
+
+ /**
+ * Perform a request using LocalServer if possible. Initializes
+ * class members so that receive() will obtain data from the stream
+ * provided by the response.
+ * @param url The fully qualified URL to try in LocalServer.
+ * @return True if the url was found and is now setup to receive.
+ * False if not found, with no side-effect.
+ */
+ public synchronized boolean useLocalServerResult(String url) {
+ UrlInterceptHandlerGears handler =
+ UrlInterceptHandlerGears.getInstance();
+ if (handler == null) {
+ return false;
+ }
+ UrlInterceptHandlerGears.ServiceResponse serviceResponse =
+ handler.getServiceResponse(url, mRequestHeaders);
+ if (serviceResponse == null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "No response in LocalServer");
+ }
+ return false;
+ }
+ // LocalServer will handle this URL. Initialize stream and
+ // response.
+ mBodyInputStream = serviceResponse.getInputStream();
+ mResponseLine = serviceResponse.getStatusLine();
+ mResponseHeaders = serviceResponse.getResponseHeaders();
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Got response from LocalServer: " + mResponseLine);
+ }
+ return true;
+ }
+
+ /**
+ * Perform a request using the cache result if present. Initializes
+ * class members so that receive() will obtain data from the cache.
+ * @param url The fully qualified URL to try in the cache.
+ * @return True is the url was found and is now setup to receive
+ * from cache. False if not found, with no side-effect.
+ */
+ public synchronized boolean useCacheResult(String url) {
+ // Try the browser's cache. CacheManager wants a Map<String, String>.
+ Map<String, String> cacheRequestHeaders = new HashMap<String, String>();
+ Iterator<Map.Entry<String, String[]>> it =
+ mRequestHeaders.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, String[]> entry = it.next();
+ cacheRequestHeaders.put(
+ entry.getKey(),
+ entry.getValue()[HEADERS_MAP_INDEX_VALUE]);
+ }
+ CacheResult mCacheResult =
+ CacheManager.getCacheFile(url, cacheRequestHeaders);
+ if (mCacheResult == null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "No CacheResult for " + url);
+ }
+ return false;
+ }
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Got CacheResult from browser cache");
+ }
+ // Check for expiry. -1 is "never", otherwise milliseconds since 1970.
+ // Can be compared to System.currentTimeMillis().
+ long expires = mCacheResult.getExpires();
+ if (expires >= 0 && System.currentTimeMillis() >= expires) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "CacheResult expired "
+ + (System.currentTimeMillis() - expires)
+ + " milliseconds ago");
+ }
+ // Cache hit has expired. Do not return it.
+ return false;
+ }
+ // Setup the mBodyInputStream to come from the cache.
+ mBodyInputStream = mCacheResult.getInputStream();
+ if (mBodyInputStream == null) {
+ // Cache result may have gone away.
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "No mBodyInputStream for CacheResult " + url);
+ }
+ return false;
+ }
+ // Cache hit. Parse headers.
+ synthesizeHeadersFromCacheResult(mCacheResult);
+ return true;
+ }
+
+ /**
+ * Take the limited set of headers in a CacheResult and synthesize
+ * response headers.
+ * @param cacheResult A CacheResult to populate mResponseHeaders with.
+ */
+ private void synthesizeHeadersFromCacheResult(CacheResult cacheResult) {
+ int statusCode = cacheResult.getHttpStatusCode();
+ // The status message is informal, so we can greatly simplify it.
+ String statusMessage;
+ if (statusCode >= 200 && statusCode < 300) {
+ statusMessage = "OK";
+ } else if (statusCode >= 300 && statusCode < 400) {
+ statusMessage = "MOVED";
+ } else {
+ statusMessage = "UNAVAILABLE";
+ }
+ // Synthesize the response line.
+ mResponseLine = "HTTP/1.1 " + statusCode + " " + statusMessage;
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Synthesized " + mResponseLine);
+ }
+ // Synthesize the returned headers from cache.
+ mResponseHeaders = new HashMap<String, String[]>();
+ String contentLength = Long.toString(cacheResult.getContentLength());
+ setResponseHeader(KEY_CONTENT_LENGTH, contentLength);
+ long expires = cacheResult.getExpires();
+ if (expires >= 0) {
+ // "Expires" header is valid and finite. Milliseconds since 1970
+ // epoch, formatted as RFC-1123.
+ String expiresString = DateUtils.formatDate(new Date(expires));
+ setResponseHeader(KEY_EXPIRES, expiresString);
+ }
+ String lastModified = cacheResult.getLastModified();
+ if (lastModified != null) {
+ // Last modification time of the page. Passed end-to-end, but
+ // not used by us.
+ setResponseHeader(KEY_LAST_MODIFIED, lastModified);
+ }
+ String eTag = cacheResult.getETag();
+ if (eTag != null) {
+ // Entity tag. A kind of GUID to identify identical resources.
+ setResponseHeader(KEY_ETAG, eTag);
+ }
+ String location = cacheResult.getLocation();
+ if (location != null) {
+ // If valid, refers to the location of a redirect.
+ setResponseHeader(KEY_LOCATION, location);
+ }
+ String mimeType = cacheResult.getMimeType();
+ if (mimeType == null) {
+ // Use a safe default MIME type when none is
+ // specified. "text/plain" is safe to render in the browser
+ // window (even if large) and won't be intepreted as anything
+ // that would cause execution.
+ mimeType = DEFAULT_MIME_TYPE;
+ }
+ String encoding = cacheResult.getEncoding();
+ // Encoding may not be specified. No default.
+ String contentType = mimeType;
+ if (encoding != null) {
+ if (encoding.length() > 0) {
+ contentType += "; charset=" + encoding;
+ }
+ }
+ setResponseHeader(KEY_CONTENT_TYPE, contentType);
+ }
+
+ /**
+ * Create a CacheResult for this URL. This enables the repsonse body
+ * to be sent in calls to appendCacheResult().
+ * @param url The fully qualified URL to add to the cache.
+ * @param responseCode The response code returned for the request, e.g 200.
+ * @param mimeType The MIME type of the body, e.g "text/plain".
+ * @param encoding The encoding, e.g "utf-8". Use "" for unknown.
+ */
+ public synchronized boolean createCacheResult(
+ String url, int responseCode, String mimeType, String encoding) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Making cache entry for " + url);
+ }
+ // Take the headers and parse them into a format needed by
+ // CacheManager.
+ Headers cacheHeaders = new Headers();
+ Iterator<Map.Entry<String, String[]>> it =
+ mResponseHeaders.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, String[]> entry = it.next();
+ // Headers.parseHeader() expects lowercase keys.
+ String keyValue = entry.getKey() + ": "
+ + entry.getValue()[HEADERS_MAP_INDEX_VALUE];
+ CharArrayBuffer buffer = new CharArrayBuffer(keyValue.length());
+ buffer.append(keyValue);
+ // Parse it into the header container.
+ cacheHeaders.parseHeader(buffer);
+ }
+ mCacheResult = CacheManager.createCacheFile(
+ url, responseCode, cacheHeaders, mimeType, true);
+ if (mCacheResult != null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Saving into cache");
+ }
+ mCacheResult.setEncoding(encoding);
+ mCacheResultUrl = url;
+ return true;
+ } else {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Couldn't create mCacheResult");
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Add data from the response body to the CacheResult created with
+ * createCacheResult().
+ * @param data A byte array of the next sequential bytes in the
+ * response body.
+ * @param bytes The number of bytes to write from the start of
+ * the array.
+ * @return True if all bytes successfully written, false on failure.
+ */
+ public synchronized boolean appendCacheResult(byte[] data, int bytes) {
+ if (mCacheResult == null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "appendCacheResult() called without a "
+ + "CacheResult initialized");
+ }
+ return false;
+ }
+ try {
+ mCacheResult.getOutputStream().write(data, 0, bytes);
+ } catch (IOException ex) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Got IOException writing cache data: " + ex);
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Save the completed CacheResult into the CacheManager. This must
+ * have been created first with createCacheResult().
+ * @return Returns true if the entry has been successfully saved.
+ */
+ public synchronized boolean saveCacheResult() {
+ if (mCacheResult == null || mCacheResultUrl == null) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Tried to save cache result but "
+ + "createCacheResult not called");
+ }
+ return false;
+ }
+
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Saving cache result");
+ }
+ CacheManager.saveCacheFile(mCacheResultUrl, mCacheResult);
+ mCacheResult = null;
+ mCacheResultUrl = null;
+ return true;
+ }
+
+
+ /**
+ * Interrupt a blocking IO operation. This will cause the child
+ * thread to expediently return from an operation if it was stuck at
+ * the time. Note that this inherently races, and unfortunately
+ * requires the caller to loop.
+ */
+ public synchronized void interrupt() {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "INTERRUPT CALLED");
+ }
+ mConnectionFailedLock.lock();
+ mConnectionFailed = true;
+ mConnectionFailedLock.unlock();
+ if (mMethod != null) {
+ mMethod.abort();
+ }
+ if (mHttpThread != null) {
+ waitUntilConnectionFinished();
+ }
+ }
+
+ /**
+ * Receive the next sequential bytes of the response body after
+ * successful connection. This will receive up to the size of the
+ * provided byte array. If there is no body, this will return 0
+ * bytes on the first call after connection.
+ * @param buf A pre-allocated byte array to receive data into.
+ * @return The number of bytes from the start of the array which
+ * have been filled, 0 on EOF, or negative on error.
+ */
+ public synchronized int receive(byte[] buf) {
+ if (mBodyInputStream == null) {
+ // If this is the first call, setup the InputStream. This may
+ // fail if there were headers, but no body returned by the
+ // server.
+ try {
+ if (mResponse != null) {
+ HttpEntity entity = mResponse.getEntity();
+ mBodyInputStream = entity.getContent();
+ }
+ } catch (IOException inputException) {
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Failed to connect InputStream: "
+ + inputException);
+ }
+ // Not unexpected. For example, 404 response return headers,
+ // and sometimes a body with a detailed error.
+ }
+ if (mBodyInputStream == null) {
+ // No error stream either. Treat as a 0 byte response.
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "No InputStream");
+ }
+ return 0; // EOF.
+ }
+ }
+ int ret;
+ try {
+ int got = mBodyInputStream.read(buf);
+ if (got > 0) {
+ // Got some bytes, not EOF.
+ ret = got;
+ } else {
+ // EOF.
+ mBodyInputStream.close();
+ ret = 0;
+ }
+ } catch (IOException e) {
+ // An abort() interrupts us by calling close() on our stream.
+ if (Config.LOGV) {
+ Log.i(LOG_TAG, "Got IOException in mBodyInputStream.read(): ", e);
+ }
+ ret = -1;
+ }
+ return ret;
+ }
+
+ /**
+ * For POST method requests, send a stream of data provided by the
+ * native side in repeated callbacks.
+ * We put the data in mBuffer, and wait until it is consumed
+ * by the StreamEntity in the request thread.
+ * @param data A byte array containing the data to sent, or null
+ * if indicating EOF.
+ * @param bytes The number of bytes from the start of the array to
+ * send, or 0 if indicating EOF.
+ * @return True if all bytes were successfully sent, false on error.
+ */
+ public boolean sendPostData(byte[] data, int bytes) {
+ mConnectionFailedLock.lock();
+ if (mConnectionFailed) {
+ mConnectionFailedLock.unlock();
+ return false;
+ }
+ mConnectionFailedLock.unlock();
+ if (mPostEntity == null) return false;
+
+ // We block until the outputstream is available
+ // (or in case of connection error)
+ if (!mPostEntity.isReady()) return false;
+
+ if (data == null && bytes == 0) {
+ mBuffer.put(null);
+ } else {
+ mBuffer.put(new DataPacket(data, bytes));
+ }
+ mSignal.waitUntilPacketConsumed();
+
+ mConnectionFailedLock.lock();
+ if (mConnectionFailed) {
+ Log.e(LOG_TAG, "failure");
+ mConnectionFailedLock.unlock();
+ return false;
+ }
+ mConnectionFailedLock.unlock();
+ return true;
+ }
+
+}
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index c22023c..19ec77d 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -696,7 +696,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
setWillNotDraw(false);
setAlwaysDrawnWithCacheEnabled(false);
setScrollingCacheEnabled(true);
- setScrollContainer(true);
}
private void useDefaultSelector() {
diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java
index 1591791..024b663 100644
--- a/core/java/android/widget/AutoCompleteTextView.java
+++ b/core/java/android/widget/AutoCompleteTextView.java
@@ -514,8 +514,48 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
protected CharSequence convertSelectionToString(Object selectedItem) {
return mFilter.convertResultToString(selectedItem);
}
+
+ /**
+ * <p>Clear the list selection. This may only be temporary, as user input will often bring
+ * it back.
+ */
+ public void clearListSelection() {
+ if (mDropDownList != null) {
+ mDropDownList.hideSelector();
+ mDropDownList.requestLayout();
+ }
+ }
+
+ /**
+ * Set the position of the dropdown view selection.
+ *
+ * @param position The position to move the selector to.
+ */
+ public void setListSelection(int position) {
+ if (mPopup.isShowing() && (mDropDownList != null)) {
+ mDropDownList.setSelection(position);
+ // ListView.setSelection() will call requestLayout()
+ }
+ }
/**
+ * Get the position of the dropdown view selection, if there is one. Returns
+ * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if there is no dropdown or if
+ * there is no selection.
+ *
+ * @return the position of the current selection, if there is one, or
+ * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if not.
+ *
+ * @see ListView#getSelectedItemPosition()
+ */
+ public int getListSelection() {
+ if (mPopup.isShowing() && (mDropDownList != null)) {
+ return mDropDownList.getSelectedItemPosition();
+ }
+ return ListView.INVALID_POSITION;
+ }
+
+ /**
* <p>Starts filtering the content of the drop down list. The filtering
* pattern is the content of the edit box. Subclasses should override this
* method to filter with a different pattern, for instance a substring of
diff --git a/core/java/android/widget/CursorAdapter.java b/core/java/android/widget/CursorAdapter.java
index 555f216..3d758e7 100644
--- a/core/java/android/widget/CursorAdapter.java
+++ b/core/java/android/widget/CursorAdapter.java
@@ -242,6 +242,9 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable,
* @param cursor the new cursor to be used
*/
public void changeCursor(Cursor cursor) {
+ if (cursor == mCursor) {
+ return;
+ }
if (mCursor != null) {
mCursor.unregisterContentObserver(mChangeObserver);
mCursor.unregisterDataSetObserver(mDataSetObserver);
diff --git a/core/java/android/widget/Filter.java b/core/java/android/widget/Filter.java
index 49888f7..7f1601e 100644
--- a/core/java/android/widget/Filter.java
+++ b/core/java/android/widget/Filter.java
@@ -42,6 +42,8 @@ public abstract class Filter {
private Handler mThreadHandler;
private Handler mResultHandler;
+ private String mConstraint;
+ private boolean mConstraintIsValid = false;
/**
* <p>Creates a new asynchronous filter.</p>
@@ -79,6 +81,14 @@ public abstract class Filter {
*/
public final void filter(CharSequence constraint, FilterListener listener) {
synchronized (this) {
+ String constraintAsString = constraint != null ? constraint.toString() : null;
+ if (mConstraintIsValid && (
+ (constraintAsString == null && mConstraint == null) ||
+ (constraintAsString != null && constraintAsString.equals(mConstraint)))) {
+ // nothing to do
+ return;
+ }
+
if (mThreadHandler == null) {
HandlerThread thread = new HandlerThread(THREAD_NAME);
thread.start();
@@ -88,13 +98,18 @@ public abstract class Filter {
Message message = mThreadHandler.obtainMessage(FILTER_TOKEN);
RequestArguments args = new RequestArguments();
- args.constraint = constraint;
+ // make sure we use an immutable copy of the constraint, so that
+ // it doesn't change while the filter operation is in progress
+ args.constraint = constraintAsString;
args.listener = listener;
message.obj = args;
mThreadHandler.removeMessages(FILTER_TOKEN);
mThreadHandler.removeMessages(FINISH_TOKEN);
mThreadHandler.sendMessage(message);
+
+ mConstraint = constraintAsString;
+ mConstraintIsValid = true;
}
}
diff --git a/core/java/android/widget/Gallery.java b/core/java/android/widget/Gallery.java
index cabca40..7b9735c 100644
--- a/core/java/android/widget/Gallery.java
+++ b/core/java/android/widget/Gallery.java
@@ -45,6 +45,9 @@ import android.widget.Scroller;
* {@link android.R.styleable#Theme_galleryItemBackground} as the background for
* each View given to the Gallery from the Adapter. If you are not doing this,
* you may need to adjust some Gallery properties, such as the spacing.
+ * <p>
+ * Views given to the Gallery should use {@link Gallery.LayoutParams} as their
+ * layout parameters type.
*
* @attr ref android.R.styleable#Gallery_animationDuration
* @attr ref android.R.styleable#Gallery_spacing
diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java
index b5c4384..50248c1 100644
--- a/core/java/android/widget/PopupWindow.java
+++ b/core/java/android/widget/PopupWindow.java
@@ -85,8 +85,10 @@ public class PopupWindow {
private int mWidthMode;
private int mWidth;
+ private int mLastWidth;
private int mHeightMode;
private int mHeight;
+ private int mLastHeight;
private int mPopupWidth;
private int mPopupHeight;
@@ -634,8 +636,8 @@ public class PopupWindow {
mPopupView.refreshDrawableState();
}
mAboveAnchor = findDropDownPosition(anchor, p, xoff, yoff);
- if (mHeightMode < 0) p.height = mHeightMode;
- if (mWidthMode < 0) p.width = mWidthMode;
+ if (mHeightMode < 0) p.height = mLastHeight = mHeightMode;
+ if (mWidthMode < 0) p.width = mLastWidth = mWidthMode;
p.windowAnimations = computeAnimationResource();
invokePopup(p);
}
@@ -708,8 +710,8 @@ public class PopupWindow {
// by setting the x and y offsets to match the anchor's bottom
// left corner
p.gravity = Gravity.LEFT | Gravity.TOP;
- p.width = mWidth;
- p.height = mHeight;
+ p.width = mLastWidth = mWidth;
+ p.height = mLastHeight = mHeight;
if (mBackground != null) {
p.format = mBackground.getOpacity();
} else {
@@ -953,15 +955,15 @@ public class PopupWindow {
boolean update = false;
- final int finalWidth = mWidthMode < 0 ? mWidthMode : p.width;
+ final int finalWidth = mWidthMode < 0 ? mWidthMode : mLastWidth;
if (width != -1 && p.width != finalWidth) {
- p.width = finalWidth;
+ p.width = mLastWidth = finalWidth;
update = true;
}
- final int finalHeight = mHeightMode < 0 ? mHeightMode : p.height;
+ final int finalHeight = mHeightMode < 0 ? mHeightMode : mLastHeight;
if (height != -1 && p.height != finalHeight) {
- p.height = finalHeight;
+ p.height = mLastHeight = finalHeight;
update = true;
}
diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java
index a2133b2..20166cf 100644
--- a/core/java/android/widget/ScrollView.java
+++ b/core/java/android/widget/ScrollView.java
@@ -125,18 +125,7 @@ public class ScrollView extends FrameLayout {
private boolean mSmoothScrollingEnabled = true;
public ScrollView(Context context) {
- super(context);
- initScrollView();
-
- setVerticalScrollBarEnabled(true);
- setVerticalFadingEdgeEnabled(true);
-
- TypedArray a = context.obtainStyledAttributes(R.styleable.View);
-
- initializeScrollbars(a);
- initializeFadingEdge(a);
-
- a.recycle();
+ this(context, null);
}
public ScrollView(Context context, AttributeSet attrs) {
@@ -199,7 +188,6 @@ public class ScrollView extends FrameLayout {
setFocusable(true);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setWillNotDraw(false);
- setScrollContainer(true);
}
@Override
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 73c2b3e..8baed7d 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -714,10 +714,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
bufferType = BufferType.EDITABLE;
break;
}
- mInputType = EditorInfo.TYPE_CLASS_TEXT;
}
- if (password) {
+ if (password && (mInputType&EditorInfo.TYPE_MASK_CLASS)
+ == EditorInfo.TYPE_CLASS_TEXT) {
mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
| EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
}
@@ -3772,8 +3772,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return super.onKeyUp(keyCode, event);
}
+ @Override public boolean onCheckIsTextEditor() {
+ return mInputType != EditorInfo.TYPE_NULL;
+ }
+
@Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
- if (mInputType != EditorInfo.TYPE_NULL) {
+ if (onCheckIsTextEditor()) {
if (mInputMethodState == null) {
mInputMethodState = new InputMethodState();
}
@@ -5334,12 +5338,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
mLayout != null) {
boolean moved = mMovement.onTouchEvent(this, (Spannable) mText, event);
- if (mText instanceof Editable
- && mInputType != EditorInfo.TYPE_NULL) {
+ if (mText instanceof Editable && onCheckIsTextEditor()) {
if (event.getAction() == MotionEvent.ACTION_UP && isFocused()) {
InputMethodManager imm = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.showSoftInput(this);
+ imm.showSoftInput(this, 0);
}
}
diff --git a/core/java/com/android/internal/app/AlertController.java b/core/java/com/android/internal/app/AlertController.java
index 989f972..5c44b2d 100644
--- a/core/java/com/android/internal/app/AlertController.java
+++ b/core/java/com/android/internal/app/AlertController.java
@@ -27,7 +27,6 @@ import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
-import android.text.TextUtils.TruncateAt;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -172,11 +171,33 @@ public class AlertController {
mHandler = new ButtonHandler(di);
}
+ static boolean canTextInput(View v) {
+ if (v.onCheckIsTextEditor()) {
+ return true;
+ }
+
+ if (!(v instanceof ViewGroup)) {
+ return false;
+ }
+
+ ViewGroup vg = (ViewGroup)v;
+ int i = vg.getChildCount();
+ while (i > 0) {
+ i--;
+ v = vg.getChildAt(i);
+ if (canTextInput(v)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
public void installContent() {
/* We use a custom title so never request a window title */
mWindow.requestFeature(Window.FEATURE_NO_TITLE);
- if (mView == null) {
+ if (mView == null || !canTextInput(mView)) {
mWindow.setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
}
diff --git a/core/java/com/android/internal/os/HandlerCaller.java b/core/java/com/android/internal/os/HandlerCaller.java
index e88a36f..a7081e3 100644
--- a/core/java/com/android/internal/os/HandlerCaller.java
+++ b/core/java/com/android/internal/os/HandlerCaller.java
@@ -115,11 +115,15 @@ public class HandlerCaller {
return mH.obtainMessage(what, 0, 0, arg1);
}
+ public Message obtainMessageI(int what, int arg1) {
+ return mH.obtainMessage(what, arg1);
+ }
+
public Message obtainMessageIO(int what, int arg1, Object arg2) {
return mH.obtainMessage(what, arg1, 0, arg2);
}
- public Message obtainMessageIO(int what, int arg1, int arg2, Object arg3) {
+ public Message obtainMessageIIO(int what, int arg1, int arg2, Object arg3) {
return mH.obtainMessage(what, arg1, arg2, arg3);
}
diff --git a/core/java/com/android/internal/view/IInputMethod.aidl b/core/java/com/android/internal/view/IInputMethod.aidl
index 87bf473..f650713 100644
--- a/core/java/com/android/internal/view/IInputMethod.aidl
+++ b/core/java/com/android/internal/view/IInputMethod.aidl
@@ -48,7 +48,7 @@ oneway interface IInputMethod {
void revokeSession(IInputMethodSession session);
- void showSoftInput();
+ void showSoftInput(boolean explicit);
void hideSoftInput();
}
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index 1c9e797..2a15bdb 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -37,15 +37,15 @@ interface IInputMethodManager {
InputBindResult startInput(in IInputMethodClient client,
in EditorInfo attribute, boolean initial, boolean needResult);
void finishInput(in IInputMethodClient client);
- void showSoftInput(in IInputMethodClient client);
- void hideSoftInput(in IInputMethodClient client);
+ void showSoftInput(in IInputMethodClient client, int flags);
+ void hideSoftInput(in IInputMethodClient client, int flags);
void windowGainedFocus(in IInputMethodClient client,
- boolean viewHasFocus, int softInputMode, boolean first,
- int windowFlags);
+ boolean viewHasFocus, boolean isTextEditor,
+ int softInputMode, boolean first, int windowFlags);
void showInputMethodPickerFromClient(in IInputMethodClient client);
void setInputMethod(in IBinder token, String id);
- void hideMySoftInput(in IBinder token);
+ void hideMySoftInput(in IBinder token, int flags);
void updateStatusIcon(int iconId, String iconPackage);
boolean setInputMethodEnabled(String id, boolean enabled);
diff --git a/core/java/com/google/android/gdata/client/AndroidGDataClient.java b/core/java/com/google/android/gdata/client/AndroidGDataClient.java
index 17f86d6..1d8e9c5 100644
--- a/core/java/com/google/android/gdata/client/AndroidGDataClient.java
+++ b/core/java/com/google/android/gdata/client/AndroidGDataClient.java
@@ -30,6 +30,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
+import java.io.BufferedInputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
@@ -124,6 +125,7 @@ public class AndroidGDataClient implements GDataClient {
public AndroidGDataClient(ContentResolver resolver) {
mHttpClient = new GoogleHttpClient(resolver, USER_AGENT_APP_VERSION,
true /* gzip capable */);
+ mHttpClient.enableCurlLogging(TAG, Log.VERBOSE);
mResolver = resolver;
}
@@ -213,7 +215,7 @@ public class AndroidGDataClient implements GDataClient {
Log.w(TAG, "StatusLine is null.");
throw new NullPointerException("StatusLine is null -- should not happen.");
}
-
+
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, response.getStatusLine().toString());
for (Header h : response.getAllHeaders()) {
@@ -225,7 +227,11 @@ public class AndroidGDataClient implements GDataClient {
HttpEntity entity = response.getEntity();
if ((status >= 200) && (status < 300) && entity != null) {
- return AndroidHttpClient.getUngzippedContent(entity);
+ InputStream in = AndroidHttpClient.getUngzippedContent(entity);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ in = logInputStreamContents(in);
+ }
+ return in;
}
// TODO: handle 301, 307?
@@ -308,6 +314,43 @@ public class AndroidGDataClient implements GDataClient {
throw new IOException("Unable to access feed.");
}
+ /**
+ * Log the contents of the input stream.
+ * The original input stream is consumed, so the caller must use the
+ * BufferedInputStream that is returned.
+ * @param in InputStream
+ * @return replacement input stream for caller to use
+ * @throws IOException
+ */
+ private InputStream logInputStreamContents(InputStream in) throws IOException {
+ if (in == null) {
+ return in;
+ }
+ // bufferSize is the (arbitrary) maximum amount to log.
+ // The original InputStream is wrapped in a
+ // BufferedInputStream with a 16K buffer. This lets
+ // us read up to 16K, write it to the log, and then
+ // reset the stream so the the original client can
+ // then read the data. The BufferedInputStream
+ // provides the mark and reset support, even when
+ // the original InputStream does not.
+ final int bufferSize = 16384;
+ BufferedInputStream bin = new BufferedInputStream(in, bufferSize);
+ bin.mark(bufferSize);
+ int wanted = bufferSize;
+ int totalReceived = 0;
+ byte buf[] = new byte[wanted];
+ while (wanted > 0) {
+ int got = bin.read(buf, totalReceived, wanted);
+ if (got <= 0) break; // EOF
+ wanted -= got;
+ totalReceived += got;
+ }
+ Log.d(TAG, new String(buf, 0, totalReceived, "UTF-8"));
+ bin.reset();
+ return bin;
+ }
+
public InputStream getMediaEntryAsStream(String mediaEntryUrl, String authToken)
throws HttpException, IOException {