diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-01-15 16:12:10 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-01-15 16:12:10 -0800 |
commit | 9266c558bf1d21ff647525ff99f7dadbca417309 (patch) | |
tree | 1630b1ba80f4793caf39d865528e662bdb1037fe /core/java/android | |
parent | b798689749c64baba81f02e10cf2157c747d6b46 (diff) | |
download | frameworks_base-9266c558bf1d21ff647525ff99f7dadbca417309.zip frameworks_base-9266c558bf1d21ff647525ff99f7dadbca417309.tar.gz frameworks_base-9266c558bf1d21ff647525ff99f7dadbca417309.tar.bz2 |
auto import from //branches/cupcake/...@126645
Diffstat (limited to 'core/java/android')
42 files changed, 2797 insertions, 713 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; "MMMM dd, yyyy h:mmaa" -> "April 6, 1970 3:23am"<br/> "E, MMMM dd, yyyy h:mmaa" -> "Mon, April 6, 1970 3:23am&<br/> "EEEE, MMMM dd, yyyy h:mmaa" -> "Monday, April 6, 1970 3:23am"<br/> - "'Best day evar: 'M/d/yy" -> "Best day evar: 4/6/70" + "'Noteworthy day: 'M/d/yy" -> "Noteworthy day: 4/6/70" */ 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); } } |