diff options
Diffstat (limited to 'core/java')
121 files changed, 12537 insertions, 5566 deletions
diff --git a/core/java/android/app/ActionBar.java b/core/java/android/app/ActionBar.java new file mode 100644 index 0000000..77633c6 --- /dev/null +++ b/core/java/android/app/ActionBar.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.graphics.drawable.Drawable; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +/** + * This is the public interface to the contextual ActionBar. + * The ActionBar acts as a replacement for the title bar in Activities. + * It provides facilities for creating toolbar actions as well as + * methods of navigating around an application. + */ +public abstract class ActionBar { + /** + * Normal/standard navigation mode. Consists of either a logo or icon + * and title text with an optional subtitle. Clicking any of these elements + * will dispatch onActionItemSelected to the registered Callback with + * a MenuItem with item ID android.R.id.home. + */ + public static final int NAVIGATION_MODE_NORMAL = 0; + + /** + * Dropdown list navigation mode. Instead of static title text this mode + * presents a dropdown menu for navigation within the activity. + */ + public static final int NAVIGATION_MODE_DROPDOWN_LIST = 1; + + /** + * Tab navigation mode. Instead of static title text this mode + * presents a series of tabs for navigation within the activity. + */ + public static final int NAVIGATION_MODE_TABS = 2; + + /** + * Custom navigation mode. This navigation mode is set implicitly whenever + * a custom navigation view is set. See {@link #setCustomNavigationView(View)}. + */ + public static final int NAVIGATION_MODE_CUSTOM = 3; + + /** + * Use logo instead of icon if available. This flag will cause appropriate + * navigation modes to use a wider logo in place of the standard icon. + */ + public static final int DISPLAY_USE_LOGO = 0x1; + + /** + * Hide 'home' elements in this action bar, leaving more space for other + * navigation elements. This includes logo and icon. + */ + public static final int DISPLAY_HIDE_HOME = 0x2; + + /** + * Set the callback that the ActionBar will use to handle events + * and populate menus. + * @param callback Callback to use + */ + public abstract void setCallback(Callback callback); + + /** + * Set a custom navigation view. + * + * Custom navigation views appear between the application icon and + * any action buttons and may use any space available there. Common + * use cases for custom navigation views might include an address bar + * for a browser or other navigation mechanisms that do not translate + * well to provided navigation modes. + * + * Setting a non-null custom navigation view will also set the + * navigation mode to NAVMODE_CUSTOM. + * + * @param view Custom navigation view to place in the ActionBar. + */ + public abstract void setCustomNavigationView(View view); + + /** + * Set the ActionBar's title. + * + * This is set automatically to the name of your Activity, + * but may be changed here. + * + * @param title Title text + */ + public abstract void setTitle(CharSequence title); + + /** + * Set the ActionBar's subtitle. + * + * The subtitle is usually displayed as a second line of text + * under the title. Good for extended descriptions of activity state. + * + * @param subtitle Subtitle text. + */ + public abstract void setSubtitle(CharSequence subtitle); + + /** + * Set the navigation mode. + * + * @param mode One of {@link #NAVIGATION_MODE_NORMAL}, {@link #NAVIGATION_MODE_DROPDOWN_LIST}, + * {@link #NAVIGATION_MODE_TABS}, or {@link #NAVIGATION_MODE_CUSTOM}. + */ + public abstract void setNavigationMode(int mode); + + /** + * Set display options. This changes all display option bits at once. To change + * a limited subset of display options, see {@link #setDisplayOptions(int, int)}. + * + * @param options A combination of the bits defined by the DISPLAY_ constants + * defined in ActionBar. + */ + public abstract void setDisplayOptions(int options); + + /** + * Set selected display options. Only the options specified by mask will be changed. + * To change all display option bits at once, see {@link #setDisplayOptions(int)}. + * + * <p>Example: setDisplayOptions(0, DISPLAY_HIDE_HOME) will disable the + * {@link #DISPLAY_HIDE_HOME} option. + * setDisplayOptions(DISPLAY_HIDE_HOME, DISPLAY_HIDE_HOME | DISPLAY_USE_LOGO) + * will enable {@link #DISPLAY_HIDE_HOME} and disable {@link #DISPLAY_USE_LOGO}. + * + * @param options A combination of the bits defined by the DISPLAY_ constants + * defined in ActionBar. + * @param mask A bit mask declaring which display options should be changed. + */ + public abstract void setDisplayOptions(int options, int mask); + + /** + * Set the ActionBar's background. + * + * @param d Background drawable + */ + public abstract void setBackgroundDrawable(Drawable d); + + /** + * Set a drawable to use as a divider between sections of the ActionBar. + * + * @param d Divider drawable + */ + public abstract void setDividerDrawable(Drawable d); + + /** + * @return The current custom navigation view. + */ + public abstract View getCustomNavigationView(); + + /** + * @return The current ActionBar title. + */ + public abstract CharSequence getTitle(); + + /** + * @return The current ActionBar subtitle. + */ + public abstract CharSequence getSubtitle(); + + /** + * @return The current navigation mode. + */ + public abstract int getNavigationMode(); + + /** + * @return The current set of display options. + */ + public abstract int getDisplayOptions(); + + /** + * Request an update of the items in the action menu. + * This will result in a call to Callback.onUpdateActionMenu(Menu) + * and the ActionBar will update based on any changes made there. + */ + public abstract void updateActionMenu(); + + /** + * Callback interface for ActionBar events. + */ + public interface Callback { + /** + * Initialize the always-visible contents of the action bar. + * You should place your menu items into <var>menu</var>. + * + * <p>This is only called once, the first time the action bar is displayed. + * + * @param menu The action menu in which to place your items. + * @return You must return true for actions to be displayed; + * if you return false they will not be shown. + * + * @see #onActionItemSelected(MenuItem) + */ + public boolean onCreateActionMenu(Menu menu); + + /** + * Update the action bar. This is called in response to {@link #updateActionMenu()} + * calls, which may be application-initiated or the result of changing fragment state. + * + * @return true if the action bar should update based on altered menu contents, + * false if no changes are necessary. + */ + public boolean onUpdateActionMenu(Menu menu); + + /** + * This hook is called whenever an item in your action bar is selected. + * The default implementation simply returns false to have the normal + * processing happen (sending a message to its handler). You can use this + * method for any items for which you would like to do processing without + * those other facilities. + * + * @param item The action bar item that was selected. + * @return boolean Return false to allow normal menu processing to proceed, + * true to consume it here. + */ + public boolean onActionItemSelected(MenuItem item); + + /* + * In progress + */ + public boolean onCreateContextMode(int modeId, Menu menu); + public boolean onPrepareContextMode(int modeId, Menu menu); + public boolean onContextItemSelected(int modeId, MenuItem item); + } + + /** + * Simple stub implementations of ActionBar.Callback methods. + * Extend this if you only need a subset of Callback functionality. + */ + public static class SimpleCallback implements Callback { + public boolean onCreateActionMenu(Menu menu) { + return false; + } + + public boolean onUpdateActionMenu(Menu menu) { + return false; + } + + public boolean onActionItemSelected(MenuItem item) { + return false; + } + + public boolean onCreateContextMode(int modeId, Menu menu) { + return false; + } + + public boolean onPrepareContextMode(int modeId, Menu menu) { + return false; + } + + public boolean onContextItemSelected(int modeId, MenuItem item) { + return false; + } + } + +} diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index a962391..090f664 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -16,19 +16,21 @@ package android.app; -import com.android.internal.policy.PolicyManager; +import java.util.ArrayList; +import java.util.HashMap; import android.content.ComponentCallbacks; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; import android.content.IIntentSender; +import android.content.Intent; import android.content.IntentSender; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; +import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -39,7 +41,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; -import android.os.Looper; +import android.os.Parcelable; import android.os.RemoteException; import android.text.Selection; import android.text.SpannableStringBuilder; @@ -50,8 +52,10 @@ import android.util.Config; import android.util.EventLog; import android.util.Log; import android.util.SparseArray; +import android.view.ActionBarView; import android.view.ContextMenu; import android.view.ContextThemeWrapper; +import android.view.InflateException; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; @@ -68,6 +72,10 @@ import android.view.View.OnCreateContextMenuListener; import android.view.ViewGroup.LayoutParams; import android.view.accessibility.AccessibilityEvent; import android.widget.AdapterView; +import android.widget.LinearLayout; + +import com.android.internal.app.SplitActionBar; +import com.android.internal.policy.PolicyManager; import java.util.ArrayList; import java.util.HashMap; @@ -606,6 +614,7 @@ public class Activity extends ContextThemeWrapper private static long sInstanceCount = 0; private static final String WINDOW_HIERARCHY_TAG = "android:viewHierarchyState"; + private static final String FRAGMENTS_TAG = "android:fragments"; private static final String SAVED_DIALOG_IDS_KEY = "android:savedDialogIds"; private static final String SAVED_DIALOGS_TAG = "android:savedDialogs"; private static final String SAVED_DIALOG_KEY_PREFIX = "android:dialog_"; @@ -627,18 +636,25 @@ public class Activity extends ContextThemeWrapper private ComponentName mComponent; /*package*/ ActivityInfo mActivityInfo; /*package*/ ActivityThread mMainThread; - /*package*/ Object mLastNonConfigurationInstance; - /*package*/ HashMap<String,Object> mLastNonConfigurationChildInstances; Activity mParent; boolean mCalled; private boolean mResumed; private boolean mStopped; boolean mFinished; boolean mStartedActivity; + /** true if the activity is being destroyed in order to recreate it with a new configuration */ + /*package*/ boolean mChangingConfigurations = false; /*package*/ int mConfigChangeFlags; /*package*/ Configuration mCurrentConfig; private SearchManager mSearchManager; + static final class NonConfigurationInstances { + Object activity; + HashMap<String, Object> children; + ArrayList<Fragment> fragments; + } + /* package */ NonConfigurationInstances mLastNonConfigurationInstances; + private Window mWindow; private WindowManager mWindowManager; @@ -646,10 +662,13 @@ public class Activity extends ContextThemeWrapper /*package*/ boolean mWindowAdded = false; /*package*/ boolean mVisibleFromServer = false; /*package*/ boolean mVisibleFromClient = true; + /*package*/ ActionBar mActionBar = null; private CharSequence mTitle; private int mTitleColor = 0; + final FragmentManager mFragments = new FragmentManager(); + private static final class ManagedCursor { ManagedCursor(Cursor cursor) { mCursor = cursor; @@ -676,7 +695,7 @@ public class Activity extends ContextThemeWrapper protected static final int[] FOCUSED_STATE_SET = {com.android.internal.R.attr.state_focused}; private Thread mUiThread; - private final Handler mHandler = new Handler(); + final Handler mHandler = new Handler(); // Used for debug only /* @@ -800,6 +819,12 @@ public class Activity extends ContextThemeWrapper protected void onCreate(Bundle savedInstanceState) { mVisibleFromClient = !mWindow.getWindowStyle().getBoolean( com.android.internal.R.styleable.Window_windowNoDisplay, false); + if (savedInstanceState != null) { + Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG); + mFragments.restoreAllState(p, mLastNonConfigurationInstances != null + ? mLastNonConfigurationInstances.fragments : null); + } + mFragments.dispatchCreate(); mCalled = true; } @@ -1084,6 +1109,10 @@ public class Activity extends ContextThemeWrapper */ protected void onSaveInstanceState(Bundle outState) { outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState()); + Parcelable p = mFragments.saveAllState(); + if (p != null) { + outState.putParcelable(FRAGMENTS_TAG, p); + } } /** @@ -1388,7 +1417,8 @@ public class Activity extends ContextThemeWrapper * {@link #onRetainNonConfigurationInstance()}. */ public Object getLastNonConfigurationInstance() { - return mLastNonConfigurationInstance; + return mLastNonConfigurationInstances != null + ? mLastNonConfigurationInstances.activity : null; } /** @@ -1444,8 +1474,9 @@ public class Activity extends ContextThemeWrapper * @return Returns the object previously returned by * {@link #onRetainNonConfigurationChildInstances()} */ - HashMap<String,Object> getLastNonConfigurationChildInstances() { - return mLastNonConfigurationChildInstances; + HashMap<String, Object> getLastNonConfigurationChildInstances() { + return mLastNonConfigurationInstances != null + ? mLastNonConfigurationInstances.children : null; } /** @@ -1459,11 +1490,34 @@ public class Activity extends ContextThemeWrapper return null; } + NonConfigurationInstances retainNonConfigurationInstances() { + Object activity = onRetainNonConfigurationInstance(); + HashMap<String, Object> children = onRetainNonConfigurationChildInstances(); + ArrayList<Fragment> fragments = mFragments.retainNonConfig(); + if (activity == null && children == null && fragments == null) { + return null; + } + + NonConfigurationInstances nci = new NonConfigurationInstances(); + nci.activity = activity; + nci.children = children; + nci.fragments = fragments; + return nci; + } + public void onLowMemory() { mCalled = true; } /** + * Start a series of edit operations on the Fragments associated with + * this activity. + */ + public FragmentTransaction openFragmentTransaction() { + return new BackStackEntry(mFragments); + } + + /** * Wrapper around * {@link ContentResolver#query(android.net.Uri , String[], String, String[], String)} * that gives the resulting {@link Cursor} to call @@ -1525,40 +1579,6 @@ public class Activity extends ContextThemeWrapper } /** - * Wrapper around {@link Cursor#commitUpdates()} that takes care of noting - * that the Cursor needs to be requeried. You can call this method in - * {@link #onPause} or {@link #onStop} to have the system call - * {@link Cursor#requery} for you if the activity is later resumed. This - * allows you to avoid determing when to do the requery yourself (which is - * required for the Cursor to see any data changes that were committed with - * it). - * - * @param c The Cursor whose changes are to be committed. - * - * @see #managedQuery(android.net.Uri , String[], String, String[], String) - * @see #startManagingCursor - * @see Cursor#commitUpdates() - * @see Cursor#requery - * @hide - */ - @Deprecated - public void managedCommitUpdates(Cursor c) { - synchronized (mManagedCursors) { - final int N = mManagedCursors.size(); - for (int i=0; i<N; i++) { - ManagedCursor mc = mManagedCursors.get(i); - if (mc.mCursor == c) { - c.commitUpdates(); - mc.mUpdated = true; - return; - } - } - throw new RuntimeException( - "Cursor " + c + " is not currently managed"); - } - } - - /** * This method allows the activity to take care of managing the given * {@link Cursor}'s lifecycle for you based on the activity's lifecycle. * That is, when the activity is stopped it will automatically call @@ -1636,7 +1656,60 @@ public class Activity extends ContextThemeWrapper public View findViewById(int id) { return getWindow().findViewById(id); } - + + /** + * Retrieve a reference to this activity's ActionBar. + * + * <p><em>Note:</em> The ActionBar is initialized when a content view + * is set. This function will return null if called before {@link #setContentView} + * or {@link #addContentView}. + * @return The Activity's ActionBar, or null if it does not have one. + */ + public ActionBar getActionBar() { + return mActionBar; + } + + /** + * Creates a new ActionBar, locates the inflated ActionBarView, + * initializes the ActionBar with the view, and sets mActionBar. + */ + private void initActionBar() { + if (!getWindow().hasFeature(Window.FEATURE_ACTION_BAR)) { + return; + } + + ActionBarView view = (ActionBarView) findViewById(com.android.internal.R.id.action_bar); + if (view != null) { + LinearLayout splitView = + (LinearLayout) findViewById(com.android.internal.R.id.context_action_bar); + if (splitView != null) { + mActionBar = new SplitActionBar(view, splitView); + } + } else { + Log.e(TAG, "Could not create action bar; view not found in window decor."); + } + } + + /** + * Finds a fragment that was identified by the given id either when inflated + * from XML or as the container ID when added in a transaction. This only + * returns fragments that are currently added to the activity's content. + * @return The fragment if found or null otherwise. + */ + public Fragment findFragmentById(int id) { + return mFragments.findFragmentById(id); + } + + /** + * Finds a fragment that was identified by the given tag either when inflated + * from XML or as supplied when added in a transaction. This only + * returns fragments that are currently added to the activity's content. + * @return The fragment if found or null otherwise. + */ + public Fragment findFragmentByTag(String tag) { + return mFragments.findFragmentByTag(tag); + } + /** * Set the activity content from a layout resource. The resource will be * inflated, adding all top-level views to the activity. @@ -1645,6 +1718,7 @@ public class Activity extends ContextThemeWrapper */ public void setContentView(int layoutResID) { getWindow().setContentView(layoutResID); + initActionBar(); } /** @@ -1656,6 +1730,7 @@ public class Activity extends ContextThemeWrapper */ public void setContentView(View view) { getWindow().setContentView(view); + initActionBar(); } /** @@ -1668,6 +1743,7 @@ public class Activity extends ContextThemeWrapper */ public void setContentView(View view, ViewGroup.LayoutParams params) { getWindow().setContentView(view, params); + initActionBar(); } /** @@ -1679,6 +1755,7 @@ public class Activity extends ContextThemeWrapper */ public void addContentView(View view, ViewGroup.LayoutParams params) { getWindow().addContentView(view, params); + initActionBar(); } /** @@ -1902,12 +1979,25 @@ public class Activity extends ContextThemeWrapper } /** + * Pop the last fragment transition from the local activity's fragment + * back stack. If there is nothing to pop, false is returned. + * @param name If non-null, this is the name of a previous back state + * to look for; if found, all states up to (but not including) that + * state will be popped. If null, only the top state is popped. + */ + public boolean popBackStack(String name) { + return mFragments.popBackStackState(mHandler, name); + } + + /** * Called when the activity has detected the user's press of the back * key. The default implementation simply finishes the current activity, * but you can override this to do whatever you want. */ public void onBackPressed() { - finish(); + if (!popBackStack(null)) { + finish(); + } } /** @@ -1977,6 +2067,11 @@ public class Activity extends ContextThemeWrapper } public void onContentChanged() { + // First time content is available, let the fragment manager + // attach all of the fragments to it. + if (mFragments.mCurState < Fragment.CONTENT) { + mFragments.moveToState(Fragment.CONTENT, false); + } } /** @@ -3066,6 +3161,36 @@ public class Activity extends ContextThemeWrapper } /** + * This is called when a Fragment in this activity calls its + * {@link Fragment#startActivity} or {@link Fragment#startActivityForResult} + * method. + * + * <p>This method throws {@link android.content.ActivityNotFoundException} + * if there was no Activity found to run the given Intent. + * + * @param fragment The fragment making the call. + * @param intent The intent to start. + * @param requestCode Reply request code. < 0 if reply is not requested. + * + * @throws android.content.ActivityNotFoundException + * + * @see Fragment#startActivity + * @see Fragment#startActivityForResult + */ + public void startActivityFromFragment(Fragment fragment, Intent intent, + int requestCode) { + Instrumentation.ActivityResult ar = + mInstrumentation.execStartActivity( + this, mMainThread.getApplicationThread(), mToken, fragment, + intent, requestCode); + if (ar != null) { + mMainThread.sendActivityResult( + mToken, fragment.mWho, requestCode, + ar.getResultCode(), ar.getResultData()); + } + } + + /** * Like {@link #startActivityFromChild(Activity, Intent, int)}, but * taking a IntentSender; see * {@link #startIntentSenderForResult(IntentSender, int, Intent, int, int, int)} @@ -3224,6 +3349,19 @@ public class Activity extends ContextThemeWrapper } /** + * Check to see whether this activity is in the process of being destroyed in order to be + * recreated with a new configuration. This is often used in + * {@link #onStop} to determine whether the state needs to be cleaned up or will be passed + * on to the next instance of the activity via {@link #onRetainNonConfigurationInstance()}. + * + * @return If the activity is being torn down in order to be recreated with a new configuration, + * returns true; else returns false. + */ + public boolean isChangingConfigurations() { + return mChangingConfigurations; + } + + /** * Call this when your activity is done and should be closed. The * ActivityResult is propagated back to whoever launched you via * onActivityResult(). @@ -3324,8 +3462,7 @@ public class Activity extends ContextThemeWrapper * @see #createPendingResult * @see #setResult(int) */ - protected void onActivityResult(int requestCode, int resultCode, - Intent data) { + protected void onActivityResult(int requestCode, int resultCode, Intent data) { } /** @@ -3709,15 +3846,68 @@ public class Activity extends ContextThemeWrapper } /** - * Stub implementation of {@link android.view.LayoutInflater.Factory#onCreateView} used when - * inflating with the LayoutInflater returned by {@link #getSystemService}. This - * implementation simply returns null for all view names. + * Standard implementation of + * {@link android.view.LayoutInflater.Factory#onCreateView} used when + * inflating with the LayoutInflater returned by {@link #getSystemService}. + * This implementation handles <fragment> tags to embed fragments inside + * of the activity. * * @see android.view.LayoutInflater#createView * @see android.view.Window#getLayoutInflater */ public View onCreateView(String name, Context context, AttributeSet attrs) { - return null; + if (!"fragment".equals(name)) { + return null; + } + + TypedArray a = + context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Fragment); + String fname = a.getString(com.android.internal.R.styleable.Fragment_name); + int id = a.getResourceId(com.android.internal.R.styleable.Fragment_id, 0); + String tag = a.getString(com.android.internal.R.styleable.Fragment_tag); + a.recycle(); + + if (id == 0) { + throw new IllegalArgumentException(attrs.getPositionDescription() + + ": Must specify unique android:id for " + fname); + } + + try { + // If we restored from a previous state, we may already have + // instantiated this fragment from the state and should use + // that instance instead of making a new one. + Fragment fragment = mFragments.findFragmentById(id); + if (FragmentManager.DEBUG) Log.v(TAG, "onCreateView: id=0x" + + Integer.toHexString(id) + " fname=" + fname + + " existing=" + fragment); + if (fragment == null) { + fragment = Fragment.instantiate(this, fname); + fragment.mFromLayout = true; + fragment.mFragmentId = id; + fragment.mTag = tag; + mFragments.addFragment(fragment, true); + } + // If this fragment is newly instantiated (either right now, or + // from last saved state), then give it the attributes to + // initialize itself. + if (!fragment.mRetaining) { + fragment.onInflate(this, attrs, fragment.mSavedFragmentState); + } + if (fragment.mView == null) { + throw new IllegalStateException("Fragment " + fname + + " did not create a view."); + } + fragment.mView.setId(id); + if (fragment.mView.getTag() == null) { + fragment.mView.setTag(tag); + } + return fragment.mView; + } catch (Exception e) { + InflateException ie = new InflateException(attrs.getPositionDescription() + + ": Error inflating fragment " + fname); + ie.initCause(e); + throw ie; + } } // ------------------ Internal API ------------------ @@ -3728,23 +3918,25 @@ public class Activity extends ContextThemeWrapper final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, Application application, Intent intent, ActivityInfo info, CharSequence title, - Activity parent, String id, Object lastNonConfigurationInstance, + Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config) { attach(context, aThread, instr, token, 0, application, intent, info, title, parent, id, - lastNonConfigurationInstance, null, config); + lastNonConfigurationInstances, config); } final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, - Object lastNonConfigurationInstance, - HashMap<String,Object> lastNonConfigurationChildInstances, + NonConfigurationInstances lastNonConfigurationInstances, Configuration config) { attachBaseContext(context); + mFragments.attachActivity(this); + mWindow = PolicyManager.makeNewWindow(this); mWindow.setCallback(this); + mWindow.getLayoutInflater().setFactory(this); if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) { mWindow.setSoftInputMode(info.softInputMode); } @@ -3761,8 +3953,7 @@ public class Activity extends ContextThemeWrapper mTitle = title; mParent = parent; mEmbeddedID = id; - mLastNonConfigurationInstance = lastNonConfigurationInstance; - mLastNonConfigurationChildInstances = lastNonConfigurationChildInstances; + mLastNonConfigurationInstances = lastNonConfigurationInstances; mWindow.setWindowManager(null, mToken, mComponent.flattenToString()); if (mParent != null) { @@ -3776,6 +3967,10 @@ public class Activity extends ContextThemeWrapper return mParent != null ? mParent.getActivityToken() : mToken; } + final void performCreate(Bundle icicle) { + onCreate(icicle); + } + final void performStart() { mCalled = false; mInstrumentation.callActivityOnStart(this); @@ -3784,6 +3979,7 @@ public class Activity extends ContextThemeWrapper "Activity " + mComponent.toShortString() + " did not call through to super.onStart()"); } + mFragments.dispatchStart(); } final void performRestart() { @@ -3815,7 +4011,7 @@ public class Activity extends ContextThemeWrapper final void performResume() { performRestart(); - mLastNonConfigurationInstance = null; + mLastNonConfigurationInstances = null; // First call onResume() -before- setting mResumed, so we don't // send out any status bar / menu notifications the client makes. @@ -3830,6 +4026,9 @@ public class Activity extends ContextThemeWrapper // Now really resume, and install the current status bar and menu. mResumed = true; mCalled = false; + + mFragments.dispatchResume(); + onPostResume(); if (!mCalled) { throw new SuperNotCalledException( @@ -3839,6 +4038,7 @@ public class Activity extends ContextThemeWrapper } final void performPause() { + mFragments.dispatchPause(); onPause(); } @@ -3853,6 +4053,8 @@ public class Activity extends ContextThemeWrapper mWindow.closeAllPanels(); } + mFragments.dispatchStop(); + mCalled = false; mInstrumentation.callActivityOnStop(this); if (!mCalled) { @@ -3877,6 +4079,11 @@ public class Activity extends ContextThemeWrapper mResumed = false; } + final void performDestroy() { + mFragments.dispatchDestroy(); + onDestroy(); + } + final boolean isResumed() { return mResumed; } @@ -3888,6 +4095,11 @@ public class Activity extends ContextThemeWrapper + ", resCode=" + resultCode + ", data=" + data); if (who == null) { onActivityResult(requestCode, resultCode, data); + } else { + Fragment frag = mFragments.findFragmentByWho(who); + if (frag != null) { + frag.onActivityResult(requestCode, resultCode, data); + } } } } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 773c344..1f2fa26 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -1300,8 +1300,7 @@ public final class ActivityThread { Window window; Activity parent; String embeddedID; - Object lastNonConfigurationInstance; - HashMap<String,Object> lastNonConfigurationChildInstances; + Activity.NonConfigurationInstances lastNonConfigurationInstances; boolean paused; boolean stopped; boolean hideForNow; @@ -1466,7 +1465,7 @@ public final class ActivityThread { private static final String HEAP_COLUMN = "%17s %8s %8s %8s %8s"; private static final String ONE_COUNT_COLUMN = "%17s %8d"; private static final String TWO_COUNT_COLUMNS = "%17s %8d %17s %8d"; - private static final String DB_INFO_FORMAT = " %8d %8d %10d %s"; + private static final String DB_INFO_FORMAT = " %4d %6d %8d %14s %s"; // Formatting for checkin service - update version if row format changes private static final int ACTIVITY_THREAD_CHECKIN_VERSION = 1; @@ -1870,7 +1869,7 @@ public final class ActivityThread { for (int i = 0; i < stats.dbStats.size(); i++) { DbStats dbStats = stats.dbStats.get(i); printRow(pw, DB_INFO_FORMAT, dbStats.pageSize, dbStats.dbSize, - dbStats.lookaside, dbStats.dbName); + dbStats.lookaside, dbStats.cache, dbStats.dbName); pw.print(','); } @@ -1921,11 +1920,12 @@ public final class ActivityThread { int N = stats.dbStats.size(); if (N > 0) { pw.println(" DATABASES"); - printRow(pw, " %8s %8s %10s %s", "Pagesize", "Dbsize", "Lookaside", "Dbname"); + printRow(pw, " %4s %6s %8s %14s %s", "pgsz", "dbsz", "lkaside", "cache", + "Dbname"); for (int i = 0; i < N; i++) { DbStats dbStats = stats.dbStats.get(i); printRow(pw, DB_INFO_FORMAT, dbStats.pageSize, dbStats.dbSize, - dbStats.lookaside, dbStats.dbName); + dbStats.lookaside, dbStats.cache, dbStats.dbName); } } @@ -2478,7 +2478,7 @@ public final class ActivityThread { public final Activity startActivityNow(Activity parent, String id, Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state, - Object lastNonConfigurationInstance) { + Activity.NonConfigurationInstances lastNonConfigurationInstances) { ActivityRecord r = new ActivityRecord(); r.token = token; r.ident = 0; @@ -2487,7 +2487,7 @@ public final class ActivityThread { r.parent = parent; r.embeddedID = id; r.activityInfo = activityInfo; - r.lastNonConfigurationInstance = lastNonConfigurationInstance; + r.lastNonConfigurationInstances = lastNonConfigurationInstances; if (localLOGV) { ComponentName compname = intent.getComponent(); String name; @@ -2609,14 +2609,12 @@ public final class ActivityThread { + r.activityInfo.name + " with config " + config); activity.attach(appContext, this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, - r.embeddedID, r.lastNonConfigurationInstance, - r.lastNonConfigurationChildInstances, config); + r.embeddedID, r.lastNonConfigurationInstances, config); if (customIntent != null) { activity.mIntent = customIntent; } - r.lastNonConfigurationInstance = null; - r.lastNonConfigurationChildInstances = null; + r.lastNonConfigurationInstances = null; activity.mStartedActivity = false; int theme = r.activityInfo.getThemeResource(); if (theme != 0) { @@ -3574,6 +3572,9 @@ public final class ActivityThread { if (finishing) { r.activity.mFinished = true; } + if (getNonConfigInstance) { + r.activity.mChangingConfigurations = true; + } if (!r.paused) { try { r.activity.mCalled = false; @@ -3614,8 +3615,8 @@ public final class ActivityThread { } if (getNonConfigInstance) { try { - r.lastNonConfigurationInstance - = r.activity.onRetainNonConfigurationInstance(); + r.lastNonConfigurationInstances + = r.activity.retainNonConfigurationInstances(); } catch (Exception e) { if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( @@ -3624,22 +3625,10 @@ public final class ActivityThread { + ": " + e.toString(), e); } } - try { - r.lastNonConfigurationChildInstances - = r.activity.onRetainNonConfigurationChildInstances(); - } catch (Exception e) { - if (!mInstrumentation.onException(r.activity, e)) { - throw new RuntimeException( - "Unable to retain child activities " - + safeToComponentShortString(r.intent) - + ": " + e.toString(), e); - } - } - } try { r.activity.mCalled = false; - r.activity.onDestroy(); + mInstrumentation.callActivityOnDestroy(r.activity); if (!r.activity.mCalled) { throw new SuperNotCalledException( "Activity " + safeToComponentShortString(r.intent) + diff --git a/core/java/android/app/BackStackEntry.java b/core/java/android/app/BackStackEntry.java new file mode 100644 index 0000000..33e456d --- /dev/null +++ b/core/java/android/app/BackStackEntry.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; + +final class BackStackState implements Parcelable { + final int[] mOps; + final int mTransition; + final int mTransitionStyle; + final String mName; + + public BackStackState(FragmentManager fm, BackStackEntry bse) { + mOps = new int[bse.mNumOp*4]; + BackStackEntry.Op op = bse.mHead; + int pos = 0; + while (op != null) { + mOps[pos++] = op.cmd; + mOps[pos++] = op.fragment.mIndex; + mOps[pos++] = op.enterAnim; + mOps[pos++] = op.exitAnim; + op = op.next; + } + mTransition = bse.mTransition; + mTransitionStyle = bse.mTransitionStyle; + mName = bse.mName; + } + + public BackStackState(Parcel in) { + mOps = in.createIntArray(); + mTransition = in.readInt(); + mTransitionStyle = in.readInt(); + mName = in.readString(); + } + + public BackStackEntry instantiate(FragmentManager fm) { + BackStackEntry bse = new BackStackEntry(fm); + int pos = 0; + while (pos < mOps.length) { + BackStackEntry.Op op = new BackStackEntry.Op(); + op.cmd = mOps[pos++]; + Fragment f = fm.mActive.get(mOps[pos++]); + f.mBackStackNesting++; + op.fragment = f; + op.enterAnim = mOps[pos++]; + op.exitAnim = mOps[pos++]; + bse.addOp(op); + } + bse.mTransition = mTransition; + bse.mTransitionStyle = mTransitionStyle; + bse.mName = mName; + return bse; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeIntArray(mOps); + dest.writeInt(mTransition); + dest.writeInt(mTransitionStyle); + dest.writeString(mName); + } + + public static final Parcelable.Creator<BackStackState> CREATOR + = new Parcelable.Creator<BackStackState>() { + public BackStackState createFromParcel(Parcel in) { + return new BackStackState(in); + } + + public BackStackState[] newArray(int size) { + return new BackStackState[size]; + } + }; +} + +/** + * @hide Entry of an operation on the fragment back stack. + */ +final class BackStackEntry implements FragmentTransaction, Runnable { + final FragmentManager mManager; + + static final int OP_NULL = 0; + static final int OP_ADD = 1; + static final int OP_REMOVE = 2; + static final int OP_HIDE = 3; + static final int OP_SHOW = 4; + + static final class Op { + Op next; + Op prev; + int cmd; + Fragment fragment; + int enterAnim; + int exitAnim; + } + + Op mHead; + Op mTail; + int mNumOp; + int mEnterAnim; + int mExitAnim; + int mTransition; + int mTransitionStyle; + boolean mAddToBackStack; + String mName; + boolean mCommitted; + + public BackStackEntry(FragmentManager manager) { + mManager = manager; + } + + void addOp(Op op) { + if (mHead == null) { + mHead = mTail = op; + } else { + op.prev = mTail; + mTail.next = op; + mTail = op; + } + op.enterAnim = mEnterAnim; + op.exitAnim = mExitAnim; + mNumOp++; + } + + public FragmentTransaction add(Fragment fragment, String tag) { + return add(0, fragment, tag); + } + + public FragmentTransaction add(int containerViewId, Fragment fragment) { + return add(containerViewId, fragment, null); + } + + public FragmentTransaction add(int containerViewId, Fragment fragment, String tag) { + if (fragment.mActivity != null) { + throw new IllegalStateException("Fragment already added: " + fragment); + } + + if (tag != null) { + if (fragment.mTag != null && !tag.equals(fragment.mTag)) { + throw new IllegalStateException("Can't change tag of fragment " + + fragment + ": was " + fragment.mTag + + " now " + tag); + } + fragment.mTag = tag; + } + + if (containerViewId != 0) { + if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) { + throw new IllegalStateException("Can't change container ID of fragment " + + fragment + ": was " + fragment.mFragmentId + + " now " + containerViewId); + } + fragment.mContainerId = fragment.mFragmentId = containerViewId; + } + + Op op = new Op(); + op.cmd = OP_ADD; + op.fragment = fragment; + addOp(op); + + return this; + } + + public FragmentTransaction replace(int containerViewId, Fragment fragment) { + return replace(containerViewId, fragment, null); + } + + public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag) { + if (containerViewId == 0) { + throw new IllegalArgumentException("Must use non-zero containerViewId"); + } + if (mManager.mAdded != null) { + for (int i=0; i<mManager.mAdded.size(); i++) { + Fragment old = mManager.mAdded.get(i); + if (old.mContainerId == containerViewId) { + remove(old); + } + } + } + return add(containerViewId, fragment, tag); + } + + public FragmentTransaction remove(Fragment fragment) { + if (fragment.mActivity == null) { + throw new IllegalStateException("Fragment not added: " + fragment); + } + + Op op = new Op(); + op.cmd = OP_REMOVE; + op.fragment = fragment; + addOp(op); + + return this; + } + + public FragmentTransaction hide(Fragment fragment) { + if (fragment.mActivity == null) { + throw new IllegalStateException("Fragment not added: " + fragment); + } + + Op op = new Op(); + op.cmd = OP_HIDE; + op.fragment = fragment; + addOp(op); + + return this; + } + + public FragmentTransaction show(Fragment fragment) { + if (fragment.mActivity == null) { + throw new IllegalStateException("Fragment not added: " + fragment); + } + + Op op = new Op(); + op.cmd = OP_SHOW; + op.fragment = fragment; + addOp(op); + + return this; + } + + public FragmentTransaction setCustomAnimations(int enter, int exit) { + mEnterAnim = enter; + mExitAnim = exit; + return this; + } + + public FragmentTransaction setTransition(int transition) { + mTransition = transition; + return this; + } + + public FragmentTransaction setTransitionStyle(int styleRes) { + mTransitionStyle = styleRes; + return this; + } + + public FragmentTransaction addToBackStack(String name) { + mAddToBackStack = true; + mName = name; + return this; + } + + public void commit() { + if (mCommitted) throw new IllegalStateException("commit already called"); + mCommitted = true; + mManager.mActivity.mHandler.post(this); + } + + public void run() { + Op op = mHead; + while (op != null) { + switch (op.cmd) { + case OP_ADD: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting++; + } + f.mNextAnim = op.enterAnim; + mManager.addFragment(f, false); + } break; + case OP_REMOVE: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting++; + } + f.mNextAnim = op.exitAnim; + mManager.removeFragment(f, mTransition, mTransitionStyle); + } break; + case OP_HIDE: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting++; + } + f.mNextAnim = op.exitAnim; + mManager.hideFragment(f, mTransition, mTransitionStyle); + } break; + case OP_SHOW: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting++; + } + f.mNextAnim = op.enterAnim; + mManager.showFragment(f, mTransition, mTransitionStyle); + } break; + default: { + throw new IllegalArgumentException("Unknown cmd: " + op.cmd); + } + } + + op = op.next; + } + + mManager.moveToState(mManager.mCurState, mTransition, + mTransitionStyle, true); + if (mAddToBackStack) { + mManager.addBackStackState(this); + } + } + + public void popFromBackStack() { + Op op = mTail; + while (op != null) { + switch (op.cmd) { + case OP_ADD: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting--; + } + mManager.removeFragment(f, + FragmentManager.reverseTransit(mTransition), + mTransitionStyle); + } break; + case OP_REMOVE: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting--; + } + mManager.addFragment(f, false); + } break; + case OP_HIDE: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting--; + } + mManager.showFragment(f, + FragmentManager.reverseTransit(mTransition), mTransitionStyle); + } break; + case OP_SHOW: { + Fragment f = op.fragment; + if (mAddToBackStack) { + f.mBackStackNesting--; + } + mManager.hideFragment(f, + FragmentManager.reverseTransit(mTransition), mTransitionStyle); + } break; + default: { + throw new IllegalArgumentException("Unknown cmd: " + op.cmd); + } + } + + op = op.prev; + } + + mManager.moveToState(mManager.mCurState, + FragmentManager.reverseTransit(mTransition), mTransitionStyle, true); + } + + public String getName() { + return mName; + } + + public int getTransition() { + return mTransition; + } + + public int getTransitionStyle() { + return mTransitionStyle; + } +} diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 11b7b02..5217f5e 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -620,7 +620,8 @@ class ContextImpl extends Context { + " Is this really what you want?"); } mMainThread.getInstrumentation().execStartActivity( - getOuterContext(), mMainThread.getApplicationThread(), null, null, intent, -1); + getOuterContext(), mMainThread.getApplicationThread(), null, + (Activity)null, intent, -1); } @Override @@ -2733,6 +2734,13 @@ class ContextImpl extends Context { return v != null ? v : defValue; } } + + public Set<String> getStringSet(String key, Set<String> defValues) { + synchronized (this) { + Set<String> v = (Set<String>) mMap.get(key); + return v != null ? v : defValues; + } + } public int getInt(String key, int defValue) { synchronized (this) { @@ -2775,6 +2783,12 @@ class ContextImpl extends Context { return this; } } + public Editor putStringSet(String key, Set<String> values) { + synchronized (this) { + mModified.put(key, values); + return this; + } + } public Editor putInt(String key, int value) { synchronized (this) { mModified.put(key, value); diff --git a/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java new file mode 100644 index 0000000..d3f56ad --- /dev/null +++ b/core/java/android/app/Fragment.java @@ -0,0 +1,529 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.content.ComponentCallbacks; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; + +final class FragmentState implements Parcelable { + static final String VIEW_STATE_TAG = "android:view_state"; + + final String mClassName; + final int mIndex; + final boolean mFromLayout; + final int mFragmentId; + final int mContainerId; + final String mTag; + final boolean mRetainInstance; + + Bundle mSavedFragmentState; + + Fragment mInstance; + + public FragmentState(Fragment frag) { + mClassName = frag.getClass().getName(); + mIndex = frag.mIndex; + mFromLayout = frag.mFromLayout; + mFragmentId = frag.mFragmentId; + mContainerId = frag.mContainerId; + mTag = frag.mTag; + mRetainInstance = frag.mRetainInstance; + } + + public FragmentState(Parcel in) { + mClassName = in.readString(); + mIndex = in.readInt(); + mFromLayout = in.readInt() != 0; + mFragmentId = in.readInt(); + mContainerId = in.readInt(); + mTag = in.readString(); + mRetainInstance = in.readInt() != 0; + mSavedFragmentState = in.readBundle(); + } + + public Fragment instantiate(Activity activity) { + if (mInstance != null) { + return mInstance; + } + + try { + mInstance = Fragment.instantiate(activity, mClassName); + } catch (Exception e) { + throw new RuntimeException("Unable to restore fragment " + mClassName, e); + } + + if (mSavedFragmentState != null) { + mSavedFragmentState.setClassLoader(activity.getClassLoader()); + mInstance.mSavedFragmentState = mSavedFragmentState; + mInstance.mSavedViewState + = mSavedFragmentState.getSparseParcelableArray(VIEW_STATE_TAG); + } + mInstance.setIndex(mIndex); + mInstance.mFromLayout = mFromLayout; + mInstance.mFragmentId = mFragmentId; + mInstance.mContainerId = mContainerId; + mInstance.mTag = mTag; + mInstance.mRetainInstance = mRetainInstance; + + return mInstance; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mClassName); + dest.writeInt(mIndex); + dest.writeInt(mFromLayout ? 1 : 0); + dest.writeInt(mFragmentId); + dest.writeInt(mContainerId); + dest.writeString(mTag); + dest.writeInt(mRetainInstance ? 1 : 0); + dest.writeBundle(mSavedFragmentState); + } + + public static final Parcelable.Creator<FragmentState> CREATOR + = new Parcelable.Creator<FragmentState>() { + public FragmentState createFromParcel(Parcel in) { + return new FragmentState(in); + } + + public FragmentState[] newArray(int size) { + return new FragmentState[size]; + } + }; +} + +/** + * A Fragment is a piece of an application's user interface or behavior + * that can be placed in an {@link Activity}. + */ +public class Fragment implements ComponentCallbacks { + private static final HashMap<String, Class> sClassMap = + new HashMap<String, Class>(); + + static final int INITIALIZING = 0; // Not yet created. + static final int CREATED = 1; // Created. + static final int CONTENT = 2; // View hierarchy content available. + static final int STARTED = 3; // Created and started, not resumed. + static final int RESUMED = 4; // Created started and resumed. + + int mState = INITIALIZING; + + // When instantiated from saved state, this is the saved state. + Bundle mSavedFragmentState; + SparseArray<Parcelable> mSavedViewState; + + // Index into active fragment array. + int mIndex = -1; + + // Internal unique name for this fragment; + String mWho; + + // True if the fragment is in the list of added fragments. + boolean mAdded; + + // Set to true if this fragment was instantiated from a layout file. + boolean mFromLayout; + + // Number of active back stack entries this fragment is in. + int mBackStackNesting; + + // Activity this fragment is attached to. + Activity mActivity; + + // The optional identifier for this fragment -- either the container ID if it + // was dynamically added to the view hierarchy, or the ID supplied in + // layout. + int mFragmentId; + + // When a fragment is being dynamically added to the view hierarchy, this + // is the identifier of the parent container it is being added to. + int mContainerId; + + // The optional named tag for this fragment -- usually used to find + // fragments that are not part of the layout. + String mTag; + + // Set to true when the app has requested that this fragment be hidden + // from the user. + boolean mHidden; + + // If set this fragment would like its instance retained across + // configuration changes. + boolean mRetainInstance; + + // If set this fragment is being retained across the current config change. + boolean mRetaining; + + // Used to verify that subclasses call through to super class. + boolean mCalled; + + // If app has requested a specific animation, this is the one to use. + int mNextAnim; + + // The parent container of the fragment after dynamically added to UI. + ViewGroup mContainer; + + // The View generated for this fragment. + View mView; + + public Fragment() { + } + + static Fragment instantiate(Activity activity, String fname) + throws NoSuchMethodException, ClassNotFoundException, + IllegalArgumentException, InstantiationException, + IllegalAccessException, InvocationTargetException { + Class clazz = sClassMap.get(fname); + + if (clazz == null) { + // Class not found in the cache, see if it's real, and try to add it + clazz = activity.getClassLoader().loadClass(fname); + sClassMap.put(fname, clazz); + } + return (Fragment)clazz.newInstance(); + } + + void restoreViewState() { + if (mSavedViewState != null) { + mView.restoreHierarchyState(mSavedViewState); + mSavedViewState = null; + } + } + + void setIndex(int index) { + mIndex = index; + mWho = "android:fragment:" + mIndex; + } + + void clearIndex() { + mIndex = -1; + mWho = null; + } + + /** + * Subclasses can not override equals(). + */ + @Override final public boolean equals(Object o) { + return super.equals(o); + } + + /** + * Subclasses can not override hashCode(). + */ + @Override final public int hashCode() { + return super.hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("Fragment{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + if (mIndex >= 0) { + sb.append(" #"); + sb.append(mIndex); + } + if (mFragmentId != 0) { + sb.append(" id=0x"); + sb.append(Integer.toHexString(mFragmentId)); + } + if (mTag != null) { + sb.append(" "); + sb.append(mTag); + } + sb.append('}'); + return sb.toString(); + } + + /** + * Return the identifier this fragment is known by. This is either + * the android:id value supplied in a layout or the container view ID + * supplied when adding the fragment. + */ + public int getId() { + return mFragmentId; + } + + /** + * Get the tag name of the fragment, if specified. + */ + public String getTag() { + return mTag; + } + + /** + * Return the Activity this fragment is currently associated with. + */ + public Activity getActivity() { + return mActivity; + } + + /** + * Return true if the fragment is currently added to its activity. + */ + public boolean isAdded() { + return mActivity != null && mActivity.mFragments.mAdded.contains(this); + } + + /** + * Return true if the fragment is currently visible to the user. This means + * it: (1) has been added, (2) has its view attached to the window, and + * (3) is not hidden. + */ + public boolean isVisible() { + return isAdded() && !isHidden() && mView != null + && mView.getWindowToken() != null && mView.getVisibility() == View.VISIBLE; + } + + /** + * Return true if the fragment has been hidden. By default fragments + * are shown. You can find out about changes to this state with + * {@link #onHiddenChanged()}. Note that the hidden state is orthogonal + * to other states -- that is, to be visible to the user, a fragment + * must be both started and not hidden. + */ + public boolean isHidden() { + return mHidden; + } + + /** + * Called when the hidden state (as returned by {@link #isHidden()} of + * the fragment has changed. Fragments start out not hidden; this will + * be called whenever the fragment changes state from that. + * @param hidden True if the fragment is now hidden, false if it is not + * visible. + */ + public void onHiddenChanged(boolean hidden) { + } + + /** + * Control whether a fragment instance is retained across Activity + * re-creation (such as from a configuration change). This can only + * be used with fragments not in the back stack. If set, the fragment + * lifecycle will be slightly different when an activity is recreated: + * <ul> + * <li> {@link #onDestroy()} will not be called (but {@link #onDetach()} still + * will be, because the fragment is being detached from its current activity). + * <li> {@link #onCreate(Bundle)} will not be called since the fragment + * is not being re-created. + * <li> {@link #onAttach(Activity)} and {@link #onReady(Bundle)} <b>will</b> + * still be called. + * </ul> + */ + public void setRetainInstance(boolean retain) { + mRetainInstance = retain; + } + + public boolean getRetainInstance() { + return mRetainInstance; + } + + /** + * Call {@link Activity#startActivity(Intent)} on the fragment's + * containing Activity. + */ + public void startActivity(Intent intent) { + mActivity.startActivityFromFragment(this, intent, -1); + } + + /** + * Call {@link Activity#startActivityForResult(Intent, int)} on the fragment's + * containing Activity. + */ + public void startActivityForResult(Intent intent, int requestCode) { + mActivity.startActivityFromFragment(this, intent, requestCode); + } + + /** + * Receive the result from a previous call to + * {@link #startActivityForResult(Intent, int)}. This follows the + * related Activity API as described there in + * {@link Activity#onActivityResult(int, int, Intent)}. + * + * @param requestCode The integer request code originally supplied to + * startActivityForResult(), allowing you to identify who this + * result came from. + * @param resultCode The integer result code returned by the child activity + * through its setResult(). + * @param data An Intent, which can return result data to the caller + * (various data can be attached to Intent "extras"). + */ + public void onActivityResult(int requestCode, int resultCode, Intent data) { + } + + /** + * Called when a fragment is being created as part of a view layout + * inflation, typically from setting the content view of an activity. This + * will be called both the first time the fragment is created, as well + * later when it is being re-created from its saved state (which is also + * given here). + * + * XXX This is kind-of yucky... maybe we could just supply the + * AttributeSet to onCreate()? + * + * @param activity The Activity that is inflating the fragment. + * @param attrs The attributes at the tag where the fragment is + * being created. + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onInflate(Activity activity, AttributeSet attrs, + Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called when a fragment is first attached to its activity. + * {@link #onCreate(Bundle)} will be called after this. + */ + public void onAttach(Activity activity) { + mCalled = true; + } + + public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { + return null; + } + + /** + * Called to do initial creation of a fragment. This is called after + * {@link #onAttach(Activity)} and before {@link #onReady(Bundle)}. + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onCreate(Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called to have the fragment instantiate its user interface view. + * This is optional, and non-graphical fragments can return null (which + * is the default implementation). This will be called between + * {@link #onCreate(Bundle)} and {@link #onReady(Bundle)}. + * + * @param inflater The LayoutInflater object that can be used to inflate + * any views in the fragment, + * @param container If non-null, this is the parent view that the fragment's + * UI should be attached to. The fragment should not add the view itself, + * but this can be used to generate the LayoutParams of the view. + * @param savedInstanceState If non-null, this fragment is being re-constructed + * from a previous saved state as given here. + * + * @return Return the View for the fragment's UI, or null. + */ + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return null; + } + + public View getView() { + return mView; + } + + /** + * Called when the activity is ready for the fragment to run. This is + * most useful for fragments that use {@link #setRetainInstance(boolean)} + * instance, as this tells the fragment when it is fully associated with + * the new activity instance. This is called after {@link #onCreate(Bundle)} + * and before {@link #onStart()}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onReady(Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called when the Fragment is visible to the user. This is generally + * tied to {@link Activity#onStart() Activity.onStart} of the containing + * Activity's lifecycle. + */ + public void onStart() { + mCalled = true; + } + + /** + * Called when the fragment is visible to the user and actively running. + * This is generally + * tied to {@link Activity#onResume() Activity.onResume} of the containing + * Activity's lifecycle. + */ + public void onResume() { + mCalled = true; + } + + public void onSaveInstanceState(Bundle outState) { + } + + public void onConfigurationChanged(Configuration newConfig) { + mCalled = true; + } + + /** + * Called when the Fragment is no longer resumed. This is generally + * tied to {@link Activity#onPause() Activity.onPause} of the containing + * Activity's lifecycle. + */ + public void onPause() { + mCalled = true; + } + + /** + * Called when the Fragment is no longer started. This is generally + * tied to {@link Activity#onStop() Activity.onStop} of the containing + * Activity's lifecycle. + */ + public void onStop() { + mCalled = true; + } + + public void onLowMemory() { + mCalled = true; + } + + /** + * Called when the fragment is no longer in use. This is called + * after {@link #onStop()} and before {@link #onDetach()}. + */ + public void onDestroy() { + mCalled = true; + } + + /** + * Called when the fragment is no longer attached to its activity. This + * is called after {@link #onDestroy()}. + */ + public void onDetach() { + mCalled = true; + } +} diff --git a/core/java/android/app/FragmentManager.java b/core/java/android/app/FragmentManager.java new file mode 100644 index 0000000..a10a191 --- /dev/null +++ b/core/java/android/app/FragmentManager.java @@ -0,0 +1,842 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; + +import java.util.ArrayList; + +final class FragmentManagerState implements Parcelable { + FragmentState[] mActive; + int[] mAdded; + BackStackState[] mBackStack; + + public FragmentManagerState() { + } + + public FragmentManagerState(Parcel in) { + mActive = in.createTypedArray(FragmentState.CREATOR); + mAdded = in.createIntArray(); + mBackStack = in.createTypedArray(BackStackState.CREATOR); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedArray(mActive, flags); + dest.writeIntArray(mAdded); + dest.writeTypedArray(mBackStack, flags); + } + + public static final Parcelable.Creator<FragmentManagerState> CREATOR + = new Parcelable.Creator<FragmentManagerState>() { + public FragmentManagerState createFromParcel(Parcel in) { + return new FragmentManagerState(in); + } + + public FragmentManagerState[] newArray(int size) { + return new FragmentManagerState[size]; + } + }; +} + +/** + * @hide + * Container for fragments associated with an activity. + */ +public class FragmentManager { + static final boolean DEBUG = true; + static final String TAG = "FragmentManager"; + + ArrayList<Fragment> mActive; + ArrayList<Fragment> mAdded; + ArrayList<Integer> mAvailIndices; + ArrayList<BackStackEntry> mBackStack; + + int mCurState = Fragment.INITIALIZING; + Activity mActivity; + + // Temporary vars for state save and restore. + Bundle mStateBundle = null; + SparseArray<Parcelable> mStateArray = null; + + Animation loadAnimation(Fragment fragment, int transit, boolean enter, + int transitionStyle) { + Animation animObj = fragment.onCreateAnimation(transitionStyle, enter, + fragment.mNextAnim); + if (animObj != null) { + return animObj; + } + + if (fragment.mNextAnim != 0) { + Animation anim = AnimationUtils.loadAnimation(mActivity, fragment.mNextAnim); + if (anim != null) { + return anim; + } + } + + if (transit == 0) { + return null; + } + + int styleIndex = transitToStyleIndex(transit, enter); + if (styleIndex < 0) { + return null; + } + + if (transitionStyle == 0 && mActivity.getWindow() != null) { + transitionStyle = mActivity.getWindow().getAttributes().windowAnimations; + } + if (transitionStyle == 0) { + return null; + } + + TypedArray attrs = mActivity.obtainStyledAttributes(transitionStyle, + com.android.internal.R.styleable.WindowAnimation); + int anim = attrs.getResourceId(styleIndex, 0); + attrs.recycle(); + + if (anim == 0) { + return null; + } + + return AnimationUtils.loadAnimation(mActivity, anim); + } + + void moveToState(Fragment f, int newState, int transit, int transitionStyle) { + // Fragments that are not currently added will sit in the onCreate() state. + if (!f.mAdded && newState > Fragment.CREATED) { + newState = Fragment.CREATED; + } + + if (f.mState < newState) { + switch (f.mState) { + case Fragment.INITIALIZING: + if (DEBUG) Log.v(TAG, "moveto CREATED: " + f); + f.mActivity = mActivity; + f.mCalled = false; + f.onAttach(mActivity); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onAttach()"); + } + + if (!f.mRetaining) { + f.mCalled = false; + f.onCreate(f.mSavedFragmentState); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onCreate()"); + } + } + f.mRetaining = false; + if (f.mFromLayout) { + // For fragments that are part of the content view + // layout, we need to instantiate the view immediately + // and the inflater will take care of adding it. + f.mView = f.onCreateView(mActivity.getLayoutInflater(), + null, f.mSavedFragmentState); + if (f.mView != null) { + f.mView.setSaveFromParentEnabled(false); + f.restoreViewState(); + if (f.mHidden) f.mView.setVisibility(View.GONE); + } + } + case Fragment.CREATED: + if (newState > Fragment.CREATED) { + if (DEBUG) Log.v(TAG, "moveto CONTENT: " + f); + if (!f.mFromLayout) { + ViewGroup container = null; + if (f.mContainerId != 0) { + container = (ViewGroup)mActivity.findViewById(f.mContainerId); + if (container == null) { + throw new IllegalArgumentException("New view found for id 0x" + + Integer.toHexString(f.mContainerId) + + " for fragment " + f); + } + } + f.mContainer = container; + f.mView = f.onCreateView(mActivity.getLayoutInflater(), + container, f.mSavedFragmentState); + if (f.mView != null) { + f.mView.setSaveFromParentEnabled(false); + if (container != null) { + Animation anim = loadAnimation(f, transit, true, + transitionStyle); + if (anim != null) { + f.mView.setAnimation(anim); + } + container.addView(f.mView); + f.restoreViewState(); + } + if (f.mHidden) f.mView.setVisibility(View.GONE); + } + } + + f.mCalled = false; + f.onReady(f.mSavedFragmentState); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onReady()"); + } + f.mSavedFragmentState = null; + } + case Fragment.CONTENT: + if (newState > Fragment.CONTENT) { + if (DEBUG) Log.v(TAG, "moveto STARTED: " + f); + f.mCalled = false; + f.onStart(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onStart()"); + } + } + case Fragment.STARTED: + if (newState > Fragment.STARTED) { + if (DEBUG) Log.v(TAG, "moveto RESUMED: " + f); + f.mCalled = false; + f.onResume(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onResume()"); + } + } + } + } else if (f.mState > newState) { + switch (f.mState) { + case Fragment.RESUMED: + if (newState < Fragment.RESUMED) { + if (DEBUG) Log.v(TAG, "movefrom RESUMED: " + f); + f.mCalled = false; + f.onPause(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onPause()"); + } + } + case Fragment.STARTED: + if (newState < Fragment.STARTED) { + if (DEBUG) Log.v(TAG, "movefrom STARTED: " + f); + f.mCalled = false; + f.onStop(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onStop()"); + } + } + case Fragment.CONTENT: + if (newState < Fragment.CONTENT) { + if (DEBUG) Log.v(TAG, "movefrom CONTENT: " + f); + if (f.mView != null) { + // Need to save the current view state if not + // done already. + if (!mActivity.isFinishing() && f.mSavedFragmentState == null) { + saveFragmentViewState(f); + } + if (f.mContainer != null) { + if (mCurState > Fragment.INITIALIZING) { + Animation anim = loadAnimation(f, transit, false, + transitionStyle); + if (anim != null) { + f.mView.setAnimation(anim); + } + } + f.mContainer.removeView(f.mView); + } + } + f.mContainer = null; + f.mView = null; + } + case Fragment.CREATED: + if (newState < Fragment.CREATED) { + if (DEBUG) Log.v(TAG, "movefrom CREATED: " + f); + if (!f.mRetaining) { + f.mCalled = false; + f.onDestroy(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onDestroy()"); + } + } + + f.mCalled = false; + f.onDetach(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onDetach()"); + } + f.mActivity = null; + } + } + } + + f.mState = newState; + } + + void moveToState(int newState, boolean always) { + moveToState(newState, 0, 0, always); + } + + void moveToState(int newState, int transit, int transitStyle, boolean always) { + if (mActivity == null && newState != Fragment.INITIALIZING) { + throw new IllegalStateException("No activity"); + } + + if (!always && mCurState == newState) { + return; + } + + mCurState = newState; + if (mActive != null) { + for (int i=0; i<mActive.size(); i++) { + Fragment f = mActive.get(i); + if (f != null) { + moveToState(f, newState, transit, transitStyle); + } + } + } + } + + void makeActive(Fragment f) { + if (f.mIndex >= 0) { + return; + } + + if (mAvailIndices == null || mAvailIndices.size() <= 0) { + if (mActive == null) { + mActive = new ArrayList<Fragment>(); + } + f.setIndex(mActive.size()); + mActive.add(f); + + } else { + f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1)); + mActive.set(f.mIndex, f); + } + } + + void makeInactive(Fragment f) { + if (f.mIndex < 0) { + return; + } + + mActive.set(f.mIndex, null); + if (mAvailIndices == null) { + mAvailIndices = new ArrayList<Integer>(); + } + mAvailIndices.add(f.mIndex); + f.clearIndex(); + } + + public void addFragment(Fragment fragment, boolean moveToStateNow) { + if (DEBUG) Log.v(TAG, "add: " + fragment); + if (mAdded == null) { + mAdded = new ArrayList<Fragment>(); + } + mAdded.add(fragment); + makeActive(fragment); + fragment.mAdded = true; + if (moveToStateNow) { + moveToState(fragment, mCurState, 0, 0); + } + } + + public void removeFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "remove: " + fragment); + mAdded.remove(fragment); + final boolean inactive = fragment.mBackStackNesting <= 0; + if (inactive) { + makeInactive(fragment); + } + fragment.mAdded = false; + moveToState(fragment, inactive ? Fragment.INITIALIZING : Fragment.CREATED, + transition, transitionStyle); + } + + public void hideFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "hide: " + fragment); + if (!fragment.mHidden) { + fragment.mHidden = true; + if (fragment.mView != null) { + Animation anim = loadAnimation(fragment, transition, false, + transitionStyle); + if (anim != null) { + fragment.mView.setAnimation(anim); + } + fragment.mView.setVisibility(View.GONE); + } + fragment.onHiddenChanged(true); + } + } + + public void showFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "show: " + fragment); + if (fragment.mHidden) { + fragment.mHidden = false; + if (fragment.mView != null) { + Animation anim = loadAnimation(fragment, transition, true, + transitionStyle); + if (anim != null) { + fragment.mView.setAnimation(anim); + } + fragment.mView.setVisibility(View.VISIBLE); + } + fragment.onHiddenChanged(false); + } + } + + public Fragment findFragmentById(int id) { + if (mActive != null) { + // First look through added fragments. + for (int i=mAdded.size()-1; i>=0; i--) { + Fragment f = mAdded.get(i); + if (f != null && f.mFragmentId == id) { + return f; + } + } + // Now for any known fragment. + for (int i=mActive.size()-1; i>=0; i--) { + Fragment f = mActive.get(i); + if (f != null && f.mFragmentId == id) { + return f; + } + } + } + return null; + } + + public Fragment findFragmentByTag(String tag) { + if (mActive != null && tag != null) { + // First look through added fragments. + for (int i=mAdded.size()-1; i>=0; i--) { + Fragment f = mAdded.get(i); + if (f != null && tag.equals(f.mTag)) { + return f; + } + } + // Now for any known fragment. + for (int i=mActive.size()-1; i>=0; i--) { + Fragment f = mActive.get(i); + if (f != null && tag.equals(f.mTag)) { + return f; + } + } + } + return null; + } + + public Fragment findFragmentByWho(String who) { + if (mActive != null && who != null) { + for (int i=mActive.size()-1; i>=0; i--) { + Fragment f = mActive.get(i); + if (f != null && who.equals(f.mWho)) { + return f; + } + } + } + return null; + } + + public void addBackStackState(BackStackEntry state) { + if (mBackStack == null) { + mBackStack = new ArrayList<BackStackEntry>(); + } + mBackStack.add(state); + } + + public boolean popBackStackState(Handler handler, String name) { + if (mBackStack == null) { + return false; + } + if (name == null) { + int last = mBackStack.size()-1; + if (last < 0) { + return false; + } + final BackStackEntry bss = mBackStack.remove(last); + handler.post(new Runnable() { + public void run() { + bss.popFromBackStack(); + moveToState(mCurState, reverseTransit(bss.getTransition()), + bss.getTransitionStyle(), true); + } + }); + } else { + int index = mBackStack.size()-1; + while (index >= 0) { + BackStackEntry bss = mBackStack.get(index); + if (name.equals(bss.getName())) { + break; + } + } + if (index < 0 || index == mBackStack.size()-1) { + return false; + } + final ArrayList<BackStackEntry> states + = new ArrayList<BackStackEntry>(); + for (int i=mBackStack.size()-1; i>index; i--) { + states.add(mBackStack.remove(i)); + } + handler.post(new Runnable() { + public void run() { + for (int i=0; i<states.size(); i++) { + states.get(i).popFromBackStack(); + } + moveToState(mCurState, true); + } + }); + } + return true; + } + + ArrayList<Fragment> retainNonConfig() { + ArrayList<Fragment> fragments = null; + if (mActive != null) { + for (int i=0; i<mActive.size(); i++) { + Fragment f = mActive.get(i); + if (f != null && f.mRetainInstance) { + if (fragments == null) { + fragments = new ArrayList<Fragment>(); + } + fragments.add(f); + f.mRetaining = true; + } + } + } + return fragments; + } + + void saveFragmentViewState(Fragment f) { + if (f.mView == null) { + return; + } + if (mStateArray == null) { + mStateArray = new SparseArray<Parcelable>(); + } + f.mView.saveHierarchyState(mStateArray); + if (mStateArray.size() > 0) { + f.mSavedViewState = mStateArray; + mStateArray = null; + } + } + + Parcelable saveAllState() { + if (mActive == null || mActive.size() <= 0) { + return null; + } + + // First collect all active fragments. + int N = mActive.size(); + FragmentState[] active = new FragmentState[N]; + boolean haveFragments = false; + for (int i=0; i<N; i++) { + Fragment f = mActive.get(i); + if (f != null) { + haveFragments = true; + + FragmentState fs = new FragmentState(f); + active[i] = fs; + + if (mStateBundle == null) { + mStateBundle = new Bundle(); + } + f.onSaveInstanceState(mStateBundle); + if (!mStateBundle.isEmpty()) { + fs.mSavedFragmentState = mStateBundle; + mStateBundle = null; + } + + if (f.mView != null) { + saveFragmentViewState(f); + if (f.mSavedViewState != null) { + if (fs.mSavedFragmentState == null) { + fs.mSavedFragmentState = new Bundle(); + } + fs.mSavedFragmentState.putSparseParcelableArray( + FragmentState.VIEW_STATE_TAG, f.mSavedViewState); + } + } + + } + } + + if (!haveFragments) { + return null; + } + + int[] added = null; + BackStackState[] backStack = null; + + // Build list of currently added fragments. + N = mAdded.size(); + if (N > 0) { + added = new int[N]; + for (int i=0; i<N; i++) { + added[i] = mAdded.get(i).mIndex; + } + } + + // Now save back stack. + if (mBackStack != null) { + N = mBackStack.size(); + if (N > 0) { + backStack = new BackStackState[N]; + for (int i=0; i<N; i++) { + backStack[i] = new BackStackState(this, mBackStack.get(i)); + } + } + } + + FragmentManagerState fms = new FragmentManagerState(); + fms.mActive = active; + fms.mAdded = added; + fms.mBackStack = backStack; + return fms; + } + + void restoreAllState(Parcelable state, ArrayList<Fragment> nonConfig) { + // If there is no saved state at all, then there can not be + // any nonConfig fragments either, so that is that. + if (state == null) return; + FragmentManagerState fms = (FragmentManagerState)state; + if (fms.mActive == null) return; + + // First re-attach any non-config instances we are retaining back + // to their saved state, so we don't try to instantiate them again. + if (nonConfig != null) { + for (int i=0; i<nonConfig.size(); i++) { + Fragment f = nonConfig.get(i); + FragmentState fs = fms.mActive[f.mIndex]; + fs.mInstance = f; + f.mSavedViewState = null; + f.mBackStackNesting = 0; + f.mAdded = false; + if (fs.mSavedFragmentState != null) { + f.mSavedViewState = fs.mSavedFragmentState.getSparseParcelableArray( + FragmentState.VIEW_STATE_TAG); + } + } + } + + // Build the full list of active fragments, instantiating them from + // their saved state. + mActive = new ArrayList<Fragment>(fms.mActive.length); + if (mAvailIndices != null) { + mAvailIndices.clear(); + } + for (int i=0; i<fms.mActive.length; i++) { + FragmentState fs = fms.mActive[i]; + if (fs != null) { + mActive.add(fs.instantiate(mActivity)); + } else { + mActive.add(null); + if (mAvailIndices == null) { + mAvailIndices = new ArrayList<Integer>(); + } + mAvailIndices.add(i); + } + } + + // Build the list of currently added fragments. + if (fms.mAdded != null) { + mAdded = new ArrayList<Fragment>(fms.mAdded.length); + for (int i=0; i<fms.mAdded.length; i++) { + Fragment f = mActive.get(fms.mAdded[i]); + if (f == null) { + throw new IllegalStateException( + "No instantiated fragment for index #" + fms.mAdded[i]); + } + f.mAdded = true; + mAdded.add(f); + } + } else { + mAdded = null; + } + + // Build the back stack. + if (fms.mBackStack != null) { + mBackStack = new ArrayList<BackStackEntry>(fms.mBackStack.length); + for (int i=0; i<fms.mBackStack.length; i++) { + BackStackEntry bse = fms.mBackStack[i].instantiate(this); + mBackStack.add(bse); + } + } else { + mBackStack = null; + } + } + + public void attachActivity(Activity activity) { + if (mActivity != null) throw new IllegalStateException(); + mActivity = activity; + } + + public void dispatchCreate() { + moveToState(Fragment.CREATED, false); + } + + public void dispatchStart() { + moveToState(Fragment.STARTED, false); + } + + public void dispatchResume() { + moveToState(Fragment.RESUMED, false); + } + + public void dispatchPause() { + moveToState(Fragment.STARTED, false); + } + + public void dispatchStop() { + moveToState(Fragment.CONTENT, false); + } + + public void dispatchDestroy() { + moveToState(Fragment.INITIALIZING, false); + mActivity = null; + } + + public static int reverseTransit(int transit) { + int rev = 0; + switch (transit) { + case FragmentTransaction.TRANSIT_ENTER: + rev = FragmentTransaction.TRANSIT_EXIT; + break; + case FragmentTransaction.TRANSIT_EXIT: + rev = FragmentTransaction.TRANSIT_ENTER; + break; + case FragmentTransaction.TRANSIT_SHOW: + rev = FragmentTransaction.TRANSIT_HIDE; + break; + case FragmentTransaction.TRANSIT_HIDE: + rev = FragmentTransaction.TRANSIT_SHOW; + break; + case FragmentTransaction.TRANSIT_ACTIVITY_OPEN: + rev = FragmentTransaction.TRANSIT_ACTIVITY_CLOSE; + break; + case FragmentTransaction.TRANSIT_ACTIVITY_CLOSE: + rev = FragmentTransaction.TRANSIT_ACTIVITY_OPEN; + break; + case FragmentTransaction.TRANSIT_TASK_OPEN: + rev = FragmentTransaction.TRANSIT_TASK_CLOSE; + break; + case FragmentTransaction.TRANSIT_TASK_CLOSE: + rev = FragmentTransaction.TRANSIT_TASK_OPEN; + break; + case FragmentTransaction.TRANSIT_TASK_TO_FRONT: + rev = FragmentTransaction.TRANSIT_TASK_TO_BACK; + break; + case FragmentTransaction.TRANSIT_TASK_TO_BACK: + rev = FragmentTransaction.TRANSIT_TASK_TO_FRONT; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_OPEN: + rev = FragmentTransaction.TRANSIT_WALLPAPER_CLOSE; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_CLOSE: + rev = FragmentTransaction.TRANSIT_WALLPAPER_OPEN; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_INTRA_OPEN: + rev = FragmentTransaction.TRANSIT_WALLPAPER_INTRA_CLOSE; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_INTRA_CLOSE: + rev = FragmentTransaction.TRANSIT_WALLPAPER_INTRA_OPEN; + break; + } + return rev; + + } + + public static int transitToStyleIndex(int transit, boolean enter) { + int animAttr = -1; + switch (transit) { + case FragmentTransaction.TRANSIT_ENTER: + animAttr = com.android.internal.R.styleable.WindowAnimation_windowEnterAnimation; + break; + case FragmentTransaction.TRANSIT_EXIT: + animAttr = com.android.internal.R.styleable.WindowAnimation_windowExitAnimation; + break; + case FragmentTransaction.TRANSIT_SHOW: + animAttr = com.android.internal.R.styleable.WindowAnimation_windowShowAnimation; + break; + case FragmentTransaction.TRANSIT_HIDE: + animAttr = com.android.internal.R.styleable.WindowAnimation_windowHideAnimation; + break; + case FragmentTransaction.TRANSIT_ACTIVITY_OPEN: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_activityOpenEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_activityOpenExitAnimation; + break; + case FragmentTransaction.TRANSIT_ACTIVITY_CLOSE: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_activityCloseEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_activityCloseExitAnimation; + break; + case FragmentTransaction.TRANSIT_TASK_OPEN: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_taskOpenEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_taskOpenExitAnimation; + break; + case FragmentTransaction.TRANSIT_TASK_CLOSE: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_taskCloseEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_taskCloseExitAnimation; + break; + case FragmentTransaction.TRANSIT_TASK_TO_FRONT: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_taskToFrontEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_taskToFrontExitAnimation; + break; + case FragmentTransaction.TRANSIT_TASK_TO_BACK: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_taskToBackEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_taskToBackExitAnimation; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_OPEN: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_wallpaperOpenEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_wallpaperOpenExitAnimation; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_CLOSE: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_wallpaperCloseEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_wallpaperCloseExitAnimation; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_INTRA_OPEN: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation; + break; + case FragmentTransaction.TRANSIT_WALLPAPER_INTRA_CLOSE: + animAttr = enter + ? com.android.internal.R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation + : com.android.internal.R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation; + break; + } + return animAttr; + } +} diff --git a/core/java/android/app/FragmentTransaction.java b/core/java/android/app/FragmentTransaction.java new file mode 100644 index 0000000..840f274 --- /dev/null +++ b/core/java/android/app/FragmentTransaction.java @@ -0,0 +1,151 @@ +package android.app; + +/** + * API for performing a set of Fragment operations. + */ +public interface FragmentTransaction { + /** + * Calls {@link #add(int, Fragment, String)} with a 0 containerViewId. + */ + public FragmentTransaction add(Fragment fragment, String tag); + + /** + * Calls {@link #add(int, Fragment, String)} with a null tag. + */ + public FragmentTransaction add(int containerViewId, Fragment fragment); + + /** + * Add a fragment to the activity state. This fragment may optionally + * also have its view (if {@link Fragment#onCreateView Fragment.onCreateView} + * returns non-null) into a container view of the activity. + * + * @param containerViewId Optional identifier of the container this fragment is + * to be placed in. If 0, it will not be placed in a container. + * @param fragment The fragment to be added. This fragment must not already + * be added to the activity. + * @param tag Optional tag name for the fragment, to later retrieve the + * fragment with {@link Activity#findFragmentByTag(String) + * Activity.findFragmentByTag(String)}. + * + * @return Returns the same FragmentTransaction instance. + */ + public FragmentTransaction add(int containerViewId, Fragment fragment, String tag); + + /** + * Calls {@link #replace(int, Fragment, String)} with a null tag. + */ + public FragmentTransaction replace(int containerViewId, Fragment fragment); + + /** + * Replace an existing fragment that was added to a container. This is + * essentially the same as calling {@link #remove(Fragment)} for all + * currently added fragments that were added with the same containerViewId + * and then {@link #add(int, Fragment, String)} with the same arguments + * given here. + * + * @param containerViewId Identifier of the container whose fragment(s) are + * to be replaced. + * @param fragment The new fragment to place in the container. + * @param tag Optional tag name for the fragment, to later retrieve the + * fragment with {@link Activity#findFragmentByTag(String) + * Activity.findFragmentByTag(String)}. + * + * @return Returns the same FragmentTransaction instance. + */ + public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag); + + /** + * Remove an existing fragment. If it was added to a container, its view + * is also removed from that container. + * + * @param fragment The fragment to be removed. + * + * @return Returns the same FragmentTransaction instance. + */ + public FragmentTransaction remove(Fragment fragment); + + /** + * Hides an existing fragment. This is only relevant for fragments whose + * views have been added to a container, as this will cause the view to + * be hidden. + * + * @param fragment The fragment to be hidden. + * + * @return Returns the same FragmentTransaction instance. + */ + public FragmentTransaction hide(Fragment fragment); + + /** + * Hides a previously hidden fragment. This is only relevant for fragments whose + * views have been added to a container, as this will cause the view to + * be shown. + * + * @param fragment The fragment to be shown. + * + * @return Returns the same FragmentTransaction instance. + */ + public FragmentTransaction show(Fragment fragment); + + /** + * Bit mask that is set for all enter transitions. + */ + public final int TRANSIT_ENTER_MASK = 0x1000; + + /** + * Bit mask that is set for all exit transitions. + */ + public final int TRANSIT_EXIT_MASK = 0x2000; + + /** Not set up for a transition. */ + public final int TRANSIT_UNSET = -1; + /** No animation for transition. */ + public final int TRANSIT_NONE = 0; + /** Window has been added to the screen. */ + public final int TRANSIT_ENTER = 1 | TRANSIT_ENTER_MASK; + /** Window has been removed from the screen. */ + public final int TRANSIT_EXIT = 2 | TRANSIT_EXIT_MASK; + /** Window has been made visible. */ + public final int TRANSIT_SHOW = 3 | TRANSIT_ENTER_MASK; + /** Window has been made invisible. */ + public final int TRANSIT_HIDE = 4 | TRANSIT_EXIT_MASK; + /** The "application starting" preview window is no longer needed, and will + * animate away to show the real window. */ + public final int TRANSIT_PREVIEW_DONE = 5; + /** A window in a new activity is being opened on top of an existing one + * in the same task. */ + public final int TRANSIT_ACTIVITY_OPEN = 6 | TRANSIT_ENTER_MASK; + /** The window in the top-most activity is being closed to reveal the + * previous activity in the same task. */ + public final int TRANSIT_ACTIVITY_CLOSE = 7 | TRANSIT_EXIT_MASK; + /** A window in a new task is being opened on top of an existing one + * in another activity's task. */ + public final int TRANSIT_TASK_OPEN = 8 | TRANSIT_ENTER_MASK; + /** A window in the top-most activity is being closed to reveal the + * previous activity in a different task. */ + public final int TRANSIT_TASK_CLOSE = 9 | TRANSIT_EXIT_MASK; + /** A window in an existing task is being displayed on top of an existing one + * in another activity's task. */ + public final int TRANSIT_TASK_TO_FRONT = 10 | TRANSIT_ENTER_MASK; + /** A window in an existing task is being put below all other tasks. */ + public final int TRANSIT_TASK_TO_BACK = 11 | TRANSIT_EXIT_MASK; + /** A window in a new activity that doesn't have a wallpaper is being + * opened on top of one that does, effectively closing the wallpaper. */ + public final int TRANSIT_WALLPAPER_CLOSE = 12 | TRANSIT_EXIT_MASK; + /** A window in a new activity that does have a wallpaper is being + * opened on one that didn't, effectively opening the wallpaper. */ + public final int TRANSIT_WALLPAPER_OPEN = 13 | TRANSIT_ENTER_MASK; + /** A window in a new activity is being opened on top of an existing one, + * and both are on top of the wallpaper. */ + public final int TRANSIT_WALLPAPER_INTRA_OPEN = 14 | TRANSIT_ENTER_MASK; + /** The window in the top-most activity is being closed to reveal the + * previous activity, and both are on top of he wallpaper. */ + public final int TRANSIT_WALLPAPER_INTRA_CLOSE = 15 | TRANSIT_EXIT_MASK; + + public FragmentTransaction setCustomAnimations(int enter, int exit); + + public FragmentTransaction setTransition(int transit); + public FragmentTransaction setTransitionStyle(int styleRes); + + public FragmentTransaction addToBackStack(String name); + public void commit(); +} diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index b8c3aa3..4d5f36a 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -997,8 +997,10 @@ public class Instrumentation { IllegalAccessException { Activity activity = (Activity)clazz.newInstance(); ActivityThread aThread = null; - activity.attach(context, aThread, this, token, application, intent, info, title, - parent, id, lastNonConfigurationInstance, new Configuration()); + activity.attach(context, aThread, this, token, application, intent, + info, title, parent, id, + (Activity.NonConfigurationInstances)lastNonConfigurationInstance, + new Configuration()); return activity; } @@ -1058,21 +1060,23 @@ public class Instrumentation { } public void callActivityOnDestroy(Activity activity) { - if (mWaitingActivities != null) { - synchronized (mSync) { - final int N = mWaitingActivities.size(); - for (int i=0; i<N; i++) { - final ActivityWaiter aw = mWaitingActivities.get(i); - final Intent intent = aw.intent; - if (intent.filterEquals(activity.getIntent())) { - aw.activity = activity; - mMessageQueue.addIdleHandler(new ActivityGoing(aw)); - } - } - } - } + // TODO: the following block causes intermittent hangs when using startActivity + // temporarily comment out until root cause is fixed (bug 2630683) +// if (mWaitingActivities != null) { +// synchronized (mSync) { +// final int N = mWaitingActivities.size(); +// for (int i=0; i<N; i++) { +// final ActivityWaiter aw = mWaitingActivities.get(i); +// final Intent intent = aw.intent; +// if (intent.filterEquals(activity.getIntent())) { +// aw.activity = activity; +// mMessageQueue.addIdleHandler(new ActivityGoing(aw)); +// } +// } +// } +// } - activity.onDestroy(); + activity.performDestroy(); if (mActivityMonitors != null) { synchronized (mSync) { @@ -1331,7 +1335,7 @@ public class Instrumentation { * is being started. * @param token Internal token identifying to the system who is starting * the activity; may be null. - * @param target Which activity is perform the start (and thus receiving + * @param target Which activity is performing the start (and thus receiving * any result); may be null if this call is not being made * from an activity. * @param intent The actual Intent to start. @@ -1381,6 +1385,64 @@ public class Instrumentation { return null; } + /** + * Like {@link #execStartActivity(Context, IBinder, IBinder, Activity, Intent, int)}, + * but for calls from a {#link Fragment}. + * + * @param who The Context from which the activity is being started. + * @param contextThread The main thread of the Context from which the activity + * is being started. + * @param token Internal token identifying to the system who is starting + * the activity; may be null. + * @param target Which fragment is performing the start (and thus receiving + * any result). + * @param intent The actual Intent to start. + * @param requestCode Identifier for this request's result; less than zero + * if the caller is not expecting a result. + * + * @return To force the return of a particular result, return an + * ActivityResult object containing the desired data; otherwise + * return null. The default implementation always returns null. + * + * @throws android.content.ActivityNotFoundException + * + * @see Activity#startActivity(Intent) + * @see Activity#startActivityForResult(Intent, int) + * @see Activity#startActivityFromChild + * + * {@hide} + */ + public ActivityResult execStartActivity( + Context who, IBinder contextThread, IBinder token, Fragment target, + Intent intent, int requestCode) { + IApplicationThread whoThread = (IApplicationThread) contextThread; + if (mActivityMonitors != null) { + synchronized (mSync) { + final int N = mActivityMonitors.size(); + for (int i=0; i<N; i++) { + final ActivityMonitor am = mActivityMonitors.get(i); + if (am.match(who, null, intent)) { + am.mHits++; + if (am.isBlocking()) { + return requestCode >= 0 ? am.getResult() : null; + } + break; + } + } + } + } + try { + int result = ActivityManagerNative.getDefault() + .startActivity(whoThread, intent, + intent.resolveTypeIfNeeded(who.getContentResolver()), + null, 0, token, target != null ? target.mWho : null, + requestCode, false, false); + checkStartActivityResult(result, intent); + } catch (RemoteException e) { + } + return null; + } + /*package*/ final void init(ActivityThread thread, Context instrContext, Context appContext, ComponentName component, IInstrumentationWatcher watcher) { diff --git a/core/java/android/app/LoaderManagingFragment.java b/core/java/android/app/LoaderManagingFragment.java new file mode 100644 index 0000000..1659adf --- /dev/null +++ b/core/java/android/app/LoaderManagingFragment.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.content.Loader; +import android.os.Bundle; + +import java.util.HashMap; + +/** + * A Fragment that has utility methods for managing {@link Loader}s. + * + * @param <D> The type of data returned by the Loader. If you're using multiple Loaders with + * different return types use Object and case the results. + */ +public abstract class LoaderManagingFragment<D> extends Fragment + implements Loader.OnLoadCompleteListener<D> { + private boolean mStarted = false; + + static final class LoaderInfo<D> { + public Bundle args; + public Loader<D> loader; + } + private HashMap<Integer, LoaderInfo<D>> mLoaders; + private HashMap<Integer, LoaderInfo<D>> mInactiveLoaders; + + /** + * Registers a loader with this activity, registers the callbacks on it, and starts it loading. + * If a loader with the same id has previously been started it will automatically be destroyed + * when the new loader completes it's work. The callback will be delivered before the old loader + * is destroyed. + */ + protected Loader<D> startLoading(int id, Bundle args) { + LoaderInfo<D> info = mLoaders.get(id); + if (info != null) { + // Keep track of the previous instance of this loader so we can destroy + // it when the new one completes. + mInactiveLoaders.put(id, info); + } + info = new LoaderInfo<D>(); + info.args = args; + mLoaders.put(id, info); + Loader<D> loader = onCreateLoader(id, args); + info.loader = loader; + if (mStarted) { + // The activity will start all existing loaders in it's onStart(), so only start them + // here if we're past that point of the activitiy's life cycle + loader.registerListener(id, this); + loader.startLoading(); + } + return loader; + } + + protected abstract Loader<D> onCreateLoader(int id, Bundle args); + protected abstract void onInitializeLoaders(); + protected abstract void onLoadFinished(Loader<D> loader, D data); + + public final void onLoadComplete(Loader<D> loader, D data) { + // Notify of the new data so the app can switch out the old data before + // we try to destroy it. + onLoadFinished(loader, data); + + // Look for an inactive loader and destroy it if found + int id = loader.getId(); + LoaderInfo<D> info = mInactiveLoaders.get(id); + if (info != null) { + Loader<D> oldLoader = info.loader; + if (oldLoader != null) { + oldLoader.destroy(); + } + mInactiveLoaders.remove(id); + } + } + + @Override + public void onCreate(Bundle savedState) { + super.onCreate(savedState); + + if (mLoaders == null) { + // Look for a passed along loader and create a new one if it's not there +// TODO: uncomment once getLastNonConfigurationInstance method is available +// mLoaders = (HashMap<Integer, LoaderInfo>) getLastNonConfigurationInstance(); + if (mLoaders == null) { + mLoaders = new HashMap<Integer, LoaderInfo<D>>(); + onInitializeLoaders(); + } + } + if (mInactiveLoaders == null) { + mInactiveLoaders = new HashMap<Integer, LoaderInfo<D>>(); + } + } + + @Override + public void onStart() { + super.onStart(); + + // Call out to sub classes so they can start their loaders + // Let the existing loaders know that we want to be notified when a load is complete + for (HashMap.Entry<Integer, LoaderInfo<D>> entry : mLoaders.entrySet()) { + LoaderInfo<D> info = entry.getValue(); + Loader<D> loader = info.loader; + int id = entry.getKey(); + if (loader == null) { + loader = onCreateLoader(id, info.args); + info.loader = loader; + } + loader.registerListener(id, this); + loader.startLoading(); + } + + mStarted = true; + } + + @Override + public void onStop() { + super.onStop(); + + for (HashMap.Entry<Integer, LoaderInfo<D>> entry : mLoaders.entrySet()) { + LoaderInfo<D> info = entry.getValue(); + Loader<D> loader = info.loader; + if (loader == null) { + continue; + } + + // Let the loader know we're done with it + loader.unregisterListener(this); + + // The loader isn't getting passed along to the next instance so ask it to stop loading + if (!getActivity().isChangingConfigurations()) { + loader.stopLoading(); + } + } + + mStarted = false; + } + + /** TO DO: This needs to be turned into a retained fragment. + @Override + public Object onRetainNonConfigurationInstance() { + // Pass the loader along to the next guy + Object result = mLoaders; + mLoaders = null; + return result; + } + **/ + + @Override + public void onDestroy() { + super.onDestroy(); + + if (mLoaders != null) { + for (HashMap.Entry<Integer, LoaderInfo<D>> entry : mLoaders.entrySet()) { + LoaderInfo<D> info = entry.getValue(); + Loader<D> loader = info.loader; + if (loader == null) { + continue; + } + loader.destroy(); + } + } + } + + /** + * @return the Loader with the given id or null if no matching Loader + * is found. + */ + public Loader<D> getLoader(int id) { + LoaderInfo<D> loaderInfo = mLoaders.get(id); + if (loaderInfo != null) { + return mLoaders.get(id).loader; + } + return null; + } +} diff --git a/core/java/android/app/LocalActivityManager.java b/core/java/android/app/LocalActivityManager.java index a24fcae..be8f413 100644 --- a/core/java/android/app/LocalActivityManager.java +++ b/core/java/android/app/LocalActivityManager.java @@ -112,11 +112,16 @@ public class LocalActivityManager { if (r.curState == INITIALIZING) { // Get the lastNonConfigurationInstance for the activity - HashMap<String,Object> lastNonConfigurationInstances = - mParent.getLastNonConfigurationChildInstances(); - Object instance = null; + HashMap<String, Object> lastNonConfigurationInstances = + mParent.getLastNonConfigurationChildInstances(); + Object instanceObj = null; if (lastNonConfigurationInstances != null) { - instance = lastNonConfigurationInstances.get(r.id); + instanceObj = lastNonConfigurationInstances.get(r.id); + } + Activity.NonConfigurationInstances instance = null; + if (instanceObj != null) { + instance = new Activity.NonConfigurationInstances(); + instance.activity = instanceObj; } // We need to have always created the activity. diff --git a/core/java/android/content/AsyncTaskLoader.java b/core/java/android/content/AsyncTaskLoader.java new file mode 100644 index 0000000..f43921f --- /dev/null +++ b/core/java/android/content/AsyncTaskLoader.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.os.AsyncTask; + +/** + * Abstract Loader that provides an {@link AsyncTask} to do the work. + * + * @param <D> the data type to be loaded. + */ +public abstract class AsyncTaskLoader<D> extends Loader<D> { + final class LoadTask extends AsyncTask<Void, Void, D> { + /* Runs on a worker thread */ + @Override + protected D doInBackground(Void... params) { + return AsyncTaskLoader.this.loadInBackground(); + } + + /* Runs on the UI thread */ + @Override + protected void onPostExecute(D data) { + AsyncTaskLoader.this.dispatchOnLoadComplete(data); + } + } + + LoadTask mTask; + + public AsyncTaskLoader(Context context) { + super(context); + } + + /** + * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously + * loaded data set and load a new one. + */ + @Override + public void forceLoad() { + mTask = new LoadTask(); + mTask.execute((Void[]) null); + } + + /** + * Attempt to cancel the current load task. See {@link AsyncTask#cancel(boolean)} + * for more info. + * + * @return <tt>false</tt> if the task could not be canceled, + * typically because it has already completed normally, or + * because {@link #startLoading()} hasn't been called, and + * <tt>true</tt> otherwise + */ + public boolean cancelLoad() { + if (mTask != null) { + return mTask.cancel(false); + } + return false; + } + + void dispatchOnLoadComplete(D data) { + mTask = null; + deliverResult(data); + } + + /** + * Called on a worker thread to perform the actual load. Implementations should not deliver the + * results directly, but should return them from this method, which will eventually end up + * calling deliverResult on the UI thread. If implementations need to process + * the results on the UI thread they may override deliverResult and do so + * there. + * + * @return the result of the load + */ + public abstract D loadInBackground(); +} diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 30822d4..0afd6d2 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -1372,7 +1372,6 @@ public abstract class Context { public static final String SENSOR_SERVICE = "sensor"; /** - * @hide * Use with {@link #getSystemService} to retrieve a {@link * android.os.storage.StorageManager} for accesssing system storage * functions. diff --git a/core/java/android/content/CursorLoader.java b/core/java/android/content/CursorLoader.java new file mode 100644 index 0000000..e1f9dca --- /dev/null +++ b/core/java/android/content/CursorLoader.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.database.Cursor; +import android.net.Uri; + +/** + * A loader that queries the {@link ContentResolver} and returns a {@link Cursor}. + */ +public class CursorLoader extends AsyncTaskLoader<Cursor> { + Cursor mCursor; + ForceLoadContentObserver mObserver; + boolean mStopped; + Uri mUri; + String[] mProjection; + String mSelection; + String[] mSelectionArgs; + String mSortOrder; + + /* Runs on a worker thread */ + @Override + public Cursor loadInBackground() { + Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection, + mSelectionArgs, mSortOrder); + // Ensure the cursor window is filled + if (cursor != null) { + cursor.getCount(); + cursor.registerContentObserver(mObserver); + } + return cursor; + } + + /* Runs on the UI thread */ + @Override + public void deliverResult(Cursor cursor) { + if (mStopped) { + // An async query came in while the loader is stopped + cursor.close(); + return; + } + mCursor = cursor; + super.deliverResult(cursor); + } + + public CursorLoader(Context context, Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + super(context); + mObserver = new ForceLoadContentObserver(); + mUri = uri; + mProjection = projection; + mSelection = selection; + mSelectionArgs = selectionArgs; + mSortOrder = sortOrder; + } + + /** + * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks + * will be called on the UI thread. If a previous load has been completed and is still valid + * the result may be passed to the callbacks immediately. + * + * Must be called from the UI thread + */ + @Override + public void startLoading() { + mStopped = false; + + if (mCursor != null) { + deliverResult(mCursor); + } else { + forceLoad(); + } + } + + /** + * Must be called from the UI thread + */ + @Override + public void stopLoading() { + if (mCursor != null && !mCursor.isClosed()) { + mCursor.close(); + mCursor = null; + } + + // Attempt to cancel the current load task if possible. + cancelLoad(); + + // Make sure that any outstanding loads clean themselves up properly + mStopped = true; + } + + @Override + public void destroy() { + // Ensure the loader is stopped + stopLoading(); + } + + public Uri getUri() { + return mUri; + } + + public void setUri(Uri uri) { + mUri = uri; + } + + public String[] getProjection() { + return mProjection; + } + + public void setProjection(String[] projection) { + mProjection = projection; + } + + public String getSelection() { + return mSelection; + } + + public void setSelection(String selection) { + mSelection = selection; + } + + public String[] getSelectionArgs() { + return mSelectionArgs; + } + + public void setSelectionArgs(String[] selectionArgs) { + mSelectionArgs = selectionArgs; + } + + public String getSortOrder() { + return mSortOrder; + } + + public void setSortOrder(String sortOrder) { + mSortOrder = sortOrder; + } +} diff --git a/core/java/android/content/Loader.java b/core/java/android/content/Loader.java new file mode 100644 index 0000000..db40e48 --- /dev/null +++ b/core/java/android/content/Loader.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.database.ContentObserver; +import android.os.Handler; + +/** + * An abstract class that performs asynchronous loading of data. While Loaders are active + * they should monitor the source of their data and deliver new results when the contents + * change. + * + * @param <D> The result returned when the load is complete + */ +public abstract class Loader<D> { + int mId; + OnLoadCompleteListener<D> mListener; + Context mContext; + + public final class ForceLoadContentObserver extends ContentObserver { + public ForceLoadContentObserver() { + super(new Handler()); + } + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + forceLoad(); + } + } + + public interface OnLoadCompleteListener<D> { + /** + * Called on the thread that created the Loader when the load is complete. + * + * @param loader the loader that completed the load + * @param data the result of the load + */ + public void onLoadComplete(Loader<D> loader, D data); + } + + /** + * Stores away the application context associated with context. Since Loaders can be used + * across multiple activities it's dangerous to store the context directly. + * + * @param context used to retrieve the application context. + */ + public Loader(Context context) { + mContext = context.getApplicationContext(); + } + + /** + * Sends the result of the load to the registered listener. Should only be called by subclasses. + * + * Must be called from the UI thread. + * + * @param data the result of the load + */ + public void deliverResult(D data) { + if (mListener != null) { + mListener.onLoadComplete(this, data); + } + } + + /** + * @return an application context retrieved from the Context passed to the constructor. + */ + public Context getContext() { + return mContext; + } + + /** + * @return the ID of this loader + */ + public int getId() { + return mId; + } + + /** + * Registers a class that will receive callbacks when a load is complete. The callbacks will + * be called on the UI thread so it's safe to pass the results to widgets. + * + * Must be called from the UI thread + */ + public void registerListener(int id, OnLoadCompleteListener<D> listener) { + if (mListener != null) { + throw new IllegalStateException("There is already a listener registered"); + } + mListener = listener; + mId = id; + } + + /** + * Must be called from the UI thread + */ + public void unregisterListener(OnLoadCompleteListener<D> listener) { + if (mListener == null) { + throw new IllegalStateException("No listener register"); + } + if (mListener != listener) { + throw new IllegalArgumentException("Attempting to unregister the wrong listener"); + } + mListener = null; + } + + /** + * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks + * will be called on the UI thread. If a previous load has been completed and is still valid + * the result may be passed to the callbacks immediately. The loader will monitor the source of + * the data set and may deliver future callbacks if the source changes. Calling + * {@link #stopLoading} will stop the delivery of callbacks. + * + * Must be called from the UI thread + */ + public abstract void startLoading(); + + /** + * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously + * loaded data set and load a new one. + */ + public abstract void forceLoad(); + + /** + * Stops delivery of updates until the next time {@link #startLoading()} is called + * + * Must be called from the UI thread + */ + public abstract void stopLoading(); + + /** + * Destroys the loader and frees its resources, making it unusable. + * + * Must be called from the UI thread + */ + public abstract void destroy(); +}
\ No newline at end of file diff --git a/core/java/android/content/SharedPreferences.java b/core/java/android/content/SharedPreferences.java index a15e29e..5847216 100644 --- a/core/java/android/content/SharedPreferences.java +++ b/core/java/android/content/SharedPreferences.java @@ -17,6 +17,7 @@ package android.content; import java.util.Map; +import java.util.Set; /** * Interface for accessing and modifying preference data returned by {@link @@ -69,6 +70,17 @@ public interface SharedPreferences { Editor putString(String key, String value); /** + * Set a set of String values in the preferences editor, to be written + * back once {@link #commit} is called. + * + * @param key The name of the preference to modify. + * @param values The new values for the preference. + * @return Returns a reference to the same Editor object, so you can + * chain put calls together. + */ + Editor putStringSet(String key, Set<String> values); + + /** * Set an int value in the preferences editor, to be written back once * {@link #commit} is called. * @@ -186,6 +198,20 @@ public interface SharedPreferences { String getString(String key, String defValue); /** + * Retrieve a set of String values from the preferences. + * + * @param key The name of the preference to retrieve. + * @param defValues Values to return if this preference does not exist. + * + * @return Returns the preference values if they exist, or defValues. + * Throws ClassCastException if there is a preference with this name + * that is not a Set. + * + * @throws ClassCastException + */ + Set<String> getStringSet(String key, Set<String> defValues); + + /** * Retrieve an int value from the preferences. * * @param key The name of the preference to retrieve. diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java index d0b67cc..7f749bb 100644 --- a/core/java/android/content/SyncManager.java +++ b/core/java/android/content/SyncManager.java @@ -16,6 +16,8 @@ package android.content; +import com.google.android.collect.Maps; + import com.android.internal.R; import com.android.internal.util.ArrayUtils; @@ -55,6 +57,7 @@ import android.util.Pair; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Random; @@ -126,14 +129,13 @@ public class SyncManager implements OnAccountsUpdateListener { private static final int INITIALIZATION_UNBIND_DELAY_MS = 5000; - private static final String SYNC_WAKE_LOCK = "SyncManagerSyncWakeLock"; + private static final String SYNC_WAKE_LOCK_PREFIX = "SyncWakeLock"; private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarmWakeLock"; private Context mContext; private volatile Account[] mAccounts = INITIAL_ACCOUNTS_ARRAY; - volatile private PowerManager.WakeLock mSyncWakeLock; volatile private PowerManager.WakeLock mHandleAlarmWakeLock; volatile private boolean mDataConnectionIsConnected = false; volatile private boolean mStorageIsLow = false; @@ -195,6 +197,8 @@ public class SyncManager implements OnAccountsUpdateListener { private static final Account[] INITIAL_ACCOUNTS_ARRAY = new Account[0]; + private final PowerManager mPowerManager; + public void onAccountsUpdated(Account[] accounts) { // remember if this was the first time this was called after an update final boolean justBootedUp = mAccounts == INITIAL_ACCOUNTS_ARRAY; @@ -356,15 +360,13 @@ public class SyncManager implements OnAccountsUpdateListener { } else { mNotificationMgr = null; } - PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - mSyncWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, SYNC_WAKE_LOCK); - mSyncWakeLock.setReferenceCounted(false); + mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); // This WakeLock is used to ensure that we stay awake between the time that we receive // a sync alarm notification and when we finish processing it. We need to do this // because we don't do the work in the alarm handler, rather we do it in a message // handler. - mHandleAlarmWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + mHandleAlarmWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, HANDLE_SYNC_ALARM_WAKE_LOCK); mHandleAlarmWakeLock.setReferenceCounted(false); @@ -1302,6 +1304,9 @@ public class SyncManager implements OnAccountsUpdateListener { public final SyncNotificationInfo mSyncNotificationInfo = new SyncNotificationInfo(); private Long mAlarmScheduleTime = null; public final SyncTimeTracker mSyncTimeTracker = new SyncTimeTracker(); + private PowerManager.WakeLock mSyncWakeLock; + private final HashMap<Pair<String, String>, PowerManager.WakeLock> mWakeLocks = + Maps.newHashMap(); // used to track if we have installed the error notification so that we don't reinstall // it if sync is still failing @@ -1315,6 +1320,18 @@ public class SyncManager implements OnAccountsUpdateListener { } } + private PowerManager.WakeLock getSyncWakeLock(String accountType, String authority) { + final Pair<String, String> wakeLockKey = Pair.create(accountType, authority); + PowerManager.WakeLock wakeLock = mWakeLocks.get(wakeLockKey); + if (wakeLock == null) { + final String name = SYNC_WAKE_LOCK_PREFIX + "_" + authority + "_" + accountType; + wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name); + wakeLock.setReferenceCounted(false); + mWakeLocks.put(wakeLockKey, wakeLock); + } + return wakeLock; + } + private void waitUntilReadyToRun() { CountDownLatch latch = mReadyToRunLatch; if (latch != null) { @@ -1477,8 +1494,9 @@ public class SyncManager implements OnAccountsUpdateListener { } } finally { final boolean isSyncInProgress = mActiveSyncContext != null; - if (!isSyncInProgress) { + if (!isSyncInProgress && mSyncWakeLock != null) { mSyncWakeLock.release(); + mSyncWakeLock = null; } manageSyncNotification(); manageErrorNotification(); @@ -1704,7 +1722,26 @@ public class SyncManager implements OnAccountsUpdateListener { return; } - mSyncWakeLock.acquire(); + // Find the wakelock for this account and authority and store it in mSyncWakeLock. + // Be sure to release the previous wakelock so that we don't end up with it being + // held until it is used again. + // There are a couple tricky things about this code: + // - make sure that we acquire the new wakelock before releasing the old one, + // otherwise the device might go to sleep as soon as we release it. + // - since we use non-reference counted wakelocks we have to be sure not to do + // the release if the wakelock didn't change. Othewise we would do an + // acquire followed by a release on the same lock, resulting in no lock + // being held. + PowerManager.WakeLock oldWakeLock = mSyncWakeLock; + try { + mSyncWakeLock = getSyncWakeLock(op.account.type, op.authority); + mSyncWakeLock.acquire(); + } finally { + if (oldWakeLock != null && oldWakeLock != mSyncWakeLock) { + oldWakeLock.release(); + } + } + // no need to schedule an alarm, as that will be done by our caller. // the next step will occur when we get either a timeout or a diff --git a/core/java/android/database/AbstractCursor.java b/core/java/android/database/AbstractCursor.java index 038eedf..6170bae 100644 --- a/core/java/android/database/AbstractCursor.java +++ b/core/java/android/database/AbstractCursor.java @@ -18,16 +18,11 @@ package android.database; import android.content.ContentResolver; import android.net.Uri; +import android.os.Bundle; import android.util.Config; import android.util.Log; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; import java.lang.ref.WeakReference; -import java.lang.UnsupportedOperationException; import java.util.HashMap; import java.util.Map; @@ -88,7 +83,7 @@ public abstract class AbstractCursor implements CrossProcessCursor { } mDataSetObservable.notifyInvalidated(); } - + public boolean requery() { if (mSelfObserver != null && mSelfObserverRegistered == false) { mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver); @@ -109,22 +104,6 @@ public abstract class AbstractCursor implements CrossProcessCursor { } /** - * @hide - * @deprecated - */ - public boolean commitUpdates(Map<? extends Long,? extends Map<String,Object>> values) { - return false; - } - - /** - * @hide - * @deprecated - */ - public boolean deleteRow() { - return false; - } - - /** * This function is called every time the cursor is successfully scrolled * to a new position, giving the subclass a chance to update any state it * may have. If it returns false the move function will also do so and the @@ -320,137 +299,6 @@ public abstract class AbstractCursor implements CrossProcessCursor { return getColumnNames()[columnIndex]; } - /** - * @hide - * @deprecated - */ - public boolean updateBlob(int columnIndex, byte[] value) { - return update(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateString(int columnIndex, String value) { - return update(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateShort(int columnIndex, short value) { - return update(columnIndex, Short.valueOf(value)); - } - - /** - * @hide - * @deprecated - */ - public boolean updateInt(int columnIndex, int value) { - return update(columnIndex, Integer.valueOf(value)); - } - - /** - * @hide - * @deprecated - */ - public boolean updateLong(int columnIndex, long value) { - return update(columnIndex, Long.valueOf(value)); - } - - /** - * @hide - * @deprecated - */ - public boolean updateFloat(int columnIndex, float value) { - return update(columnIndex, Float.valueOf(value)); - } - - /** - * @hide - * @deprecated - */ - public boolean updateDouble(int columnIndex, double value) { - return update(columnIndex, Double.valueOf(value)); - } - - /** - * @hide - * @deprecated - */ - public boolean updateToNull(int columnIndex) { - return update(columnIndex, null); - } - - /** - * @hide - * @deprecated - */ - public boolean update(int columnIndex, Object obj) { - if (!supportsUpdates()) { - return false; - } - - // Long.valueOf() returns null sometimes! -// Long rowid = Long.valueOf(getLong(mRowIdColumnIndex)); - Long rowid = new Long(getLong(mRowIdColumnIndex)); - if (rowid == null) { - throw new IllegalStateException("null rowid. mRowIdColumnIndex = " + mRowIdColumnIndex); - } - - synchronized(mUpdatedRows) { - Map<String, Object> row = mUpdatedRows.get(rowid); - if (row == null) { - row = new HashMap<String, Object>(); - mUpdatedRows.put(rowid, row); - } - row.put(getColumnNames()[columnIndex], obj); - } - - return true; - } - - /** - * Returns <code>true</code> if there are pending updates that have not yet been committed. - * - * @return <code>true</code> if there are pending updates that have not yet been committed. - * @hide - * @deprecated - */ - public boolean hasUpdates() { - synchronized(mUpdatedRows) { - return mUpdatedRows.size() > 0; - } - } - - /** - * @hide - * @deprecated - */ - public void abortUpdates() { - synchronized(mUpdatedRows) { - mUpdatedRows.clear(); - } - } - - /** - * @hide - * @deprecated - */ - public boolean commitUpdates() { - return commitUpdates(null); - } - - /** - * @hide - * @deprecated - */ - public boolean supportsUpdates() { - return mRowIdColumnIndex != -1; - } - public void registerContentObserver(ContentObserver observer) { mContentObservable.registerObserver(observer); } diff --git a/core/java/android/database/BulkCursorNative.java b/core/java/android/database/BulkCursorNative.java index baa94d8..fa62d69 100644 --- a/core/java/android/database/BulkCursorNative.java +++ b/core/java/android/database/BulkCursorNative.java @@ -17,13 +17,10 @@ package android.database; import android.os.Binder; -import android.os.RemoteException; +import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; -import android.os.Bundle; - -import java.util.HashMap; -import java.util.Map; +import android.os.RemoteException; /** * Native implementation of the bulk cursor. This is only for use in implementing @@ -120,26 +117,6 @@ public abstract class BulkCursorNative extends Binder implements IBulkCursor return true; } - case UPDATE_ROWS_TRANSACTION: { - data.enforceInterface(IBulkCursor.descriptor); - // TODO - what ClassLoader should be passed to readHashMap? - // TODO - switch to Bundle - HashMap<Long, Map<String, Object>> values = data.readHashMap(null); - boolean result = updateRows(values); - reply.writeNoException(); - reply.writeInt((result == true ? 1 : 0)); - return true; - } - - case DELETE_ROW_TRANSACTION: { - data.enforceInterface(IBulkCursor.descriptor); - int position = data.readInt(); - boolean result = deleteRow(position); - reply.writeNoException(); - reply.writeInt((result == true ? 1 : 0)); - return true; - } - case ON_MOVE_TRANSACTION: { data.enforceInterface(IBulkCursor.descriptor); int position = data.readInt(); @@ -343,48 +320,6 @@ final class BulkCursorProxy implements IBulkCursor { return count; } - public boolean updateRows(Map values) throws RemoteException - { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - - data.writeInterfaceToken(IBulkCursor.descriptor); - - data.writeMap(values); - - mRemote.transact(UPDATE_ROWS_TRANSACTION, data, reply, 0); - - DatabaseUtils.readExceptionFromParcel(reply); - - boolean result = (reply.readInt() == 1 ? true : false); - - data.recycle(); - reply.recycle(); - - return result; - } - - public boolean deleteRow(int position) throws RemoteException - { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - - data.writeInterfaceToken(IBulkCursor.descriptor); - - data.writeInt(position); - - mRemote.transact(DELETE_ROW_TRANSACTION, data, reply, 0); - - DatabaseUtils.readExceptionFromParcel(reply); - - boolean result = (reply.readInt() == 1 ? true : false); - - data.recycle(); - reply.recycle(); - - return result; - } - public boolean getWantsAllOnMoveCalls() throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); diff --git a/core/java/android/database/BulkCursorToCursorAdaptor.java b/core/java/android/database/BulkCursorToCursorAdaptor.java index 1469ea2..2cb2aec 100644 --- a/core/java/android/database/BulkCursorToCursorAdaptor.java +++ b/core/java/android/database/BulkCursorToCursorAdaptor.java @@ -16,12 +16,10 @@ package android.database; -import android.os.RemoteException; import android.os.Bundle; +import android.os.RemoteException; import android.util.Log; -import java.util.Map; - /** * Adapts an {@link IBulkCursor} to a {@link Cursor} for use in the local * process. @@ -174,38 +172,6 @@ public final class BulkCursorToCursorAdaptor extends AbstractWindowedCursor { } } - /** - * @hide - * @deprecated - */ - @Override - public boolean deleteRow() { - try { - boolean result = mBulkCursor.deleteRow(mPos); - if (result != false) { - // The window contains the old value, discard it - mWindow = null; - - // Fix up the position - mCount = mBulkCursor.count(); - if (mPos < mCount) { - int oldPos = mPos; - mPos = -1; - moveToPosition(oldPos); - } else { - mPos = mCount; - } - - // Send the change notification - onChange(true); - } - return result; - } catch (RemoteException ex) { - Log.e(TAG, "Unable to delete row because the remote process is dead"); - return false; - } - } - @Override public String[] getColumnNames() { if (mColumns == null) { @@ -219,44 +185,6 @@ public final class BulkCursorToCursorAdaptor extends AbstractWindowedCursor { return mColumns; } - /** - * @hide - * @deprecated - */ - @Override - public boolean commitUpdates(Map<? extends Long, - ? extends Map<String,Object>> additionalValues) { - if (!supportsUpdates()) { - Log.e(TAG, "commitUpdates not supported on this cursor, did you include the _id column?"); - return false; - } - - synchronized(mUpdatedRows) { - if (additionalValues != null) { - mUpdatedRows.putAll(additionalValues); - } - - if (mUpdatedRows.size() <= 0) { - return false; - } - - try { - boolean result = mBulkCursor.updateRows(mUpdatedRows); - - if (result == true) { - mUpdatedRows.clear(); - - // Send the change notification - onChange(true); - } - return result; - } catch (RemoteException ex) { - Log.e(TAG, "Unable to commit updates because the remote process is dead"); - return false; - } - } - } - @Override public Bundle getExtras() { try { diff --git a/core/java/android/database/Cursor.java b/core/java/android/database/Cursor.java index 6539156..fee658a 100644 --- a/core/java/android/database/Cursor.java +++ b/core/java/android/database/Cursor.java @@ -146,22 +146,6 @@ public interface Cursor { boolean isAfterLast(); /** - * Removes the row at the current cursor position from the underlying data - * store. After this method returns the cursor will be pointing to the row - * after the row that is deleted. This has the side effect of decrementing - * the result of count() by one. - * <p> - * The query must have the row ID column in its selection, otherwise this - * call will fail. - * - * @hide - * @return whether the record was successfully deleted. - * @deprecated use {@link ContentResolver#delete(Uri, String, String[])} - */ - @Deprecated - boolean deleteRow(); - - /** * Returns the zero-based index for the given column name, or -1 if the column doesn't exist. * If you expect the column to exist use {@link #getColumnIndexOrThrow(String)} instead, which * will make the error more clear. @@ -303,188 +287,6 @@ public interface Cursor { boolean isNull(int columnIndex); /** - * Returns <code>true</code> if the cursor supports updates. - * - * @return whether the cursor supports updates. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean supportsUpdates(); - - /** - * Returns <code>true</code> if there are pending updates that have not yet been committed. - * - * @return <code>true</code> if there are pending updates that have not yet been committed. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean hasUpdates(); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateBlob(int columnIndex, byte[] value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateString(int columnIndex, String value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateShort(int columnIndex, short value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateInt(int columnIndex, int value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateLong(int columnIndex, long value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateFloat(int columnIndex, float value); - - /** - * Updates the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @param value the new value. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateDouble(int columnIndex, double value); - - /** - * Removes the value for the given column in the row the cursor is - * currently pointing at. Updates are not committed to the backing store - * until {@link #commitUpdates()} is called. - * - * @param columnIndex the zero-based index of the target column. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean updateToNull(int columnIndex); - - /** - * Atomically commits all updates to the backing store. After completion, - * this method leaves the data in an inconsistent state and you should call - * {@link #requery} before reading data from the cursor again. - * - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean commitUpdates(); - - /** - * Atomically commits all updates to the backing store, as well as the - * updates included in values. After completion, - * this method leaves the data in an inconsistent state and you should call - * {@link #requery} before reading data from the cursor again. - * - * @param values A map from row IDs to Maps associating column names with - * updated values. A null value indicates the field should be - removed. - * @return whether the operation succeeded. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - boolean commitUpdates(Map<? extends Long, - ? extends Map<String,Object>> values); - - /** - * Reverts all updates made to the cursor since the last call to - * commitUpdates. - * @hide - * @deprecated use the {@link ContentResolver} update methods instead of the Cursor - * update methods - */ - @Deprecated - void abortUpdates(); - - /** * Deactivates the Cursor, making all calls on it fail until {@link #requery} is called. * Inactive Cursors use fewer resources than active Cursors. * Calling {@link #requery} will make the cursor active again. @@ -496,6 +298,10 @@ public interface Cursor { * contents. This may be done at any time, including after a call to {@link * #deactivate}. * + * Since this method could execute a query on the database and potentially take + * a while, it could cause ANR if it is called on Main (UI) thread. + * A warning is printed if this method is being executed on Main thread. + * * @return true if the requery succeeded, false if not, in which case the * cursor becomes invalid. */ diff --git a/core/java/android/database/CursorToBulkCursorAdaptor.java b/core/java/android/database/CursorToBulkCursorAdaptor.java index 748eb99..8bc7de2 100644 --- a/core/java/android/database/CursorToBulkCursorAdaptor.java +++ b/core/java/android/database/CursorToBulkCursorAdaptor.java @@ -16,16 +16,12 @@ package android.database; -import android.database.sqlite.SQLiteMisuseException; -import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.util.Config; import android.util.Log; -import java.util.Map; - /** * Wraps a BulkCursor around an existing Cursor making it remotable. @@ -38,7 +34,6 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative private final CrossProcessCursor mCursor; private CursorWindow mWindow; private final String mProviderName; - private final boolean mReadOnly; private ContentObserverProxy mObserver; private static final class ContentObserverProxy extends ContentObserver @@ -98,7 +93,6 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative "Only CrossProcessCursor cursors are supported across process for now", e); } mProviderName = providerName; - mReadOnly = !allowWrite; createAndRegisterObserverProxy(observer); } @@ -197,31 +191,6 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative } } - public boolean updateRows(Map<? extends Long, ? extends Map<String, Object>> values) { - if (mReadOnly) { - Log.w("ContentProvider", "Permission Denial: modifying " - + mProviderName - + " from pid=" + Binder.getCallingPid() - + ", uid=" + Binder.getCallingUid()); - return false; - } - return mCursor.commitUpdates(values); - } - - public boolean deleteRow(int position) { - if (mReadOnly) { - Log.w("ContentProvider", "Permission Denial: modifying " - + mProviderName - + " from pid=" + Binder.getCallingPid() - + ", uid=" + Binder.getCallingUid()); - return false; - } - if (mCursor.moveToPosition(position) == false) { - return false; - } - return mCursor.deleteRow(); - } - public Bundle getExtras() { return mCursor.getExtras(); } diff --git a/core/java/android/database/CursorWrapper.java b/core/java/android/database/CursorWrapper.java index f0aa7d7..633b2b3 100644 --- a/core/java/android/database/CursorWrapper.java +++ b/core/java/android/database/CursorWrapper.java @@ -32,14 +32,6 @@ public class CursorWrapper implements Cursor { public CursorWrapper(Cursor cursor) { mCursor = cursor; } - - /** - * @hide - * @deprecated - */ - public void abortUpdates() { - mCursor.abortUpdates(); - } public void close() { mCursor.close(); @@ -49,23 +41,6 @@ public class CursorWrapper implements Cursor { return mCursor.isClosed(); } - /** - * @hide - * @deprecated - */ - public boolean commitUpdates() { - return mCursor.commitUpdates(); - } - - /** - * @hide - * @deprecated - */ - public boolean commitUpdates( - Map<? extends Long, ? extends Map<String, Object>> values) { - return mCursor.commitUpdates(values); - } - public int getCount() { return mCursor.getCount(); } @@ -74,14 +49,6 @@ public class CursorWrapper implements Cursor { mCursor.deactivate(); } - /** - * @hide - * @deprecated - */ - public boolean deleteRow() { - return mCursor.deleteRow(); - } - public boolean moveToFirst() { return mCursor.moveToFirst(); } @@ -147,14 +114,6 @@ public class CursorWrapper implements Cursor { return mCursor.getWantsAllOnMoveCalls(); } - /** - * @hide - * @deprecated - */ - public boolean hasUpdates() { - return mCursor.hasUpdates(); - } - public boolean isAfterLast() { return mCursor.isAfterLast(); } @@ -219,14 +178,6 @@ public class CursorWrapper implements Cursor { mCursor.setNotificationUri(cr, uri); } - /** - * @hide - * @deprecated - */ - public boolean supportsUpdates() { - return mCursor.supportsUpdates(); - } - public void unregisterContentObserver(ContentObserver observer) { mCursor.unregisterContentObserver(observer); } @@ -235,71 +186,6 @@ public class CursorWrapper implements Cursor { mCursor.unregisterDataSetObserver(observer); } - /** - * @hide - * @deprecated - */ - public boolean updateDouble(int columnIndex, double value) { - return mCursor.updateDouble(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateFloat(int columnIndex, float value) { - return mCursor.updateFloat(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateInt(int columnIndex, int value) { - return mCursor.updateInt(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateLong(int columnIndex, long value) { - return mCursor.updateLong(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateShort(int columnIndex, short value) { - return mCursor.updateShort(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateString(int columnIndex, String value) { - return mCursor.updateString(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateBlob(int columnIndex, byte[] value) { - return mCursor.updateBlob(columnIndex, value); - } - - /** - * @hide - * @deprecated - */ - public boolean updateToNull(int columnIndex) { - return mCursor.updateToNull(columnIndex); - } - - private Cursor mCursor; - + private Cursor mCursor; } diff --git a/core/java/android/database/DataSetObservable.java b/core/java/android/database/DataSetObservable.java index 9200e81..51c72c1 100644 --- a/core/java/android/database/DataSetObservable.java +++ b/core/java/android/database/DataSetObservable.java @@ -27,8 +27,12 @@ public class DataSetObservable extends Observable<DataSetObserver> { */ public void notifyChanged() { synchronized(mObservers) { - for (DataSetObserver observer : mObservers) { - observer.onChanged(); + // since onChanged() is implemented by the app, it could do anything, including + // removing itself from {@link mObservers} - and that could cause problems if + // an iterator is used on the ArrayList {@link mObservers}. + // to avoid such problems, just march thru the list in the reverse order. + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onChanged(); } } } @@ -39,8 +43,8 @@ public class DataSetObservable extends Observable<DataSetObserver> { */ public void notifyInvalidated() { synchronized (mObservers) { - for (DataSetObserver observer : mObservers) { - observer.onInvalidated(); + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onInvalidated(); } } } diff --git a/core/java/android/database/DatabaseErrorHandler.java b/core/java/android/database/DatabaseErrorHandler.java new file mode 100644 index 0000000..f0c5452 --- /dev/null +++ b/core/java/android/database/DatabaseErrorHandler.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.database; + +import android.database.sqlite.SQLiteDatabase; + +/** + * An interface to let the apps define the actions to take when the following errors are detected + * database corruption + */ +public interface DatabaseErrorHandler { + + /** + * defines the method to be invoked when database corruption is detected. + * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption + * is detected. + */ + void onCorruption(SQLiteDatabase dbObj); +} diff --git a/core/java/android/database/DefaultDatabaseErrorHandler.java b/core/java/android/database/DefaultDatabaseErrorHandler.java new file mode 100644 index 0000000..0bad37a --- /dev/null +++ b/core/java/android/database/DefaultDatabaseErrorHandler.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.database; + +import java.io.File; +import java.util.ArrayList; + +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.util.Log; +import android.util.Pair; + +/** + * Default class used defining the actions to take when the following errors are detected + * database corruption + */ +public final class DefaultDatabaseErrorHandler implements DatabaseErrorHandler { + + private static final String TAG = "DefaultDatabaseErrorHandler"; + + /** + * defines the default method to be invoked when database corruption is detected. + * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption + * is detected. + */ + public void onCorruption(SQLiteDatabase dbObj) { + Log.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath()); + + // is the corruption detected even before database could be 'opened'? + if (!dbObj.isOpen()) { + // database files are not even openable. delete this database file. + // NOTE if the database has attached databases, then any of them could be corrupt. + // and not deleting all of them could cause corrupted database file to remain and + // make the application crash on database open operation. To avoid this problem, + // the application should provide its own {@link DatabaseErrorHandler} impl class + // to delete ALL files of the database (including the attached databases). + if (!dbObj.getPath().equalsIgnoreCase(":memory:")) { + // not memory database. + try { + new File(dbObj.getPath()).delete(); + } catch (Exception e) { + /* ignore */ + } + } + return; + } + + ArrayList<Pair<String, String>> attachedDbs = null; + try { + // Close the database, which will cause subsequent operations to fail. + // before that, get the attached database list first. + attachedDbs = dbObj.getAttachedDbs(); + try { + dbObj.close(); + } catch (SQLiteException e) { + /* ignore */ + } + } finally { + // Delete all files of this corrupt database and/or attached databases + if (attachedDbs != null) { + for (Pair<String, String> p : attachedDbs) { + // delete file if it is a non-memory database file + if (p.second.equalsIgnoreCase(":memory:") || p.second.trim().length() == 0) { + continue; + } + Log.e(TAG, "deleting the database file: " + p.second); + try { + new File(p.second).delete(); + } catch (Exception e) { + /* ignore */ + } + } + } + } + } +} diff --git a/core/java/android/database/IBulkCursor.java b/core/java/android/database/IBulkCursor.java index 46790a3..244c88f 100644 --- a/core/java/android/database/IBulkCursor.java +++ b/core/java/android/database/IBulkCursor.java @@ -16,16 +16,14 @@ package android.database; -import android.os.RemoteException; +import android.os.Bundle; import android.os.IBinder; import android.os.IInterface; -import android.os.Bundle; - -import java.util.Map; +import android.os.RemoteException; /** * This interface provides a low-level way to pass bulk cursor data across - * both process and language boundries. Application code should use the Cursor + * both process and language boundaries. Application code should use the Cursor * interface directly. * * {@hide} @@ -54,10 +52,6 @@ public interface IBulkCursor extends IInterface { */ public String[] getColumnNames() throws RemoteException; - public boolean updateRows(Map<? extends Long, ? extends Map<String, Object>> values) throws RemoteException; - - public boolean deleteRow(int position) throws RemoteException; - public void deactivate() throws RemoteException; public void close() throws RemoteException; @@ -76,8 +70,6 @@ public interface IBulkCursor extends IInterface { static final int GET_CURSOR_WINDOW_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION; static final int COUNT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 1; static final int GET_COLUMN_NAMES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2; - static final int UPDATE_ROWS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3; - static final int DELETE_ROW_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 4; static final int DEACTIVATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 5; static final int REQUERY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 6; static final int ON_MOVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 7; diff --git a/core/java/android/database/MergeCursor.java b/core/java/android/database/MergeCursor.java index 722d707..cb6d7ac 100644 --- a/core/java/android/database/MergeCursor.java +++ b/core/java/android/database/MergeCursor.java @@ -92,32 +92,6 @@ public class MergeCursor extends AbstractCursor return false; } - /** - * @hide - * @deprecated - */ - @Override - public boolean deleteRow() - { - return mCursor.deleteRow(); - } - - /** - * @hide - * @deprecated - */ - @Override - public boolean commitUpdates() { - int length = mCursors.length; - for (int i = 0 ; i < length ; i++) { - if (mCursors[i] != null) { - mCursors[i].commitUpdates(); - } - } - onChange(true); - return true; - } - @Override public String getString(int column) { diff --git a/core/java/android/database/RequeryOnUiThreadException.java b/core/java/android/database/RequeryOnUiThreadException.java new file mode 100644 index 0000000..97a50d8 --- /dev/null +++ b/core/java/android/database/RequeryOnUiThreadException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.database; + +/** + * An exception that indicates invoking {@link Cursor#requery()} on Main thread could cause ANR. + * This exception should encourage apps to invoke {@link Cursor#requery()} in a background thread. + * @hide + */ +public class RequeryOnUiThreadException extends RuntimeException { + public RequeryOnUiThreadException(String packageName) { + super("In " + packageName + " Requery is executing on main (UI) thread. could cause ANR. " + + "do it in background thread."); + } +} diff --git a/core/java/android/database/sqlite/DatabaseObjectNotClosedException.java b/core/java/android/database/sqlite/DatabaseObjectNotClosedException.java index 8ac4c0f..f28c70f 100644 --- a/core/java/android/database/sqlite/DatabaseObjectNotClosedException.java +++ b/core/java/android/database/sqlite/DatabaseObjectNotClosedException.java @@ -21,13 +21,11 @@ package android.database.sqlite; * that is not explicitly closed * @hide */ -public class DatabaseObjectNotClosedException extends RuntimeException -{ +public class DatabaseObjectNotClosedException extends RuntimeException { private static final String s = "Application did not close the cursor or database object " + "that was opened here"; - public DatabaseObjectNotClosedException() - { + public DatabaseObjectNotClosedException() { super(s); } } diff --git a/core/java/android/database/sqlite/SQLiteCompiledSql.java b/core/java/android/database/sqlite/SQLiteCompiledSql.java index 25aa9b3..16ff2ab 100644 --- a/core/java/android/database/sqlite/SQLiteCompiledSql.java +++ b/core/java/android/database/sqlite/SQLiteCompiledSql.java @@ -78,20 +78,13 @@ import android.util.Log; * existing compiled SQL program already around */ private void compile(String sql, boolean forceCompilation) { - if (!mDatabase.isOpen()) { - throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); - } + mDatabase.verifyLockOwner(); // Only compile if we don't have a valid statement already or the caller has // explicitly requested a recompile. if (forceCompilation) { - mDatabase.lock(); - try { - // Note that the native_compile() takes care of destroying any previously - // existing programs before it compiles. - native_compile(sql); - } finally { - mDatabase.unlock(); - } + // Note that the native_compile() takes care of destroying any previously + // existing programs before it compiles. + native_compile(sql); } } @@ -134,6 +127,10 @@ import android.util.Log; mInUse = false; } + /* package */ synchronized boolean isInUse() { + return mInUse; + } + /** * Make sure that the native resource is cleaned up. */ diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index c7e58fa..eecd01e 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -16,20 +16,19 @@ package android.database.sqlite; +import android.app.ActivityThread; import android.database.AbstractWindowedCursor; import android.database.CursorWindow; import android.database.DataSetObserver; -import android.database.SQLException; - +import android.database.RequeryOnUiThreadException; import android.os.Handler; +import android.os.Looper; import android.os.Message; import android.os.Process; -import android.text.TextUtils; import android.util.Config; import android.util.Log; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; @@ -77,6 +76,11 @@ public class SQLiteCursor extends AbstractWindowedCursor { private int mCursorState = 0; private ReentrantLock mLock = null; private boolean mPendingData = false; + + /** + * Used by {@link #requery()} to remember for which database we've already shown the warning. + */ + private static final HashMap<String, Boolean> sAlreadyWarned = new HashMap<String, Boolean>(); /** * support for a cursor variant that doesn't always read all results @@ -321,166 +325,11 @@ public class SQLiteCursor extends AbstractWindowedCursor { } } - /** - * @hide - * @deprecated - */ - @Override - public boolean deleteRow() { - checkPosition(); - - // Only allow deletes if there is an ID column, and the ID has been read from it - if (mRowIdColumnIndex == -1 || mCurrentRowID == null) { - Log.e(TAG, - "Could not delete row because either the row ID column is not available or it" + - "has not been read."); - return false; - } - - boolean success; - - /* - * Ensure we don't change the state of the database when another - * thread is holding the database lock. requery() and moveTo() are also - * synchronized here to make sure they get the state of the database - * immediately following the DELETE. - */ - mDatabase.lock(); - try { - try { - mDatabase.delete(mEditTable, mColumns[mRowIdColumnIndex] + "=?", - new String[] {mCurrentRowID.toString()}); - success = true; - } catch (SQLException e) { - success = false; - } - - int pos = mPos; - requery(); - - /* - * Ensure proper cursor state. Note that mCurrentRowID changes - * in this call. - */ - moveToPosition(pos); - } finally { - mDatabase.unlock(); - } - - if (success) { - onChange(true); - return true; - } else { - return false; - } - } - @Override public String[] getColumnNames() { return mColumns; } - /** - * @hide - * @deprecated - */ - @Override - public boolean supportsUpdates() { - return super.supportsUpdates() && !TextUtils.isEmpty(mEditTable); - } - - /** - * @hide - * @deprecated - */ - @Override - public boolean commitUpdates(Map<? extends Long, - ? extends Map<String, Object>> additionalValues) { - if (!supportsUpdates()) { - Log.e(TAG, "commitUpdates not supported on this cursor, did you " - + "include the _id column?"); - return false; - } - - /* - * Prevent other threads from changing the updated rows while they're - * being processed here. - */ - synchronized (mUpdatedRows) { - if (additionalValues != null) { - mUpdatedRows.putAll(additionalValues); - } - - if (mUpdatedRows.size() == 0) { - return true; - } - - /* - * Prevent other threads from changing the database state while - * we process the updated rows, and prevents us from changing the - * database behind the back of another thread. - */ - mDatabase.beginTransaction(); - try { - StringBuilder sql = new StringBuilder(128); - - // For each row that has been updated - for (Map.Entry<Long, Map<String, Object>> rowEntry : - mUpdatedRows.entrySet()) { - Map<String, Object> values = rowEntry.getValue(); - Long rowIdObj = rowEntry.getKey(); - - if (rowIdObj == null || values == null) { - throw new IllegalStateException("null rowId or values found! rowId = " - + rowIdObj + ", values = " + values); - } - - if (values.size() == 0) { - continue; - } - - long rowId = rowIdObj.longValue(); - - Iterator<Map.Entry<String, Object>> valuesIter = - values.entrySet().iterator(); - - sql.setLength(0); - sql.append("UPDATE " + mEditTable + " SET "); - - // For each column value that has been updated - Object[] bindings = new Object[values.size()]; - int i = 0; - while (valuesIter.hasNext()) { - Map.Entry<String, Object> entry = valuesIter.next(); - sql.append(entry.getKey()); - sql.append("=?"); - bindings[i] = entry.getValue(); - if (valuesIter.hasNext()) { - sql.append(", "); - } - i++; - } - - sql.append(" WHERE " + mColumns[mRowIdColumnIndex] - + '=' + rowId); - sql.append(';'); - mDatabase.execSQL(sql.toString(), bindings); - mDatabase.rowUpdated(mEditTable, rowId); - } - mDatabase.setTransactionSuccessful(); - } finally { - mDatabase.endTransaction(); - } - - mUpdatedRows.clear(); - } - - // Let any change observers know about the update - onChange(true); - - return true; - } - private void deactivateCommon() { if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this); mCursorState = 0; @@ -506,11 +355,30 @@ public class SQLiteCursor extends AbstractWindowedCursor { mDriver.cursorClosed(); } + /** + * Show a warning against the use of requery() if called on the main thread. + * This warning is shown per database per process. + */ + private void warnIfUiThread() { + if (Looper.getMainLooper() == Looper.myLooper()) { + String databasePath = mDatabase.getPath(); + // We show the warning once per database in order not to spam logcat. + if (!sAlreadyWarned.containsKey(databasePath)) { + sAlreadyWarned.put(databasePath, true); + String packageName = ActivityThread.currentPackageName(); + Log.w(TAG, "should not attempt requery on main (UI) thread: app = " + + packageName == null ? "'unknown'" : packageName, + new RequeryOnUiThreadException(packageName)); + } + } + } + @Override public boolean requery() { if (isClosed()) { return false; } + warnIfUiThread(); long timeStart = 0; if (Config.LOGV) { timeStart = System.currentTimeMillis(); diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index d4f9b20..47de286 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -16,12 +16,12 @@ package android.database.sqlite; -import com.google.android.collect.Maps; - import android.app.ActivityThread; import android.content.ContentValues; import android.database.Cursor; +import android.database.DatabaseErrorHandler; import android.database.DatabaseUtils; +import android.database.DefaultDatabaseErrorHandler; import android.database.SQLException; import android.database.sqlite.SQLiteDebug.DbStats; import android.os.Debug; @@ -35,11 +35,11 @@ import android.util.Pair; import java.io.File; import java.lang.ref.WeakReference; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import java.util.Random; @@ -251,7 +251,7 @@ public class SQLiteDatabase extends SQLiteClosable { private WeakHashMap<SQLiteClosable, Object> mPrograms; /** - * for each instance of this class, a cache is maintained to store + * for each instance of this class, a LRU cache is maintained to store * the compiled query statement ids returned by sqlite database. * key = sql statement with "?" for bind args * value = {@link SQLiteCompiledSql} @@ -263,15 +263,42 @@ public class SQLiteDatabase extends SQLiteClosable { * invoked. * * this cache has an upper limit of mMaxSqlCacheSize (settable by calling the method - * (@link setMaxCacheSize(int)}). its default is 0 - i.e., no caching by default because - * most of the apps don't use "?" syntax in their sql, caching is not useful for them. - */ - /* package */ Map<String, SQLiteCompiledSql> mCompiledQueries = Maps.newHashMap(); + * (@link setMaxSqlCacheSize(int)}). + */ + // default statement-cache size per database connection ( = instance of this class) + private int mMaxSqlCacheSize = 25; + /* package */ Map<String, SQLiteCompiledSql> mCompiledQueries = + new LinkedHashMap<String, SQLiteCompiledSql>(mMaxSqlCacheSize + 1, 0.75f, true) { + @Override + public boolean removeEldestEntry(Map.Entry<String, SQLiteCompiledSql> eldest) { + // eldest = least-recently used entry + // if it needs to be removed to accommodate a new entry, + // close {@link SQLiteCompiledSql} represented by this entry, if not in use + // and then let it be removed from the Map. + // when this is called, the caller must be trying to add a just-compiled stmt + // to cache; i.e., caller should already have acquired database lock AND + // the lock on mCompiledQueries. do as assert of these two 2 facts. + verifyLockOwner(); + if (this.size() <= mMaxSqlCacheSize) { + // cache is not full. nothing needs to be removed + return false; + } + // cache is full. eldest will be removed. + SQLiteCompiledSql entry = eldest.getValue(); + if (!entry.isInUse()) { + // this {@link SQLiteCompiledSql} is not in use. release it. + entry.releaseSqlStatement(); + } + // return true, so that this entry is removed automatically by the caller. + return true; + } + }; /** - * @hide + * absolute max value that can be set by {@link #setMaxSqlCacheSize(int)} + * size of each prepared-statement is between 1K - 6K, depending on the complexity of the + * sql statement & schema. */ - public static final int MAX_SQL_CACHE_SIZE = 250; - private int mMaxSqlCacheSize = MAX_SQL_CACHE_SIZE; // max cache size per Database instance + public static final int MAX_SQL_CACHE_SIZE = 100; private int mCacheFullWarnings; private static final int MAX_WARNINGS_ON_CACHESIZE_CONDITION = 1; @@ -279,10 +306,6 @@ public class SQLiteDatabase extends SQLiteClosable { private int mNumCacheHits; private int mNumCacheMisses; - /** the following 2 members maintain the time when a database is opened and closed */ - private String mTimeOpened = null; - private String mTimeClosed = null; - /** Used to find out where this object was created in case it never got closed. */ private Throwable mStackTrace = null; @@ -290,6 +313,11 @@ public class SQLiteDatabase extends SQLiteClosable { private static final String LOG_SLOW_QUERIES_PROPERTY = "db.log.slow_query_threshold"; private final int mSlowQueryThreshold; + /** {@link DatabaseErrorHandler} to be used when SQLite returns any of the following errors + * Corruption + * */ + private DatabaseErrorHandler errorHandler; + /** * @param closable */ @@ -314,9 +342,6 @@ public class SQLiteDatabase extends SQLiteClosable { @Override protected void onAllReferencesReleased() { if (isOpen()) { - if (SQLiteDebug.DEBUG_SQL_CACHE) { - mTimeClosed = getTime(); - } dbclose(); } } @@ -347,19 +372,8 @@ public class SQLiteDatabase extends SQLiteClosable { private boolean mLockingEnabled = true; /* package */ void onCorruption() { - Log.e(TAG, "Removing corrupt database: " + mPath); EventLog.writeEvent(EVENT_DB_CORRUPT, mPath); - try { - // Close the database (if we can), which will cause subsequent operations to fail. - close(); - } finally { - // Delete the corrupt file. Don't re-create it now -- that would just confuse people - // -- but the next time someone tries to open it, they can set it up from scratch. - if (!mPath.equalsIgnoreCase(":memory")) { - // delete is only for non-memory database files - new File(mPath).delete(); - } - } + errorHandler.onCorruption(this); } /** @@ -811,10 +825,25 @@ public class SQLiteDatabase extends SQLiteClosable { * @throws SQLiteException if the database cannot be opened */ public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags) { - SQLiteDatabase sqliteDatabase = null; + return openDatabase(path, factory, flags, new DefaultDatabaseErrorHandler()); + } + + /** + * same as {@link #openDatabase(String, CursorFactory, int)} except for an additional param + * errorHandler. + * @param errorHandler the {@link DatabaseErrorHandler} obj to be used when database + * corruption is detected on the database. + */ + public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags, + DatabaseErrorHandler errorHandler) { + SQLiteDatabase sqliteDatabase = new SQLiteDatabase(path, factory, flags); + + // set the ErrorHandler to be used when SQLite reports exceptions + sqliteDatabase.errorHandler = errorHandler; + try { // Open the database. - sqliteDatabase = new SQLiteDatabase(path, factory, flags); + sqliteDatabase.openDatabase(path, flags); if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { sqliteDatabase.enableSqlTracing(path); } @@ -822,14 +851,8 @@ public class SQLiteDatabase extends SQLiteClosable { sqliteDatabase.enableSqlProfiling(path); } } catch (SQLiteDatabaseCorruptException e) { - // Try to recover from this, if we can. - // TODO: should we do this for other open failures? - Log.e(TAG, "Deleting and re-creating corrupt database " + path, e); - EventLog.writeEvent(EVENT_DB_CORRUPT, path); - if (!path.equalsIgnoreCase(":memory")) { - // delete is only for non-memory database files - new File(path).delete(); - } + // Database is not even openable. + errorHandler.onCorruption(sqliteDatabase); sqliteDatabase = new SQLiteDatabase(path, factory, flags); } ActiveDatabases.getInstance().mActiveDatabases.add( @@ -837,6 +860,18 @@ public class SQLiteDatabase extends SQLiteClosable { return sqliteDatabase; } + private void openDatabase(String path, int flags) { + // Open the database. + dbopen(path, flags); + try { + setLocale(Locale.getDefault()); + } catch (RuntimeException e) { + Log.e(TAG, "Failed to setLocale(). closing the database", e); + dbclose(); + throw e; + } + } + /** * Equivalent to openDatabase(file.getPath(), factory, CREATE_IF_NECESSARY). */ @@ -852,6 +887,17 @@ public class SQLiteDatabase extends SQLiteClosable { } /** + * same as {@link #openOrCreateDatabase(String, CursorFactory)} except for an additional param + * errorHandler. + * @param errorHandler the {@link DatabaseErrorHandler} obj to be used when database + * corruption is detected on the database. + */ + public static SQLiteDatabase openOrCreateDatabase(String path, CursorFactory factory, + DatabaseErrorHandler errorHandler) { + return openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler); + } + + /** * Create a memory backed SQLite database. Its contents will be destroyed * when the database is closed. * @@ -1817,25 +1863,7 @@ public class SQLiteDatabase extends SQLiteClosable { mSlowQueryThreshold = SystemProperties.getInt(LOG_SLOW_QUERIES_PROPERTY, -1); mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); mFactory = factory; - dbopen(mPath, mFlags); - if (SQLiteDebug.DEBUG_SQL_CACHE) { - mTimeOpened = getTime(); - } mPrograms = new WeakHashMap<SQLiteClosable,Object>(); - try { - setLocale(Locale.getDefault()); - } catch (RuntimeException e) { - Log.e(TAG, "Failed to setLocale() when constructing, closing the database", e); - dbclose(); - if (SQLiteDebug.DEBUG_SQL_CACHE) { - mTimeClosed = getTime(); - } - throw e; - } - } - - private String getTime() { - return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ").format(System.currentTimeMillis()); } /** @@ -1961,6 +1989,15 @@ public class SQLiteDatabase extends SQLiteClosable { } } + /* package */ void verifyLockOwner() { + if (!isOpen()) { + throw new IllegalStateException("database " + getPath() + " already closed"); + } + if (!isDbLockedByCurrentThread() && mLockingEnabled) { + throw new IllegalStateException("Don't have database lock!"); + } + } + /* * ============================================================================ * @@ -1976,14 +2013,6 @@ public class SQLiteDatabase extends SQLiteClosable { * mapping is NOT replaced with the new mapping). */ /* package */ void addToCompiledQueries(String sql, SQLiteCompiledSql compiledStatement) { - if (mMaxSqlCacheSize == 0) { - // for this database, there is no cache of compiled sql. - if (SQLiteDebug.DEBUG_SQL_CACHE) { - Log.v(TAG, "|NOT adding_sql_to_cache|" + getPath() + "|" + sql); - } - return; - } - SQLiteCompiledSql compiledSql = null; synchronized(mCompiledQueries) { // don't insert the new mapping if a mapping already exists @@ -1991,35 +2020,30 @@ public class SQLiteDatabase extends SQLiteClosable { if (compiledSql != null) { return; } - // add this <sql, compiledStatement> to the cache + if (mCompiledQueries.size() == mMaxSqlCacheSize) { /* * cache size of {@link #mMaxSqlCacheSize} is not enough for this app. - * log a warning MAX_WARNINGS_ON_CACHESIZE_CONDITION times - * chances are it is NOT using ? for bindargs - so caching is useless. - * TODO: either let the callers set max cchesize for their app, or intelligently - * figure out what should be cached for a given app. + * log a warning. + * chances are it is NOT using ? for bindargs - or cachesize is too small. */ if (++mCacheFullWarnings == MAX_WARNINGS_ON_CACHESIZE_CONDITION) { Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " + - getPath() + "; i.e., NO space for this sql statement in cache: " + - sql + ". Please change your sql statements to use '?' for " + - "bindargs, instead of using actual values"); - } - // don't add this entry to cache - } else { - // cache is NOT full. add this to cache. - mCompiledQueries.put(sql, compiledStatement); - if (SQLiteDebug.DEBUG_SQL_CACHE) { - Log.v(TAG, "|adding_sql_to_cache|" + getPath() + "|" + - mCompiledQueries.size() + "|" + sql); + getPath() + ". Consider increasing cachesize."); } + } + /* add the given SQLiteCompiledSql compiledStatement to cache. + * no need to worry about the cache size - because {@link #mCompiledQueries} + * self-limits its size to {@link #mMaxSqlCacheSize}. + */ + mCompiledQueries.put(sql, compiledStatement); + if (SQLiteDebug.DEBUG_SQL_CACHE) { + Log.v(TAG, "|adding_sql_to_cache|" + getPath() + "|" + + mCompiledQueries.size() + "|" + sql); } } - return; } - private void deallocCachedSqlStatements() { synchronized (mCompiledQueries) { for (SQLiteCompiledSql compiledSql : mCompiledQueries.values()) { @@ -2037,13 +2061,6 @@ public class SQLiteDatabase extends SQLiteClosable { SQLiteCompiledSql compiledStatement = null; boolean cacheHit; synchronized(mCompiledQueries) { - if (mMaxSqlCacheSize == 0) { - // for this database, there is no cache of compiled sql. - if (SQLiteDebug.DEBUG_SQL_CACHE) { - Log.v(TAG, "|cache NOT found|" + getPath()); - } - return null; - } cacheHit = (compiledStatement = mCompiledQueries.get(sql)) != null; } if (cacheHit) { @@ -2056,63 +2073,23 @@ public class SQLiteDatabase extends SQLiteClosable { Log.v(TAG, "|cache_stats|" + getPath() + "|" + mCompiledQueries.size() + "|" + mNumCacheHits + "|" + mNumCacheMisses + - "|" + cacheHit + "|" + mTimeOpened + "|" + mTimeClosed + "|" + sql); + "|" + cacheHit + "|" + sql); } return compiledStatement; } /** - * returns true if the given sql is cached in compiled-sql cache. - * @hide - */ - public boolean isInCompiledSqlCache(String sql) { - synchronized(mCompiledQueries) { - return mCompiledQueries.containsKey(sql); - } - } - - /** - * purges the given sql from the compiled-sql cache. - * @hide - */ - public void purgeFromCompiledSqlCache(String sql) { - synchronized(mCompiledQueries) { - mCompiledQueries.remove(sql); - } - } - - /** - * remove everything from the compiled sql cache - * @hide - */ - public void resetCompiledSqlCache() { - synchronized(mCompiledQueries) { - mCompiledQueries.clear(); - } - } - - /** - * return the current maxCacheSqlCacheSize - * @hide - */ - public synchronized int getMaxSqlCacheSize() { - return mMaxSqlCacheSize; - } - - /** - * set the max size of the compiled sql cache for this database after purging the cache. + * set the max size of the prepared-statement cache for this database. * (size of the cache = number of compiled-sql-statements stored in the cache). * - * max cache size can ONLY be increased from its current size (default = 0). + * max cache size can ONLY be increased from its current size (default = 10). * if this method is called with smaller size than the current value of mMaxSqlCacheSize, * then IllegalStateException is thrown * * synchronized because we don't want t threads to change cache size at the same time. - * @param cacheSize the size of the cache. can be (0 to MAX_SQL_CACHE_SIZE) - * @throws IllegalStateException if input cacheSize > MAX_SQL_CACHE_SIZE or < 0 or - * < the value set with previous setMaxSqlCacheSize() call. - * - * @hide + * @param cacheSize the size of the cache. can be (0 to {@link #MAX_SQL_CACHE_SIZE}) + * @throws IllegalStateException if input cacheSize > {@link #MAX_SQL_CACHE_SIZE} or + * > the value set with previous setMaxSqlCacheSize() call. */ public synchronized void setMaxSqlCacheSize(int cacheSize) { if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) { @@ -2152,7 +2129,7 @@ public class SQLiteDatabase extends SQLiteClosable { String lastnode = path.substring((indx != -1) ? ++indx : 0); // get list of attached dbs and for each db, get its size and pagesize - ArrayList<Pair<String, String>> attachedDbs = getAttachedDbs(db); + ArrayList<Pair<String, String>> attachedDbs = db.getAttachedDbs(); if (attachedDbs == null) { continue; } @@ -2177,7 +2154,8 @@ public class SQLiteDatabase extends SQLiteClosable { } if (pageCount > 0) { dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(), - lookasideUsed)); + lookasideUsed, db.mNumCacheHits, db.mNumCacheMisses, + db.mCompiledQueries.size())); } } } @@ -2205,24 +2183,74 @@ public class SQLiteDatabase extends SQLiteClosable { } /** - * returns list of full pathnames of all attached databases - * including the main database - * TODO: move this to {@link DatabaseUtils} + * returns list of full pathnames of all attached databases including the main database + * @return ArrayList of pairs of (database name, database file path) or null if the database + * is not open. */ - private static ArrayList<Pair<String, String>> getAttachedDbs(SQLiteDatabase dbObj) { - if (!dbObj.isOpen()) { + public ArrayList<Pair<String, String>> getAttachedDbs() { + if (!isOpen()) { return null; } ArrayList<Pair<String, String>> attachedDbs = new ArrayList<Pair<String, String>>(); - Cursor c = dbObj.rawQuery("pragma database_list;", null); - while (c.moveToNext()) { - attachedDbs.add(new Pair<String, String>(c.getString(1), c.getString(2))); + Cursor c = null; + try { + c = rawQuery("pragma database_list;", null); + while (c.moveToNext()) { + // sqlite returns a row for each database in the returned list of databases. + // in each row, + // 1st column is the database name such as main, or the database + // name specified on the "ATTACH" command + // 2nd column is the database file path. + attachedDbs.add(new Pair<String, String>(c.getString(1), c.getString(2))); + } + } finally { + if (c != null) { + c.close(); + } } - c.close(); return attachedDbs; } /** + * run pragma integrity_check on the given database (and all the attached databases) + * and return true if the given database (and all its attached databases) pass integrity_check, + * false otherwise. + * + * if the result is false, then this method logs the errors reported by the integrity_check + * command execution. + * + * @return true if the given database (and all its attached databases) pass integrity_check, + * false otherwise + */ + public boolean isDatabaseIntegrityOk() { + if (!isOpen()) { + throw new IllegalStateException("database: " + getPath() + " is NOT open"); + } + ArrayList<Pair<String, String>> attachedDbs = getAttachedDbs(); + if (attachedDbs == null) { + throw new IllegalStateException("databaselist for: " + getPath() + " couldn't " + + "be retrieved. probably because the database is closed"); + } + boolean isDatabaseCorrupt = false; + for (int i = 0; i < attachedDbs.size(); i++) { + Pair<String, String> p = attachedDbs.get(i); + SQLiteStatement prog = null; + try { + prog = compileStatement("PRAGMA " + p.first + ".integrity_check(1);"); + String rslt = prog.simpleQueryForString(); + if (!rslt.equalsIgnoreCase("ok")) { + // integrity_checker failed on main or attached databases + isDatabaseCorrupt = true; + Log.e(TAG, "PRAGMA integrity_check on " + p.second + " returned: " + rslt); + } + } finally { + if (prog != null) prog.close(); + } + } + return isDatabaseCorrupt; + } + + /** * Native call to open the database. * * @param path The full path to the database diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java index 89c3f96..9496079 100644 --- a/core/java/android/database/sqlite/SQLiteDebug.java +++ b/core/java/android/database/sqlite/SQLiteDebug.java @@ -132,11 +132,16 @@ public final class SQLiteDebug { /** documented here http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html */ public int lookaside; - public DbStats(String dbName, long pageCount, long pageSize, int lookaside) { + /** statement cache stats: hits/misses/cachesize */ + public String cache; + + public DbStats(String dbName, long pageCount, long pageSize, int lookaside, + int hits, int misses, int cachesize) { this.dbName = dbName; - this.pageSize = pageSize; + this.pageSize = pageSize / 1024; dbSize = (pageCount * pageSize) / 1024; this.lookaside = lookaside; + this.cache = hits + "/" + misses + "/" + cachesize; } } diff --git a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java index 2144fc3..ac60b27 100644 --- a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java +++ b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java @@ -39,9 +39,11 @@ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver { public Cursor query(CursorFactory factory, String[] selectionArgs) { // Compile the query - SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs); + SQLiteQuery query = null; try { + mDatabase.lock(); + query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs); // Arg binding int numArgs = selectionArgs == null ? 0 : selectionArgs.length; for (int i = 0; i < numArgs; i++) { @@ -61,6 +63,7 @@ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver { } finally { // Make sure this object is cleaned up if something happens if (query != null) query.close(); + mDatabase.unlock(); } } diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java index 52aac3a..d4907d9 100644 --- a/core/java/android/database/sqlite/SQLiteOpenHelper.java +++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java @@ -99,6 +99,10 @@ public abstract class SQLiteOpenHelper { } int version = db.getVersion(); + if (version > mNewVersion) { + throw new IllegalStateException("Database " + mName + + " cannot be downgraded. instead, please uninstall new version first."); + } if (version != mNewVersion) { db.beginTransaction(); try { diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java index 4d96f12..c37385c0 100644 --- a/core/java/android/database/sqlite/SQLiteProgram.java +++ b/core/java/android/database/sqlite/SQLiteProgram.java @@ -150,7 +150,7 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @return a unique identifier for this program */ public final int getUniqueId() { - return nStatement; + return (mCompiledSql != null) ? mCompiledSql.nStatement : 0; } /* package */ String getSqlString() { diff --git a/core/java/android/net/Downloads.java b/core/java/android/net/Downloads.java index fd33781..ddde5c1 100644 --- a/core/java/android/net/Downloads.java +++ b/core/java/android/net/Downloads.java @@ -430,11 +430,10 @@ public final class Downloads { ContentResolver cr = context.getContentResolver(); - Cursor c = cr.query( - downloadUri, DOWNLOADS_PROJECTION, null /* selection */, null /* selection args */, - null /* sort order */); + Cursor c = cr.query(downloadUri, DOWNLOADS_PROJECTION, null /* selection */, + null /* selection args */, null /* sort order */); try { - if (!c.moveToNext()) { + if (c == null || !c.moveToNext()) { return result; } diff --git a/core/java/android/net/http/CertificateChainValidator.java b/core/java/android/net/http/CertificateChainValidator.java index c527fe4..c36ad38 100644 --- a/core/java/android/net/http/CertificateChainValidator.java +++ b/core/java/android/net/http/CertificateChainValidator.java @@ -80,14 +80,10 @@ class CertificateChainValidator { throws IOException { X509Certificate[] serverCertificates = null; - // start handshake, close the socket if we fail - try { - sslSocket.setUseClientMode(true); - sslSocket.startHandshake(); - } catch (IOException e) { - closeSocketThrowException( - sslSocket, e.getMessage(), - "failed to perform SSL handshake"); + // get a valid SSLSession, close the socket if we fail + SSLSession sslSession = sslSession = sslSocket.getSession(); + if (!sslSession.isValid()) { + closeSocketThrowException(sslSocket, "failed to perform SSL handshake"); } // retrieve the chain of the server peer certificates diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java index d28148c..e74697a 100644 --- a/core/java/android/os/AsyncTask.java +++ b/core/java/android/os/AsyncTask.java @@ -16,16 +16,16 @@ package android.os; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.Callable; -import java.util.concurrent.FutureTask; +import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicInteger; /** @@ -36,8 +36,8 @@ import java.util.concurrent.atomic.AtomicInteger; * <p>An asynchronous task is defined by a computation that runs on a background thread and * whose result is published on the UI thread. An asynchronous task is defined by 3 generic * types, called <code>Params</code>, <code>Progress</code> and <code>Result</code>, - * and 4 steps, called <code>begin</code>, <code>doInBackground</code>, - * <code>processProgress</code> and <code>end</code>.</p> + * and 4 steps, called <code>onPreExecute</code>, <code>doInBackground</code>, + * <code>onProgressUpdate</code> and <code>onPostExecute</code>.</p> * * <h2>Usage</h2> * <p>AsyncTask must be subclassed to be used. The subclass will override at least diff --git a/core/java/android/os/storage/StorageEventListener.java b/core/java/android/os/storage/StorageEventListener.java index 7b883a7..d3d39d6 100644 --- a/core/java/android/os/storage/StorageEventListener.java +++ b/core/java/android/os/storage/StorageEventListener.java @@ -18,7 +18,6 @@ package android.os.storage; /** * Used for receiving notifications from the StorageManager - * @hide */ public abstract class StorageEventListener { /** diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index a12603c..b49979c 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -45,8 +45,6 @@ import java.util.List; * {@link android.content.Context#getSystemService(java.lang.String)} with an argument * of {@link android.content.Context#STORAGE_SERVICE}. * - * @hide - * */ public class StorageManager diff --git a/core/java/android/os/storage/StorageResultCode.java b/core/java/android/os/storage/StorageResultCode.java index 075f47f..07d95df 100644 --- a/core/java/android/os/storage/StorageResultCode.java +++ b/core/java/android/os/storage/StorageResultCode.java @@ -19,8 +19,6 @@ package android.os.storage; /** * Class that provides access to constants returned from StorageManager * and lower level MountService APIs. - * - * @hide */ public class StorageResultCode { diff --git a/core/java/android/pim/RecurrenceSet.java b/core/java/android/pim/RecurrenceSet.java index 635323e..282417d 100644 --- a/core/java/android/pim/RecurrenceSet.java +++ b/core/java/android/pim/RecurrenceSet.java @@ -181,7 +181,9 @@ public class RecurrenceSet { boolean inUtc = start.parse(dtstart); boolean allDay = start.allDay; - if (inUtc) { + // We force TimeZone to UTC for "all day recurring events" as the server is sending no + // TimeZone in DTSTART for them + if (inUtc || allDay) { tzid = Time.TIMEZONE_UTC; } @@ -204,10 +206,7 @@ public class RecurrenceSet { } if (allDay) { - // TODO: also change tzid to be UTC? that would be consistent, but - // that would not reflect the original timezone value back to the - // server. - start.timezone = Time.TIMEZONE_UTC; + start.timezone = Time.TIMEZONE_UTC; } long millis = start.toMillis(false /* use isDst */); values.put(Calendar.Events.DTSTART, millis); diff --git a/core/java/android/pim/vcard/JapaneseUtils.java b/core/java/android/pim/vcard/JapaneseUtils.java index 875c29e..dcfe980 100644 --- a/core/java/android/pim/vcard/JapaneseUtils.java +++ b/core/java/android/pim/vcard/JapaneseUtils.java @@ -27,7 +27,6 @@ import java.util.Map; new HashMap<Character, String>(); static { - // There's no logical mapping rule in Unicode. Sigh. sHalfWidthMap.put('\u3001', "\uFF64"); sHalfWidthMap.put('\u3002', "\uFF61"); sHalfWidthMap.put('\u300C', "\uFF62"); @@ -366,11 +365,11 @@ import java.util.Map; } /** - * Return half-width version of that character if possible. Return null if not possible + * Returns half-width version of that character if possible. Returns null if not possible * @param ch input character * @return CharSequence object if the mapping for ch exists. Return null otherwise. */ - public static String tryGetHalfWidthText(char ch) { + public static String tryGetHalfWidthText(final char ch) { if (sHalfWidthMap.containsKey(ch)) { return sHalfWidthMap.get(ch); } else { diff --git a/core/java/android/pim/vcard/VCardBuilder.java b/core/java/android/pim/vcard/VCardBuilder.java index 1da6d7a..789b5f8 100644 --- a/core/java/android/pim/vcard/VCardBuilder.java +++ b/core/java/android/pim/vcard/VCardBuilder.java @@ -47,7 +47,23 @@ import java.util.Map; import java.util.Set; /** - * The class which lets users create their own vCard String. + * <p> + * The class which lets users create their own vCard String. Typical usage is as follows: + * </p> + * <pre class="prettyprint">final VCardBuilder builder = new VCardBuilder(vcardType); + * builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) + * .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) + * .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE)) + * .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) + * .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) + * .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) + * .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)) + * .appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)) + * .appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) + * .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) + * .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) + * .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); + * return builder.toString();</pre> */ public class VCardBuilder { private static final String LOG_TAG = "VCardBuilder"; @@ -75,13 +91,14 @@ public class VCardBuilder { private static final String VCARD_WS = " "; private static final String VCARD_PARAM_EQUAL = "="; - private static final String VCARD_PARAM_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"; - - private static final String VCARD_PARAM_ENCODING_BASE64_V21 = "ENCODING=BASE64"; - private static final String VCARD_PARAM_ENCODING_BASE64_V30 = "ENCODING=b"; + private static final String VCARD_PARAM_ENCODING_QP = + "ENCODING=" + VCardConstants.PARAM_ENCODING_QP; + private static final String VCARD_PARAM_ENCODING_BASE64_V21 = + "ENCODING=" + VCardConstants.PARAM_ENCODING_BASE64; + private static final String VCARD_PARAM_ENCODING_BASE64_V30 = + "ENCODING=" + VCardConstants.PARAM_ENCODING_B; private static final String SHIFT_JIS = "SHIFT_JIS"; - private static final String UTF_8 = "UTF-8"; private final int mVCardType; @@ -92,21 +109,28 @@ public class VCardBuilder { private final boolean mShouldUseQuotedPrintable; private final boolean mUsesAndroidProperty; private final boolean mUsesDefactProperty; - private final boolean mUsesUtf8; - private final boolean mUsesShiftJis; private final boolean mAppendTypeParamName; private final boolean mRefrainsQPToNameProperties; private final boolean mNeedsToConvertPhoneticString; private final boolean mShouldAppendCharsetParam; - private final String mCharsetString; + private final String mCharset; private final String mVCardCharsetParameter; private StringBuilder mBuilder; private boolean mEndAppended; public VCardBuilder(final int vcardType) { + // Default charset should be used + this(vcardType, null); + } + + /** + * @param vcardType + * @param charset If null, we use default charset for export. + */ + public VCardBuilder(final int vcardType, String charset) { mVCardType = vcardType; mIsV30 = VCardConfig.isV30(vcardType); @@ -116,40 +140,74 @@ public class VCardBuilder { mOnlyOneNoteFieldIsAvailable = VCardConfig.onlyOneNoteFieldIsAvailable(vcardType); mUsesAndroidProperty = VCardConfig.usesAndroidSpecificProperty(vcardType); mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType); - mUsesUtf8 = VCardConfig.usesUtf8(vcardType); - mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); mRefrainsQPToNameProperties = VCardConfig.shouldRefrainQPToNameProperties(vcardType); mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType); mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType); - mShouldAppendCharsetParam = !(mIsV30 && mUsesUtf8); - - if (mIsDoCoMo) { - String charset; - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); - charset = SHIFT_JIS; - } - mCharsetString = charset; - // Do not use mCharsetString bellow since it is different from "SHIFT_JIS" but - // may be "DOCOMO_SHIFT_JIS" or something like that (internal expression used in - // Android, not shown to the public). - mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; - } else if (mUsesShiftJis) { - String charset; - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); - charset = SHIFT_JIS; - } - mCharsetString = charset; + // vCard 2.1 requires charset. + // vCard 3.0 does not allow it but we found some devices use it to determine + // the exact charset. + // We currently append it only when charset other than UTF_8 is used. + mShouldAppendCharsetParam = !(mIsV30 && "UTF-8".equalsIgnoreCase(charset)); + + if (VCardConfig.isDoCoMo(vcardType)) { + if (!SHIFT_JIS.equalsIgnoreCase(charset)) { + Log.w(LOG_TAG, + "The charset \"" + charset + "\" is used while " + + SHIFT_JIS + " is needed to be used."); + if (TextUtils.isEmpty(charset)) { + mCharset = SHIFT_JIS; + } else { + try { + charset = CharsetUtils.charsetForVendor(charset).name(); + } catch (UnsupportedCharsetException e) { + Log.i(LOG_TAG, + "Career-specific \"" + charset + "\" was not found (as usual). " + + "Use it as is."); + } + mCharset = charset; + } + } else { + if (mIsDoCoMo) { + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, + "DoCoMo-specific SHIFT_JIS was not found. " + + "Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + } else { + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, + "Career-specific SHIFT_JIS was not found. " + + "Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + } + mCharset = charset; + } mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; } else { - mCharsetString = UTF_8; - mVCardCharsetParameter = "CHARSET=" + UTF_8; + if (TextUtils.isEmpty(charset)) { + Log.i(LOG_TAG, + "Use the charset \"" + VCardConfig.DEFAULT_EXPORT_CHARSET + + "\" for export."); + mCharset = VCardConfig.DEFAULT_EXPORT_CHARSET; + mVCardCharsetParameter = "CHARSET=" + VCardConfig.DEFAULT_EXPORT_CHARSET; + } else { + try { + charset = CharsetUtils.charsetForVendor(charset).name(); + } catch (UnsupportedCharsetException e) { + Log.i(LOG_TAG, + "Career-specific \"" + charset + "\" was not found (as usual). " + + "Use it as is."); + } + mCharset = charset; + mVCardCharsetParameter = "CHARSET=" + charset; + } } clear(); } @@ -379,8 +437,8 @@ public class VCardBuilder { mBuilder.append(VCardConstants.PROPERTY_FN); // Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it - // when it would be useful for external importers, assuming no external - // importer allows this vioration. + // when it would be useful or necessary for external importers, + // assuming the external importer allows this vioration of the spec. if (shouldAppendCharsetParam(displayName)) { mBuilder.append(VCARD_PARAM_SEPARATOR); mBuilder.append(mVCardCharsetParameter); @@ -454,18 +512,18 @@ public class VCardBuilder { mBuilder.append(VCARD_END_OF_LINE); } else if (mIsJapaneseMobilePhone) { // Note: There is no appropriate property for expressing - // phonetic name in vCard 2.1, while there is in + // phonetic name (Yomigana in Japanese) in vCard 2.1, while there is in // vCard 3.0 (SORT-STRING). - // We chose to use DoCoMo's way when the device is Japanese one - // since it is supported by - // a lot of Japanese mobile phones. This is "X-" property, so - // any parser hopefully would not get confused with this. + // We use DoCoMo's way when the device is Japanese one since it is already + // supported by a lot of Japanese mobile phones. + // This is "X-" property, so any parser hopefully would not get + // confused with this. // // Also, DoCoMo's specification requires vCard composer to use just the first // column. // i.e. - // o SOUND;X-IRMC-N:Miyakawa Daisuke;;;; - // x SOUND;X-IRMC-N:Miyakawa;Daisuke;;; + // good: SOUND;X-IRMC-N:Miyakawa Daisuke;;;; + // bad : SOUND;X-IRMC-N:Miyakawa;Daisuke;;; mBuilder.append(VCardConstants.PROPERTY_SOUND); mBuilder.append(VCARD_PARAM_SEPARATOR); mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N); @@ -519,10 +577,10 @@ public class VCardBuilder { mBuilder.append(encodedPhoneticGivenName); } } - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); - mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); // family;given + mBuilder.append(VCARD_ITEM_SEPARATOR); // given;middle + mBuilder.append(VCARD_ITEM_SEPARATOR); // middle;prefix + mBuilder.append(VCARD_ITEM_SEPARATOR); // prefix;suffix mBuilder.append(VCARD_END_OF_LINE); } @@ -549,7 +607,7 @@ public class VCardBuilder { mBuilder.append(VCARD_DATA_SEPARATOR); mBuilder.append(encodedPhoneticGivenName); mBuilder.append(VCARD_END_OF_LINE); - } + } // if (!TextUtils.isEmpty(phoneticGivenName)) if (!TextUtils.isEmpty(phoneticMiddleName)) { final boolean reallyUseQuotedPrintable = (mShouldUseQuotedPrintable && @@ -572,7 +630,7 @@ public class VCardBuilder { mBuilder.append(VCARD_DATA_SEPARATOR); mBuilder.append(encodedPhoneticMiddleName); mBuilder.append(VCARD_END_OF_LINE); - } + } // if (!TextUtils.isEmpty(phoneticGivenName)) if (!TextUtils.isEmpty(phoneticFamilyName)) { final boolean reallyUseQuotedPrintable = (mShouldUseQuotedPrintable && @@ -595,7 +653,7 @@ public class VCardBuilder { mBuilder.append(VCARD_DATA_SEPARATOR); mBuilder.append(encodedPhoneticFamilyName); mBuilder.append(VCARD_END_OF_LINE); - } + } // if (!TextUtils.isEmpty(phoneticFamilyName)) } } @@ -922,21 +980,21 @@ public class VCardBuilder { encodedCountry = escapeCharacters(rawCountry); encodedNeighborhood = escapeCharacters(rawNeighborhood); } - final StringBuffer addressBuffer = new StringBuffer(); - addressBuffer.append(encodedPoBox); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedStreet); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedLocality); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedRegion); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedPostalCode); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedCountry); + final StringBuilder addressBuilder = new StringBuilder(); + addressBuilder.append(encodedPoBox); + addressBuilder.append(VCARD_ITEM_SEPARATOR); // PO BOX ; Extended Address + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Extended Address : Street + addressBuilder.append(encodedStreet); + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Street : Locality + addressBuilder.append(encodedLocality); + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Locality : Region + addressBuilder.append(encodedRegion); + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Region : Postal Code + addressBuilder.append(encodedPostalCode); + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Postal Code : Country + addressBuilder.append(encodedCountry); return new PostalStruct( - reallyUseQuotedPrintable, appendCharset, addressBuffer.toString()); + reallyUseQuotedPrintable, appendCharset, addressBuilder.toString()); } else { // VCardUtils.areAllEmpty(rawAddressArray) == true // Try to use FORMATTED_ADDRESS instead. final String rawFormattedAddress = @@ -959,16 +1017,16 @@ public class VCardBuilder { // We use the second value ("Extended Address") just because Japanese mobile phones // do so. If the other importer expects the value be in the other field, some flag may // be needed. - final StringBuffer addressBuffer = new StringBuffer(); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(encodedFormattedAddress); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(VCARD_ITEM_SEPARATOR); - addressBuffer.append(VCARD_ITEM_SEPARATOR); + final StringBuilder addressBuilder = new StringBuilder(); + addressBuilder.append(VCARD_ITEM_SEPARATOR); // PO BOX ; Extended Address + addressBuilder.append(encodedFormattedAddress); + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Extended Address : Street + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Street : Locality + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Locality : Region + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Region : Postal Code + addressBuilder.append(VCARD_ITEM_SEPARATOR); // Postal Code : Country return new PostalStruct( - reallyUseQuotedPrintable, appendCharset, addressBuffer.toString()); + reallyUseQuotedPrintable, appendCharset, addressBuilder.toString()); } } @@ -1165,6 +1223,8 @@ public class VCardBuilder { } public VCardBuilder appendEvents(final List<ContentValues> contentValuesList) { + // There's possibility where a given object may have more than one birthday, which + // is inappropriate. We just build one birthday. if (contentValuesList != null) { String primaryBirthday = null; String secondaryBirthday = null; @@ -1232,16 +1292,19 @@ public class VCardBuilder { return this; } + /** + * @param emitEveryTime If true, builder builds the line even when there's no entry. + */ public void appendPostalLine(final int type, final String label, final ContentValues contentValues, - final boolean isPrimary, final boolean emitLineEveryTime) { + final boolean isPrimary, final boolean emitEveryTime) { final boolean reallyUseQuotedPrintable; final boolean appendCharset; final String addressValue; { PostalStruct postalStruct = tryConstructPostalStruct(contentValues); if (postalStruct == null) { - if (emitLineEveryTime) { + if (emitEveryTime) { reallyUseQuotedPrintable = false; appendCharset = false; addressValue = ""; @@ -1556,7 +1619,8 @@ public class VCardBuilder { mBuilder.append(VCARD_END_OF_LINE); } - public void appendAndroidSpecificProperty(final String mimeType, ContentValues contentValues) { + public void appendAndroidSpecificProperty( + final String mimeType, ContentValues contentValues) { if (!sAllowedAndroidPropertySet.contains(mimeType)) { return; } @@ -1678,7 +1742,7 @@ public class VCardBuilder { encodedValue = encodeQuotedPrintable(rawValue); } else { // TODO: one line may be too huge, which may be invalid in vCard spec, though - // several (even well-known) applications do not care this. + // several (even well-known) applications do not care that violation. encodedValue = escapeCharacters(rawValue); } @@ -1813,9 +1877,9 @@ public class VCardBuilder { byte[] strArray = null; try { - strArray = str.getBytes(mCharsetString); + strArray = str.getBytes(mCharset); } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Charset " + mCharsetString + " cannot be used. " + Log.e(LOG_TAG, "Charset " + mCharset + " cannot be used. " + "Try default charset"); strArray = str.getBytes(); } diff --git a/core/java/android/pim/vcard/VCardComposer.java b/core/java/android/pim/vcard/VCardComposer.java index 0e8b665..170d6fa 100644 --- a/core/java/android/pim/vcard/VCardComposer.java +++ b/core/java/android/pim/vcard/VCardComposer.java @@ -41,6 +41,7 @@ import android.provider.ContactsContract.CommonDataKinds.Relation; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.CommonDataKinds.Website; +import android.text.TextUtils; import android.util.CharsetUtils; import android.util.Log; @@ -61,15 +62,11 @@ import java.util.Map; /** * <p> - * The class for composing VCard from Contacts information. Note that this is - * completely differnt implementation from - * android.syncml.pim.vcard.VCardComposer, which is not maintained anymore. + * The class for composing vCard from Contacts information. * </p> - * * <p> * Usually, this class should be used like this. * </p> - * * <pre class="prettyprint">VCardComposer composer = null; * try { * composer = new VCardComposer(context); @@ -93,15 +90,18 @@ import java.util.Map; * if (composer != null) { * composer.terminate(); * } - * } </pre> + * }</pre> + * <p> + * Users have to manually take care of memory efficiency. Even one vCard may contain + * image of non-trivial size for mobile devices. + * </p> + * <p> + * {@link VCardBuilder} is used to build each vCard. + * </p> */ public class VCardComposer { private static final String LOG_TAG = "VCardComposer"; - public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME; - public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME; - public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER; - public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = "Failed to get database information"; @@ -119,6 +119,8 @@ public class VCardComposer { public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; + // Strictly speaking, "Shift_JIS" is the most appropriate, but we use upper version here, + // since usual vCard devices for Japanese devices already use it. private static final String SHIFT_JIS = "SHIFT_JIS"; private static final String UTF_8 = "UTF-8"; @@ -141,7 +143,7 @@ public class VCardComposer { sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); - // Google talk is a special case. + // We don't add Google talk here since it has to be handled separately. } public static interface OneEntryHandler { @@ -152,37 +154,37 @@ public class VCardComposer { /** * <p> - * An useful example handler, which emits VCard String to outputstream one by one. + * An useful handler for emitting vCard String to an OutputStream object one by one. * </p> * <p> * The input OutputStream object is closed() on {@link #onTerminate()}. - * Must not close the stream outside. + * Must not close the stream outside this class. * </p> */ - public class HandlerForOutputStream implements OneEntryHandler { + public final class HandlerForOutputStream implements OneEntryHandler { @SuppressWarnings("hiding") - private static final String LOG_TAG = "vcard.VCardComposer.HandlerForOutputStream"; - - final private OutputStream mOutputStream; // mWriter will close this. - private Writer mWriter; + private static final String LOG_TAG = "VCardComposer.HandlerForOutputStream"; private boolean mOnTerminateIsCalled = false; + private final OutputStream mOutputStream; // mWriter will close this. + private Writer mWriter; + /** * Input stream will be closed on the detruction of this object. */ - public HandlerForOutputStream(OutputStream outputStream) { + public HandlerForOutputStream(final OutputStream outputStream) { mOutputStream = outputStream; } - public boolean onInit(Context context) { + public boolean onInit(final Context context) { try { mWriter = new BufferedWriter(new OutputStreamWriter( - mOutputStream, mCharsetString)); + mOutputStream, mCharset)); } catch (UnsupportedEncodingException e1) { - Log.e(LOG_TAG, "Unsupported charset: " + mCharsetString); + Log.e(LOG_TAG, "Unsupported charset: " + mCharset); mErrorReason = "Encoding is not supported (usually this does not happen!): " - + mCharsetString; + + mCharset; return false; } @@ -235,14 +237,19 @@ public class VCardComposer { "IOException during closing the output stream: " + e.getMessage()); } finally { - try { - mWriter.close(); - } catch (IOException e) { - } + closeOutputStream(); } } } + public void closeOutputStream() { + try { + mWriter.close(); + } catch (IOException e) { + Log.w(LOG_TAG, "IOException is thrown during close(). Ignoring."); + } + } + @Override public void finalize() { if (!mOnTerminateIsCalled) { @@ -257,11 +264,10 @@ public class VCardComposer { private final ContentResolver mContentResolver; private final boolean mIsDoCoMo; - private final boolean mUsesShiftJis; private Cursor mCursor; private int mIdColumn; - private final String mCharsetString; + private final String mCharset; private boolean mTerminateIsCalled; private final List<OneEntryHandler> mHandlerList; @@ -272,52 +278,107 @@ public class VCardComposer { }; public VCardComposer(Context context) { - this(context, VCardConfig.VCARD_TYPE_DEFAULT, true); + this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true); } + /** + * The variant which sets charset to null and sets careHandlerErrors to true. + */ public VCardComposer(Context context, int vcardType) { - this(context, vcardType, true); + this(context, vcardType, null, true); } - public VCardComposer(Context context, String vcardTypeStr, boolean careHandlerErrors) { - this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), careHandlerErrors); + public VCardComposer(Context context, int vcardType, String charset) { + this(context, vcardType, charset, true); } /** - * Construct for supporting call log entry vCard composing. + * The variant which sets charset to null. */ public VCardComposer(final Context context, final int vcardType, final boolean careHandlerErrors) { + this(context, vcardType, null, careHandlerErrors); + } + + /** + * Construct for supporting call log entry vCard composing. + * + * @param context Context to be used during the composition. + * @param vcardType The type of vCard, typically available via {@link VCardConfig}. + * @param charset The charset to be used. Use null when you don't need the charset. + * @param careHandlerErrors If true, This object returns false everytime + * a Handler object given via {{@link #addHandler(OneEntryHandler)} returns false. + * If false, this ignores those errors. + */ + public VCardComposer(final Context context, final int vcardType, String charset, + final boolean careHandlerErrors) { mContext = context; mVCardType = vcardType; mCareHandlerErrors = careHandlerErrors; mContentResolver = context.getContentResolver(); mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); - mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); mHandlerList = new ArrayList<OneEntryHandler>(); - if (mIsDoCoMo) { - String charset; - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); - charset = SHIFT_JIS; - } - mCharsetString = charset; - } else if (mUsesShiftJis) { - String charset; - try { - charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); - } catch (UnsupportedCharsetException e) { - Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); - charset = SHIFT_JIS; + charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset); + final boolean shouldAppendCharsetParam = !( + VCardConfig.isV30(vcardType) && UTF_8.equalsIgnoreCase(charset)); + + if (mIsDoCoMo || shouldAppendCharsetParam) { + if (SHIFT_JIS.equalsIgnoreCase(charset)) { + if (mIsDoCoMo) { + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, + "DoCoMo-specific SHIFT_JIS was not found. " + + "Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + } else { + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, + "Career-specific SHIFT_JIS was not found. " + + "Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + } + mCharset = charset; + } else { + Log.w(LOG_TAG, + "The charset \"" + charset + "\" is used while " + + SHIFT_JIS + " is needed to be used."); + if (TextUtils.isEmpty(charset)) { + mCharset = SHIFT_JIS; + } else { + try { + charset = CharsetUtils.charsetForVendor(charset).name(); + } catch (UnsupportedCharsetException e) { + Log.i(LOG_TAG, + "Career-specific \"" + charset + "\" was not found (as usual). " + + "Use it as is."); + } + mCharset = charset; + } } - mCharsetString = charset; } else { - mCharsetString = UTF_8; + if (TextUtils.isEmpty(charset)) { + mCharset = UTF_8; + } else { + try { + charset = CharsetUtils.charsetForVendor(charset).name(); + } catch (UnsupportedCharsetException e) { + Log.i(LOG_TAG, + "Career-specific \"" + charset + "\" was not found (as usual). " + + "Use it as is."); + } + mCharset = charset; + } } + + Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\""); } /** @@ -351,7 +412,7 @@ public class VCardComposer { } if (mCareHandlerErrors) { - List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( + final List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( mHandlerList.size()); for (OneEntryHandler handler : mHandlerList) { if (!handler.onInit(mContext)) { @@ -414,7 +475,7 @@ public class VCardComposer { mErrorReason = FAILURE_REASON_NOT_INITIALIZED; return false; } - String vcard; + final String vcard; try { if (mIdColumn >= 0) { vcard = createOneEntryInternal(mCursor.getString(mIdColumn), @@ -437,8 +498,7 @@ public class VCardComposer { mCursor.moveToNext(); } - // This function does not care the OutOfMemoryError on the handler side - // :-P + // This function does not care the OutOfMemoryError on the handler side :-P if (mCareHandlerErrors) { List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( mHandlerList.size()); @@ -457,7 +517,7 @@ public class VCardComposer { } private String createOneEntryInternal(final String contactId, - Method getEntityIteratorMethod) throws VCardException { + final Method getEntityIteratorMethod) throws VCardException { final Map<String, List<ContentValues>> contentValuesListMap = new HashMap<String, List<ContentValues>>(); // The resolver may return the entity iterator with no data. It is possible. @@ -471,7 +531,7 @@ public class VCardComposer { final String selection = Data.CONTACT_ID + "=?"; final String[] selectionArgs = new String[] {contactId}; if (getEntityIteratorMethod != null) { - // Please note that this branch is executed by some tests only + // Please note that this branch is executed by unit tests only try { entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null, mContentResolver, uri, selection, selectionArgs, null); @@ -527,22 +587,33 @@ public class VCardComposer { } } - final VCardBuilder builder = new VCardBuilder(mVCardType); - builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) - .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) - .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE)) - .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) - .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) - .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) - .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)); - if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) { - builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)); + return buildVCard(contentValuesListMap); + } + + /** + * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in + * {ContactsContract}. Developers can override this method to customize the output. + */ + public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) { + if (contentValuesListMap == null) { + Log.e(LOG_TAG, "The given map is null. Ignore and return empty String"); + return ""; + } else { + final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset); + builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) + .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) + .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE)) + .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) + .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) + .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) + .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)) + .appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)) + .appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) + .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) + .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) + .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); + return builder.toString(); } - builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) - .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) - .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) - .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); - return builder.toString(); } public void terminate() { @@ -565,26 +636,38 @@ public class VCardComposer { @Override public void finalize() { if (!mTerminateIsCalled) { + Log.w(LOG_TAG, "terminate() is not called yet. We call it in finalize() step."); terminate(); } } + /** + * @return returns the number of available entities. The return value is undefined + * when this object is not ready yet (typically when {{@link #init()} is not called + * or when {@link #terminate()} is already called). + */ public int getCount() { if (mCursor == null) { + Log.w(LOG_TAG, "This object is not ready yet."); return 0; } return mCursor.getCount(); } + /** + * @return true when there's no entity to be built. The return value is undefined + * when this object is not ready yet. + */ public boolean isAfterLast() { if (mCursor == null) { + Log.w(LOG_TAG, "This object is not ready yet."); return false; } return mCursor.isAfterLast(); } /** - * @return Return the error reason if possible. + * @return Returns the error reason. */ public String getErrorReason() { return mErrorReason; diff --git a/core/java/android/pim/vcard/VCardConfig.java b/core/java/android/pim/vcard/VCardConfig.java index 8219840..80709f3 100644 --- a/core/java/android/pim/vcard/VCardConfig.java +++ b/core/java/android/pim/vcard/VCardConfig.java @@ -38,16 +38,32 @@ public class VCardConfig { /* package */ static final int LOG_LEVEL = LOG_LEVEL_NONE; - /* package */ static final int PARSE_TYPE_UNKNOWN = 0; - /* package */ static final int PARSE_TYPE_APPLE = 1; - /* package */ static final int PARSE_TYPE_MOBILE_PHONE_JP = 2; // For Japanese mobile phones. - /* package */ static final int PARSE_TYPE_FOMA = 3; // For Japanese FOMA mobile phones. - /* package */ static final int PARSE_TYPE_WINDOWS_MOBILE_JP = 4; - - // Assumes that "iso-8859-1" is able to map "all" 8bit characters to some unicode and - // decode the unicode to the original charset. If not, this setting will cause some bug. - public static final String DEFAULT_CHARSET = "iso-8859-1"; - + /** + * <p> + * The charset used during import. + * </p> + * <p> + * We cannot determine which charset should be used to interpret a given vCard file + * at first, while we have to decode sime encoded data (e.g. BASE64) to binary. + * In order to avoid "misinterpretation" of charset as much as possible, + * "ISO-8859-1" (a.k.a Latin-1) is first used for reading a stream. + * When charset is specified in a property (with "CHARSET=..." parameter), + * the string is decoded to raw bytes and encoded into the specific charset, + * assuming "ISO-8859-1" is able to map "all" 8bit characters to some unicode, + * and it has 1 to 1 mapping in all 8bit characters. + * If the assumption is not correct, this setting will cause some bug. + * </p> + * @hide made public just for unit test + */ + public static final String DEFAULT_INTERMEDIATE_CHARSET = "ISO-8859-1"; + + /** + * The charset used when there's no information affbout what charset should be used to + * encode the binary given from vCard. + */ + public static final String DEFAULT_IMPORT_CHARSET = "UTF-8"; + public static final String DEFAULT_EXPORT_CHARSET = "UTF-8"; + public static final int FLAG_V21 = 0; public static final int FLAG_V30 = 1; @@ -59,144 +75,140 @@ public class VCardConfig { private static final int NAME_ORDER_MASK = 0xC; // 0x10 is reserved for safety - - private static final int FLAG_CHARSET_UTF8 = 0; - private static final int FLAG_CHARSET_SHIFT_JIS = 0x100; - private static final int FLAG_CHARSET_MASK = 0xF00; /** + * <p> * The flag indicating the vCard composer will add some "X-" properties used only in Android * when the formal vCard specification does not have appropriate fields for that data. - * + * </p> + * <p> * For example, Android accepts nickname information while vCard 2.1 does not. * When this flag is on, vCard composer emits alternative "X-" property (like "X-NICKNAME") * instead of just dropping it. - * + * </p> + * <p> * vCard parser code automatically parses the field emitted even when this flag is off. - * - * Note that this flag does not assure all the information must be hold in the emitted vCard. + * </p> */ private static final int FLAG_USE_ANDROID_PROPERTY = 0x80000000; /** + * <p> * The flag indicating the vCard composer will add some "X-" properties seen in the * vCard data emitted by the other softwares/devices when the formal vCard specification - * does not have appropriate field(s) for that data. - * + * does not have appropriate field(s) for that data. + * </p> + * <p> * One example is X-PHONETIC-FIRST-NAME/X-PHONETIC-MIDDLE-NAME/X-PHONETIC-LAST-NAME, which are * for phonetic name (how the name is pronounced), seen in the vCard emitted by some other * non-Android devices/softwares. We chose to enable the vCard composer to use those * defact properties since they are also useful for Android devices. - * + * </p> + * <p> * Note for developers: only "X-" properties should be added with this flag. vCard 2.1/3.0 * allows any kind of "X-" properties but does not allow non-"X-" properties (except IANA tokens * in vCard 3.0). Some external parsers may get confused with non-valid, non-"X-" properties. + * </p> */ private static final int FLAG_USE_DEFACT_PROPERTY = 0x40000000; /** - * The flag indicating some specific dialect seen in vcard of DoCoMo (one of Japanese + * <p> + * The flag indicating some specific dialect seen in vCard of DoCoMo (one of Japanese * mobile careers) should be used. This flag does not include any other information like * that "the vCard is for Japanese". So it is "possible" that "the vCard should have DoCoMo's * dialect but the name order should be European", but it is not recommended. + * </p> */ private static final int FLAG_DOCOMO = 0x20000000; /** - * <P> + * <p> * The flag indicating the vCard composer does "NOT" use Quoted-Printable toward "primary" * properties even though it is required by vCard 2.1 (QP is prohibited in vCard 3.0). - * </P> - * <P> + * </p> + * <p> * We actually cannot define what is the "primary" property. Note that this is NOT defined * in vCard specification either. Also be aware that it is NOT related to "primary" notion * used in {@link android.provider.ContactsContract}. * This notion is just for vCard composition in Android. - * </P> - * <P> + * </p> + * <p> * We added this Android-specific notion since some (incomplete) vCard exporters for vCard 2.1 * do NOT use Quoted-Printable encoding toward some properties related names like "N", "FN", etc. * even when their values contain non-ascii or/and CR/LF, while they use the encoding in the * other properties like "ADR", "ORG", etc. - * <P> + * <p> * We are afraid of the case where some vCard importer also forget handling QP presuming QP is * not used in such fields. - * </P> - * <P> + * </p> + * <p> * This flag is useful when some target importer you are going to focus on does not accept * such properties with Quoted-Printable encoding. - * </P> - * <P> + * </p> + * <p> * Again, we should not use this flag at all for complying vCard 2.1 spec. - * </P> - * <P> + * </p> + * <p> * In vCard 3.0, Quoted-Printable is explicitly "prohibitted", so we don't need to care this * kind of problem (hopefully). - * </P> + * </p> + * @hide */ public static final int FLAG_REFRAIN_QP_TO_NAME_PROPERTIES = 0x10000000; /** - * <P> + * <p> * The flag indicating that phonetic name related fields must be converted to * appropriate form. Note that "appropriate" is not defined in any vCard specification. * This is Android-specific. - * </P> - * <P> + * </p> + * <p> * One typical (and currently sole) example where we need this flag is the time when * we need to emit Japanese phonetic names into vCard entries. The property values * should be encoded into half-width katakana when the target importer is Japanese mobile * phones', which are probably not able to parse full-width hiragana/katakana for * historical reasons, while the vCard importers embedded to softwares for PC should be * able to parse them as we expect. - * </P> + * </p> */ - public static final int FLAG_CONVERT_PHONETIC_NAME_STRINGS = 0x0800000; + public static final int FLAG_CONVERT_PHONETIC_NAME_STRINGS = 0x08000000; /** - * <P> + * <p> * The flag indicating the vCard composer "for 2.1" emits "TYPE=" string toward TYPE params * every time possible. The default behavior does not emit it and is valid in the spec. * In vCrad 3.0, this flag is unnecessary, since "TYPE=" is MUST in vCard 3.0 specification. - * </P> - * <P> + * </p> + * <p> * Detail: * How more than one TYPE fields are expressed is different between vCard 2.1 and vCard 3.0. * </p> - * <P> - * e.g.<BR /> - * 1) Probably valid in both vCard 2.1 and vCard 3.0: "ADR;TYPE=DOM;TYPE=HOME:..."<BR /> - * 2) Valid in vCard 2.1 but not in vCard 3.0: "ADR;DOM;HOME:..."<BR /> - * 3) Valid in vCard 3.0 but not in vCard 2.1: "ADR;TYPE=DOM,HOME:..."<BR /> - * </P> - * <P> - * 2) had been the default of VCard exporter/importer in Android, but it is found that - * some external exporter is not able to parse the type format like 2) but only 3). - * </P> - * <P> + * <p> + * e.g. + * </p> + * <ol> + * <li>Probably valid in both vCard 2.1 and vCard 3.0: "ADR;TYPE=DOM;TYPE=HOME:..."</li> + * <li>Valid in vCard 2.1 but not in vCard 3.0: "ADR;DOM;HOME:..."</li> + * <li>Valid in vCard 3.0 but not in vCard 2.1: "ADR;TYPE=DOM,HOME:..."</li> + * </ol> + * <p> * If you are targeting to the importer which cannot accept TYPE params without "TYPE=" * strings (which should be rare though), please use this flag. - * </P> - * <P> - * Example usage: int vcardType = (VCARD_TYPE_V21_GENERIC | FLAG_APPEND_TYPE_PARAM); - * </P> + * </p> + * <p> + * Example usage: + * <pre class="prettyprint">int type = (VCARD_TYPE_V21_GENERIC | FLAG_APPEND_TYPE_PARAM);</pre> + * </p> */ public static final int FLAG_APPEND_TYPE_PARAM = 0x04000000; /** - * <P> - * The flag asking exporter to refrain image export. - * </P> - * @hide will be deleted in the near future. - */ - public static final int FLAG_REFRAIN_IMAGE_EXPORT = 0x02000000; - - /** - * <P> + * <p> * The flag indicating the vCard composer does touch nothing toward phone number Strings * but leave it as is. - * </P> - * <P> + * </p> + * <p> * The vCard specifications mention nothing toward phone numbers, while some devices * do (wrongly, but with innevitable reasons). * For example, there's a possibility Japanese mobile phones are expected to have @@ -207,187 +219,185 @@ public class VCardConfig { * becomes "111-222-3333"). * Unfortunate side effect of that use was some control characters used in the other * areas may be badly affected by the formatting. - * </P> - * <P> + * </p> + * <p> * This flag disables that formatting, affecting both importer and exporter. * If the user is aware of some side effects due to the implicit formatting, use this flag. - * </P> + * </p> */ public static final int FLAG_REFRAIN_PHONE_NUMBER_FORMATTING = 0x02000000; + /** + * <p> + * For importer only. Ignored in exporter. + * </p> + * <p> + * The flag indicating the parser should handle a nested vCard, in which vCard clause starts + * in another vCard clause. Here's a typical example. + * </p> + * <pre class="prettyprint">BEGIN:VCARD + * BEGIN:VCARD + * VERSION:2.1 + * ... + * END:VCARD + * END:VCARD</pre> + * <p> + * The vCard 2.1 specification allows the nest, but also let parsers ignore nested entries, + * while some mobile devices emit nested ones as primary data to be imported. + * </p> + * <p> + * This flag forces a vCard parser to torelate such a nest and understand its content. + * </p> + */ + public static final int FLAG_TORELATE_NEST = 0x01000000; + //// The followings are VCard types available from importer/exporter. //// /** - * <P> - * Generic vCard format with the vCard 2.1. Uses UTF-8 for the charset. - * When composing a vCard entry, the US convension will be used toward formatting - * some values. - * </P> - * <P> + * <p> + * The type indicating nothing. Used by {@link VCardSourceDetector} when it + * was not able to guess the exact vCard type. + * </p> + */ + public static final int VCARD_TYPE_UNKNOWN = 0; + + /** + * <p> + * Generic vCard format with the vCard 2.1. When composing a vCard entry, + * the US convension will be used toward formatting some values. + * </p> + * <p> * e.g. The order of the display name would be "Prefix Given Middle Family Suffix", * while it should be "Prefix Family Middle Given Suffix" in Japan for example. - * </P> + * </p> + * <p> + * Uses UTF-8 for the charset as a charset for exporting. Note that old vCard importer + * outside Android cannot accept it since vCard 2.1 specifically does not allow + * that charset, while we need to use it to support various languages around the world. + * </p> + * <p> + * If you want to use alternative charset, you should notify the charset to the other + * compontent to be used. + * </p> */ - public static final int VCARD_TYPE_V21_GENERIC_UTF8 = - (FLAG_V21 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + public static final int VCARD_TYPE_V21_GENERIC = + (FLAG_V21 | NAME_ORDER_DEFAULT | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static String VCARD_TYPE_V21_GENERIC_UTF8_STR = "v21_generic"; + /* package */ static String VCARD_TYPE_V21_GENERIC_STR = "v21_generic"; /** - * <P> + * <p> * General vCard format with the version 3.0. Uses UTF-8 for the charset. - * </P> - * <P> + * </p> + * <p> * Not fully ready yet. Use with caution when you use this. - * </P> + * </p> */ - public static final int VCARD_TYPE_V30_GENERIC_UTF8 = - (FLAG_V30 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + public static final int VCARD_TYPE_V30_GENERIC = + (FLAG_V30 | NAME_ORDER_DEFAULT | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static final String VCARD_TYPE_V30_GENERIC_UTF8_STR = "v30_generic"; + /* package */ static final String VCARD_TYPE_V30_GENERIC_STR = "v30_generic"; /** - * <P> + * <p> * General vCard format for the vCard 2.1 with some Europe convension. Uses Utf-8. * Currently, only name order is considered ("Prefix Middle Given Family Suffix") - * </P> + * </p> */ - public static final int VCARD_TYPE_V21_EUROPE_UTF8 = - (FLAG_V21 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V21_EUROPE_UTF8_STR = "v21_europe"; + public static final int VCARD_TYPE_V21_EUROPE = + (FLAG_V21 | NAME_ORDER_EUROPE | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V21_EUROPE_STR = "v21_europe"; /** - * <P> + * <p> * General vCard format with the version 3.0 with some Europe convension. Uses UTF-8. - * </P> - * <P> + * </p> + * <p> * Not ready yet. Use with caution when you use this. - * </P> + * </p> */ - public static final int VCARD_TYPE_V30_EUROPE_UTF8 = - (FLAG_V30 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + public static final int VCARD_TYPE_V30_EUROPE = + (FLAG_V30 | NAME_ORDER_EUROPE | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); /* package */ static final String VCARD_TYPE_V30_EUROPE_STR = "v30_europe"; /** - * <P> + * <p> * The vCard 2.1 format for miscellaneous Japanese devices, using UTF-8 as default charset. - * </P> - * <P> + * </p> + * <p> * Not ready yet. Use with caution when you use this. - * </P> + * </p> */ - public static final int VCARD_TYPE_V21_JAPANESE_UTF8 = - (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V21_JAPANESE_UTF8_STR = "v21_japanese_utf8"; + public static final int VCARD_TYPE_V21_JAPANESE = + (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /** - * <P> - * vCard 2.1 format for miscellaneous Japanese devices. Shift_Jis is used for - * parsing/composing the vCard data. - * </P> - * <P> - * Not ready yet. Use with caution when you use this. - * </P> - */ - public static final int VCARD_TYPE_V21_JAPANESE_SJIS = - (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + /* package */ static final String VCARD_TYPE_V21_JAPANESE_STR = "v21_japanese_utf8"; - /* package */ static final String VCARD_TYPE_V21_JAPANESE_SJIS_STR = "v21_japanese_sjis"; - - /** - * <P> - * vCard format for miscellaneous Japanese devices, using Shift_Jis for - * parsing/composing the vCard data. - * </P> - * <P> - * Not ready yet. Use with caution when you use this. - * </P> - */ - public static final int VCARD_TYPE_V30_JAPANESE_SJIS = - (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - - /* package */ static final String VCARD_TYPE_V30_JAPANESE_SJIS_STR = "v30_japanese_sjis"; - /** - * <P> + * <p> * The vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset. - * </P> - * <P> + * </p> + * <p> * Not ready yet. Use with caution when you use this. - * </P> + * </p> */ - public static final int VCARD_TYPE_V30_JAPANESE_UTF8 = - (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + public static final int VCARD_TYPE_V30_JAPANESE = + (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static final String VCARD_TYPE_V30_JAPANESE_UTF8_STR = "v30_japanese_utf8"; + /* package */ static final String VCARD_TYPE_V30_JAPANESE_STR = "v30_japanese_utf8"; /** - * <P> + * <p> * The vCard 2.1 based format which (partially) considers the convention in Japanese * mobile phones, where phonetic names are translated to half-width katakana if - * possible, etc. - * </P> - * <P> - * Not ready yet. Use with caution when you use this. - * </P> + * possible, etc. It would be better to use Shift_JIS as a charset for maximum + * compatibility. + * </p> + * @hide Should not be available world wide. */ public static final int VCARD_TYPE_V21_JAPANESE_MOBILE = - (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | - FLAG_CONVERT_PHONETIC_NAME_STRINGS | - FLAG_REFRAIN_QP_TO_NAME_PROPERTIES); + (FLAG_V21 | NAME_ORDER_JAPANESE | + FLAG_CONVERT_PHONETIC_NAME_STRINGS | FLAG_REFRAIN_QP_TO_NAME_PROPERTIES); /* package */ static final String VCARD_TYPE_V21_JAPANESE_MOBILE_STR = "v21_japanese_mobile"; /** - * <P> - * VCard format used in DoCoMo, which is one of Japanese mobile phone careers. + * <p> + * The vCard format used in DoCoMo, which is one of Japanese mobile phone careers. * </p> - * <P> + * <p> * Base version is vCard 2.1, but the data has several DoCoMo-specific convensions. * No Android-specific property nor defact property is included. The "Primary" properties * are NOT encoded to Quoted-Printable. - * </P> + * </p> + * @hide Should not be available world wide. */ public static final int VCARD_TYPE_DOCOMO = (VCARD_TYPE_V21_JAPANESE_MOBILE | FLAG_DOCOMO); /* package */ static final String VCARD_TYPE_DOCOMO_STR = "docomo"; - public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC_UTF8; + public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC; private static final Map<String, Integer> sVCardTypeMap; private static final Set<Integer> sJapaneseMobileTypeSet; static { sVCardTypeMap = new HashMap<String, Integer>(); - sVCardTypeMap.put(VCARD_TYPE_V21_GENERIC_UTF8_STR, VCARD_TYPE_V21_GENERIC_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V30_GENERIC_UTF8_STR, VCARD_TYPE_V30_GENERIC_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V21_EUROPE_UTF8_STR, VCARD_TYPE_V21_EUROPE_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V30_EUROPE_STR, VCARD_TYPE_V30_EUROPE_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_SJIS_STR, VCARD_TYPE_V21_JAPANESE_SJIS); - sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_UTF8_STR, VCARD_TYPE_V21_JAPANESE_UTF8); - sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_SJIS_STR, VCARD_TYPE_V30_JAPANESE_SJIS); - sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_UTF8_STR, VCARD_TYPE_V30_JAPANESE_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V21_GENERIC_STR, VCARD_TYPE_V21_GENERIC); + sVCardTypeMap.put(VCARD_TYPE_V30_GENERIC_STR, VCARD_TYPE_V30_GENERIC); + sVCardTypeMap.put(VCARD_TYPE_V21_EUROPE_STR, VCARD_TYPE_V21_EUROPE); + sVCardTypeMap.put(VCARD_TYPE_V30_EUROPE_STR, VCARD_TYPE_V30_EUROPE); + sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_STR, VCARD_TYPE_V21_JAPANESE); + sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_STR, VCARD_TYPE_V30_JAPANESE); sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_MOBILE_STR, VCARD_TYPE_V21_JAPANESE_MOBILE); sVCardTypeMap.put(VCARD_TYPE_DOCOMO_STR, VCARD_TYPE_DOCOMO); sJapaneseMobileTypeSet = new HashSet<Integer>(); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_SJIS); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_UTF8); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_SJIS); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE_SJIS); - sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE_UTF8); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE); sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_MOBILE); sJapaneseMobileTypeSet.add(VCARD_TYPE_DOCOMO); } @@ -412,14 +422,6 @@ public class VCardConfig { return !isV30(vcardType); } - public static boolean usesUtf8(final int vcardType) { - return ((vcardType & FLAG_CHARSET_MASK) == FLAG_CHARSET_UTF8); - } - - public static boolean usesShiftJis(final int vcardType) { - return ((vcardType & FLAG_CHARSET_MASK) == FLAG_CHARSET_SHIFT_JIS); - } - public static int getNameOrderType(final int vcardType) { return vcardType & NAME_ORDER_MASK; } diff --git a/core/java/android/pim/vcard/VCardConstants.java b/core/java/android/pim/vcard/VCardConstants.java index 8c07126..e11b1fd 100644 --- a/core/java/android/pim/vcard/VCardConstants.java +++ b/core/java/android/pim/vcard/VCardConstants.java @@ -109,6 +109,12 @@ public class VCardConstants { public static final String PARAM_TYPE_BBS = "BBS"; public static final String PARAM_TYPE_VIDEO = "VIDEO"; + public static final String PARAM_ENCODING_7BIT = "7BIT"; + public static final String PARAM_ENCODING_8BIT = "8BIT"; + public static final String PARAM_ENCODING_QP = "QUOTED-PRINTABLE"; + public static final String PARAM_ENCODING_BASE64 = "BASE64"; // Available in vCard 2.1 + public static final String PARAM_ENCODING_B = "B"; // Available in vCard 3.0 + // TYPE parameters for Phones, which are not formally valid in vCard (at least 2.1). // These types are basically encoded to "X-" parameters when composing vCard. // Parser passes these when "X-" is added to the parameter or not. @@ -130,10 +136,6 @@ public class VCardConstants { // Do not use in composer side. public static final String PARAM_EXTRA_TYPE_COMPANY = "COMPANY"; - // DoCoMo specific type parameter. Used with "SOUND" property, which is alternate of SORT-STRING in - // vCard 3.0. - public static final String PARAM_TYPE_X_IRMC_N = "X-IRMC-N"; - public interface ImportOnly { public static final String PROPERTY_X_NICKNAME = "X-NICKNAME"; // Some device emits this "X-" parameter for expressing Google Talk, @@ -142,6 +144,12 @@ public class VCardConstants { public static final String PROPERTY_X_GOOGLE_TALK_WITH_SPACE = "X-GOOGLE TALK"; } + //// Mainly for package constants. + + // DoCoMo specific type parameter. Used with "SOUND" property, which is alternate of + // SORT-STRING invCard 3.0. + /* package */ static final String PARAM_TYPE_X_IRMC_N = "X-IRMC-N"; + /* package */ static final int MAX_DATA_COLUMN = 15; /* package */ static final int MAX_CHARACTER_NUMS_QP = 76; diff --git a/core/java/android/pim/vcard/VCardEntry.java b/core/java/android/pim/vcard/VCardEntry.java index 7c7e9b8..5b9cf17 100644 --- a/core/java/android/pim/vcard/VCardEntry.java +++ b/core/java/android/pim/vcard/VCardEntry.java @@ -61,9 +61,6 @@ public class VCardEntry { private final static int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK; - private static final String ACCOUNT_TYPE_GOOGLE = "com.google"; - private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts"; - private static final Map<String, Integer> sImMap = new HashMap<String, Integer>(); static { @@ -78,12 +75,12 @@ public class VCardEntry { Im.PROTOCOL_GOOGLE_TALK); } - static public class PhoneData { + public static class PhoneData { public final int type; public final String data; public final String label; - // isPrimary is changable only when there's no appropriate one existing in - // the original VCard. + // isPrimary is (not final but) changable, only when there's no appropriate one existing + // in the original VCard. public boolean isPrimary; public PhoneData(int type, String data, String label, boolean isPrimary) { this.type = type; @@ -109,13 +106,11 @@ public class VCardEntry { } } - static public class EmailData { + public static class EmailData { public final int type; public final String data; // Used only when TYPE is TYPE_CUSTOM. public final String label; - // isPrimary is changable only when there's no appropriate one existing in - // the original VCard. public boolean isPrimary; public EmailData(int type, String data, String label, boolean isPrimary) { this.type = type; @@ -141,9 +136,9 @@ public class VCardEntry { } } - static public class PostalData { - // Determined by vCard spec. - // PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name + public static class PostalData { + // Determined by vCard specification. + // - PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name public static final int ADDR_MAX_DATA_SIZE = 7; private final String[] dataArray; public final String pobox; @@ -248,10 +243,11 @@ public class VCardEntry { } } - static public class OrganizationData { + public static class OrganizationData { public final int type; // non-final is Intentional: we may change the values since this info is separated into - // two parts in vCard: "ORG" + "TITLE". + // two parts in vCard: "ORG" + "TITLE", and we have to cope with each field in + // different timing. public String companyName; public String departmentName; public String titleName; @@ -313,7 +309,7 @@ public class VCardEntry { } } - static public class ImData { + public static class ImData { public final int protocol; public final String customProtocol; public final int type; @@ -441,7 +437,7 @@ public class VCardEntry { private String mSuffix; // Used only when no family nor given name is found. - private String mFullName; + private String mFormattedName; private String mPhoneticFamilyName; private String mPhoneticGivenName; @@ -469,7 +465,7 @@ public class VCardEntry { private final Account mAccount; public VCardEntry() { - this(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); + this(VCardConfig.VCARD_TYPE_V21_GENERIC); } public VCardEntry(int vcardType) { @@ -499,7 +495,6 @@ public class VCardEntry { } } - // Use NANP in default when there's no information about locale. final int formattingType = VCardUtils.getPhoneNumberFormat(mVCardType); formattedNumber = PhoneNumberUtils.formatNumber(builder.toString(), formattingType); } @@ -754,11 +749,11 @@ public class VCardEntry { if (propName.equals(VCardConstants.PROPERTY_VERSION)) { // vCard version. Ignore this. } else if (propName.equals(VCardConstants.PROPERTY_FN)) { - mFullName = propValue; - } else if (propName.equals(VCardConstants.PROPERTY_NAME) && mFullName == null) { + mFormattedName = propValue; + } else if (propName.equals(VCardConstants.PROPERTY_NAME) && mFormattedName == null) { // Only in vCard 3.0. Use this if FN, which must exist in vCard 3.0 but may not // actually exist in the real vCard data, does not exist. - mFullName = propValue; + mFormattedName = propValue; } else if (propName.equals(VCardConstants.PROPERTY_N)) { handleNProperty(propValueList); } else if (propName.equals(VCardConstants.PROPERTY_SORT_STRING)) { @@ -1016,8 +1011,8 @@ public class VCardEntry { */ private void constructDisplayName() { // FullName (created via "FN" or "NAME" field) is prefered. - if (!TextUtils.isEmpty(mFullName)) { - mDisplayName = mFullName; + if (!TextUtils.isEmpty(mFormattedName)) { + mDisplayName = mFormattedName; } else if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) { mDisplayName = VCardUtils.constructNameFromElements(mVCardType, mFamilyName, mMiddleName, mGivenName, mPrefix, mSuffix); @@ -1062,23 +1057,6 @@ public class VCardEntry { if (mAccount != null) { builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name); builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type); - - // Assume that caller side creates this group if it does not exist. - if (ACCOUNT_TYPE_GOOGLE.equals(mAccount.type)) { - final Cursor cursor = resolver.query(Groups.CONTENT_URI, new String[] { - Groups.SOURCE_ID }, - Groups.TITLE + "=?", new String[] { - GOOGLE_MY_CONTACTS_GROUP }, null); - try { - if (cursor != null && cursor.moveToFirst()) { - myGroupsId = cursor.getString(0); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } } else { builder.withValue(RawContacts.ACCOUNT_NAME, null); builder.withValue(RawContacts.ACCOUNT_TYPE, null); @@ -1320,7 +1298,7 @@ public class VCardEntry { && TextUtils.isEmpty(mGivenName) && TextUtils.isEmpty(mPrefix) && TextUtils.isEmpty(mSuffix) - && TextUtils.isEmpty(mFullName) + && TextUtils.isEmpty(mFormattedName) && TextUtils.isEmpty(mPhoneticFamilyName) && TextUtils.isEmpty(mPhoneticMiddleName) && TextUtils.isEmpty(mPhoneticGivenName) @@ -1379,7 +1357,7 @@ public class VCardEntry { } public String getFullName() { - return mFullName; + return mFormattedName; } public String getPhoneticFamilyName() { diff --git a/core/java/android/pim/vcard/VCardEntryCommitter.java b/core/java/android/pim/vcard/VCardEntryCommitter.java index 59a2baf..a8c8057 100644 --- a/core/java/android/pim/vcard/VCardEntryCommitter.java +++ b/core/java/android/pim/vcard/VCardEntryCommitter.java @@ -52,9 +52,9 @@ public class VCardEntryCommitter implements VCardEntryHandler { } } - public void onEntryCreated(final VCardEntry contactStruct) { + public void onEntryCreated(final VCardEntry vcardEntry) { long start = System.currentTimeMillis(); - mCreatedUris.add(contactStruct.pushIntoContentResolver(mContentResolver)); + mCreatedUris.add(vcardEntry.pushIntoContentResolver(mContentResolver)); mTimeToCommit += System.currentTimeMillis() - start; } diff --git a/core/java/android/pim/vcard/VCardEntryConstructor.java b/core/java/android/pim/vcard/VCardEntryConstructor.java index 290ca2b..a0abae8 100644 --- a/core/java/android/pim/vcard/VCardEntryConstructor.java +++ b/core/java/android/pim/vcard/VCardEntryConstructor.java @@ -16,12 +16,11 @@ package android.pim.vcard; import android.accounts.Account; +import android.text.TextUtils; import android.util.CharsetUtils; import android.util.Log; -import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.net.QuotedPrintableCodec; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; @@ -30,64 +29,73 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +/** + * <p> + * The {@link VCardInterpreter} implementation which enables {@link VCardEntryHandler} objects + * to easily handle each vCard entry. + * </p> + * <p> + * This class understand details inside vCard and translates it to {@link VCardEntry}. + * Then the class throw it to {@link VCardEntryHandler} registered via + * {@link #addEntryHandler(VCardEntryHandler)}, so that all those registered objects + * are able to handle the {@link VCardEntry} object. + * </p> + * <p> + * If you want to know the detail inside vCard, it would be better to implement + * {@link VCardInterpreter} directly, instead of relying on this class and + * {@link VCardEntry} created by the object. + * </p> + */ public class VCardEntryConstructor implements VCardInterpreter { private static String LOG_TAG = "VCardEntryConstructor"; - /** - * If there's no other information available, this class uses this charset for encoding - * byte arrays to String. - */ - /* package */ static final String DEFAULT_CHARSET_FOR_DECODED_BYTES = "UTF-8"; - private VCardEntry.Property mCurrentProperty = new VCardEntry.Property(); - private VCardEntry mCurrentContactStruct; + private VCardEntry mCurrentVCardEntry; private String mParamType; - /** - * The charset using which {@link VCardInterpreter} parses the text. - */ - private String mInputCharset; - - /** - * The charset with which byte array is encoded to String. - */ - final private String mCharsetForDecodedBytes; - final private boolean mStrictLineBreakParsing; - final private int mVCardType; - final private Account mAccount; + // The charset using which {@link VCardInterpreter} parses the text. + // Each String is first decoded into binary stream with this charset, and encoded back + // to "target charset", which may be explicitly specified by the vCard with "CHARSET" + // property or implicitly mentioned by its version (e.g. vCard 3.0 recommends UTF-8). + private final String mSourceCharset; + + private final boolean mStrictLineBreaking; + private final int mVCardType; + private final Account mAccount; - /** For measuring performance. */ + // For measuring performance. private long mTimePushIntoContentResolver; - final private List<VCardEntryHandler> mEntryHandlers = new ArrayList<VCardEntryHandler>(); + private final List<VCardEntryHandler> mEntryHandlers = new ArrayList<VCardEntryHandler>(); public VCardEntryConstructor() { - this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8, null); + this(VCardConfig.VCARD_TYPE_V21_GENERIC, null, null, false); } public VCardEntryConstructor(final int vcardType) { - this(null, null, false, vcardType, null); + this(vcardType, null, null, false); + } + + public VCardEntryConstructor(final int vcardType, final Account account) { + this(vcardType, account, null, false); } - public VCardEntryConstructor(final String charset, final boolean strictLineBreakParsing, - final int vcardType, final Account account) { - this(null, charset, strictLineBreakParsing, vcardType, account); + public VCardEntryConstructor(final int vcardType, final Account account, + final String inputCharset) { + this(vcardType, account, inputCharset, false); } - public VCardEntryConstructor(final String inputCharset, final String charsetForDetodedBytes, - final boolean strictLineBreakParsing, final int vcardType, - final Account account) { + /** + * @hide + */ + public VCardEntryConstructor(final int vcardType, final Account account, + final String inputCharset, final boolean strictLineBreakParsing) { if (inputCharset != null) { - mInputCharset = inputCharset; + mSourceCharset = inputCharset; } else { - mInputCharset = VCardConfig.DEFAULT_CHARSET; + mSourceCharset = VCardConfig.DEFAULT_INTERMEDIATE_CHARSET; } - if (charsetForDetodedBytes != null) { - mCharsetForDecodedBytes = charsetForDetodedBytes; - } else { - mCharsetForDecodedBytes = DEFAULT_CHARSET_FOR_DECODED_BYTES; - } - mStrictLineBreakParsing = strictLineBreakParsing; + mStrictLineBreaking = strictLineBreakParsing; mVCardType = vcardType; mAccount = account; } @@ -108,30 +116,24 @@ public class VCardEntryConstructor implements VCardInterpreter { } } - /** - * Called when the parse failed between {@link #startEntry()} and {@link #endEntry()}. - */ public void clear() { - mCurrentContactStruct = null; + mCurrentVCardEntry = null; mCurrentProperty = new VCardEntry.Property(); } - /** - * Assume that VCard is not nested. In other words, this code does not accept - */ public void startEntry() { - if (mCurrentContactStruct != null) { + if (mCurrentVCardEntry != null) { Log.e(LOG_TAG, "Nested VCard code is not supported now."); } - mCurrentContactStruct = new VCardEntry(mVCardType, mAccount); + mCurrentVCardEntry = new VCardEntry(mVCardType, mAccount); } public void endEntry() { - mCurrentContactStruct.consolidateFields(); + mCurrentVCardEntry.consolidateFields(); for (VCardEntryHandler entryHandler : mEntryHandlers) { - entryHandler.onEntryCreated(mCurrentContactStruct); + entryHandler.onEntryCreated(mCurrentVCardEntry); } - mCurrentContactStruct = null; + mCurrentVCardEntry = null; } public void startProperty() { @@ -139,7 +141,7 @@ public class VCardEntryConstructor implements VCardInterpreter { } public void endProperty() { - mCurrentContactStruct.addProperty(mCurrentProperty); + mCurrentVCardEntry.addProperty(mCurrentProperty); } public void propertyName(String name) { @@ -166,113 +168,41 @@ public class VCardEntryConstructor implements VCardInterpreter { mParamType = null; } - private String encodeString(String originalString, String charsetForDecodedBytes) { - if (mInputCharset.equalsIgnoreCase(charsetForDecodedBytes)) { + private static String encodeToSystemCharset(String originalString, + String sourceCharset, String targetCharset) { + if (sourceCharset.equalsIgnoreCase(targetCharset)) { return originalString; } - Charset charset = Charset.forName(mInputCharset); - ByteBuffer byteBuffer = charset.encode(originalString); + final Charset charset = Charset.forName(sourceCharset); + final ByteBuffer byteBuffer = charset.encode(originalString); // byteBuffer.array() "may" return byte array which is larger than // byteBuffer.remaining(). Here, we keep on the safe side. - byte[] bytes = new byte[byteBuffer.remaining()]; + final byte[] bytes = new byte[byteBuffer.remaining()]; byteBuffer.get(bytes); try { - return new String(bytes, charsetForDecodedBytes); + String ret = new String(bytes, targetCharset); + return ret; } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Failed to encode: charset=" + charsetForDecodedBytes); + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); return null; } } - private String handleOneValue(String value, String charsetForDecodedBytes, String encoding) { + private String handleOneValue(String value, + String sourceCharset, String targetCharset, String encoding) { if (encoding != null) { if (encoding.equals("BASE64") || encoding.equals("B")) { mCurrentProperty.setPropertyBytes(Base64.decodeBase64(value.getBytes())); return value; } else if (encoding.equals("QUOTED-PRINTABLE")) { - // "= " -> " ", "=\t" -> "\t". - // Previous code had done this replacement. Keep on the safe side. - StringBuilder builder = new StringBuilder(); - int length = value.length(); - for (int i = 0; i < length; i++) { - char ch = value.charAt(i); - if (ch == '=' && i < length - 1) { - char nextCh = value.charAt(i + 1); - if (nextCh == ' ' || nextCh == '\t') { - - builder.append(nextCh); - i++; - continue; - } - } - builder.append(ch); - } - String quotedPrintable = builder.toString(); - - String[] lines; - if (mStrictLineBreakParsing) { - lines = quotedPrintable.split("\r\n"); - } else { - builder = new StringBuilder(); - length = quotedPrintable.length(); - ArrayList<String> list = new ArrayList<String>(); - for (int i = 0; i < length; i++) { - char ch = quotedPrintable.charAt(i); - if (ch == '\n') { - list.add(builder.toString()); - builder = new StringBuilder(); - } else if (ch == '\r') { - list.add(builder.toString()); - builder = new StringBuilder(); - if (i < length - 1) { - char nextCh = quotedPrintable.charAt(i + 1); - if (nextCh == '\n') { - i++; - } - } - } else { - builder.append(ch); - } - } - String finalLine = builder.toString(); - if (finalLine.length() > 0) { - list.add(finalLine); - } - lines = list.toArray(new String[0]); - } - - builder = new StringBuilder(); - for (String line : lines) { - if (line.endsWith("=")) { - line = line.substring(0, line.length() - 1); - } - builder.append(line); - } - byte[] bytes; - try { - bytes = builder.toString().getBytes(mInputCharset); - } catch (UnsupportedEncodingException e1) { - Log.e(LOG_TAG, "Failed to encode: charset=" + mInputCharset); - bytes = builder.toString().getBytes(); - } - - try { - bytes = QuotedPrintableCodec.decodeQuotedPrintable(bytes); - } catch (DecoderException e) { - Log.e(LOG_TAG, "Failed to decode quoted-printable: " + e); - return ""; - } - - try { - return new String(bytes, charsetForDecodedBytes); - } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Failed to encode: charset=" + charsetForDecodedBytes); - return new String(bytes); - } + return VCardUtils.parseQuotedPrintable( + value, mStrictLineBreaking, sourceCharset, targetCharset); } - // Unknown encoding. Fall back to default. + Log.w(LOG_TAG, "Unknown encoding. Fall back to default."); } - return encodeString(value, charsetForDecodedBytes); + + // Just translate the charset of a given String from inputCharset to a system one. + return encodeToSystemCharset(value, sourceCharset, targetCharset); } public void propertyValues(List<String> values) { @@ -281,23 +211,24 @@ public class VCardEntryConstructor implements VCardInterpreter { } final Collection<String> charsetCollection = mCurrentProperty.getParameters("CHARSET"); - final String charset = - ((charsetCollection != null) ? charsetCollection.iterator().next() : null); final Collection<String> encodingCollection = mCurrentProperty.getParameters("ENCODING"); final String encoding = ((encodingCollection != null) ? encodingCollection.iterator().next() : null); - - String charsetForDecodedBytes = CharsetUtils.nameForDefaultVendor(charset); - if (charsetForDecodedBytes == null || charsetForDecodedBytes.length() == 0) { - charsetForDecodedBytes = mCharsetForDecodedBytes; + String targetCharset = CharsetUtils.nameForDefaultVendor( + ((charsetCollection != null) ? charsetCollection.iterator().next() : null)); + if (TextUtils.isEmpty(targetCharset)) { + targetCharset = VCardConfig.DEFAULT_IMPORT_CHARSET; } for (final String value : values) { mCurrentProperty.addToPropertyValueList( - handleOneValue(value, charsetForDecodedBytes, encoding)); + handleOneValue(value, mSourceCharset, targetCharset, encoding)); } } + /** + * @hide + */ public void showPerformanceInfo() { Log.d(LOG_TAG, "time for insert ContactStruct to database: " + mTimePushIntoContentResolver + " ms"); diff --git a/core/java/android/pim/vcard/VCardEntryHandler.java b/core/java/android/pim/vcard/VCardEntryHandler.java index 83a67fe..56bf69d 100644 --- a/core/java/android/pim/vcard/VCardEntryHandler.java +++ b/core/java/android/pim/vcard/VCardEntryHandler.java @@ -16,8 +16,13 @@ package android.pim.vcard; /** - * The interface called by {@link VCardEntryConstructor}. Useful when you don't want to - * handle detailed information as what {@link VCardParser} provides via {@link VCardInterpreter}. + * <p> + * The interface called by {@link VCardEntryConstructor}. + * </p> + * <p> + * This class is useful when you don't want to know vCard data in detail. If you want to know + * it, it would be better to consider using {@link VCardInterpreter}. + * </p> */ public interface VCardEntryHandler { /** diff --git a/core/java/android/pim/vcard/VCardInterpreter.java b/core/java/android/pim/vcard/VCardInterpreter.java index b5237c0..03704a2 100644 --- a/core/java/android/pim/vcard/VCardInterpreter.java +++ b/core/java/android/pim/vcard/VCardInterpreter.java @@ -20,7 +20,7 @@ import java.util.List; /** * <P> * The interface which should be implemented by the classes which have to analyze each - * vCard entry more minutely than {@link VCardEntry} class analysis. + * vCard entry minutely. * </P> * <P> * Here, there are several terms specific to vCard (and this library). diff --git a/core/java/android/pim/vcard/VCardInterpreterCollection.java b/core/java/android/pim/vcard/VCardInterpreterCollection.java index 99f81f7..4952dc7 100644 --- a/core/java/android/pim/vcard/VCardInterpreterCollection.java +++ b/core/java/android/pim/vcard/VCardInterpreterCollection.java @@ -23,7 +23,7 @@ import java.util.List; * {@link VCardInterpreter} objects and make a user object treat them as one * {@link VCardInterpreter} object. */ -public class VCardInterpreterCollection implements VCardInterpreter { +public final class VCardInterpreterCollection implements VCardInterpreter { private final Collection<VCardInterpreter> mInterpreterCollection; public VCardInterpreterCollection(Collection<VCardInterpreter> interpreterCollection) { diff --git a/core/java/android/pim/vcard/VCardParser.java b/core/java/android/pim/vcard/VCardParser.java index 57c52a6..31b9369 100644 --- a/core/java/android/pim/vcard/VCardParser.java +++ b/core/java/android/pim/vcard/VCardParser.java @@ -20,82 +20,36 @@ import android.pim.vcard.exception.VCardException; import java.io.IOException; import java.io.InputStream; -public abstract class VCardParser { - protected final int mParseType; - protected boolean mCanceled; - - public VCardParser() { - this(VCardConfig.PARSE_TYPE_UNKNOWN); - } - - public VCardParser(int parseType) { - mParseType = parseType; - } - +public interface VCardParser { /** - * <P> - * Parses the given stream and send the VCard data into VCardBuilderBase object. - * </P. - * <P> + * <p> + * Parses the given stream and send the vCard data into VCardBuilderBase object. + * </p>. + * <p> * Note that vCard 2.1 specification allows "CHARSET" parameter, and some career sets * local encoding to it. For example, Japanese phone career uses Shift_JIS, which is - * formally allowed in VCard 2.1, but not recommended in VCard 3.0. In VCard 2.1, - * In some exreme case, some VCard may have different charsets in one VCard (though - * we do not see any device which emits such kind of malicious data) - * </P> - * <P> - * In order to avoid "misunderstanding" charset as much as possible, this method - * use "ISO-8859-1" for reading the stream. When charset is specified in some property - * (with "CHARSET=..." parameter), the string is decoded to raw bytes and encoded to - * the charset. This method assumes that "ISO-8859-1" has 1 to 1 mapping in all 8bit - * characters, which is not completely sure. In some cases, this "decoding-encoding" - * scheme may fail. To avoid the case, - * </P> - * <P> - * We recommend you to use {@link VCardSourceDetector} and detect which kind of source the - * VCard comes from and explicitly specify a charset using the result. - * </P> + * formally allowed in vCard 2.1, but not allowed in vCard 3.0. In vCard 2.1, + * In some exreme case, it is allowed for vCard to have different charsets in one vCard. + * </p> + * <p> + * We recommend you use {@link VCardSourceDetector} and detect which kind of source the + * vCard comes from and explicitly specify a charset using the result. + * </p> * * @param is The source to parse. * @param interepreter A {@link VCardInterpreter} object which used to construct data. - * @return Returns true for success. Otherwise returns false. - * @throws IOException, VCardException - */ - public abstract boolean parse(InputStream is, VCardInterpreter interepreter) - throws IOException, VCardException; - - /** - * <P> - * The method variants which accept charset. - * </P> - * <P> - * RFC 2426 "recommends" (not forces) to use UTF-8, so it may be OK to use - * UTF-8 as an encoding when parsing vCard 3.0. But note that some Japanese - * phone uses Shift_JIS as a charset (e.g. W61SH), and another uses - * "CHARSET=SHIFT_JIS", which is explicitly prohibited in vCard 3.0 specification (e.g. W53K). - * </P> - * - * @param is The source to parse. - * @param charset Charset to be used. - * @param builder The VCardBuilderBase object. - * @return Returns true when successful. Otherwise returns false. * @throws IOException, VCardException */ - public abstract boolean parse(InputStream is, String charset, VCardInterpreter builder) + public void parse(InputStream is, VCardInterpreter interepreter) throws IOException, VCardException; - - /** - * The method variants which tells this object the operation is already canceled. - */ - public abstract void parse(InputStream is, String charset, - VCardInterpreter builder, boolean canceled) - throws IOException, VCardException; - + /** - * Cancel parsing. - * Actual cancel is done after the end of the current one vcard entry parsing. + * <p> + * Cancel parsing vCard. Useful when you want to stop the parse in the other threads. + * </p> + * <p> + * Actual cancel is done after parsing the current vcard. + * </p> */ - public void cancel() { - mCanceled = true; - } + public abstract void cancel(); } diff --git a/core/java/android/pim/vcard/VCardParserImpl_V21.java b/core/java/android/pim/vcard/VCardParserImpl_V21.java new file mode 100644 index 0000000..7d294cc --- /dev/null +++ b/core/java/android/pim/vcard/VCardParserImpl_V21.java @@ -0,0 +1,967 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.pim.vcard; + +import android.pim.vcard.exception.VCardAgentNotSupportedException; +import android.pim.vcard.exception.VCardException; +import android.pim.vcard.exception.VCardInvalidCommentLineException; +import android.pim.vcard.exception.VCardInvalidLineException; +import android.pim.vcard.exception.VCardNestedException; +import android.pim.vcard.exception.VCardVersionException; +import android.text.TextUtils; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +/** + * <p> + * Basic implementation achieving vCard parsing. Based on vCard 2.1, + * </p> + * @hide + */ +/* package */ class VCardParserImpl_V21 { + private static final String LOG_TAG = "VCardParserImpl_V21"; + + private static final class CustomBufferedReader extends BufferedReader { + private long mTime; + + public CustomBufferedReader(Reader in) { + super(in); + } + + @Override + public String readLine() throws IOException { + long start = System.currentTimeMillis(); + String ret = super.readLine(); + long end = System.currentTimeMillis(); + mTime += end - start; + return ret; + } + + public long getTotalmillisecond() { + return mTime; + } + } + + private static final String sDefaultEncoding = "8BIT"; + + protected boolean mCanceled; + protected VCardInterpreter mInterpreter; + + protected final String mImportCharset; + + /** + * <p> + * The encoding type for deconding byte streams. This member variable is + * reset to a default encoding every time when a new item comes. + * </p> + * <p> + * "Encoding" in vCard is different from "Charset". It is mainly used for + * addresses, notes, images. "7BIT", "8BIT", "BASE64", and + * "QUOTED-PRINTABLE" are known examples. + * </p> + */ + protected String mCurrentEncoding; + + /** + * <p> + * The reader object to be used internally. + * </p> + * <p> + * Developers should not directly read a line from this object. Use + * getLine() unless there some reason. + * </p> + */ + protected BufferedReader mReader; + + /** + * <p> + * Set for storing unkonwn TYPE attributes, which is not acceptable in vCard + * specification, but happens to be seen in real world vCard. + * </p> + */ + protected final Set<String> mUnknownTypeSet = new HashSet<String>(); + + /** + * <p> + * Set for storing unkonwn VALUE attributes, which is not acceptable in + * vCard specification, but happens to be seen in real world vCard. + * </p> + */ + protected final Set<String> mUnknownValueSet = new HashSet<String>(); + + + // In some cases, vCard is nested. Currently, we only consider the most + // interior vCard data. + // See v21_foma_1.vcf in test directory for more information. + // TODO: Don't ignore by using count, but read all of information outside vCard. + private int mNestCount; + + // Used only for parsing END:VCARD. + private String mPreviousLine; + + // For measuring performance. + private long mTimeTotal; + private long mTimeReadStartRecord; + private long mTimeReadEndRecord; + private long mTimeStartProperty; + private long mTimeEndProperty; + private long mTimeParseItems; + private long mTimeParseLineAndHandleGroup; + private long mTimeParsePropertyValues; + private long mTimeParseAdrOrgN; + private long mTimeHandleMiscPropertyValue; + private long mTimeHandleQuotedPrintable; + private long mTimeHandleBase64; + + public VCardParserImpl_V21() { + this(VCardConfig.VCARD_TYPE_DEFAULT, null); + } + + public VCardParserImpl_V21(int vcardType) { + this(vcardType, null); + } + + public VCardParserImpl_V21(int vcardType, String importCharset) { + if ((vcardType & VCardConfig.FLAG_TORELATE_NEST) != 0) { + mNestCount = 1; + } + + mImportCharset = (!TextUtils.isEmpty(importCharset) ? importCharset : + VCardConfig.DEFAULT_INTERMEDIATE_CHARSET); + } + + /** + * <p> + * Parses the file at the given position. + * </p> + */ + // <pre class="prettyprint">vcard_file = [wsls] vcard [wsls]</pre> + protected void parseVCardFile() throws IOException, VCardException { + boolean readingFirstFile = true; + while (true) { + if (mCanceled) { + break; + } + if (!parseOneVCard(readingFirstFile)) { + break; + } + readingFirstFile = false; + } + + if (mNestCount > 0) { + boolean useCache = true; + for (int i = 0; i < mNestCount; i++) { + readEndVCard(useCache, true); + useCache = false; + } + } + } + + /** + * @return true when a given property name is a valid property name. + */ + protected boolean isValidPropertyName(final String propertyName) { + if (!(getKnownPropertyNameSet().contains(propertyName.toUpperCase()) || + propertyName.startsWith("X-")) + && !mUnknownTypeSet.contains(propertyName)) { + mUnknownTypeSet.add(propertyName); + Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName); + } + return true; + } + + /** + * @return String. It may be null, or its length may be 0 + * @throws IOException + */ + protected String getLine() throws IOException { + return mReader.readLine(); + } + + /** + * @return String with it's length > 0 + * @throws IOException + * @throws VCardException when the stream reached end of line + */ + protected String getNonEmptyLine() throws IOException, VCardException { + String line; + while (true) { + line = getLine(); + if (line == null) { + throw new VCardException("Reached end of buffer."); + } else if (line.trim().length() > 0) { + return line; + } + } + } + + /* + * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF + * items *CRLF + * "END" [ws] ":" [ws] "VCARD" + */ + private boolean parseOneVCard(boolean firstRead) throws IOException, VCardException { + boolean allowGarbage = false; + if (firstRead) { + if (mNestCount > 0) { + for (int i = 0; i < mNestCount; i++) { + if (!readBeginVCard(allowGarbage)) { + return false; + } + allowGarbage = true; + } + } + } + + if (!readBeginVCard(allowGarbage)) { + return false; + } + long start; + if (mInterpreter != null) { + start = System.currentTimeMillis(); + mInterpreter.startEntry(); + mTimeReadStartRecord += System.currentTimeMillis() - start; + } + start = System.currentTimeMillis(); + parseItems(); + mTimeParseItems += System.currentTimeMillis() - start; + readEndVCard(true, false); + if (mInterpreter != null) { + start = System.currentTimeMillis(); + mInterpreter.endEntry(); + mTimeReadEndRecord += System.currentTimeMillis() - start; + } + return true; + } + + /** + * @return True when successful. False when reaching the end of line + * @throws IOException + * @throws VCardException + */ + protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { + String line; + do { + while (true) { + line = getLine(); + if (line == null) { + return false; + } else if (line.trim().length() > 0) { + break; + } + } + String[] strArray = line.split(":", 2); + int length = strArray.length; + + // Though vCard 2.1/3.0 specification does not allow lower cases, + // vCard file emitted by some external vCard expoter have such + // invalid Strings. + // So we allow it. + // e.g. BEGIN:vCard + if (length == 2 && strArray[0].trim().equalsIgnoreCase("BEGIN") + && strArray[1].trim().equalsIgnoreCase("VCARD")) { + return true; + } else if (!allowGarbage) { + if (mNestCount > 0) { + mPreviousLine = line; + return false; + } else { + throw new VCardException("Expected String \"BEGIN:VCARD\" did not come " + + "(Instead, \"" + line + "\" came)"); + } + } + } while (allowGarbage); + + throw new VCardException("Reached where must not be reached."); + } + + /** + * <p> + * The arguments useCache and allowGarbase are usually true and false + * accordingly when this function is called outside this function itself. + * </p> + * + * @param useCache When true, line is obtained from mPreviousline. + * Otherwise, getLine() is used. + * @param allowGarbage When true, ignore non "END:VCARD" line. + * @throws IOException + * @throws VCardException + */ + protected void readEndVCard(boolean useCache, boolean allowGarbage) throws IOException, + VCardException { + String line; + do { + if (useCache) { + // Though vCard specification does not allow lower cases, + // some data may have them, so we allow it. + line = mPreviousLine; + } else { + while (true) { + line = getLine(); + if (line == null) { + throw new VCardException("Expected END:VCARD was not found."); + } else if (line.trim().length() > 0) { + break; + } + } + } + + String[] strArray = line.split(":", 2); + if (strArray.length == 2 && strArray[0].trim().equalsIgnoreCase("END") + && strArray[1].trim().equalsIgnoreCase("VCARD")) { + return; + } else if (!allowGarbage) { + throw new VCardException("END:VCARD != \"" + mPreviousLine + "\""); + } + useCache = false; + } while (allowGarbage); + } + + /* + * items = *CRLF item / item + */ + protected void parseItems() throws IOException, VCardException { + boolean ended = false; + + if (mInterpreter != null) { + long start = System.currentTimeMillis(); + mInterpreter.startProperty(); + mTimeStartProperty += System.currentTimeMillis() - start; + } + ended = parseItem(); + if (mInterpreter != null && !ended) { + long start = System.currentTimeMillis(); + mInterpreter.endProperty(); + mTimeEndProperty += System.currentTimeMillis() - start; + } + + while (!ended) { + // follow VCARD ,it wont reach endProperty + if (mInterpreter != null) { + long start = System.currentTimeMillis(); + mInterpreter.startProperty(); + mTimeStartProperty += System.currentTimeMillis() - start; + } + try { + ended = parseItem(); + } catch (VCardInvalidCommentLineException e) { + Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored."); + ended = false; + } + if (mInterpreter != null && !ended) { + long start = System.currentTimeMillis(); + mInterpreter.endProperty(); + mTimeEndProperty += System.currentTimeMillis() - start; + } + } + } + + /* + * item = [groups "."] name [params] ":" value CRLF / [groups "."] "ADR" + * [params] ":" addressparts CRLF / [groups "."] "ORG" [params] ":" orgparts + * CRLF / [groups "."] "N" [params] ":" nameparts CRLF / [groups "."] + * "AGENT" [params] ":" vcard CRLF + */ + protected boolean parseItem() throws IOException, VCardException { + mCurrentEncoding = sDefaultEncoding; + + final String line = getNonEmptyLine(); + long start = System.currentTimeMillis(); + + String[] propertyNameAndValue = separateLineAndHandleGroup(line); + if (propertyNameAndValue == null) { + return true; + } + if (propertyNameAndValue.length != 2) { + throw new VCardInvalidLineException("Invalid line \"" + line + "\""); + } + String propertyName = propertyNameAndValue[0].toUpperCase(); + String propertyValue = propertyNameAndValue[1]; + + mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start; + + if (propertyName.equals("ADR") || propertyName.equals("ORG") || propertyName.equals("N")) { + start = System.currentTimeMillis(); + handleMultiplePropertyValue(propertyName, propertyValue); + mTimeParseAdrOrgN += System.currentTimeMillis() - start; + return false; + } else if (propertyName.equals("AGENT")) { + handleAgent(propertyValue); + return false; + } else if (isValidPropertyName(propertyName)) { + if (propertyName.equals("BEGIN")) { + if (propertyValue.equals("VCARD")) { + throw new VCardNestedException("This vCard has nested vCard data in it."); + } else { + throw new VCardException("Unknown BEGIN type: " + propertyValue); + } + } else if (propertyName.equals("VERSION") && !propertyValue.equals(getVersionString())) { + throw new VCardVersionException("Incompatible version: " + propertyValue + " != " + + getVersionString()); + } + start = System.currentTimeMillis(); + handlePropertyValue(propertyName, propertyValue); + mTimeParsePropertyValues += System.currentTimeMillis() - start; + return false; + } + + throw new VCardException("Unknown property name: \"" + propertyName + "\""); + } + + // For performance reason, the states for group and property name are merged into one. + static private final int STATE_GROUP_OR_PROPERTY_NAME = 0; + static private final int STATE_PARAMS = 1; + // vCard 3.0 specification allows double-quoted parameters, while vCard 2.1 does not. + static private final int STATE_PARAMS_IN_DQUOTE = 2; + + protected String[] separateLineAndHandleGroup(String line) throws VCardException { + final String[] propertyNameAndValue = new String[2]; + final int length = line.length(); + if (length > 0 && line.charAt(0) == '#') { + throw new VCardInvalidCommentLineException(); + } + + int state = STATE_GROUP_OR_PROPERTY_NAME; + int nameIndex = 0; + + // This loop is developed so that we don't have to take care of bottle neck here. + // Refactor carefully when you need to do so. + for (int i = 0; i < length; i++) { + final char ch = line.charAt(i); + switch (state) { + case STATE_GROUP_OR_PROPERTY_NAME: { + if (ch == ':') { // End of a property name. + final String propertyName = line.substring(nameIndex, i); + if (propertyName.equalsIgnoreCase("END")) { + mPreviousLine = line; + return null; + } + if (mInterpreter != null) { + mInterpreter.propertyName(propertyName); + } + propertyNameAndValue[0] = propertyName; + if (i < length - 1) { + propertyNameAndValue[1] = line.substring(i + 1); + } else { + propertyNameAndValue[1] = ""; + } + return propertyNameAndValue; + } else if (ch == '.') { // Each group is followed by the dot. + final String groupName = line.substring(nameIndex, i); + if (groupName.length() == 0) { + Log.w(LOG_TAG, "Empty group found. Ignoring."); + } else if (mInterpreter != null) { + mInterpreter.propertyGroup(groupName); + } + nameIndex = i + 1; // Next should be another group or a property name. + } else if (ch == ';') { // End of property name and beginneng of parameters. + final String propertyName = line.substring(nameIndex, i); + if (propertyName.equalsIgnoreCase("END")) { + mPreviousLine = line; + return null; + } + if (mInterpreter != null) { + mInterpreter.propertyName(propertyName); + } + propertyNameAndValue[0] = propertyName; + nameIndex = i + 1; + state = STATE_PARAMS; // Start parameter parsing. + } + break; + } + case STATE_PARAMS: { + if (ch == '"') { + if (VCardConstants.VERSION_V21.equalsIgnoreCase(getVersionString())) { + Log.w(LOG_TAG, "Double-quoted params found in vCard 2.1. " + + "Silently allow it"); + } + state = STATE_PARAMS_IN_DQUOTE; + } else if (ch == ';') { // Starts another param. + handleParams(line.substring(nameIndex, i)); + nameIndex = i + 1; + } else if (ch == ':') { // End of param and beginenning of values. + handleParams(line.substring(nameIndex, i)); + if (i < length - 1) { + propertyNameAndValue[1] = line.substring(i + 1); + } else { + propertyNameAndValue[1] = ""; + } + return propertyNameAndValue; + } + break; + } + case STATE_PARAMS_IN_DQUOTE: { + if (ch == '"') { + if (VCardConstants.VERSION_V21.equalsIgnoreCase(getVersionString())) { + Log.w(LOG_TAG, "Double-quoted params found in vCard 2.1. " + + "Silently allow it"); + } + state = STATE_PARAMS; + } + break; + } + } + } + + throw new VCardInvalidLineException("Invalid line: \"" + line + "\""); + } + + /* + * params = ";" [ws] paramlist paramlist = paramlist [ws] ";" [ws] param / + * param param = "TYPE" [ws] "=" [ws] ptypeval / "VALUE" [ws] "=" [ws] + * pvalueval / "ENCODING" [ws] "=" [ws] pencodingval / "CHARSET" [ws] "=" + * [ws] charsetval / "LANGUAGE" [ws] "=" [ws] langval / "X-" word [ws] "=" + * [ws] word / knowntype + */ + protected void handleParams(String params) throws VCardException { + final String[] strArray = params.split("=", 2); + if (strArray.length == 2) { + final String paramName = strArray[0].trim().toUpperCase(); + String paramValue = strArray[1].trim(); + if (paramName.equals("TYPE")) { + handleType(paramValue); + } else if (paramName.equals("VALUE")) { + handleValue(paramValue); + } else if (paramName.equals("ENCODING")) { + handleEncoding(paramValue); + } else if (paramName.equals("CHARSET")) { + handleCharset(paramValue); + } else if (paramName.equals("LANGUAGE")) { + handleLanguage(paramValue); + } else if (paramName.startsWith("X-")) { + handleAnyParam(paramName, paramValue); + } else { + throw new VCardException("Unknown type \"" + paramName + "\""); + } + } else { + handleParamWithoutName(strArray[0]); + } + } + + /** + * vCard 3.0 parser implementation may throw VCardException. + */ + @SuppressWarnings("unused") + protected void handleParamWithoutName(final String paramValue) throws VCardException { + handleType(paramValue); + } + + /* + * ptypeval = knowntype / "X-" word + */ + protected void handleType(final String ptypeval) { + if (!(getKnownTypeSet().contains(ptypeval.toUpperCase()) + || ptypeval.startsWith("X-")) + && !mUnknownTypeSet.contains(ptypeval)) { + mUnknownTypeSet.add(ptypeval); + Log.w(LOG_TAG, String.format("TYPE unsupported by %s: ", getVersion(), ptypeval)); + } + if (mInterpreter != null) { + mInterpreter.propertyParamType("TYPE"); + mInterpreter.propertyParamValue(ptypeval); + } + } + + /* + * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word + */ + protected void handleValue(final String pvalueval) { + if (!(getKnownValueSet().contains(pvalueval.toUpperCase()) + || pvalueval.startsWith("X-") + || mUnknownValueSet.contains(pvalueval))) { + mUnknownValueSet.add(pvalueval); + Log.w(LOG_TAG, String.format( + "The value unsupported by TYPE of %s: ", getVersion(), pvalueval)); + } + if (mInterpreter != null) { + mInterpreter.propertyParamType("VALUE"); + mInterpreter.propertyParamValue(pvalueval); + } + } + + /* + * pencodingval = "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word + */ + protected void handleEncoding(String pencodingval) throws VCardException { + if (getAvailableEncodingSet().contains(pencodingval) || + pencodingval.startsWith("X-")) { + if (mInterpreter != null) { + mInterpreter.propertyParamType("ENCODING"); + mInterpreter.propertyParamValue(pencodingval); + } + mCurrentEncoding = pencodingval; + } else { + throw new VCardException("Unknown encoding \"" + pencodingval + "\""); + } + } + + /** + * <p> + * vCard 2.1 specification only allows us-ascii and iso-8859-xxx (See RFC 1521), + * but recent vCard files often contain other charset like UTF-8, SHIFT_JIS, etc. + * We allow any charset. + * </p> + */ + protected void handleCharset(String charsetval) { + if (mInterpreter != null) { + mInterpreter.propertyParamType("CHARSET"); + mInterpreter.propertyParamValue(charsetval); + } + } + + /** + * See also Section 7.1 of RFC 1521 + */ + protected void handleLanguage(String langval) throws VCardException { + String[] strArray = langval.split("-"); + if (strArray.length != 2) { + throw new VCardException("Invalid Language: \"" + langval + "\""); + } + String tmp = strArray[0]; + int length = tmp.length(); + for (int i = 0; i < length; i++) { + if (!isAsciiLetter(tmp.charAt(i))) { + throw new VCardException("Invalid Language: \"" + langval + "\""); + } + } + tmp = strArray[1]; + length = tmp.length(); + for (int i = 0; i < length; i++) { + if (!isAsciiLetter(tmp.charAt(i))) { + throw new VCardException("Invalid Language: \"" + langval + "\""); + } + } + if (mInterpreter != null) { + mInterpreter.propertyParamType("LANGUAGE"); + mInterpreter.propertyParamValue(langval); + } + } + + private boolean isAsciiLetter(char ch) { + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { + return true; + } + return false; + } + + /** + * Mainly for "X-" type. This accepts any kind of type without check. + */ + protected void handleAnyParam(String paramName, String paramValue) { + if (mInterpreter != null) { + mInterpreter.propertyParamType(paramName); + mInterpreter.propertyParamValue(paramValue); + } + } + + protected void handlePropertyValue(String propertyName, String propertyValue) + throws IOException, VCardException { + final String upperEncoding = mCurrentEncoding.toUpperCase(); + if (upperEncoding.equals(VCardConstants.PARAM_ENCODING_QP)) { + final long start = System.currentTimeMillis(); + final String result = getQuotedPrintable(propertyValue); + if (mInterpreter != null) { + ArrayList<String> v = new ArrayList<String>(); + v.add(result); + mInterpreter.propertyValues(v); + } + mTimeHandleQuotedPrintable += System.currentTimeMillis() - start; + } else if (upperEncoding.equals(VCardConstants.PARAM_ENCODING_BASE64) + || upperEncoding.equals(VCardConstants.PARAM_ENCODING_B)) { + final long start = System.currentTimeMillis(); + // It is very rare, but some BASE64 data may be so big that + // OutOfMemoryError occurs. To ignore such cases, use try-catch. + try { + final String result = getBase64(propertyValue); + if (mInterpreter != null) { + ArrayList<String> arrayList = new ArrayList<String>(); + arrayList.add(result); + mInterpreter.propertyValues(arrayList); + } + } catch (OutOfMemoryError error) { + Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!"); + if (mInterpreter != null) { + mInterpreter.propertyValues(null); + } + } + mTimeHandleBase64 += System.currentTimeMillis() - start; + } else { + if (!(upperEncoding.equals("7BIT") || upperEncoding.equals("8BIT") || + upperEncoding.startsWith("X-"))) { + Log.w(LOG_TAG, + String.format("The encoding \"%s\" is unsupported by vCard %s", + mCurrentEncoding, getVersionString())); + } + + final long start = System.currentTimeMillis(); + if (mInterpreter != null) { + ArrayList<String> v = new ArrayList<String>(); + v.add(maybeUnescapeText(propertyValue)); + mInterpreter.propertyValues(v); + } + mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start; + } + } + + /** + * <p> + * Parses and returns Quoted-Printable. + * </p> + * + * @param firstString The string following a parameter name and attributes. + * Example: "string" in + * "ADR:ENCODING=QUOTED-PRINTABLE:string\n\r". + * @return whole Quoted-Printable string, including a given argument and + * following lines. Excludes the last empty line following to Quoted + * Printable lines. + * @throws IOException + * @throws VCardException + */ + private String getQuotedPrintable(String firstString) throws IOException, VCardException { + // Specifically, there may be some padding between = and CRLF. + // See the following: + // + // qp-line := *(qp-segment transport-padding CRLF) + // qp-part transport-padding + // qp-segment := qp-section *(SPACE / TAB) "=" + // ; Maximum length of 76 characters + // + // e.g. (from RFC 2045) + // Now's the time = + // for all folk to come= + // to the aid of their country. + if (firstString.trim().endsWith("=")) { + // remove "transport-padding" + int pos = firstString.length() - 1; + while (firstString.charAt(pos) != '=') { + } + StringBuilder builder = new StringBuilder(); + builder.append(firstString.substring(0, pos + 1)); + builder.append("\r\n"); + String line; + while (true) { + line = getLine(); + if (line == null) { + throw new VCardException("File ended during parsing a Quoted-Printable String"); + } + if (line.trim().endsWith("=")) { + // remove "transport-padding" + pos = line.length() - 1; + while (line.charAt(pos) != '=') { + } + builder.append(line.substring(0, pos + 1)); + builder.append("\r\n"); + } else { + builder.append(line); + break; + } + } + return builder.toString(); + } else { + return firstString; + } + } + + protected String getBase64(String firstString) throws IOException, VCardException { + StringBuilder builder = new StringBuilder(); + builder.append(firstString); + + while (true) { + String line = getLine(); + if (line == null) { + throw new VCardException("File ended during parsing BASE64 binary"); + } + if (line.length() == 0) { + break; + } + builder.append(line); + } + + return builder.toString(); + } + + /** + * <p> + * Mainly for "ADR", "ORG", and "N" + * </p> + */ + /* + * addressparts = 0*6(strnosemi ";") strnosemi ; PO Box, Extended Addr, + * Street, Locality, Region, Postal Code, Country Name orgparts = + * *(strnosemi ";") strnosemi ; First is Organization Name, remainder are + * Organization Units. nameparts = 0*4(strnosemi ";") strnosemi ; Family, + * Given, Middle, Prefix, Suffix. ; Example:Public;John;Q.;Reverend Dr.;III, + * Esq. strnosemi = *(*nonsemi ("\;" / "\" CRLF)) *nonsemi ; To include a + * semicolon in this string, it must be escaped ; with a "\" character. We + * do not care the number of "strnosemi" here. We are not sure whether we + * should add "\" CRLF to each value. We exclude them for now. + */ + protected void handleMultiplePropertyValue(String propertyName, String propertyValue) + throws IOException, VCardException { + // vCard 2.1 does not allow QUOTED-PRINTABLE here, but some + // softwares/devices + // emit such data. + if (mCurrentEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { + propertyValue = getQuotedPrintable(propertyValue); + } + + if (mInterpreter != null) { + mInterpreter.propertyValues(VCardUtils.constructListFromValue(propertyValue, + (getVersion() == VCardConfig.FLAG_V30))); + } + } + + /* + * vCard 2.1 specifies AGENT allows one vcard entry. Currently we emit an + * error toward the AGENT property. + * // TODO: Support AGENT property. + * item = + * ... / [groups "."] "AGENT" [params] ":" vcard CRLF vcard = "BEGIN" [ws] + * ":" [ws] "VCARD" [ws] 1*CRLF items *CRLF "END" [ws] ":" [ws] "VCARD" + */ + protected void handleAgent(final String propertyValue) throws VCardException { + if (!propertyValue.toUpperCase().contains("BEGIN:VCARD")) { + // Apparently invalid line seen in Windows Mobile 6.5. Ignore them. + return; + } else { + throw new VCardAgentNotSupportedException("AGENT Property is not supported now."); + } + } + + /** + * For vCard 3.0. + */ + protected String maybeUnescapeText(final String text) { + return text; + } + + /** + * Returns unescaped String if the character should be unescaped. Return + * null otherwise. e.g. In vCard 2.1, "\;" should be unescaped into ";" + * while "\x" should not be. + */ + protected String maybeUnescapeCharacter(final char ch) { + return unescapeCharacter(ch); + } + + /* package */ static String unescapeCharacter(final char ch) { + // Original vCard 2.1 specification does not allow transformation + // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous + // implementation of + // this class allowed them, so keep it as is. + if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') { + return String.valueOf(ch); + } else { + return null; + } + } + + private void showPerformanceInfo() { + Log.d(LOG_TAG, "Total parsing time: " + mTimeTotal + " ms"); + if (mReader instanceof CustomBufferedReader) { + Log.d(LOG_TAG, "Total readLine time: " + + ((CustomBufferedReader) mReader).getTotalmillisecond() + " ms"); + } + Log.d(LOG_TAG, "Time for handling the beggining of the record: " + mTimeReadStartRecord + + " ms"); + Log.d(LOG_TAG, "Time for handling the end of the record: " + mTimeReadEndRecord + " ms"); + Log.d(LOG_TAG, "Time for parsing line, and handling group: " + mTimeParseLineAndHandleGroup + + " ms"); + Log.d(LOG_TAG, "Time for parsing ADR, ORG, and N fields:" + mTimeParseAdrOrgN + " ms"); + Log.d(LOG_TAG, "Time for parsing property values: " + mTimeParsePropertyValues + " ms"); + Log.d(LOG_TAG, "Time for handling normal property values: " + mTimeHandleMiscPropertyValue + + " ms"); + Log.d(LOG_TAG, "Time for handling Quoted-Printable: " + mTimeHandleQuotedPrintable + " ms"); + Log.d(LOG_TAG, "Time for handling Base64: " + mTimeHandleBase64 + " ms"); + } + + /** + * @return {@link VCardConfig#FLAG_V21} + */ + protected int getVersion() { + return VCardConfig.FLAG_V21; + } + + /** + * @return {@link VCardConfig#FLAG_V30} + */ + protected String getVersionString() { + return VCardConstants.VERSION_V21; + } + + protected Set<String> getKnownPropertyNameSet() { + return VCardParser_V21.sKnownPropertyNameSet; + } + + protected Set<String> getKnownTypeSet() { + return VCardParser_V21.sKnownTypeSet; + } + + protected Set<String> getKnownValueSet() { + return VCardParser_V21.sKnownValueSet; + } + + protected Set<String> getAvailableEncodingSet() { + return VCardParser_V21.sAvailableEncoding; + } + + protected String getDefaultEncoding() { + return sDefaultEncoding; + } + + + public void parse(InputStream is, VCardInterpreter interpreter) + throws IOException, VCardException { + if (is == null) { + throw new NullPointerException("InputStream must not be null."); + } + + final InputStreamReader tmpReader = new InputStreamReader(is, mImportCharset); + if (VCardConfig.showPerformanceLog()) { + mReader = new CustomBufferedReader(tmpReader); + } else { + mReader = new BufferedReader(tmpReader); + } + + mInterpreter = interpreter; + + final long start = System.currentTimeMillis(); + if (mInterpreter != null) { + mInterpreter.start(); + } + parseVCardFile(); + if (mInterpreter != null) { + mInterpreter.end(); + } + mTimeTotal += System.currentTimeMillis() - start; + + if (VCardConfig.showPerformanceLog()) { + showPerformanceInfo(); + } + } + + public final void cancel() { + mCanceled = true; + } +} diff --git a/core/java/android/pim/vcard/VCardParserImpl_V30.java b/core/java/android/pim/vcard/VCardParserImpl_V30.java new file mode 100644 index 0000000..a48a3b4 --- /dev/null +++ b/core/java/android/pim/vcard/VCardParserImpl_V30.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.pim.vcard; + +import java.io.IOException; +import java.util.Set; + +import android.pim.vcard.exception.VCardException; +import android.util.Log; + +/** + * <p> + * Basic implementation achieving vCard 3.0 parsing. + * </p> + * <p> + * This class inherits vCard 2.1 implementation since technically they are similar, + * while specifically there's logical no relevance between them. + * So that developers are not confused with the inheritance, + * {@link VCardParser_V30} does not inherit {@link VCardParser_V21}, while + * {@link VCardParserImpl_V30} inherits {@link VCardParserImpl_V21}. + * </p> + * @hide + */ +/* package */ class VCardParserImpl_V30 extends VCardParserImpl_V21 { + private static final String LOG_TAG = "VCardParserImpl_V30"; + + private String mPreviousLine; + private boolean mEmittedAgentWarning = false; + + public VCardParserImpl_V30() { + super(); + } + + public VCardParserImpl_V30(int vcardType) { + super(vcardType, null); + } + + public VCardParserImpl_V30(int vcardType, String importCharset) { + super(vcardType, importCharset); + } + + @Override + protected int getVersion() { + return VCardConfig.FLAG_V30; + } + + @Override + protected String getVersionString() { + return VCardConstants.VERSION_V30; + } + + @Override + protected String getLine() throws IOException { + if (mPreviousLine != null) { + String ret = mPreviousLine; + mPreviousLine = null; + return ret; + } else { + return mReader.readLine(); + } + } + + /** + * vCard 3.0 requires that the line with space at the beginning of the line + * must be combined with previous line. + */ + @Override + protected String getNonEmptyLine() throws IOException, VCardException { + String line; + StringBuilder builder = null; + while (true) { + line = mReader.readLine(); + if (line == null) { + if (builder != null) { + return builder.toString(); + } else if (mPreviousLine != null) { + String ret = mPreviousLine; + mPreviousLine = null; + return ret; + } + throw new VCardException("Reached end of buffer."); + } else if (line.length() == 0) { + if (builder != null) { + return builder.toString(); + } else if (mPreviousLine != null) { + String ret = mPreviousLine; + mPreviousLine = null; + return ret; + } + } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') { + if (builder != null) { + // See Section 5.8.1 of RFC 2425 (MIME-DIR document). + // Following is the excerpts from it. + // + // DESCRIPTION:This is a long description that exists on a long line. + // + // Can be represented as: + // + // DESCRIPTION:This is a long description + // that exists on a long line. + // + // It could also be represented as: + // + // DESCRIPTION:This is a long descrip + // tion that exists o + // n a long line. + builder.append(line.substring(1)); + } else if (mPreviousLine != null) { + builder = new StringBuilder(); + builder.append(mPreviousLine); + mPreviousLine = null; + builder.append(line.substring(1)); + } else { + throw new VCardException("Space exists at the beginning of the line"); + } + } else { + if (mPreviousLine == null) { + mPreviousLine = line; + if (builder != null) { + return builder.toString(); + } + } else { + String ret = mPreviousLine; + mPreviousLine = line; + return ret; + } + } + } + } + + /* + * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF + * 1 * (contentline) + * ;A vCard object MUST include the VERSION, FN and N types. + * [group "."] "END" ":" "VCARD" 1 * CRLF + */ + @Override + protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { + // TODO: vCard 3.0 supports group. + return super.readBeginVCard(allowGarbage); + } + + @Override + protected void readEndVCard(boolean useCache, boolean allowGarbage) + throws IOException, VCardException { + // TODO: vCard 3.0 supports group. + super.readEndVCard(useCache, allowGarbage); + } + + /** + * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not. + */ + @Override + protected void handleParams(final String params) throws VCardException { + try { + super.handleParams(params); + } catch (VCardException e) { + // maybe IANA type + String[] strArray = params.split("=", 2); + if (strArray.length == 2) { + handleAnyParam(strArray[0], strArray[1]); + } else { + // Must not come here in the current implementation. + throw new VCardException( + "Unknown params value: " + params); + } + } + } + + @Override + protected void handleAnyParam(final String paramName, final String paramValue) { + super.handleAnyParam(paramName, paramValue); + } + + @Override + protected void handleParamWithoutName(final String paramValue) throws VCardException { + super.handleParamWithoutName(paramValue); + } + + /* + * vCard 3.0 defines + * + * param = param-name "=" param-value *("," param-value) + * param-name = iana-token / x-name + * param-value = ptext / quoted-string + * quoted-string = DQUOTE QSAFE-CHAR DQUOTE + */ + @Override + protected void handleType(final String ptypevalues) { + String[] ptypeArray = ptypevalues.split(","); + mInterpreter.propertyParamType("TYPE"); + for (String value : ptypeArray) { + int length = value.length(); + if (length >= 2 && value.startsWith("\"") && value.endsWith("\"")) { + mInterpreter.propertyParamValue(value.substring(1, value.length() - 1)); + } else { + mInterpreter.propertyParamValue(value); + } + } + } + + @Override + protected void handleAgent(final String propertyValue) { + // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1. + // + // e.g. + // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n + // TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n + // ET:jfriday@host.com\nEND:VCARD\n + // + // TODO: fix this. + // + // issue: + // vCard 3.0 also allows this as an example. + // + // AGENT;VALUE=uri: + // CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com + // + // This is not vCard. Should we support this? + // + // Just ignore the line for now, since we cannot know how to handle it... + if (!mEmittedAgentWarning) { + Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it"); + mEmittedAgentWarning = true; + } + } + + /** + * vCard 3.0 does not require two CRLF at the last of BASE64 data. + * It only requires that data should be MIME-encoded. + */ + @Override + protected String getBase64(final String firstString) + throws IOException, VCardException { + final StringBuilder builder = new StringBuilder(); + builder.append(firstString); + + while (true) { + final String line = getLine(); + if (line == null) { + throw new VCardException("File ended during parsing BASE64 binary"); + } + if (line.length() == 0) { + break; + } else if (!line.startsWith(" ") && !line.startsWith("\t")) { + mPreviousLine = line; + break; + } + builder.append(line); + } + + return builder.toString(); + } + + /** + * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N") + * ; \\ encodes \, \n or \N encodes newline + * ; \; encodes ;, \, encodes , + * + * Note: Apple escapes ':' into '\:' while does not escape '\' + */ + @Override + protected String maybeUnescapeText(final String text) { + return unescapeText(text); + } + + public static String unescapeText(final String text) { + StringBuilder builder = new StringBuilder(); + final int length = text.length(); + for (int i = 0; i < length; i++) { + char ch = text.charAt(i); + if (ch == '\\' && i < length - 1) { + final char next_ch = text.charAt(++i); + if (next_ch == 'n' || next_ch == 'N') { + builder.append("\n"); + } else { + builder.append(next_ch); + } + } else { + builder.append(ch); + } + } + return builder.toString(); + } + + @Override + protected String maybeUnescapeCharacter(final char ch) { + return unescapeCharacter(ch); + } + + public static String unescapeCharacter(final char ch) { + if (ch == 'n' || ch == 'N') { + return "\n"; + } else { + return String.valueOf(ch); + } + } + + @Override + protected Set<String> getKnownPropertyNameSet() { + return VCardParser_V30.sKnownPropertyNameSet; + } +} diff --git a/core/java/android/pim/vcard/VCardParser_V21.java b/core/java/android/pim/vcard/VCardParser_V21.java index fe8cfb0..b625695 100644 --- a/core/java/android/pim/vcard/VCardParser_V21.java +++ b/core/java/android/pim/vcard/VCardParser_V21.java @@ -15,922 +15,99 @@ */ package android.pim.vcard; -import android.pim.vcard.exception.VCardAgentNotSupportedException; import android.pim.vcard.exception.VCardException; -import android.pim.vcard.exception.VCardInvalidCommentLineException; -import android.pim.vcard.exception.VCardInvalidLineException; -import android.pim.vcard.exception.VCardNestedException; -import android.pim.vcard.exception.VCardVersionException; -import android.util.Log; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.Set; /** - * This class is used to parse vCard. Please refer to vCard Specification 2.1 for more detail. + * </p> + * vCard parser for vCard 2.1. See the specification for more detail about the spec itself. + * </p> + * <p> + * The spec is written in 1996, and currently various types of "vCard 2.1" exist. + * To handle real the world vCard formats appropriately and effectively, this class does not + * obey with strict vCard 2.1. + * In stead, not only vCard spec but also real world vCard is considered. + * </p> + * e.g. A lot of devices and softwares let vCard importer/exporter to use + * the PNG format to determine the type of image, while it is not allowed in + * the original specification. As of 2010, we can see even the FLV format + * (possible in Japanese mobile phones). + * </p> */ -public class VCardParser_V21 extends VCardParser { - private static final String LOG_TAG = "VCardParser_V21"; - - /** Store the known-type */ - private static final HashSet<String> sKnownTypeSet = new HashSet<String>( - Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK", - "PREF", "VOICE", "FAX", "MSG", "CELL", "PAGER", "BBS", - "MODEM", "CAR", "ISDN", "VIDEO", "AOL", "APPLELINK", - "ATTMAIL", "CIS", "EWORLD", "INTERNET", "IBMMAIL", - "MCIMAIL", "POWERSHARE", "PRODIGY", "TLX", "X400", "GIF", - "CGM", "WMF", "BMP", "MET", "PMB", "DIB", "PICT", "TIFF", - "PDF", "PS", "JPEG", "QTIME", "MPEG", "MPEG2", "AVI", - "WAVE", "AIFF", "PCM", "X509", "PGP")); - - /** Store the known-value */ - private static final HashSet<String> sKnownValueSet = new HashSet<String>( - Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID")); - - /** Store the property names available in vCard 2.1 */ - private static final HashSet<String> sAvailablePropertyNameSetV21 = - new HashSet<String>(Arrays.asList( - "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", - "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", - "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER")); - - /** - * Though vCard 2.1 specification does not allow "B" encoding, some data may have it. - * We allow it for safety... - */ - private static final HashSet<String> sAvailableEncodingV21 = - new HashSet<String>(Arrays.asList( - "7BIT", "8BIT", "QUOTED-PRINTABLE", "BASE64", "B")); - - // Used only for parsing END:VCARD. - private String mPreviousLine; - - /** The builder to build parsed data */ - protected VCardInterpreter mBuilder = null; - - /** - * The encoding type. "Encoding" in vCard is different from "Charset". - * e.g. 7BIT, 8BIT, QUOTED-PRINTABLE. - */ - protected String mEncoding = null; - - protected final String sDefaultEncoding = "8BIT"; - - // Should not directly read a line from this object. Use getLine() instead. - protected BufferedReader mReader; - - // In some cases, vCard is nested. Currently, we only consider the most interior vCard data. - // See v21_foma_1.vcf in test directory for more information. - private int mNestCount; - - // In order to reduce warning message as much as possible, we hold the value which made Logger - // emit a warning message. - protected Set<String> mUnknownTypeMap = new HashSet<String>(); - protected Set<String> mUnknownValueMap = new HashSet<String>(); - - // For measuring performance. - private long mTimeTotal; - private long mTimeReadStartRecord; - private long mTimeReadEndRecord; - private long mTimeStartProperty; - private long mTimeEndProperty; - private long mTimeParseItems; - private long mTimeParseLineAndHandleGroup; - private long mTimeParsePropertyValues; - private long mTimeParseAdrOrgN; - private long mTimeHandleMiscPropertyValue; - private long mTimeHandleQuotedPrintable; - private long mTimeHandleBase64; - - public VCardParser_V21() { - this(null); - } - - public VCardParser_V21(VCardSourceDetector detector) { - this(detector != null ? detector.getEstimatedType() : VCardConfig.PARSE_TYPE_UNKNOWN); - } - - public VCardParser_V21(int parseType) { - super(parseType); - if (parseType == VCardConfig.PARSE_TYPE_FOMA) { - mNestCount = 1; - } - } - - /** - * Parses the file at the given position. - * - * vcard_file = [wsls] vcard [wsls] - */ - protected void parseVCardFile() throws IOException, VCardException { - boolean firstReading = true; - while (true) { - if (mCanceled) { - break; - } - if (!parseOneVCard(firstReading)) { - break; - } - firstReading = false; - } - - if (mNestCount > 0) { - boolean useCache = true; - for (int i = 0; i < mNestCount; i++) { - readEndVCard(useCache, true); - useCache = false; - } - } - } - - protected int getVersion() { - return VCardConfig.FLAG_V21; - } - - protected String getVersionString() { - return VCardConstants.VERSION_V21; - } - - /** - * @return true when the propertyName is a valid property name. - */ - protected boolean isValidPropertyName(String propertyName) { - if (!(sAvailablePropertyNameSetV21.contains(propertyName.toUpperCase()) || - propertyName.startsWith("X-")) && - !mUnknownTypeMap.contains(propertyName)) { - mUnknownTypeMap.add(propertyName); - Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName); - } - return true; - } - - /** - * @return true when the encoding is a valid encoding. - */ - protected boolean isValidEncoding(String encoding) { - return sAvailableEncodingV21.contains(encoding.toUpperCase()); - } - - /** - * @return String. It may be null, or its length may be 0 - * @throws IOException - */ - protected String getLine() throws IOException { - return mReader.readLine(); - } - - /** - * @return String with it's length > 0 - * @throws IOException - * @throws VCardException when the stream reached end of line - */ - protected String getNonEmptyLine() throws IOException, VCardException { - String line; - while (true) { - line = getLine(); - if (line == null) { - throw new VCardException("Reached end of buffer."); - } else if (line.trim().length() > 0) { - return line; - } - } - } - - /** - * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF - * items *CRLF - * "END" [ws] ":" [ws] "VCARD" - */ - private boolean parseOneVCard(boolean firstReading) throws IOException, VCardException { - boolean allowGarbage = false; - if (firstReading) { - if (mNestCount > 0) { - for (int i = 0; i < mNestCount; i++) { - if (!readBeginVCard(allowGarbage)) { - return false; - } - allowGarbage = true; - } - } - } - - if (!readBeginVCard(allowGarbage)) { - return false; - } - long start; - if (mBuilder != null) { - start = System.currentTimeMillis(); - mBuilder.startEntry(); - mTimeReadStartRecord += System.currentTimeMillis() - start; - } - start = System.currentTimeMillis(); - parseItems(); - mTimeParseItems += System.currentTimeMillis() - start; - readEndVCard(true, false); - if (mBuilder != null) { - start = System.currentTimeMillis(); - mBuilder.endEntry(); - mTimeReadEndRecord += System.currentTimeMillis() - start; - } - return true; - } - +public final class VCardParser_V21 implements VCardParser { /** - * @return True when successful. False when reaching the end of line - * @throws IOException - * @throws VCardException + * A unmodifiable Set storing the property names available in the vCard 2.1 specification. */ - protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { - String line; - do { - while (true) { - line = getLine(); - if (line == null) { - return false; - } else if (line.trim().length() > 0) { - break; - } - } - String[] strArray = line.split(":", 2); - int length = strArray.length; - - // Though vCard 2.1/3.0 specification does not allow lower cases, - // vCard file emitted by some external vCard expoter have such invalid Strings. - // So we allow it. - // e.g. BEGIN:vCard - if (length == 2 && - strArray[0].trim().equalsIgnoreCase("BEGIN") && - strArray[1].trim().equalsIgnoreCase("VCARD")) { - return true; - } else if (!allowGarbage) { - if (mNestCount > 0) { - mPreviousLine = line; - return false; - } else { - throw new VCardException( - "Expected String \"BEGIN:VCARD\" did not come " - + "(Instead, \"" + line + "\" came)"); - } - } - } while(allowGarbage); - - throw new VCardException("Reached where must not be reached."); - } + /* package */ static final Set<String> sKnownPropertyNameSet = + Collections.unmodifiableSet(new HashSet<String>( + Arrays.asList("BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", + "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", + "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER"))); /** - * The arguments useCache and allowGarbase are usually true and false accordingly when - * this function is called outside this function itself. - * - * @param useCache When true, line is obtained from mPreviousline. Otherwise, getLine() - * is used. - * @param allowGarbage When true, ignore non "END:VCARD" line. - * @throws IOException - * @throws VCardException + * A unmodifiable Set storing the types known in vCard 2.1. */ - protected void readEndVCard(boolean useCache, boolean allowGarbage) - throws IOException, VCardException { - String line; - do { - if (useCache) { - // Though vCard specification does not allow lower cases, - // some data may have them, so we allow it. - line = mPreviousLine; - } else { - while (true) { - line = getLine(); - if (line == null) { - throw new VCardException("Expected END:VCARD was not found."); - } else if (line.trim().length() > 0) { - break; - } - } - } + /* package */ static final Set<String> sKnownTypeSet = + Collections.unmodifiableSet(new HashSet<String>( + Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK", + "PREF", "VOICE", "FAX", "MSG", "CELL", "PAGER", "BBS", + "MODEM", "CAR", "ISDN", "VIDEO", "AOL", "APPLELINK", + "ATTMAIL", "CIS", "EWORLD", "INTERNET", "IBMMAIL", + "MCIMAIL", "POWERSHARE", "PRODIGY", "TLX", "X400", "GIF", + "CGM", "WMF", "BMP", "MET", "PMB", "DIB", "PICT", "TIFF", + "PDF", "PS", "JPEG", "QTIME", "MPEG", "MPEG2", "AVI", + "WAVE", "AIFF", "PCM", "X509", "PGP"))); - String[] strArray = line.split(":", 2); - if (strArray.length == 2 && - strArray[0].trim().equalsIgnoreCase("END") && - strArray[1].trim().equalsIgnoreCase("VCARD")) { - return; - } else if (!allowGarbage) { - throw new VCardException("END:VCARD != \"" + mPreviousLine + "\""); - } - useCache = false; - } while (allowGarbage); - } - /** - * items = *CRLF item - * / item + * A unmodifiable Set storing the values for the type "VALUE", available in the vCard 2.1. */ - protected void parseItems() throws IOException, VCardException { - boolean ended = false; - - if (mBuilder != null) { - long start = System.currentTimeMillis(); - mBuilder.startProperty(); - mTimeStartProperty += System.currentTimeMillis() - start; - } - ended = parseItem(); - if (mBuilder != null && !ended) { - long start = System.currentTimeMillis(); - mBuilder.endProperty(); - mTimeEndProperty += System.currentTimeMillis() - start; - } + /* package */ static final Set<String> sKnownValueSet = + Collections.unmodifiableSet(new HashSet<String>( + Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID"))); - while (!ended) { - // follow VCARD ,it wont reach endProperty - if (mBuilder != null) { - long start = System.currentTimeMillis(); - mBuilder.startProperty(); - mTimeStartProperty += System.currentTimeMillis() - start; - } - try { - ended = parseItem(); - } catch (VCardInvalidCommentLineException e) { - Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored."); - ended = false; - } - if (mBuilder != null && !ended) { - long start = System.currentTimeMillis(); - mBuilder.endProperty(); - mTimeEndProperty += System.currentTimeMillis() - start; - } - } - } - /** - * item = [groups "."] name [params] ":" value CRLF - * / [groups "."] "ADR" [params] ":" addressparts CRLF - * / [groups "."] "ORG" [params] ":" orgparts CRLF - * / [groups "."] "N" [params] ":" nameparts CRLF - * / [groups "."] "AGENT" [params] ":" vcard CRLF - */ - protected boolean parseItem() throws IOException, VCardException { - mEncoding = sDefaultEncoding; - - final String line = getNonEmptyLine(); - long start = System.currentTimeMillis(); - - String[] propertyNameAndValue = separateLineAndHandleGroup(line); - if (propertyNameAndValue == null) { - return true; - } - if (propertyNameAndValue.length != 2) { - throw new VCardInvalidLineException("Invalid line \"" + line + "\""); - } - String propertyName = propertyNameAndValue[0].toUpperCase(); - String propertyValue = propertyNameAndValue[1]; - - mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start; - - if (propertyName.equals("ADR") || propertyName.equals("ORG") || - propertyName.equals("N")) { - start = System.currentTimeMillis(); - handleMultiplePropertyValue(propertyName, propertyValue); - mTimeParseAdrOrgN += System.currentTimeMillis() - start; - return false; - } else if (propertyName.equals("AGENT")) { - handleAgent(propertyValue); - return false; - } else if (isValidPropertyName(propertyName)) { - if (propertyName.equals("BEGIN")) { - if (propertyValue.equals("VCARD")) { - throw new VCardNestedException("This vCard has nested vCard data in it."); - } else { - throw new VCardException("Unknown BEGIN type: " + propertyValue); - } - } else if (propertyName.equals("VERSION") && - !propertyValue.equals(getVersionString())) { - throw new VCardVersionException("Incompatible version: " + - propertyValue + " != " + getVersionString()); - } - start = System.currentTimeMillis(); - handlePropertyValue(propertyName, propertyValue); - mTimeParsePropertyValues += System.currentTimeMillis() - start; - return false; - } - - throw new VCardException("Unknown property name: \"" + propertyName + "\""); - } - - static private final int STATE_GROUP_OR_PROPNAME = 0; - static private final int STATE_PARAMS = 1; - // vCard 3.0 specification allows double-quoted param-value, while vCard 2.1 does not. - // This is just for safety. - static private final int STATE_PARAMS_IN_DQUOTE = 2; - - protected String[] separateLineAndHandleGroup(String line) throws VCardException { - int state = STATE_GROUP_OR_PROPNAME; - int nameIndex = 0; - - final String[] propertyNameAndValue = new String[2]; - - final int length = line.length(); - if (length > 0 && line.charAt(0) == '#') { - throw new VCardInvalidCommentLineException(); - } - - for (int i = 0; i < length; i++) { - char ch = line.charAt(i); - switch (state) { - case STATE_GROUP_OR_PROPNAME: { - if (ch == ':') { - final String propertyName = line.substring(nameIndex, i); - if (propertyName.equalsIgnoreCase("END")) { - mPreviousLine = line; - return null; - } - if (mBuilder != null) { - mBuilder.propertyName(propertyName); - } - propertyNameAndValue[0] = propertyName; - if (i < length - 1) { - propertyNameAndValue[1] = line.substring(i + 1); - } else { - propertyNameAndValue[1] = ""; - } - return propertyNameAndValue; - } else if (ch == '.') { - String groupName = line.substring(nameIndex, i); - if (mBuilder != null) { - mBuilder.propertyGroup(groupName); - } - nameIndex = i + 1; - } else if (ch == ';') { - String propertyName = line.substring(nameIndex, i); - if (propertyName.equalsIgnoreCase("END")) { - mPreviousLine = line; - return null; - } - if (mBuilder != null) { - mBuilder.propertyName(propertyName); - } - propertyNameAndValue[0] = propertyName; - nameIndex = i + 1; - state = STATE_PARAMS; - } - break; - } - case STATE_PARAMS: { - if (ch == '"') { - state = STATE_PARAMS_IN_DQUOTE; - } else if (ch == ';') { - handleParams(line.substring(nameIndex, i)); - nameIndex = i + 1; - } else if (ch == ':') { - handleParams(line.substring(nameIndex, i)); - if (i < length - 1) { - propertyNameAndValue[1] = line.substring(i + 1); - } else { - propertyNameAndValue[1] = ""; - } - return propertyNameAndValue; - } - break; - } - case STATE_PARAMS_IN_DQUOTE: { - if (ch == '"') { - state = STATE_PARAMS; - } - break; - } - } - } - - throw new VCardInvalidLineException("Invalid line: \"" + line + "\""); - } - - /** - * params = ";" [ws] paramlist - * paramlist = paramlist [ws] ";" [ws] param - * / param - * param = "TYPE" [ws] "=" [ws] ptypeval - * / "VALUE" [ws] "=" [ws] pvalueval - * / "ENCODING" [ws] "=" [ws] pencodingval - * / "CHARSET" [ws] "=" [ws] charsetval - * / "LANGUAGE" [ws] "=" [ws] langval - * / "X-" word [ws] "=" [ws] word - * / knowntype - */ - protected void handleParams(String params) throws VCardException { - String[] strArray = params.split("=", 2); - if (strArray.length == 2) { - final String paramName = strArray[0].trim().toUpperCase(); - String paramValue = strArray[1].trim(); - if (paramName.equals("TYPE")) { - handleType(paramValue); - } else if (paramName.equals("VALUE")) { - handleValue(paramValue); - } else if (paramName.equals("ENCODING")) { - handleEncoding(paramValue); - } else if (paramName.equals("CHARSET")) { - handleCharset(paramValue); - } else if (paramName.equals("LANGUAGE")) { - handleLanguage(paramValue); - } else if (paramName.startsWith("X-")) { - handleAnyParam(paramName, paramValue); - } else { - throw new VCardException("Unknown type \"" + paramName + "\""); - } - } else { - handleParamWithoutName(strArray[0]); - } - } - - /** - * vCard 3.0 parser may throw VCardException. - */ - @SuppressWarnings("unused") - protected void handleParamWithoutName(final String paramValue) throws VCardException { - handleType(paramValue); - } - - /** - * ptypeval = knowntype / "X-" word - */ - protected void handleType(final String ptypeval) { - String upperTypeValue = ptypeval; - if (!(sKnownTypeSet.contains(upperTypeValue) || upperTypeValue.startsWith("X-")) && - !mUnknownTypeMap.contains(ptypeval)) { - mUnknownTypeMap.add(ptypeval); - Log.w(LOG_TAG, "TYPE unsupported by vCard 2.1: " + ptypeval); - } - if (mBuilder != null) { - mBuilder.propertyParamType("TYPE"); - mBuilder.propertyParamValue(upperTypeValue); - } - } - - /** - * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word - */ - protected void handleValue(final String pvalueval) { - if (!sKnownValueSet.contains(pvalueval.toUpperCase()) && - pvalueval.startsWith("X-") && - !mUnknownValueMap.contains(pvalueval)) { - mUnknownValueMap.add(pvalueval); - Log.w(LOG_TAG, "VALUE unsupported by vCard 2.1: " + pvalueval); - } - if (mBuilder != null) { - mBuilder.propertyParamType("VALUE"); - mBuilder.propertyParamValue(pvalueval); - } - } - - /** - * pencodingval = "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word - */ - protected void handleEncoding(String pencodingval) throws VCardException { - if (isValidEncoding(pencodingval) || - pencodingval.startsWith("X-")) { - if (mBuilder != null) { - mBuilder.propertyParamType("ENCODING"); - mBuilder.propertyParamValue(pencodingval); - } - mEncoding = pencodingval; - } else { - throw new VCardException("Unknown encoding \"" + pencodingval + "\""); - } - } - - /** - * vCard 2.1 specification only allows us-ascii and iso-8859-xxx (See RFC 1521), - * but today's vCard often contains other charset, so we allow them. - */ - protected void handleCharset(String charsetval) { - if (mBuilder != null) { - mBuilder.propertyParamType("CHARSET"); - mBuilder.propertyParamValue(charsetval); - } - } - - /** - * See also Section 7.1 of RFC 1521 - */ - protected void handleLanguage(String langval) throws VCardException { - String[] strArray = langval.split("-"); - if (strArray.length != 2) { - throw new VCardException("Invalid Language: \"" + langval + "\""); - } - String tmp = strArray[0]; - int length = tmp.length(); - for (int i = 0; i < length; i++) { - if (!isLetter(tmp.charAt(i))) { - throw new VCardException("Invalid Language: \"" + langval + "\""); - } - } - tmp = strArray[1]; - length = tmp.length(); - for (int i = 0; i < length; i++) { - if (!isLetter(tmp.charAt(i))) { - throw new VCardException("Invalid Language: \"" + langval + "\""); - } - } - if (mBuilder != null) { - mBuilder.propertyParamType("LANGUAGE"); - mBuilder.propertyParamValue(langval); - } - } - - /** - * Mainly for "X-" type. This accepts any kind of type without check. - */ - protected void handleAnyParam(String paramName, String paramValue) { - if (mBuilder != null) { - mBuilder.propertyParamType(paramName); - mBuilder.propertyParamValue(paramValue); - } - } - - protected void handlePropertyValue(String propertyName, String propertyValue) - throws IOException, VCardException { - if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { - final long start = System.currentTimeMillis(); - final String result = getQuotedPrintable(propertyValue); - if (mBuilder != null) { - ArrayList<String> v = new ArrayList<String>(); - v.add(result); - mBuilder.propertyValues(v); - } - mTimeHandleQuotedPrintable += System.currentTimeMillis() - start; - } else if (mEncoding.equalsIgnoreCase("BASE64") || - mEncoding.equalsIgnoreCase("B")) { - final long start = System.currentTimeMillis(); - // It is very rare, but some BASE64 data may be so big that - // OutOfMemoryError occurs. To ignore such cases, use try-catch. - try { - final String result = getBase64(propertyValue); - if (mBuilder != null) { - ArrayList<String> v = new ArrayList<String>(); - v.add(result); - mBuilder.propertyValues(v); - } - } catch (OutOfMemoryError error) { - Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!"); - if (mBuilder != null) { - mBuilder.propertyValues(null); - } - } - mTimeHandleBase64 += System.currentTimeMillis() - start; - } else { - if (!(mEncoding == null || mEncoding.equalsIgnoreCase("7BIT") - || mEncoding.equalsIgnoreCase("8BIT") - || mEncoding.toUpperCase().startsWith("X-"))) { - Log.w(LOG_TAG, "The encoding unsupported by vCard spec: \"" + mEncoding + "\"."); - } - - final long start = System.currentTimeMillis(); - if (mBuilder != null) { - ArrayList<String> v = new ArrayList<String>(); - v.add(maybeUnescapeText(propertyValue)); - mBuilder.propertyValues(v); - } - mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start; - } - } - - protected String getQuotedPrintable(String firstString) throws IOException, VCardException { - // Specifically, there may be some padding between = and CRLF. - // See the following: - // - // qp-line := *(qp-segment transport-padding CRLF) - // qp-part transport-padding - // qp-segment := qp-section *(SPACE / TAB) "=" - // ; Maximum length of 76 characters - // - // e.g. (from RFC 2045) - // Now's the time = - // for all folk to come= - // to the aid of their country. - if (firstString.trim().endsWith("=")) { - // remove "transport-padding" - int pos = firstString.length() - 1; - while(firstString.charAt(pos) != '=') { - } - StringBuilder builder = new StringBuilder(); - builder.append(firstString.substring(0, pos + 1)); - builder.append("\r\n"); - String line; - while (true) { - line = getLine(); - if (line == null) { - throw new VCardException( - "File ended during parsing quoted-printable String"); - } - if (line.trim().endsWith("=")) { - // remove "transport-padding" - pos = line.length() - 1; - while(line.charAt(pos) != '=') { - } - builder.append(line.substring(0, pos + 1)); - builder.append("\r\n"); - } else { - builder.append(line); - break; - } - } - return builder.toString(); - } else { - return firstString; - } - } - - protected String getBase64(String firstString) throws IOException, VCardException { - StringBuilder builder = new StringBuilder(); - builder.append(firstString); - - while (true) { - String line = getLine(); - if (line == null) { - throw new VCardException( - "File ended during parsing BASE64 binary"); - } - if (line.length() == 0) { - break; - } - builder.append(line); - } - - return builder.toString(); - } - - /** - * Mainly for "ADR", "ORG", and "N" - * We do not care the number of strnosemi here. - * - * addressparts = 0*6(strnosemi ";") strnosemi - * ; PO Box, Extended Addr, Street, Locality, Region, - * Postal Code, Country Name - * orgparts = *(strnosemi ";") strnosemi - * ; First is Organization Name, - * remainder are Organization Units. - * nameparts = 0*4(strnosemi ";") strnosemi - * ; Family, Given, Middle, Prefix, Suffix. - * ; Example:Public;John;Q.;Reverend Dr.;III, Esq. - * strnosemi = *(*nonsemi ("\;" / "\" CRLF)) *nonsemi - * ; To include a semicolon in this string, it must be escaped - * ; with a "\" character. - * - * We are not sure whether we should add "\" CRLF to each value. - * For now, we exclude them. + * <p> + * A unmodifiable Set storing the values for the type "ENCODING", available in the vCard 2.1. + * </p> + * <p> + * Though vCard 2.1 specification does not allow "B" encoding, some data may have it. + * We allow it for safety. + * </p> */ - protected void handleMultiplePropertyValue(String propertyName, String propertyValue) - throws IOException, VCardException { - // vCard 2.1 does not allow QUOTED-PRINTABLE here, - // but some softwares/devices emit such data. - if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { - propertyValue = getQuotedPrintable(propertyValue); - } + /* package */ static final Set<String> sAvailableEncoding = + Collections.unmodifiableSet(new HashSet<String>( + Arrays.asList(VCardConstants.PARAM_ENCODING_7BIT, + VCardConstants.PARAM_ENCODING_8BIT, + VCardConstants.PARAM_ENCODING_QP, + VCardConstants.PARAM_ENCODING_BASE64, + VCardConstants.PARAM_ENCODING_B))); - if (mBuilder != null) { - mBuilder.propertyValues(VCardUtils.constructListFromValue( - propertyValue, (getVersion() == VCardConfig.FLAG_V30))); - } - } + private final VCardParserImpl_V21 mVCardParserImpl; - /** - * vCard 2.1 specifies AGENT allows one vcard entry. It is not encoded at all. - * - * item = ... - * / [groups "."] "AGENT" - * [params] ":" vcard CRLF - * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF - * items *CRLF "END" [ws] ":" [ws] "VCARD" - */ - protected void handleAgent(final String propertyValue) throws VCardException { - if (!propertyValue.toUpperCase().contains("BEGIN:VCARD")) { - // Apparently invalid line seen in Windows Mobile 6.5. Ignore them. - return; - } else { - throw new VCardAgentNotSupportedException("AGENT Property is not supported now."); - } - // TODO: Support AGENT property. - } - - /** - * For vCard 3.0. - */ - protected String maybeUnescapeText(final String text) { - return text; + public VCardParser_V21() { + mVCardParserImpl = new VCardParserImpl_V21(); } - /** - * Returns unescaped String if the character should be unescaped. Return null otherwise. - * e.g. In vCard 2.1, "\;" should be unescaped into ";" while "\x" should not be. - */ - protected String maybeUnescapeCharacter(final char ch) { - return unescapeCharacter(ch); + public VCardParser_V21(int vcardType) { + mVCardParserImpl = new VCardParserImpl_V21(vcardType); } - public static String unescapeCharacter(final char ch) { - // Original vCard 2.1 specification does not allow transformation - // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous implementation of - // this class allowed them, so keep it as is. - if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') { - return String.valueOf(ch); - } else { - return null; - } + public VCardParser_V21(int parseType, String inputCharset) { + mVCardParserImpl = new VCardParserImpl_V21(parseType, null); } - - @Override - public boolean parse(final InputStream is, final VCardInterpreter builder) - throws IOException, VCardException { - return parse(is, VCardConfig.DEFAULT_CHARSET, builder); - } - - @Override - public boolean parse(InputStream is, String charset, VCardInterpreter builder) - throws IOException, VCardException { - if (charset == null) { - charset = VCardConfig.DEFAULT_CHARSET; - } - final InputStreamReader tmpReader = new InputStreamReader(is, charset); - if (VCardConfig.showPerformanceLog()) { - mReader = new CustomBufferedReader(tmpReader); - } else { - mReader = new BufferedReader(tmpReader); - } - - mBuilder = builder; - long start = System.currentTimeMillis(); - if (mBuilder != null) { - mBuilder.start(); - } - parseVCardFile(); - if (mBuilder != null) { - mBuilder.end(); - } - mTimeTotal += System.currentTimeMillis() - start; - - if (VCardConfig.showPerformanceLog()) { - showPerformanceInfo(); - } - - return true; - } - - @Override - public void parse(InputStream is, String charset, VCardInterpreter builder, boolean canceled) + public void parse(InputStream is, VCardInterpreter interepreter) throws IOException, VCardException { - mCanceled = canceled; - parse(is, charset, builder); - } - - private void showPerformanceInfo() { - Log.d(LOG_TAG, "Total parsing time: " + mTimeTotal + " ms"); - if (mReader instanceof CustomBufferedReader) { - Log.d(LOG_TAG, "Total readLine time: " + - ((CustomBufferedReader)mReader).getTotalmillisecond() + " ms"); - } - Log.d(LOG_TAG, "Time for handling the beggining of the record: " + - mTimeReadStartRecord + " ms"); - Log.d(LOG_TAG, "Time for handling the end of the record: " + - mTimeReadEndRecord + " ms"); - Log.d(LOG_TAG, "Time for parsing line, and handling group: " + - mTimeParseLineAndHandleGroup + " ms"); - Log.d(LOG_TAG, "Time for parsing ADR, ORG, and N fields:" + mTimeParseAdrOrgN + " ms"); - Log.d(LOG_TAG, "Time for parsing property values: " + mTimeParsePropertyValues + " ms"); - Log.d(LOG_TAG, "Time for handling normal property values: " + - mTimeHandleMiscPropertyValue + " ms"); - Log.d(LOG_TAG, "Time for handling Quoted-Printable: " + - mTimeHandleQuotedPrintable + " ms"); - Log.d(LOG_TAG, "Time for handling Base64: " + mTimeHandleBase64 + " ms"); + mVCardParserImpl.parse(is, interepreter); } - private boolean isLetter(char ch) { - if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { - return true; - } - return false; - } -} - -class CustomBufferedReader extends BufferedReader { - private long mTime; - - public CustomBufferedReader(Reader in) { - super(in); - } - - @Override - public String readLine() throws IOException { - long start = System.currentTimeMillis(); - String ret = super.readLine(); - long end = System.currentTimeMillis(); - mTime += end - start; - return ret; - } - - public long getTotalmillisecond() { - return mTime; + public void cancel() { + mVCardParserImpl.cancel(); } } diff --git a/core/java/android/pim/vcard/VCardParser_V30.java b/core/java/android/pim/vcard/VCardParser_V30.java index 4ecfe97..40792ab 100644 --- a/core/java/android/pim/vcard/VCardParser_V30.java +++ b/core/java/android/pim/vcard/VCardParser_V30.java @@ -16,343 +16,76 @@ package android.pim.vcard; import android.pim.vcard.exception.VCardException; -import android.util.Log; import java.io.IOException; +import java.io.InputStream; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; +import java.util.Set; /** - * The class used to parse vCard 3.0. - * Please refer to vCard Specification 3.0 (http://tools.ietf.org/html/rfc2426). + * <p> + * vCard parser for vCard 3.0. See RFC 2426 for more detail. + * </p> + * <p> + * This parser allows vCard format which is not allowed in the RFC, since + * we have seen several vCard 3.0 files which don't comply with it. + * </p> + * <p> + * e.g. vCard 3.0 does not allow "CHARSET" attribute, but some actual files + * have it and they uses non UTF-8 charsets. UTF-8 is recommended in RFC 2426, + * but it is not a must. We silently allow "CHARSET". + * </p> */ -public class VCardParser_V30 extends VCardParser_V21 { - private static final String LOG_TAG = "VCardParser_V30"; - - private static final HashSet<String> sAcceptablePropsWithParam = new HashSet<String>( - Arrays.asList( +public class VCardParser_V30 implements VCardParser { + /* package */ static final Set<String> sKnownPropertyNameSet = + Collections.unmodifiableSet(new HashSet<String>(Arrays.asList( "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER", // 2.1 "NAME", "PROFILE", "SOURCE", "NICKNAME", "CLASS", - "SORT-STRING", "CATEGORIES", "PRODID")); // 3.0 - - // Although "7bit" and "BASE64" is not allowed in vCard 3.0, we allow it for safety. - private static final HashSet<String> sAcceptableEncodingV30 = new HashSet<String>( - Arrays.asList("7BIT", "8BIT", "BASE64", "B")); - - // Although RFC 2426 specifies some property must not have parameters, we allow it, - // since there may be some careers which violates the RFC... - private static final HashSet<String> acceptablePropsWithoutParam = new HashSet<String>(); - - private String mPreviousLine; - - private boolean mEmittedAgentWarning = false; - - /** - * True when the caller wants the parser to be strict about the input. - * Currently this is only for testing. - */ - private final boolean mStrictParsing; - - public VCardParser_V30() { - super(); - mStrictParsing = false; - } + "SORT-STRING", "CATEGORIES", "PRODID"))); // 3.0 /** - * @param strictParsing when true, this object throws VCardException when the vcard is not - * valid from the view of vCard 3.0 specification (defined in RFC 2426). Note that this class - * is not fully yet for being used with this flag and may not notice invalid line(s). - * - * @hide currently only for testing! + * <p> + * A unmodifiable Set storing the values for the type "ENCODING", available in the vCard 3.0. + * </p> + * <p> + * Though vCard 2.1 specification does not allow "7BIT" or "BASE64", we allow them for safety. + * </p> + * <p> + * "QUOTED-PRINTABLE" is not allowed in vCard 3.0 and not in this parser either, + * because the encoding ambiguates how the vCard file to be parsed. + * </p> */ - public VCardParser_V30(boolean strictParsing) { - super(); - mStrictParsing = strictParsing; - } - - public VCardParser_V30(int parseMode) { - super(parseMode); - mStrictParsing = false; - } + /* package */ static final Set<String> sAcceptableEncoding = + Collections.unmodifiableSet(new HashSet<String>(Arrays.asList( + VCardConstants.PARAM_ENCODING_7BIT, + VCardConstants.PARAM_ENCODING_8BIT, + VCardConstants.PARAM_ENCODING_BASE64, + VCardConstants.PARAM_ENCODING_B))); - @Override - protected int getVersion() { - return VCardConfig.FLAG_V30; - } - - @Override - protected String getVersionString() { - return VCardConstants.VERSION_V30; - } + private final VCardParserImpl_V30 mVCardParserImpl; - @Override - protected boolean isValidPropertyName(String propertyName) { - if (!(sAcceptablePropsWithParam.contains(propertyName) || - acceptablePropsWithoutParam.contains(propertyName) || - propertyName.startsWith("X-")) && - !mUnknownTypeMap.contains(propertyName)) { - mUnknownTypeMap.add(propertyName); - Log.w(LOG_TAG, "Property name unsupported by vCard 3.0: " + propertyName); - } - return true; + public VCardParser_V30() { + mVCardParserImpl = new VCardParserImpl_V30(); } - @Override - protected boolean isValidEncoding(String encoding) { - return sAcceptableEncodingV30.contains(encoding.toUpperCase()); + public VCardParser_V30(int vcardType) { + mVCardParserImpl = new VCardParserImpl_V30(vcardType); } - @Override - protected String getLine() throws IOException { - if (mPreviousLine != null) { - String ret = mPreviousLine; - mPreviousLine = null; - return ret; - } else { - return mReader.readLine(); - } - } - - /** - * vCard 3.0 requires that the line with space at the beginning of the line - * must be combined with previous line. - */ - @Override - protected String getNonEmptyLine() throws IOException, VCardException { - String line; - StringBuilder builder = null; - while (true) { - line = mReader.readLine(); - if (line == null) { - if (builder != null) { - return builder.toString(); - } else if (mPreviousLine != null) { - String ret = mPreviousLine; - mPreviousLine = null; - return ret; - } - throw new VCardException("Reached end of buffer."); - } else if (line.length() == 0) { - if (builder != null) { - return builder.toString(); - } else if (mPreviousLine != null) { - String ret = mPreviousLine; - mPreviousLine = null; - return ret; - } - } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') { - if (builder != null) { - // See Section 5.8.1 of RFC 2425 (MIME-DIR document). - // Following is the excerpts from it. - // - // DESCRIPTION:This is a long description that exists on a long line. - // - // Can be represented as: - // - // DESCRIPTION:This is a long description - // that exists on a long line. - // - // It could also be represented as: - // - // DESCRIPTION:This is a long descrip - // tion that exists o - // n a long line. - builder.append(line.substring(1)); - } else if (mPreviousLine != null) { - builder = new StringBuilder(); - builder.append(mPreviousLine); - mPreviousLine = null; - builder.append(line.substring(1)); - } else { - throw new VCardException("Space exists at the beginning of the line"); - } - } else { - if (mPreviousLine == null) { - mPreviousLine = line; - if (builder != null) { - return builder.toString(); - } - } else { - String ret = mPreviousLine; - mPreviousLine = line; - return ret; - } - } - } - } - - - /** - * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF - * 1 * (contentline) - * ;A vCard object MUST include the VERSION, FN and N types. - * [group "."] "END" ":" "VCARD" 1 * CRLF - */ - @Override - protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { - // TODO: vCard 3.0 supports group. - return super.readBeginVCard(allowGarbage); + public VCardParser_V30(int vcardType, String importCharset) { + mVCardParserImpl = new VCardParserImpl_V30(vcardType, importCharset); } - @Override - protected void readEndVCard(boolean useCache, boolean allowGarbage) + public void parse(InputStream is, VCardInterpreter interepreter) throws IOException, VCardException { - // TODO: vCard 3.0 supports group. - super.readEndVCard(useCache, allowGarbage); - } - - /** - * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not. - */ - @Override - protected void handleParams(String params) throws VCardException { - try { - super.handleParams(params); - } catch (VCardException e) { - // maybe IANA type - String[] strArray = params.split("=", 2); - if (strArray.length == 2) { - handleAnyParam(strArray[0], strArray[1]); - } else { - // Must not come here in the current implementation. - throw new VCardException( - "Unknown params value: " + params); - } - } - } - - @Override - protected void handleAnyParam(String paramName, String paramValue) { - super.handleAnyParam(paramName, paramValue); - } - - @Override - protected void handleParamWithoutName(final String paramValue) throws VCardException { - if (mStrictParsing) { - throw new VCardException("Parameter without name is not acceptable in vCard 3.0"); - } else { - super.handleParamWithoutName(paramValue); - } - } - - /** - * vCard 3.0 defines - * - * param = param-name "=" param-value *("," param-value) - * param-name = iana-token / x-name - * param-value = ptext / quoted-string - * quoted-string = DQUOTE QSAFE-CHAR DQUOTE - */ - @Override - protected void handleType(String ptypevalues) { - String[] ptypeArray = ptypevalues.split(","); - mBuilder.propertyParamType("TYPE"); - for (String value : ptypeArray) { - int length = value.length(); - if (length >= 2 && value.startsWith("\"") && value.endsWith("\"")) { - mBuilder.propertyParamValue(value.substring(1, value.length() - 1)); - } else { - mBuilder.propertyParamValue(value); - } - } - } - - @Override - protected void handleAgent(String propertyValue) { - // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1. - // - // e.g. - // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n - // TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n - // ET:jfriday@host.com\nEND:VCARD\n - // - // TODO: fix this. - // - // issue: - // vCard 3.0 also allows this as an example. - // - // AGENT;VALUE=uri: - // CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com - // - // This is not vCard. Should we support this? - // - // Just ignore the line for now, since we cannot know how to handle it... - if (!mEmittedAgentWarning) { - Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it"); - mEmittedAgentWarning = true; - } - } - - /** - * vCard 3.0 does not require two CRLF at the last of BASE64 data. - * It only requires that data should be MIME-encoded. - */ - @Override - protected String getBase64(String firstString) throws IOException, VCardException { - StringBuilder builder = new StringBuilder(); - builder.append(firstString); - - while (true) { - String line = getLine(); - if (line == null) { - throw new VCardException( - "File ended during parsing BASE64 binary"); - } - if (line.length() == 0) { - break; - } else if (!line.startsWith(" ") && !line.startsWith("\t")) { - mPreviousLine = line; - break; - } - builder.append(line); - } - - return builder.toString(); - } - - /** - * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N") - * ; \\ encodes \, \n or \N encodes newline - * ; \; encodes ;, \, encodes , - * - * Note: Apple escapes ':' into '\:' while does not escape '\' - */ - @Override - protected String maybeUnescapeText(String text) { - return unescapeText(text); - } - - public static String unescapeText(String text) { - StringBuilder builder = new StringBuilder(); - int length = text.length(); - for (int i = 0; i < length; i++) { - char ch = text.charAt(i); - if (ch == '\\' && i < length - 1) { - char next_ch = text.charAt(++i); - if (next_ch == 'n' || next_ch == 'N') { - builder.append("\n"); - } else { - builder.append(next_ch); - } - } else { - builder.append(ch); - } - } - return builder.toString(); - } - - @Override - protected String maybeUnescapeCharacter(char ch) { - return unescapeCharacter(ch); + mVCardParserImpl.parse(is, interepreter); } - public static String unescapeCharacter(char ch) { - if (ch == 'n' || ch == 'N') { - return "\n"; - } else { - return String.valueOf(ch); - } + public void cancel() { + mVCardParserImpl.cancel(); } } diff --git a/core/java/android/pim/vcard/VCardSourceDetector.java b/core/java/android/pim/vcard/VCardSourceDetector.java index 7297c50..291deca 100644 --- a/core/java/android/pim/vcard/VCardSourceDetector.java +++ b/core/java/android/pim/vcard/VCardSourceDetector.java @@ -15,15 +15,28 @@ */ package android.pim.vcard; +import android.text.TextUtils; + import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; /** - * Class which tries to detects the source of the vCard from its properties. - * Currently this implementation is very premature. - * @hide + * <p> + * The class which tries to detects the source of a vCard file from its contents. + * </p> + * <p> + * The specification of vCard (including both 2.1 and 3.0) is not so strict as to + * guess its format just by reading beginning few lines (usually we can, but in + * some most pessimistic case, we cannot until at almost the end of the file). + * Also we cannot store all vCard entries in memory, while there's no specification + * how big the vCard entry would become after the parse. + * </p> + * <p> + * This class is usually used for the "first scan", in which we can understand which vCard + * version is used (and how many entries exist in a file). + * </p> */ public class VCardSourceDetector implements VCardInterpreter { private static Set<String> APPLE_SIGNS = new HashSet<String>(Arrays.asList( @@ -42,8 +55,22 @@ public class VCardSourceDetector implements VCardInterpreter { "X-SD-VERN", "X-SD-FORMAT_VER", "X-SD-CATEGORIES", "X-SD-CLASS", "X-SD-DCREATED", "X-SD-DESCRIPTION")); private static String TYPE_FOMA_CHARSET_SIGN = "X-SD-CHAR_CODE"; - - private int mType = VCardConfig.PARSE_TYPE_UNKNOWN; + + + // TODO: Should replace this with types in VCardConfig + private static final int PARSE_TYPE_UNKNOWN = 0; + // For Apple's software, which does not mean this type is effective for all its products. + // We confirmed they usually use UTF-8, but not sure about vCard type. + private static final int PARSE_TYPE_APPLE = 1; + // For Japanese mobile phones, which are usually using Shift_JIS as a charset. + private static final int PARSE_TYPE_MOBILE_PHONE_JP = 2; + // For some of mobile phones released from DoCoMo, which use nested vCard. + private static final int PARSE_TYPE_DOCOMO_TORELATE_NEST = 3; + // For Japanese Windows Mobel phones. It's version is supposed to be 6.5. + private static final int PARSE_TYPE_WINDOWS_MOBILE_V65_JP = 4; + + private int mParseType = 0; // Not sure. + // Some mobile phones (like FOMA) tells us the charset of the data. private boolean mNeedParseSpecifiedCharset; private String mSpecifiedCharset; @@ -72,21 +99,22 @@ public class VCardSourceDetector implements VCardInterpreter { public void propertyName(String name) { if (name.equalsIgnoreCase(TYPE_FOMA_CHARSET_SIGN)) { - mType = VCardConfig.PARSE_TYPE_FOMA; + mParseType = PARSE_TYPE_DOCOMO_TORELATE_NEST; + // Probably Shift_JIS is used, but we should double confirm. mNeedParseSpecifiedCharset = true; return; } - if (mType != VCardConfig.PARSE_TYPE_UNKNOWN) { + if (mParseType != PARSE_TYPE_UNKNOWN) { return; } if (WINDOWS_MOBILE_PHONE_SIGNS.contains(name)) { - mType = VCardConfig.PARSE_TYPE_WINDOWS_MOBILE_JP; + mParseType = PARSE_TYPE_WINDOWS_MOBILE_V65_JP; } else if (FOMA_SIGNS.contains(name)) { - mType = VCardConfig.PARSE_TYPE_FOMA; + mParseType = PARSE_TYPE_DOCOMO_TORELATE_NEST; } else if (JAPANESE_MOBILE_PHONE_SIGNS.contains(name)) { - mType = VCardConfig.PARSE_TYPE_MOBILE_PHONE_JP; + mParseType = PARSE_TYPE_MOBILE_PHONE_JP; } else if (APPLE_SIGNS.contains(name)) { - mType = VCardConfig.PARSE_TYPE_APPLE; + mParseType = PARSE_TYPE_APPLE; } } @@ -102,25 +130,40 @@ public class VCardSourceDetector implements VCardInterpreter { } } - /* package */ int getEstimatedType() { - return mType; + /** + * @return The available type can be used with vCard parser. You probably need to + * use {{@link #getEstimatedCharset()} to understand the charset to be used. + */ + public int getEstimatedType() { + switch (mParseType) { + case PARSE_TYPE_DOCOMO_TORELATE_NEST: + return VCardConfig.VCARD_TYPE_DOCOMO | VCardConfig.FLAG_TORELATE_NEST; + case PARSE_TYPE_MOBILE_PHONE_JP: + return VCardConfig.VCARD_TYPE_V21_JAPANESE_MOBILE; + case PARSE_TYPE_APPLE: + case PARSE_TYPE_WINDOWS_MOBILE_V65_JP: + default: + return VCardConfig.VCARD_TYPE_UNKNOWN; + } } - + /** - * Return charset String guessed from the source's properties. + * <p> + * Returns charset String guessed from the source's properties. * This method must be called after parsing target file(s). + * </p> * @return Charset String. Null is returned if guessing the source fails. */ public String getEstimatedCharset() { - if (mSpecifiedCharset != null) { + if (TextUtils.isEmpty(mSpecifiedCharset)) { return mSpecifiedCharset; } - switch (mType) { - case VCardConfig.PARSE_TYPE_WINDOWS_MOBILE_JP: - case VCardConfig.PARSE_TYPE_FOMA: - case VCardConfig.PARSE_TYPE_MOBILE_PHONE_JP: + switch (mParseType) { + case PARSE_TYPE_WINDOWS_MOBILE_V65_JP: + case PARSE_TYPE_DOCOMO_TORELATE_NEST: + case PARSE_TYPE_MOBILE_PHONE_JP: return "SHIFT_JIS"; - case VCardConfig.PARSE_TYPE_APPLE: + case PARSE_TYPE_APPLE: return "UTF-8"; default: return null; diff --git a/core/java/android/pim/vcard/VCardUtils.java b/core/java/android/pim/vcard/VCardUtils.java index 11b112b..680ef6f 100644 --- a/core/java/android/pim/vcard/VCardUtils.java +++ b/core/java/android/pim/vcard/VCardUtils.java @@ -22,7 +22,12 @@ import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; +import android.util.Log; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.net.QuotedPrintableCodec; + +import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -34,8 +39,11 @@ import java.util.Set; /** * Utilities for VCard handling codes. + * @hide */ public class VCardUtils { + private static final String LOG_TAG = "VCardUtils"; + // Note that not all types are included in this map/set, since, for example, TYPE_HOME_FAX is // converted to two parameter Strings. These only contain some minor fields valid in both // vCard and current (as of 2009-08-07) Contacts structure. @@ -240,10 +248,13 @@ public class VCardUtils { } /** + * <p> * Inserts postal data into the builder object. - * + * </p> + * <p> * Note that the data structure of ContactsContract is different from that defined in vCard. * So some conversion may be performed in this method. + * </p> */ public static void insertStructuredPostalDataUsingContactsStruct(int vcardType, final ContentProviderOperation.Builder builder, @@ -329,8 +340,8 @@ public class VCardUtils { if (ch == '\\' && i < length - 1) { char nextCh = value.charAt(i + 1); final String unescapedString = - (isV30 ? VCardParser_V30.unescapeCharacter(nextCh) : - VCardParser_V21.unescapeCharacter(nextCh)); + (isV30 ? VCardParserImpl_V30.unescapeCharacter(nextCh) : + VCardParserImpl_V21.unescapeCharacter(nextCh)); if (unescapedString != null) { builder.append(unescapedString); i++; @@ -371,9 +382,13 @@ public class VCardUtils { } /** + * <p> * This is useful when checking the string should be encoded into quoted-printable * or not, which is required by vCard 2.1. + * </p> + * <p> * See the definition of "7bit" in vCard 2.1 spec for more information. + * </p> */ public static boolean containsOnlyNonCrLfPrintableAscii(final String...values) { if (values == null) { @@ -407,13 +422,16 @@ public class VCardUtils { new HashSet<Character>(Arrays.asList('[', ']', '=', ':', '.', ',', ' ')); /** + * <p> * This is useful since vCard 3.0 often requires the ("X-") properties and groups * should contain only alphabets, digits, and hyphen. - * + * </p> + * <p> * Note: It is already known some devices (wrongly) outputs properties with characters * which should not be in the field. One example is "X-GOOGLE TALK". We accept * such kind of input but must never output it unless the target is very specific - * to the device which is able to parse the malformed input. + * to the device which is able to parse the malformed input. + * </p> */ public static boolean containsOnlyAlphaDigitHyphen(final String...values) { if (values == null) { @@ -452,13 +470,13 @@ public class VCardUtils { } /** - * <P> + * <p> * Returns true when the given String is categorized as "word" specified in vCard spec 2.1. - * </P> - * <P> - * vCard 2.1 specifies:<BR /> + * </p> + * <p> + * vCard 2.1 specifies:<br /> * word = <any printable 7bit us-ascii except []=:., > - * </P> + * </p> */ public static boolean isV21Word(final String value) { if (TextUtils.isEmpty(value)) { @@ -540,6 +558,96 @@ public class VCardUtils { return true; } + //// The methods bellow may be used by unit test. + + /** + * @hide + */ + public static String parseQuotedPrintable(String value, boolean strictLineBreaking, + String sourceCharset, String targetCharset) { + // "= " -> " ", "=\t" -> "\t". + // Previous code had done this replacement. Keep on the safe side. + final String quotedPrintable; + { + final StringBuilder builder = new StringBuilder(); + final int length = value.length(); + for (int i = 0; i < length; i++) { + char ch = value.charAt(i); + if (ch == '=' && i < length - 1) { + char nextCh = value.charAt(i + 1); + if (nextCh == ' ' || nextCh == '\t') { + builder.append(nextCh); + i++; + continue; + } + } + builder.append(ch); + } + quotedPrintable = builder.toString(); + } + + String[] lines; + if (strictLineBreaking) { + lines = quotedPrintable.split("\r\n"); + } else { + StringBuilder builder = new StringBuilder(); + final int length = quotedPrintable.length(); + ArrayList<String> list = new ArrayList<String>(); + for (int i = 0; i < length; i++) { + char ch = quotedPrintable.charAt(i); + if (ch == '\n') { + list.add(builder.toString()); + builder = new StringBuilder(); + } else if (ch == '\r') { + list.add(builder.toString()); + builder = new StringBuilder(); + if (i < length - 1) { + char nextCh = quotedPrintable.charAt(i + 1); + if (nextCh == '\n') { + i++; + } + } + } else { + builder.append(ch); + } + } + final String lastLine = builder.toString(); + if (lastLine.length() > 0) { + list.add(lastLine); + } + lines = list.toArray(new String[0]); + } + + final StringBuilder builder = new StringBuilder(); + for (String line : lines) { + if (line.endsWith("=")) { + line = line.substring(0, line.length() - 1); + } + builder.append(line); + } + byte[] bytes; + try { + bytes = builder.toString().getBytes(sourceCharset); + } catch (UnsupportedEncodingException e1) { + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + bytes = builder.toString().getBytes(); + } + + try { + bytes = QuotedPrintableCodec.decodeQuotedPrintable(bytes); + } catch (DecoderException e) { + Log.e(LOG_TAG, "Failed to decode quoted-printable: " + e); + return ""; + } + + try { + return new String(bytes, targetCharset); + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + return new String(bytes); + } + } + private VCardUtils() { } } diff --git a/core/java/android/preference/MultiSelectListPreference.java b/core/java/android/preference/MultiSelectListPreference.java new file mode 100644 index 0000000..42d555c --- /dev/null +++ b/core/java/android/preference/MultiSelectListPreference.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.preference; + +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; + +import java.util.HashSet; +import java.util.Set; + +/** + * A {@link Preference} that displays a list of entries as + * a dialog. + * <p> + * This preference will store a set of strings into the SharedPreferences. + * This set will contain one or more values from the + * {@link #setEntryValues(CharSequence[])} array. + * + * @attr ref android.R.styleable#MultiSelectListPreference_entries + * @attr ref android.R.styleable#MultiSelectListPreference_entryValues + */ +public class MultiSelectListPreference extends DialogPreference { + private CharSequence[] mEntries; + private CharSequence[] mEntryValues; + private Set<String> mValues = new HashSet<String>(); + private Set<String> mNewValues = new HashSet<String>(); + private boolean mPreferenceChanged; + + public MultiSelectListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.MultiSelectListPreference, 0, 0); + mEntries = a.getTextArray(com.android.internal.R.styleable.MultiSelectListPreference_entries); + mEntryValues = a.getTextArray(com.android.internal.R.styleable.MultiSelectListPreference_entryValues); + a.recycle(); + } + + public MultiSelectListPreference(Context context) { + this(context, null); + } + + /** + * Sets the human-readable entries to be shown in the list. This will be + * shown in subsequent dialogs. + * <p> + * Each entry must have a corresponding index in + * {@link #setEntryValues(CharSequence[])}. + * + * @param entries The entries. + * @see #setEntryValues(CharSequence[]) + */ + public void setEntries(CharSequence[] entries) { + mEntries = entries; + } + + /** + * @see #setEntries(CharSequence[]) + * @param entriesResId The entries array as a resource. + */ + public void setEntries(int entriesResId) { + setEntries(getContext().getResources().getTextArray(entriesResId)); + } + + /** + * The list of entries to be shown in the list in subsequent dialogs. + * + * @return The list as an array. + */ + public CharSequence[] getEntries() { + return mEntries; + } + + /** + * The array to find the value to save for a preference when an entry from + * entries is selected. If a user clicks on the second item in entries, the + * second item in this array will be saved to the preference. + * + * @param entryValues The array to be used as values to save for the preference. + */ + public void setEntryValues(CharSequence[] entryValues) { + mEntryValues = entryValues; + } + + /** + * @see #setEntryValues(CharSequence[]) + * @param entryValuesResId The entry values array as a resource. + */ + public void setEntryValues(int entryValuesResId) { + setEntryValues(getContext().getResources().getTextArray(entryValuesResId)); + } + + /** + * Returns the array of values to be saved for the preference. + * + * @return The array of values. + */ + public CharSequence[] getEntryValues() { + return mEntryValues; + } + + /** + * Sets the value of the key. This should contain entries in + * {@link #getEntryValues()}. + * + * @param values The values to set for the key. + */ + public void setValues(Set<String> values) { + mValues = values; + + persistStringSet(values); + } + + /** + * Retrieves the current value of the key. + */ + public Set<String> getValues() { + return mValues; + } + + /** + * Returns the index of the given value (in the entry values array). + * + * @param value The value whose index should be returned. + * @return The index of the value, or -1 if not found. + */ + public int findIndexOfValue(String value) { + if (value != null && mEntryValues != null) { + for (int i = mEntryValues.length - 1; i >= 0; i--) { + if (mEntryValues[i].equals(value)) { + return i; + } + } + } + return -1; + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + super.onPrepareDialogBuilder(builder); + + if (mEntries == null || mEntryValues == null) { + throw new IllegalStateException( + "MultiSelectListPreference requires an entries array and " + + "an entryValues array."); + } + + boolean[] checkedItems = getSelectedItems(); + builder.setMultiChoiceItems(mEntries, checkedItems, + new DialogInterface.OnMultiChoiceClickListener() { + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + if (isChecked) { + mPreferenceChanged |= mNewValues.add(mEntries[which].toString()); + } else { + mPreferenceChanged |= mNewValues.remove(mEntries[which].toString()); + } + } + }); + mNewValues.clear(); + mNewValues.addAll(mValues); + } + + private boolean[] getSelectedItems() { + final CharSequence[] entries = mEntries; + final int entryCount = entries.length; + final Set<String> values = mValues; + boolean[] result = new boolean[entryCount]; + + for (int i = 0; i < entryCount; i++) { + result[i] = values.contains(entries[i].toString()); + } + + return result; + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (positiveResult && mPreferenceChanged) { + final Set<String> values = mNewValues; + if (callChangeListener(values)) { + setValues(values); + } + } + mPreferenceChanged = false; + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + final CharSequence[] defaultValues = a.getTextArray(index); + final int valueCount = defaultValues.length; + final Set<String> result = new HashSet<String>(); + + for (int i = 0; i < valueCount; i++) { + result.add(defaultValues[i].toString()); + } + + return result; + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setValues(restoreValue ? getPersistedStringSet(mValues) : (Set<String>) defaultValue); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + // No need to save instance state + return superState; + } + + final SavedState myState = new SavedState(superState); + myState.values = getValues(); + return myState; + } + + private static class SavedState extends BaseSavedState { + Set<String> values; + + public SavedState(Parcel source) { + super(source); + values = new HashSet<String>(); + String[] strings = source.readStringArray(); + + final int stringCount = strings.length; + for (int i = 0; i < stringCount; i++) { + values.add(strings[i]); + } + } + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeStringArray(values.toArray(new String[0])); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java index 197d976..381f794 100644 --- a/core/java/android/preference/Preference.java +++ b/core/java/android/preference/Preference.java @@ -16,8 +16,7 @@ package android.preference; -import java.util.ArrayList; -import java.util.List; +import com.android.internal.util.CharSequences; import android.content.Context; import android.content.Intent; @@ -28,7 +27,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.AttributeSet; -import com.android.internal.util.CharSequences; import android.view.AbsSavedState; import android.view.LayoutInflater; import android.view.View; @@ -36,6 +34,10 @@ import android.view.ViewGroup; import android.widget.ListView; import android.widget.TextView; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + /** * Represents the basic Preference UI building * block displayed by a {@link PreferenceActivity} in the form of a @@ -1250,6 +1252,61 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** + * Attempts to persist a set of Strings to the {@link android.content.SharedPreferences}. + * <p> + * This will check if this Preference is persistent, get an editor from + * the {@link PreferenceManager}, put in the strings, and check if we should commit (and + * commit if so). + * + * @param values The values to persist. + * @return True if the Preference is persistent. (This is not whether the + * value was persisted, since we may not necessarily commit if there + * will be a batch commit later.) + * @see #getPersistedString(Set) + * + * @hide Pending API approval + */ + protected boolean persistStringSet(Set<String> values) { + if (shouldPersist()) { + // Shouldn't store null + if (values.equals(getPersistedStringSet(null))) { + // It's already there, so the same as persisting + return true; + } + + SharedPreferences.Editor editor = mPreferenceManager.getEditor(); + editor.putStringSet(mKey, values); + tryCommit(editor); + return true; + } + return false; + } + + /** + * Attempts to get a persisted set of Strings from the + * {@link android.content.SharedPreferences}. + * <p> + * This will check if this Preference is persistent, get the SharedPreferences + * from the {@link PreferenceManager}, and get the value. + * + * @param defaultReturnValue The default value to return if either the + * Preference is not persistent or the Preference is not in the + * shared preferences. + * @return The value from the SharedPreferences or the default return + * value. + * @see #persistStringSet(Set) + * + * @hide Pending API approval + */ + protected Set<String> getPersistedStringSet(Set<String> defaultReturnValue) { + if (!shouldPersist()) { + return defaultReturnValue; + } + + return mPreferenceManager.getSharedPreferences().getStringSet(mKey, defaultReturnValue); + } + + /** * Attempts to persist an int to the {@link android.content.SharedPreferences}. * * @param value The value to persist. diff --git a/core/java/android/preference/PreferenceActivity.java b/core/java/android/preference/PreferenceActivity.java index 726793d..4686978 100644 --- a/core/java/android/preference/PreferenceActivity.java +++ b/core/java/android/preference/PreferenceActivity.java @@ -23,7 +23,10 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.text.TextUtils; import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; /** * Shows a hierarchy of {@link Preference} objects as @@ -69,30 +72,43 @@ import android.view.View; * As a convenience, this activity implements a click listener for any * preference in the current hierarchy, see * {@link #onPreferenceTreeClick(PreferenceScreen, Preference)}. - * + * * @see Preference * @see PreferenceScreen */ public abstract class PreferenceActivity extends ListActivity implements PreferenceManager.OnPreferenceTreeClickListener { - + private static final String PREFERENCES_TAG = "android:preferences"; - + + // extras that allow any preference activity to be launched as part of a wizard + + // show Back and Next buttons? takes boolean parameter + // Back will then return RESULT_CANCELED and Next RESULT_OK + private static final String EXTRA_PREFS_SHOW_BUTTON_BAR = "extra_prefs_show_button_bar"; + + // specify custom text for the Back or Next buttons, or cause a button to not appear + // at all by setting it to null + private static final String EXTRA_PREFS_SET_NEXT_TEXT = "extra_prefs_set_next_text"; + private static final String EXTRA_PREFS_SET_BACK_TEXT = "extra_prefs_set_back_text"; + + private Button mNextButton; + private PreferenceManager mPreferenceManager; - + private Bundle mSavedInstanceState; /** * The starting request code given out to preference framework. */ private static final int FIRST_REQUEST_CODE = 100; - + private static final int MSG_BIND_PREFERENCES = 0; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { - + case MSG_BIND_PREFERENCES: bindPreferences(); break; @@ -105,7 +121,49 @@ public abstract class PreferenceActivity extends ListActivity implements super.onCreate(savedInstanceState); setContentView(com.android.internal.R.layout.preference_list_content); - + + // see if we should show Back/Next buttons + Intent intent = getIntent(); + if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) { + + findViewById(com.android.internal.R.id.button_bar).setVisibility(View.VISIBLE); + + Button backButton = (Button)findViewById(com.android.internal.R.id.back_button); + backButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }); + mNextButton = (Button)findViewById(com.android.internal.R.id.next_button); + mNextButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + setResult(RESULT_OK); + finish(); + } + }); + + // set our various button parameters + if (intent.hasExtra(EXTRA_PREFS_SET_NEXT_TEXT)) { + String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_NEXT_TEXT); + if (TextUtils.isEmpty(buttonText)) { + mNextButton.setVisibility(View.GONE); + } + else { + mNextButton.setText(buttonText); + } + } + if (intent.hasExtra(EXTRA_PREFS_SET_BACK_TEXT)) { + String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_BACK_TEXT); + if (TextUtils.isEmpty(buttonText)) { + backButton.setVisibility(View.GONE); + } + else { + backButton.setText(buttonText); + } + } + } + mPreferenceManager = onCreatePreferenceManager(); getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); } @@ -113,14 +171,13 @@ public abstract class PreferenceActivity extends ListActivity implements @Override protected void onStop() { super.onStop(); - + mPreferenceManager.dispatchActivityStop(); } @Override protected void onDestroy() { super.onDestroy(); - mPreferenceManager.dispatchActivityDestroy(); } @@ -156,7 +213,7 @@ public abstract class PreferenceActivity extends ListActivity implements @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - + mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data); } @@ -176,7 +233,7 @@ public abstract class PreferenceActivity extends ListActivity implements if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return; mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget(); } - + private void bindPreferences() { final PreferenceScreen preferenceScreen = getPreferenceScreen(); if (preferenceScreen != null) { @@ -187,10 +244,10 @@ public abstract class PreferenceActivity extends ListActivity implements } } } - + /** * Creates the {@link PreferenceManager}. - * + * * @return The {@link PreferenceManager} used by this activity. */ private PreferenceManager onCreatePreferenceManager() { @@ -198,7 +255,7 @@ public abstract class PreferenceActivity extends ListActivity implements preferenceManager.setOnPreferenceTreeClickListener(this); return preferenceManager; } - + /** * Returns the {@link PreferenceManager} used by this activity. * @return The {@link PreferenceManager}. @@ -206,7 +263,7 @@ public abstract class PreferenceActivity extends ListActivity implements public PreferenceManager getPreferenceManager() { return mPreferenceManager; } - + private void requirePreferenceManager() { if (mPreferenceManager == null) { throw new RuntimeException("This should be called after super.onCreate."); @@ -215,7 +272,7 @@ public abstract class PreferenceActivity extends ListActivity implements /** * Sets the root of the preference hierarchy that this activity is showing. - * + * * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. */ public void setPreferenceScreen(PreferenceScreen preferenceScreen) { @@ -228,37 +285,37 @@ public abstract class PreferenceActivity extends ListActivity implements } } } - + /** * Gets the root of the preference hierarchy that this activity is showing. - * + * * @return The {@link PreferenceScreen} that is the root of the preference * hierarchy. */ public PreferenceScreen getPreferenceScreen() { return mPreferenceManager.getPreferenceScreen(); } - + /** * Adds preferences from activities that match the given {@link Intent}. - * + * * @param intent The {@link Intent} to query activities. */ public void addPreferencesFromIntent(Intent intent) { requirePreferenceManager(); - + setPreferenceScreen(mPreferenceManager.inflateFromIntent(intent, getPreferenceScreen())); } - + /** * Inflates the given XML resource and adds the preference hierarchy to the current * preference hierarchy. - * + * * @param preferencesResId The XML resource ID to inflate. */ public void addPreferencesFromResource(int preferencesResId) { requirePreferenceManager(); - + setPreferenceScreen(mPreferenceManager.inflateFromResource(this, preferencesResId, getPreferenceScreen())); } @@ -269,20 +326,20 @@ public abstract class PreferenceActivity extends ListActivity implements public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { return false; } - + /** * Finds a {@link Preference} based on its key. - * + * * @param key The key of the preference to retrieve. * @return The {@link Preference} with the key, or null. * @see PreferenceGroup#findPreference(CharSequence) */ public Preference findPreference(CharSequence key) { - + if (mPreferenceManager == null) { return null; } - + return mPreferenceManager.findPreference(key); } @@ -292,5 +349,14 @@ public abstract class PreferenceActivity extends ListActivity implements mPreferenceManager.dispatchNewIntent(intent); } } - + + // give subclasses access to the Next button + /** @hide */ + protected boolean hasNextButton() { + return mNextButton != null; + } + /** @hide */ + protected Button getNextButton() { + return mNextButton; + } } diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java index 9a09805..8459b2d 100644 --- a/core/java/android/provider/Calendar.java +++ b/core/java/android/provider/Calendar.java @@ -438,6 +438,18 @@ public final class Calendar { public static final String DTEND = "dtend"; /** + * The time the event starts with allDay events in a local tz + * <P>Type: INTEGER (long; millis since epoch)</P> + */ + public static final String DTSTART2 = "dtstart2"; + + /** + * The time the event ends with allDay events in a local tz + * <P>Type: INTEGER (long; millis since epoch)</P> + */ + public static final String DTEND2 = "dtend2"; + + /** * The duration of the event * <P>Type: TEXT (duration in RFC2445 format)</P> */ @@ -450,6 +462,12 @@ public final class Calendar { public static final String EVENT_TIMEZONE = "eventTimezone"; /** + * The timezone for the event, allDay events will have a local tz instead of UTC + * <P>Type: TEXT + */ + public static final String EVENT_TIMEZONE2 = "eventTimezone2"; + + /** * Whether the event lasts all day or not * <P>Type: INTEGER (boolean)</P> */ diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 40a408a..abeb931 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -4902,6 +4902,23 @@ public final class ContactsContract { * Type: INTEGER (boolean) */ public static final String SHOULD_SYNC = "should_sync"; + + /** + * Any newly created contacts will automatically be added to groups that have this + * flag set to true. + * <p> + * Type: INTEGER (boolean) + */ + public static final String AUTO_ADD = "auto_add"; + + /** + * When a contacts is marked as a favorites it will be automatically added + * to the groups that have this flag set, and when it is removed from favorites + * it will be removed from these groups. + * <p> + * Type: INTEGER (boolean) + */ + public static final String FAVORITES = "favorites"; } /** @@ -5042,6 +5059,8 @@ public final class ContactsContract { DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, values, DELETED); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, NOTES); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, SHOULD_SYNC); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, FAVORITES); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, AUTO_ADD); cursor.moveToNext(); return new Entity(values); } @@ -5558,6 +5577,28 @@ public final class ContactsContract { "com.android.contacts.action.SHOW_OR_CREATE_CONTACT"; /** + * Starts an Activity that lets the user select the multiple phones from a + * list of phone numbers which come from the contacts or + * {@link #EXTRA_PHONE_URIS}. + * <p> + * The phone numbers being passed in through {@link #EXTRA_PHONE_URIS} + * could belong to the contacts or not, and will be selected by default. + * <p> + * The user's selection will be returned from + * {@link android.app.Activity#onActivityResult(int, int, android.content.Intent)} + * if the resultCode is + * {@link android.app.Activity#RESULT_OK}, the array of picked phone + * numbers are in the Intent's + * {@link #EXTRA_PHONE_URIS}; otherwise, the + * {@link android.app.Activity#RESULT_CANCELED} is returned if the user + * left the Activity without changing the selection. + * + * @hide + */ + public static final String ACTION_GET_MULTIPLE_PHONES = + "com.android.contacts.action.GET_MULTIPLE_PHONES"; + + /** * Used with {@link #SHOW_OR_CREATE_CONTACT} to force creating a new * contact if no matching contact found. Otherwise, default behavior is * to prompt user with dialog before creating. @@ -5578,6 +5619,23 @@ public final class ContactsContract { "com.android.contacts.action.CREATE_DESCRIPTION"; /** + * Used with {@link #ACTION_GET_MULTIPLE_PHONES} as the input or output value. + * <p> + * The phone numbers want to be picked by default should be passed in as + * input value. These phone numbers could belong to the contacts or not. + * <p> + * The phone numbers which were picked by the user are returned as output + * value. + * <p> + * Type: array of URIs, the tel URI is used for the phone numbers which don't + * belong to any contact, the content URI is used for phone id in contacts. + * + * @hide + */ + public static final String EXTRA_PHONE_URIS = + "com.android.contacts.extra.PHONE_URIS"; + + /** * Optional extra used with {@link #SHOW_OR_CREATE_CONTACT} to specify a * dialog location using screen coordinates. When not specified, the * dialog will be centered. diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index e8c09b0..460e9b4 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3532,20 +3532,8 @@ public final class Settings { // If a shortcut is supplied, and it is already defined for // another bookmark, then remove the old definition. if (shortcut != 0) { - Cursor c = cr.query(CONTENT_URI, - sShortcutProjection, sShortcutSelection, - new String[] { String.valueOf((int) shortcut) }, null); - try { - if (c.moveToFirst()) { - while (c.getCount() > 0) { - if (!c.deleteRow()) { - Log.w(TAG, "Could not delete existing shortcut row"); - } - } - } - } finally { - if (c != null) c.close(); - } + cr.delete(CONTENT_URI, sShortcutSelection, + new String[] { String.valueOf((int) shortcut) }); } ContentValues values = new ContentValues(); diff --git a/core/java/android/text/AndroidBidi.java b/core/java/android/text/AndroidBidi.java index e4f934e..eacd40d 100644 --- a/core/java/android/text/AndroidBidi.java +++ b/core/java/android/text/AndroidBidi.java @@ -16,6 +16,8 @@ package android.text; +import android.text.Layout.Directions; + /** * Access the ICU bidi implementation. * @hide @@ -44,5 +46,132 @@ package android.text; return result; } + /** + * Returns run direction information for a line within a paragraph. + * + * @param dir base line direction, either Layout.DIR_LEFT_TO_RIGHT or + * Layout.DIR_RIGHT_TO_LEFT + * @param levels levels as returned from {@link #bidi} + * @param lstart start of the line in the levels array + * @param chars the character array (used to determine whitespace) + * @param cstart the start of the line in the chars array + * @param len the length of the line + * @return the directions + */ + public static Directions directions(int dir, byte[] levels, int lstart, + char[] chars, int cstart, int len) { + + int baseLevel = dir == Layout.DIR_LEFT_TO_RIGHT ? 0 : 1; + int curLevel = levels[lstart]; + int minLevel = curLevel; + int runCount = 1; + for (int i = lstart + 1, e = lstart + len; i < e; ++i) { + int level = levels[i]; + if (level != curLevel) { + curLevel = level; + ++runCount; + } + } + + // add final run for trailing counter-directional whitespace + int visLen = len; + if ((curLevel & 1) != (baseLevel & 1)) { + // look for visible end + while (--visLen >= 0) { + char ch = chars[cstart + visLen]; + + if (ch == '\n') { + --visLen; + break; + } + + if (ch != ' ' && ch != '\t') { + break; + } + } + ++visLen; + if (visLen != len) { + ++runCount; + } + } + + if (runCount == 1 && minLevel == baseLevel) { + // we're done, only one run on this line + if ((minLevel & 1) != 0) { + return Layout.DIRS_ALL_RIGHT_TO_LEFT; + } + return Layout.DIRS_ALL_LEFT_TO_RIGHT; + } + + int[] ld = new int[runCount * 2]; + int maxLevel = minLevel; + int levelBits = minLevel << Layout.RUN_LEVEL_SHIFT; + { + // Start of first pair is always 0, we write + // length then start at each new run, and the + // last run length after we're done. + int n = 1; + int prev = lstart; + curLevel = minLevel; + for (int i = lstart, e = lstart + visLen; i < e; ++i) { + int level = levels[i]; + if (level != curLevel) { + curLevel = level; + if (level > maxLevel) { + maxLevel = level; + } else if (level < minLevel) { + minLevel = level; + } + // XXX ignore run length limit of 2^RUN_LEVEL_SHIFT + ld[n++] = (i - prev) | levelBits; + ld[n++] = i - lstart; + levelBits = curLevel << Layout.RUN_LEVEL_SHIFT; + prev = i; + } + } + ld[n] = (lstart + visLen - prev) | levelBits; + if (visLen < len) { + ld[++n] = visLen; + ld[++n] = (len - visLen) | (baseLevel << Layout.RUN_LEVEL_SHIFT); + } + } + + // See if we need to swap any runs. + // If the min level run direction doesn't match the base + // direction, we always need to swap (at this point + // we have more than one run). + // Otherwise, we don't need to swap the lowest level. + // Since there are no logically adjacent runs at the same + // level, if the max level is the same as the (new) min + // level, we have a series of alternating levels that + // is already in order, so there's no more to do. + // + boolean swap; + if ((minLevel & 1) == baseLevel) { + minLevel += 1; + swap = maxLevel > minLevel; + } else { + swap = runCount > 1; + } + if (swap) { + for (int level = maxLevel - 1; level >= minLevel; --level) { + for (int i = 0; i < ld.length; i += 2) { + if (levels[ld[i]] >= level) { + int e = i + 2; + while (e < ld.length && levels[ld[e]] >= level) { + e += 2; + } + for (int low = i, hi = e - 2; low < hi; low += 2, hi -= 2) { + int x = ld[low]; ld[low] = ld[hi]; ld[hi] = x; + x = ld[low+1]; ld[low+1] = ld[hi+1]; ld[hi+1] = x; + } + i = e + 2; + } + } + } + } + return new Directions(ld); + } + private native static int runBidi(int dir, char[] chs, byte[] chInfo, int n, boolean haveInfo); }
\ No newline at end of file diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java index 944f735..9309b05 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -208,11 +208,11 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback * width because the width that was passed in was for the * full text, not the ellipsized form. */ - synchronized (sTemp) { - mMax = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp, - source, 0, source.length(), - null))); - } + TextLine line = TextLine.obtain(); + line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT, + Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); + mMax = (int) FloatMath.ceil(line.metrics(null)); + TextLine.recycle(line); } if (includepad) { @@ -276,14 +276,13 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback if (fm == null) { fm = new Metrics(); } - - int wid; - synchronized (sTemp) { - wid = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp, - text, 0, text.length(), fm))); - } - fm.width = wid; + TextLine line = TextLine.obtain(); + line.set(paint, text, 0, text.length(), Layout.DIR_LEFT_TO_RIGHT, + Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); + fm.width = (int) FloatMath.ceil(line.metrics(fm)); + TextLine.recycle(line); + return fm; } else { return null; @@ -389,7 +388,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback public static class Metrics extends Paint.FontMetricsInt { public int width; - + @Override public String toString() { return super.toString() + " width=" + width; } diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index 14e5655..b6aa03a 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -310,7 +310,6 @@ extends Layout Directions[] objects = new Directions[1]; - for (int i = 0; i < n; i++) { ints[START] = reflowed.getLineStart(i) | (reflowed.getParagraphDirection(i) << DIR_SHIFT) | diff --git a/core/java/android/text/GraphicsOperations.java b/core/java/android/text/GraphicsOperations.java index c3bd0ae..05697c6 100644 --- a/core/java/android/text/GraphicsOperations.java +++ b/core/java/android/text/GraphicsOperations.java @@ -34,6 +34,13 @@ extends CharSequence float x, float y, Paint p); /** + * Just like {@link Canvas#drawTextRun}. + * {@hide} + */ + void drawTextRun(Canvas c, int start, int end, + float x, float y, int flags, Paint p); + + /** * Just like {@link Paint#measureText}. */ float measureText(int start, int end, Paint p); diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 38ac9b7..3b8f295 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -16,25 +16,29 @@ package android.text; +import com.android.internal.util.ArrayUtils; + import android.emoji.EmojiFactory; -import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.RectF; import android.graphics.Path; -import com.android.internal.util.ArrayUtils; - -import junit.framework.Assert; -import android.text.style.*; +import android.graphics.Rect; import android.text.method.TextKeyListener; +import android.text.style.AlignmentSpan; +import android.text.style.LeadingMarginSpan; +import android.text.style.LineBackgroundSpan; +import android.text.style.ParagraphStyle; +import android.text.style.ReplacementSpan; +import android.text.style.TabStopSpan; import android.view.KeyEvent; +import junit.framework.Assert; + /** - * A base class that manages text layout in visual elements on - * the screen. - * <p>For text that will be edited, use a {@link DynamicLayout}, - * which will be updated as the text changes. + * A base class that manages text layout in visual elements on + * the screen. + * <p>For text that will be edited, use a {@link DynamicLayout}, + * which will be updated as the text changes. * For text that will not change, use a {@link StaticLayout}. */ public abstract class Layout { @@ -54,9 +58,7 @@ public abstract class Layout { MIN_EMOJI = -1; MAX_EMOJI = -1; } - }; - - private RectF mEmojiRect; + } /** * Return how wide a layout must be in order to display the @@ -66,7 +68,7 @@ public abstract class Layout { TextPaint paint) { return getDesiredWidth(source, 0, source.length(), paint); } - + /** * Return how wide a layout must be in order to display the * specified text slice with one line per paragraph. @@ -85,8 +87,8 @@ public abstract class Layout { next = end; // note, omits trailing paragraph char - float w = measureText(paint, workPaint, - source, i, next, null, true, null); + float w = measurePara(paint, workPaint, + source, i, next, true, null); if (w > need) need = w; @@ -116,6 +118,15 @@ public abstract class Layout { if (width < 0) throw new IllegalArgumentException("Layout: " + width + " < 0"); + // Ensure paint doesn't have baselineShift set. + // While normally we don't modify the paint the user passed in, + // we were already doing this in Styled.drawUniformRun with both + // baselineShift and bgColor. We probably should reevaluate bgColor. + if (paint != null) { + paint.bgColor = 0; + paint.baselineShift = 0; + } + mText = text; mPaint = paint; mWorkPaint = new TextPaint(); @@ -185,13 +196,13 @@ public abstract class Layout { if (dbottom < bottom) { bottom = dbottom; } - - int first = getLineForVertical(top); + + int first = getLineForVertical(top); int last = getLineForVertical(bottom); - + int previousLineBottom = getLineTop(first); int previousLineEnd = getLineStart(first); - + TextPaint paint = mPaint; CharSequence buf = mText; int width = mWidth; @@ -238,7 +249,7 @@ public abstract class Layout { previousLineBottom = getLineTop(first); previousLineEnd = getLineStart(first); spans = NO_PARA_SPANS; - } + } // There can be a highlight even without spans if we are drawing // a non-spanned transformation of a spanned editing buffer. @@ -255,7 +266,8 @@ public abstract class Layout { } Alignment align = mAlignment; - + + TextLine tl = TextLine.obtain(); // Next draw the lines, one at a time. // the baseline is the top of the following line minus the current // line's descent. @@ -271,7 +283,7 @@ public abstract class Layout { int lbaseline = lbottom - getLineDescent(i); boolean isFirstParaLine = false; - if (spannedText) { + if (spannedText) { if (start == 0 || buf.charAt(start - 1) == '\n') { isFirstParaLine = true; } @@ -282,7 +294,7 @@ public abstract class Layout { spanend = sp.nextSpanTransition(start, textLength, ParagraphStyle.class); spans = sp.getSpans(start, spanend, ParagraphStyle.class); - + align = mAlignment; for (int n = spans.length-1; n >= 0; n--) { if (spans[n] instanceof AlignmentSpan) { @@ -292,7 +304,7 @@ public abstract class Layout { } } } - + int dir = getParagraphDirection(i); int left = 0; int right = mWidth; @@ -309,7 +321,7 @@ public abstract class Layout { margin.drawLeadingMargin(c, paint, right, dir, ltop, lbaseline, lbottom, buf, start, end, isFirstParaLine, this); - + right -= margin.getLeadingMargin(isFirstParaLine); } else { margin.drawLeadingMargin(c, paint, left, dir, ltop, @@ -367,11 +379,11 @@ public abstract class Layout { // XXX: assumes there's nothing additional to be done c.drawText(buf, start, end, x, lbaseline, paint); } else { - drawText(c, buf, start, end, dir, directions, - x, ltop, lbaseline, lbottom, paint, mWorkPaint, - hasTab, spans); + tl.set(paint, buf, start, end, dir, directions, hasTab, spans); + tl.draw(c, x, ltop, lbaseline, lbottom); } } + TextLine.recycle(tl); } /** @@ -417,7 +429,7 @@ public abstract class Layout { mWidth = wid; } - + /** * Return the total height of this layout. */ @@ -450,7 +462,7 @@ public abstract class Layout { * Return the number of lines of text in this layout. */ public abstract int getLineCount(); - + /** * Return the baseline for the specified line (0…getLineCount() - 1) * If bounds is not null, return the top, left, right, bottom extents @@ -524,13 +536,95 @@ public abstract class Layout { */ public abstract int getBottomPadding(); + + /** + * Returns true if the character at offset and the preceding character + * are at different run levels (and thus there's a split caret). + * @param offset the offset + * @return true if at a level boundary + */ + private boolean isLevelBoundary(int offset) { + int line = getLineForOffset(offset); + Directions dirs = getLineDirections(line); + if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) { + return false; + } + + int[] runs = dirs.mDirections; + int lineStart = getLineStart(line); + int lineEnd = getLineEnd(line); + if (offset == lineStart || offset == lineEnd) { + int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1; + int runIndex = offset == lineStart ? 0 : runs.length - 2; + return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel; + } + + offset -= lineStart; + for (int i = 0; i < runs.length; i += 2) { + if (offset == runs[i]) { + return true; + } + } + return false; + } + + private boolean primaryIsTrailingPrevious(int offset) { + int line = getLineForOffset(offset); + int lineStart = getLineStart(line); + int lineEnd = getLineEnd(line); + int[] runs = getLineDirections(line).mDirections; + + int levelAt = -1; + for (int i = 0; i < runs.length; i += 2) { + int start = lineStart + runs[i]; + int limit = start + (runs[i+1] & RUN_LENGTH_MASK); + if (limit > lineEnd) { + limit = lineEnd; + } + if (offset >= start && offset < limit) { + if (offset > start) { + // Previous character is at same level, so don't use trailing. + return false; + } + levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; + break; + } + } + if (levelAt == -1) { + // Offset was limit of line. + levelAt = getParagraphDirection(line) == 1 ? 0 : 1; + } + + // At level boundary, check previous level. + int levelBefore = -1; + if (offset == lineStart) { + levelBefore = getParagraphDirection(line) == 1 ? 0 : 1; + } else { + offset -= 1; + for (int i = 0; i < runs.length; i += 2) { + int start = lineStart + runs[i]; + int limit = start + (runs[i+1] & RUN_LENGTH_MASK); + if (limit > lineEnd) { + limit = lineEnd; + } + if (offset >= start && offset < limit) { + levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; + break; + } + } + } + + return levelBefore < levelAt; + } + /** * Get the primary horizontal position for the specified text offset. * This is the location where a new character would be inserted in * the paragraph's primary direction. */ public float getPrimaryHorizontal(int offset) { - return getHorizontal(offset, false, true); + boolean trailing = primaryIsTrailingPrevious(offset); + return getHorizontal(offset, trailing); } /** @@ -539,19 +633,19 @@ public abstract class Layout { * the direction other than the paragraph's primary direction. */ public float getSecondaryHorizontal(int offset) { - return getHorizontal(offset, true, true); + boolean trailing = primaryIsTrailingPrevious(offset); + return getHorizontal(offset, !trailing); } - private float getHorizontal(int offset, boolean trailing, boolean alt) { + private float getHorizontal(int offset, boolean trailing) { int line = getLineForOffset(offset); - return getHorizontal(offset, trailing, alt, line); + return getHorizontal(offset, trailing, line); } - private float getHorizontal(int offset, boolean trailing, boolean alt, - int line) { + private float getHorizontal(int offset, boolean trailing, int line) { int start = getLineStart(line); - int end = getLineVisibleEnd(line); + int end = getLineEnd(line); int dir = getParagraphDirection(line); boolean tab = getLineContainsTab(line); Directions directions = getLineDirections(line); @@ -561,17 +655,10 @@ public abstract class Layout { tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); } - float wid = measureText(mPaint, mWorkPaint, mText, start, offset, end, - dir, directions, trailing, alt, tab, tabs); - - if (offset > end) { - if (dir == DIR_RIGHT_TO_LEFT) - wid -= measureText(mPaint, mWorkPaint, - mText, end, offset, null, tab, tabs); - else - wid += measureText(mPaint, mWorkPaint, - mText, end, offset, null, tab, tabs); - } + TextLine tl = TextLine.obtain(); + tl.set(mPaint, mText, start, end, dir, directions, tab, tabs); + float wid = tl.measure(offset - start, trailing, null); + TextLine.recycle(tl); Alignment align = getParagraphAlignment(line); int left = getParagraphLeft(line); @@ -673,21 +760,15 @@ public abstract class Layout { private float getLineMax(int line, Object[] tabs, boolean full) { int start = getLineStart(line); - int end; - - if (full) { - end = getLineEnd(line); - } else { - end = getLineVisibleEnd(line); - } - boolean tab = getLineContainsTab(line); - - if (tabs == null && tab && mText instanceof Spanned) { - tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); - } + int end = full ? getLineEnd(line) : getLineVisibleEnd(line); + boolean hasTabs = getLineContainsTab(line); + Directions directions = getLineDirections(line); - return measureText(mPaint, mWorkPaint, - mText, start, end, null, tab, tabs); + TextLine tl = TextLine.obtain(); + tl.set(mPaint, mText, start, end, 1, directions, hasTabs, tabs); + float width = tl.metrics(null); + TextLine.recycle(tl); + return width; } /** @@ -738,7 +819,7 @@ public abstract class Layout { } /** - * Get the character offset on the specfied line whose position is + * Get the character offset on the specified line whose position is * closest to the specified horizontal position. */ public int getOffsetForHorizontal(int line, float horiz) { @@ -752,14 +833,13 @@ public abstract class Layout { int best = min; float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz); - int here = min; - for (int i = 0; i < dirs.mDirections.length; i++) { - int there = here + dirs.mDirections[i]; - int swap = ((i & 1) == 0) ? 1 : -1; + for (int i = 0; i < dirs.mDirections.length; i += 2) { + int here = min + dirs.mDirections[i]; + int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); + int swap = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0 ? -1 : 1; if (there > max) there = max; - int high = there - 1 + 1, low = here + 1 - 1, guess; while (high - low > 1) { @@ -792,7 +872,7 @@ public abstract class Layout { if (dist < bestdist) { bestdist = dist; - best = low; + best = low; } } @@ -802,8 +882,6 @@ public abstract class Layout { bestdist = dist; best = here; } - - here = there; } float dist = Math.abs(getPrimaryHorizontal(max) - horiz); @@ -823,14 +901,14 @@ public abstract class Layout { return getLineStart(line + 1); } - /** + /** * Return the text offset after the last visible character (so whitespace * is not counted) on the specified line. */ public int getLineVisibleEnd(int line) { return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); } - + private int getLineVisibleEnd(int line, int start, int end) { if (DEBUG) { Assert.assertTrue(getLineStart(line) == start && getLineStart(line+1) == end); @@ -882,207 +960,62 @@ public abstract class Layout { return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); } - /** - * Return the text offset that would be reached by moving left - * (possibly onto another line) from the specified offset. - */ public int getOffsetToLeftOf(int offset) { - int line = getLineForOffset(offset); - int start = getLineStart(line); - int end = getLineEnd(line); - Directions dirs = getLineDirections(line); - - if (line != getLineCount() - 1) - end--; - - float horiz = getPrimaryHorizontal(offset); - - int best = offset; - float besth = Integer.MIN_VALUE; - int candidate; - - candidate = TextUtils.getOffsetBefore(mText, offset); - if (candidate >= start && candidate <= end) { - float h = getPrimaryHorizontal(candidate); - - if (h < horiz && h > besth) { - best = candidate; - besth = h; - } - } - - candidate = TextUtils.getOffsetAfter(mText, offset); - if (candidate >= start && candidate <= end) { - float h = getPrimaryHorizontal(candidate); - - if (h < horiz && h > besth) { - best = candidate; - besth = h; - } - } - - int here = start; - for (int i = 0; i < dirs.mDirections.length; i++) { - int there = here + dirs.mDirections[i]; - if (there > end) - there = end; - - float h = getPrimaryHorizontal(here); - - if (h < horiz && h > besth) { - best = here; - besth = h; - } - - candidate = TextUtils.getOffsetAfter(mText, here); - if (candidate >= start && candidate <= end) { - h = getPrimaryHorizontal(candidate); - - if (h < horiz && h > besth) { - best = candidate; - besth = h; - } - } - - candidate = TextUtils.getOffsetBefore(mText, there); - if (candidate >= start && candidate <= end) { - h = getPrimaryHorizontal(candidate); - - if (h < horiz && h > besth) { - best = candidate; - besth = h; - } - } - - here = there; - } - - float h = getPrimaryHorizontal(end); - - if (h < horiz && h > besth) { - best = end; - besth = h; - } - - if (best != offset) - return best; - - int dir = getParagraphDirection(line); - - if (dir > 0) { - if (line == 0) - return best; - else - return getOffsetForHorizontal(line - 1, 10000); - } else { - if (line == getLineCount() - 1) - return best; - else - return getOffsetForHorizontal(line + 1, 10000); - } + return getOffsetToLeftRightOf(offset, true); } - /** - * Return the text offset that would be reached by moving right - * (possibly onto another line) from the specified offset. - */ public int getOffsetToRightOf(int offset) { - int line = getLineForOffset(offset); - int start = getLineStart(line); - int end = getLineEnd(line); - Directions dirs = getLineDirections(line); - - if (line != getLineCount() - 1) - end--; - - float horiz = getPrimaryHorizontal(offset); - - int best = offset; - float besth = Integer.MAX_VALUE; - int candidate; - - candidate = TextUtils.getOffsetBefore(mText, offset); - if (candidate >= start && candidate <= end) { - float h = getPrimaryHorizontal(candidate); - - if (h > horiz && h < besth) { - best = candidate; - besth = h; - } - } - - candidate = TextUtils.getOffsetAfter(mText, offset); - if (candidate >= start && candidate <= end) { - float h = getPrimaryHorizontal(candidate); - - if (h > horiz && h < besth) { - best = candidate; - besth = h; - } - } - - int here = start; - for (int i = 0; i < dirs.mDirections.length; i++) { - int there = here + dirs.mDirections[i]; - if (there > end) - there = end; - - float h = getPrimaryHorizontal(here); - - if (h > horiz && h < besth) { - best = here; - besth = h; - } - - candidate = TextUtils.getOffsetAfter(mText, here); - if (candidate >= start && candidate <= end) { - h = getPrimaryHorizontal(candidate); + return getOffsetToLeftRightOf(offset, false); + } - if (h > horiz && h < besth) { - best = candidate; - besth = h; + private int getOffsetToLeftRightOf(int caret, boolean toLeft) { + int line = getLineForOffset(caret); + int lineStart = getLineStart(line); + int lineEnd = getLineEnd(line); + int lineDir = getParagraphDirection(line); + + boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT); + if (caret == (advance ? lineEnd : lineStart)) { + // walking off line, so look at the line we're headed to + if (caret == lineStart) { + if (line > 0) { + --line; + } else { + return caret; // at very start, don't move } - } - - candidate = TextUtils.getOffsetBefore(mText, there); - if (candidate >= start && candidate <= end) { - h = getPrimaryHorizontal(candidate); - - if (h > horiz && h < besth) { - best = candidate; - besth = h; + } else { + if (line < getLineCount() - 1) { + ++line; + } else { + return caret; // at very end, don't move } } - here = there; - } - - float h = getPrimaryHorizontal(end); - - if (h > horiz && h < besth) { - best = end; - besth = h; + lineStart = getLineStart(line); + lineEnd = getLineEnd(line); + int newDir = getParagraphDirection(line); + if (newDir != lineDir) { + // unusual case. we want to walk onto the line, but it runs + // in a different direction than this one, so we fake movement + // in the opposite direction. + toLeft = !toLeft; + lineDir = newDir; + } } - if (best != offset) - return best; - - int dir = getParagraphDirection(line); + Directions directions = getLineDirections(line); - if (dir > 0) { - if (line == getLineCount() - 1) - return best; - else - return getOffsetForHorizontal(line + 1, -10000); - } else { - if (line == 0) - return best; - else - return getOffsetForHorizontal(line - 1, -10000); - } + TextLine tl = TextLine.obtain(); + // XXX: we don't care about tabs + tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null); + caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft); + tl = TextLine.recycle(tl); + return caret; } private int getOffsetAtStartOf(int offset) { + // XXX this probably should skip local reorderings and + // zero-width characters, look at callers if (offset == 0) return 0; @@ -1115,7 +1048,7 @@ public abstract class Layout { /** * Fills in the specified Path with a representation of a cursor * at the specified offset. This will often be a vertical line - * but can be multiple discontinous lines in text with multiple + * but can be multiple discontinuous lines in text with multiple * directionalities. */ public void getCursorPath(int point, Path dest, @@ -1127,7 +1060,8 @@ public abstract class Layout { int bottom = getLineTop(line+1); float h1 = getPrimaryHorizontal(point) - 0.5f; - float h2 = getSecondaryHorizontal(point) - 0.5f; + float h2 = isLevelBoundary(point) ? + getSecondaryHorizontal(point) - 0.5f : h1; int caps = TextKeyListener.getMetaState(editingBuffer, KeyEvent.META_SHIFT_ON) | @@ -1204,9 +1138,10 @@ public abstract class Layout { if (lineend > linestart && mText.charAt(lineend - 1) == '\n') lineend--; - int here = linestart; - for (int i = 0; i < dirs.mDirections.length; i++) { - int there = here + dirs.mDirections[i]; + for (int i = 0; i < dirs.mDirections.length; i += 2) { + int here = linestart + dirs.mDirections[i]; + int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); + if (there > lineend) there = lineend; @@ -1215,14 +1150,12 @@ public abstract class Layout { int en = Math.min(end, there); if (st != en) { - float h1 = getHorizontal(st, false, false, line); - float h2 = getHorizontal(en, true, false, line); + float h1 = getHorizontal(st, false, line); + float h2 = getHorizontal(en, true, line); dest.addRect(h1, top, h2, bottom, Path.Direction.CW); } } - - here = there; } } @@ -1257,7 +1190,7 @@ public abstract class Layout { addSelection(startline, start, getLineEnd(startline), top, getLineBottom(startline), dest); - + if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) dest.addRect(getLineLeft(startline), top, 0, getLineBottom(startline), Path.Direction.CW); @@ -1371,361 +1304,28 @@ public abstract class Layout { return right; } - private void drawText(Canvas canvas, - CharSequence text, int start, int end, - int dir, Directions directions, - float x, int top, int y, int bottom, - TextPaint paint, - TextPaint workPaint, - boolean hasTabs, Object[] parspans) { - char[] buf; - if (!hasTabs) { - if (directions == DIRS_ALL_LEFT_TO_RIGHT) { - if (DEBUG) { - Assert.assertTrue(DIR_LEFT_TO_RIGHT == dir); - } - Styled.drawText(canvas, text, start, end, dir, false, x, top, y, bottom, paint, workPaint, false); - return; - } - buf = null; - } else { - buf = TextUtils.obtain(end - start); - TextUtils.getChars(text, start, end, buf, 0); - } - - float h = 0; - - int here = 0; - for (int i = 0; i < directions.mDirections.length; i++) { - int there = here + directions.mDirections[i]; - if (there > end - start) - there = end - start; - - int segstart = here; - for (int j = hasTabs ? here : there; j <= there; j++) { - if (j == there || buf[j] == '\t') { - h += Styled.drawText(canvas, text, - start + segstart, start + j, - dir, (i & 1) != 0, x + h, - top, y, bottom, paint, workPaint, - start + j != end); - - if (j != there && buf[j] == '\t') - h = dir * nextTab(text, start, end, h * dir, parspans); - - segstart = j + 1; - } else if (hasTabs && buf[j] >= 0xD800 && buf[j] <= 0xDFFF && j + 1 < there) { - int emoji = Character.codePointAt(buf, j); - - if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) { - Bitmap bm = EMOJI_FACTORY. - getBitmapFromAndroidPua(emoji); - - if (bm != null) { - h += Styled.drawText(canvas, text, - start + segstart, start + j, - dir, (i & 1) != 0, x + h, - top, y, bottom, paint, workPaint, - start + j != end); - - if (mEmojiRect == null) { - mEmojiRect = new RectF(); - } - - workPaint.set(paint); - Styled.measureText(paint, workPaint, text, - start + j, start + j + 1, - null); - - float bitmapHeight = bm.getHeight(); - float textHeight = -workPaint.ascent(); - float scale = textHeight / bitmapHeight; - float width = bm.getWidth() * scale; - - mEmojiRect.set(x + h, y - textHeight, - x + h + width, y); - - canvas.drawBitmap(bm, null, mEmojiRect, paint); - h += width; - - j++; - segstart = j + 1; - } - } - } - } - - here = there; - } - - if (hasTabs) - TextUtils.recycle(buf); - } - - private static float measureText(TextPaint paint, - TextPaint workPaint, - CharSequence text, - int start, int offset, int end, - int dir, Directions directions, - boolean trailing, boolean alt, - boolean hasTabs, Object[] tabs) { - char[] buf = null; - - if (hasTabs) { - buf = TextUtils.obtain(end - start); - TextUtils.getChars(text, start, end, buf, 0); - } - - float h = 0; - - if (alt) { - if (dir == DIR_RIGHT_TO_LEFT) - trailing = !trailing; - } - - int here = 0; - for (int i = 0; i < directions.mDirections.length; i++) { - if (alt) - trailing = !trailing; - - int there = here + directions.mDirections[i]; - if (there > end - start) - there = end - start; - - int segstart = here; - for (int j = hasTabs ? here : there; j <= there; j++) { - int codept = 0; - Bitmap bm = null; - - if (hasTabs && j < there) { - codept = buf[j]; - } - - if (codept >= 0xD800 && codept <= 0xDFFF && j + 1 < there) { - codept = Character.codePointAt(buf, j); - - if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) { - bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept); - } - } - - if (j == there || codept == '\t' || bm != null) { - float segw; - - if (offset < start + j || - (trailing && offset <= start + j)) { - if (dir == DIR_LEFT_TO_RIGHT && (i & 1) == 0) { - h += Styled.measureText(paint, workPaint, text, - start + segstart, offset, - null); - return h; - } - - if (dir == DIR_RIGHT_TO_LEFT && (i & 1) != 0) { - h -= Styled.measureText(paint, workPaint, text, - start + segstart, offset, - null); - return h; - } - } - - segw = Styled.measureText(paint, workPaint, text, - start + segstart, start + j, - null); - - if (offset < start + j || - (trailing && offset <= start + j)) { - if (dir == DIR_LEFT_TO_RIGHT) { - h += segw - Styled.measureText(paint, workPaint, - text, - start + segstart, - offset, null); - return h; - } - - if (dir == DIR_RIGHT_TO_LEFT) { - h -= segw - Styled.measureText(paint, workPaint, - text, - start + segstart, - offset, null); - return h; - } - } - - if (dir == DIR_RIGHT_TO_LEFT) - h -= segw; - else - h += segw; - - if (j != there && buf[j] == '\t') { - if (offset == start + j) - return h; - - h = dir * nextTab(text, start, end, h * dir, tabs); - } - - if (bm != null) { - workPaint.set(paint); - Styled.measureText(paint, workPaint, text, - j, j + 2, null); - - float wid = (float) bm.getWidth() * - -workPaint.ascent() / bm.getHeight(); - - if (dir == DIR_RIGHT_TO_LEFT) { - h -= wid; - } else { - h += wid; - } - - j++; - } - - segstart = j + 1; - } - } - - here = there; - } - - if (hasTabs) - TextUtils.recycle(buf); - - return h; - } - - /** - * Measure width of a run of text on a single line that is known to all be - * in the same direction as the paragraph base direction. Returns the width, - * and the line metrics in fm if fm is not null. - * - * @param paint the paint for the text; will not be modified - * @param workPaint paint available for modification - * @param text text - * @param start start of the line - * @param end limit of the line - * @param fm object to return integer metrics in, can be null - * @param hasTabs true if it is known that the line has tabs - * @param tabs tab position information - * @return the width of the text from start to end - */ - /* package */ static float measureText(TextPaint paint, - TextPaint workPaint, - CharSequence text, - int start, int end, - Paint.FontMetricsInt fm, - boolean hasTabs, Object[] tabs) { - char[] buf = null; - - if (hasTabs) { - buf = TextUtils.obtain(end - start); - TextUtils.getChars(text, start, end, buf, 0); - } - - int len = end - start; - - int lastPos = 0; - float width = 0; - int ascent = 0, descent = 0, top = 0, bottom = 0; - - if (fm != null) { - fm.ascent = 0; - fm.descent = 0; - } - - for (int pos = hasTabs ? 0 : len; pos <= len; pos++) { - int codept = 0; - Bitmap bm = null; - - if (hasTabs && pos < len) { - codept = buf[pos]; - } - - if (codept >= 0xD800 && codept <= 0xDFFF && pos < len) { - codept = Character.codePointAt(buf, pos); - - if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) { - bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept); - } - } - - if (pos == len || codept == '\t' || bm != null) { - workPaint.baselineShift = 0; - - width += Styled.measureText(paint, workPaint, text, - start + lastPos, start + pos, - fm); - - if (fm != null) { - if (workPaint.baselineShift < 0) { - fm.ascent += workPaint.baselineShift; - fm.top += workPaint.baselineShift; - } else { - fm.descent += workPaint.baselineShift; - fm.bottom += workPaint.baselineShift; - } - } - - if (pos != len) { - if (bm == null) { - // no emoji, must have hit a tab - width = nextTab(text, start, end, width, tabs); - } else { - // This sets up workPaint with the font on the emoji - // text, so that we can extract the ascent and scale. - - // We can't use the result of the previous call to - // measureText because the emoji might have its own style. - // We have to initialize workPaint here because if the - // text is unstyled measureText might not use workPaint - // at all. - workPaint.set(paint); - Styled.measureText(paint, workPaint, text, - start + pos, start + pos + 1, null); - - width += (float) bm.getWidth() * - -workPaint.ascent() / bm.getHeight(); - - // Since we had an emoji, we bump past the second half - // of the surrogate pair. - pos++; - } - } - - if (fm != null) { - if (fm.ascent < ascent) { - ascent = fm.ascent; - } - if (fm.descent > descent) { - descent = fm.descent; - } - - if (fm.top < top) { - top = fm.top; - } - if (fm.bottom > bottom) { - bottom = fm.bottom; - } - - // No need to take bitmap height into account here, - // since it is scaled to match the text height. - } - - lastPos = pos + 1; + /* package */ + static float measurePara(TextPaint paint, TextPaint workPaint, + CharSequence text, int start, int end, boolean hasTabs, + Object[] tabs) { + + MeasuredText mt = MeasuredText.obtain(); + TextLine tl = TextLine.obtain(); + try { + mt.setPara(text, start, end, DIR_REQUEST_LTR); + Directions directions; + if (mt.mEasy){ + directions = DIRS_ALL_LEFT_TO_RIGHT; + } else { + directions = AndroidBidi.directions(mt.mDir, mt.mLevels, + 0, mt.mChars, 0, mt.mLen); } + tl.set(paint, text, start, end, 1, directions, hasTabs, tabs); + return tl.metrics(null); + } finally { + TextLine.recycle(tl); + MeasuredText.recycle(mt); } - - if (fm != null) { - fm.ascent = ascent; - fm.descent = descent; - fm.top = top; - fm.bottom = bottom; - } - - if (hasTabs) - TextUtils.recycle(buf); - - return width; } /** @@ -1804,23 +1404,22 @@ public abstract class Layout { /** * Stores information about bidirectional (left-to-right or right-to-left) - * text within the layout of a line. TODO: This work is not complete - * or correct and will be fleshed out in a later revision. + * text within the layout of a line. */ public static class Directions { - private short[] mDirections; - - // The values in mDirections are the offsets from the first character - // in the line to the next flip in direction. Runs at even indices - // are left-to-right, the others are right-to-left. So, for example, - // a line that starts with a right-to-left run has 0 at mDirections[0], - // since the 'first' (ltr) run is zero length. - // - // The code currently assumes that each run is adjacent to the previous - // one, progressing in the base line direction. This isn't sufficient - // to handle nested runs, for example numeric text in an rtl context - // in an ltr paragraph. - /* package */ Directions(short[] dirs) { + // Directions represents directional runs within a line of text. + // Runs are pairs of ints listed in visual order, starting from the + // leading margin. The first int of each pair is the offset from + // the first character of the line to the start of the run. The + // second int represents both the length and level of the run. + // The length is in the lower bits, accessed by masking with + // DIR_LENGTH_MASK. The level is in the higher bits, accessed + // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK. + // To simply test for an RTL direction, test the bit using + // DIR_RTL_FLAG, if set then the direction is rtl. + + /* package */ int[] mDirections; + /* package */ Directions(int[] dirs) { mDirections = dirs; } } @@ -1831,6 +1430,7 @@ public abstract class Layout { * line is ellipsized, not getLineStart().) */ public abstract int getEllipsisStart(int line); + /** * Returns the number of characters to be ellipsized away, or 0 if * no ellipsis is to take place. @@ -1870,7 +1470,7 @@ public abstract class Layout { public int length() { return mText.length(); } - + public CharSequence subSequence(int start, int end) { char[] s = new char[end - start]; getChars(start, end, s, 0); @@ -1936,12 +1536,17 @@ public abstract class Layout { public static final int DIR_LEFT_TO_RIGHT = 1; public static final int DIR_RIGHT_TO_LEFT = -1; - + /* package */ static final int DIR_REQUEST_LTR = 1; /* package */ static final int DIR_REQUEST_RTL = -1; /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2; /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2; + /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff; + /* package */ static final int RUN_LEVEL_SHIFT = 26; + /* package */ static final int RUN_LEVEL_MASK = 0x3f; + /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT; + public enum Alignment { ALIGN_NORMAL, ALIGN_OPPOSITE, @@ -1953,9 +1558,8 @@ public abstract class Layout { private static final int TAB_INCREMENT = 20; /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT = - new Directions(new short[] { 32767 }); + new Directions(new int[] { 0, RUN_LENGTH_MASK }); /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT = - new Directions(new short[] { 0, 32767 }); - + new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG }); } diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java new file mode 100644 index 0000000..e3a113d --- /dev/null +++ b/core/java/android/text/MeasuredText.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import com.android.internal.util.ArrayUtils; + +import android.graphics.Paint; +import android.icu.text.ArabicShaping; +import android.text.style.MetricAffectingSpan; +import android.text.style.ReplacementSpan; +import android.util.Log; + +/** + * @hide + */ +class MeasuredText { + /* package */ CharSequence mText; + /* package */ int mTextStart; + /* package */ float[] mWidths; + /* package */ char[] mChars; + /* package */ byte[] mLevels; + /* package */ int mDir; + /* package */ boolean mEasy; + /* package */ int mLen; + private int mPos; + private float[] mWorkWidths; // temp buffer for Paint.measureText, arrgh + private TextPaint mWorkPaint; + + private MeasuredText() { + mWorkPaint = new TextPaint(); + } + + private static MeasuredText[] cached = new MeasuredText[3]; + + /* package */ + static MeasuredText obtain() { + MeasuredText mt; + synchronized (cached) { + for (int i = cached.length; --i >= 0;) { + if (cached[i] != null) { + mt = cached[i]; + cached[i] = null; + return mt; + } + } + } + mt = new MeasuredText(); + Log.e("MEAS", "new: " + mt); + return mt; + } + + /* package */ + static MeasuredText recycle(MeasuredText mt) { + mt.mText = null; + if (mt.mLen < 1000) { + synchronized(cached) { + for (int i = 0; i < cached.length; ++i) { + if (cached[i] == null) { + cached[i] = mt; + break; + } + } + } + } + return null; + } + + /** + * Analyzes text for + * bidirectional runs. Allocates working buffers. + */ + /* package */ + void setPara(CharSequence text, int start, int end, int bidiRequest) { + mText = text; + mTextStart = start; + + int len = end - start; + mLen = len; + mPos = 0; + + if (mWidths == null || mWidths.length < len) { + mWidths = new float[ArrayUtils.idealFloatArraySize(len)]; + mWorkWidths = new float[mWidths.length]; + } + if (mChars == null || mChars.length < len) { + mChars = new char[ArrayUtils.idealCharArraySize(len)]; + } + TextUtils.getChars(text, start, end, mChars, 0); + + if (text instanceof Spanned) { + Spanned spanned = (Spanned) text; + ReplacementSpan[] spans = spanned.getSpans(start, end, + ReplacementSpan.class); + + for (int i = 0; i < spans.length; i++) { + int startInPara = spanned.getSpanStart(spans[i]) - start; + int endInPara = spanned.getSpanEnd(spans[i]) - start; + for (int j = startInPara; j < endInPara; j++) { + mChars[j] = '\uFFFC'; + } + } + } + + if (TextUtils.doesNotNeedBidi(mChars, 0, len)) { + mDir = 1; + mEasy = true; + } else { + if (mLevels == null || mLevels.length < len) { + mLevels = new byte[ArrayUtils.idealByteArraySize(len)]; + } + mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels, len, false); + mEasy = false; + + // shape + if (mLen > 0) { + byte[] levels = mLevels; + char[] chars = mChars; + byte level = levels[0]; + int pi = 0; + for (int i = 1, e = mLen;; ++i) { + if (i == e || levels[i] != level) { + if ((level & 0x1) != 0) { + AndroidCharacter.mirror(chars, pi, i - pi); + ArabicShaping.SHAPER.shape(chars, pi, i - pi); + } + if (i == e) { + break; + } + pi = i; + level = levels[i]; + } + } + } + } + } + + float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) { + int p = mPos; + float[] w = mWidths, ww = mWorkWidths; + int count = paint.getTextWidths(mChars, p, len, ww); + int width = 0; + if (count < len) { + // must have surrogate pairs in here, pad out the array with zero + // for the trailing surrogates + char[] chars = mChars; + for (int i = 0, e = mLen; i < count; ++i) { + width += (w[p++] = ww[i]); + if (p < e && chars[p] >= '\udc00' && chars[p] < '\ue000' && + chars[p-1] >= '\ud800' && chars[p-1] < '\udc00') { + w[p++] = 0; + } + } + } else { + for (int i = 0; i < len; ++i) { + width += (w[p++] = ww[i]); + } + } + mPos = p; + if (fm != null) { + paint.getFontMetricsInt(fm); + } + return width; + } + + float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len, + Paint.FontMetricsInt fm) { + + TextPaint workPaint = mWorkPaint; + workPaint.set(paint); + // XXX paint should not have a baseline shift, but... + workPaint.baselineShift = 0; + + ReplacementSpan replacement = null; + for (int i = 0; i < spans.length; i++) { + MetricAffectingSpan span = spans[i]; + if (span instanceof ReplacementSpan) { + replacement = (ReplacementSpan)span; + } else { + span.updateMeasureState(workPaint); + } + } + + float wid; + if (replacement == null) { + wid = addStyleRun(workPaint, len, fm); + } else { + // Use original text. Shouldn't matter. + wid = replacement.getSize(workPaint, mText, mTextStart + mPos, + mTextStart + mPos + len, fm); + float[] w = mWidths; + w[mPos] = wid; + for (int i = mPos + 1, e = mPos + len; i < e; i++) + w[i] = 0; + } + + if (fm != null) { + if (workPaint.baselineShift < 0) { + fm.ascent += workPaint.baselineShift; + fm.top += workPaint.baselineShift; + } else { + fm.descent += workPaint.baselineShift; + fm.bottom += workPaint.baselineShift; + } + } + + return wid; + } + + int breakText(int start, int limit, boolean forwards, float width) { + float[] w = mWidths; + if (forwards) { + for (int i = start; i < limit; ++i) { + if ((width -= w[i]) < 0) { + return i - start; + } + } + } else { + for (int i = limit; --i >= start;) { + if ((width -= w[i]) < 0) { + return limit - i -1; + } + } + } + + return limit - start; + } + + float measure(int start, int limit) { + float width = 0; + float[] w = mWidths; + for (int i = start; i < limit; ++i) { + width += w[i]; + } + return width; + } +}
\ No newline at end of file diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java index caaafa1..7563179 100644 --- a/core/java/android/text/SpannableStringBuilder.java +++ b/core/java/android/text/SpannableStringBuilder.java @@ -17,8 +17,9 @@ package android.text; import com.android.internal.util.ArrayUtils; -import android.graphics.Paint; + import android.graphics.Canvas; +import android.graphics.Paint; import java.lang.reflect.Array; @@ -780,7 +781,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, } if (count == 0) { - return (T[]) ArrayUtils.emptyArray(kind); + return ArrayUtils.emptyArray(kind); } if (count == 1) { ret = (Object[]) Array.newInstance(kind, 1); @@ -1055,6 +1056,39 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, } /** + * Don't call this yourself -- exists for Canvas to use internally. + * {@hide} + */ + public void drawTextRun(Canvas c, int start, int end, + float x, float y, int flags, Paint p) { + checkRange("drawTextRun", start, end); + + // Assume context requires no more than 8 chars on either side. + // This is ample, only decomposed U+FDFA falls into this + // category, and no one should put a style break within it + // anyway. + int cstart = start - 8; + if (cstart < 0) { + cstart = 0; + } + int cend = end + 8; + int max = length(); + if (cend > max) { + cend = max; + } + if (cend <= mGapStart) { + c.drawTextRun(mText, start, end - start, x, y, flags, p); + } else if (cstart >= mGapStart) { + c.drawTextRun(mText, start + mGapLength, end - start, x, y, flags, p); + } else { + char[] buf = TextUtils.obtain(cend - cstart); + getChars(cstart, cend, buf, 0); + c.drawTextRun(buf, start - cstart, end - start, x, y, flags, p); + TextUtils.recycle(buf); + } + } + + /** * Don't call this yourself -- exists for Paint to use internally. * {@hide} */ diff --git a/core/java/android/text/Spanned.java b/core/java/android/text/Spanned.java index 154497d..d14fcbc 100644 --- a/core/java/android/text/Spanned.java +++ b/core/java/android/text/Spanned.java @@ -91,7 +91,7 @@ extends CharSequence public static final int SPAN_EXCLUSIVE_EXCLUSIVE = SPAN_POINT_MARK; /** - * Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand + * Non-0-length spans of type SPAN_EXCLUSIVE_INCLUSIVE expand * to include text inserted at their ending point but not at their * starting point. When 0-length, they behave like points. */ diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index f02ad2a..0c6c545 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -16,14 +16,13 @@ package android.text; +import com.android.internal.util.ArrayUtils; + import android.graphics.Bitmap; import android.graphics.Paint; -import com.android.internal.util.ArrayUtils; -import android.util.Log; import android.text.style.LeadingMarginSpan; import android.text.style.LineHeightSpan; import android.text.style.MetricAffectingSpan; -import android.text.style.ReplacementSpan; /** * StaticLayout is a Layout for text that will not be edited after it @@ -31,8 +30,9 @@ import android.text.style.ReplacementSpan; * <p>This is used by widgets to control text layout. You should not need * to use this class directly unless you are implementing your own widget * or custom display object, or would be tempted to call - * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) - * Canvas.drawText()} directly.</p> + * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, + * float, float, android.graphics.Paint) + * Canvas.drawText()} directly.</p> */ public class StaticLayout @@ -62,7 +62,7 @@ extends Layout boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { super((ellipsize == null) - ? source + ? source : (source instanceof Spanned) ? new SpannedEllipsizer(source) : new Ellipsizer(source), @@ -72,7 +72,7 @@ extends Layout * This is annoying, but we can't refer to the layout until * superclass construction is finished, and the superclass * constructor wants the reference to the display text. - * + * * This will break if the superclass constructor ever actually * cares about the content instead of just holding the reference. */ @@ -94,13 +94,13 @@ extends Layout mLineDirections = new Directions[ ArrayUtils.idealIntArraySize(2 * mColumns)]; + mMeasured = MeasuredText.obtain(); + generate(source, bufstart, bufend, paint, outerwidth, align, spacingmult, spacingadd, includepad, includepad, ellipsize != null, ellipsizedWidth, ellipsize); - mChdirs = null; - mChs = null; - mWidths = null; + mMeasured = MeasuredText.recycle(mMeasured); mFontMetricsInt = null; } @@ -111,6 +111,7 @@ extends Layout mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)]; mLineDirections = new Directions[ ArrayUtils.idealIntArraySize(2 * mColumns)]; + mMeasured = MeasuredText.obtain(); } /* package */ void generate(CharSequence source, int bufstart, int bufend, @@ -128,38 +129,22 @@ extends Layout Paint.FontMetricsInt fm = mFontMetricsInt; int[] choosehtv = null; - int end = TextUtils.indexOf(source, '\n', bufstart, bufend); - int bufsiz = end >= 0 ? end - bufstart : bufend - bufstart; - boolean first = true; - - if (mChdirs == null) { - mChdirs = new byte[ArrayUtils.idealByteArraySize(bufsiz + 1)]; - mChs = new char[ArrayUtils.idealCharArraySize(bufsiz + 1)]; - mWidths = new float[ArrayUtils.idealIntArraySize((bufsiz + 1) * 2)]; - } - - byte[] chdirs = mChdirs; - char[] chs = mChs; - float[] widths = mWidths; + MeasuredText measured = mMeasured; - AlteredCharSequence alter = null; Spanned spanned = null; - if (source instanceof Spanned) spanned = (Spanned) source; int DEFAULT_DIR = DIR_LEFT_TO_RIGHT; // XXX - for (int start = bufstart; start <= bufend; start = end) { - if (first) - first = false; - else - end = TextUtils.indexOf(source, '\n', start, bufend); - - if (end < 0) - end = bufend; + int paraEnd; + for (int paraStart = bufstart; paraStart <= bufend; paraStart = paraEnd) { + paraEnd = TextUtils.indexOf(source, '\n', paraStart, bufend); + if (paraEnd < 0) + paraEnd = bufend; else - end++; + paraEnd++; + int paraLen = paraEnd - paraStart; int firstWidthLineCount = 1; int firstwidth = outerwidth; @@ -168,19 +153,20 @@ extends Layout LineHeightSpan[] chooseht = null; if (spanned != null) { - LeadingMarginSpan[] sp; - - sp = spanned.getSpans(start, end, LeadingMarginSpan.class); + LeadingMarginSpan[] sp = spanned.getSpans(paraStart, paraEnd, + LeadingMarginSpan.class); for (int i = 0; i < sp.length; i++) { LeadingMarginSpan lms = sp[i]; firstwidth -= sp[i].getLeadingMargin(true); restwidth -= sp[i].getLeadingMargin(false); if (lms instanceof LeadingMarginSpan.LeadingMarginSpan2) { - firstWidthLineCount = ((LeadingMarginSpan.LeadingMarginSpan2)lms).getLeadingMarginLineCount(); + firstWidthLineCount = + ((LeadingMarginSpan.LeadingMarginSpan2)lms) + .getLeadingMarginLineCount(); } } - chooseht = spanned.getSpans(start, end, LineHeightSpan.class); + chooseht = spanned.getSpans(paraStart, paraEnd, LineHeightSpan.class); if (chooseht.length != 0) { if (choosehtv == null || @@ -192,11 +178,11 @@ extends Layout for (int i = 0; i < chooseht.length; i++) { int o = spanned.getSpanStart(chooseht[i]); - if (o < start) { + if (o < paraStart) { // starts in this layout, before the // current paragraph - choosehtv[i] = getLineTop(getLineForOffset(o)); + choosehtv[i] = getLineTop(getLineForOffset(o)); } else { // starts in this paragraph @@ -206,134 +192,48 @@ extends Layout } } - if (end - start > chdirs.length) { - chdirs = new byte[ArrayUtils.idealByteArraySize(end - start)]; - mChdirs = chdirs; - } - if (end - start > chs.length) { - chs = new char[ArrayUtils.idealCharArraySize(end - start)]; - mChs = chs; - } - if ((end - start) * 2 > widths.length) { - widths = new float[ArrayUtils.idealIntArraySize((end - start) * 2)]; - mWidths = widths; - } - - TextUtils.getChars(source, start, end, chs, 0); - final int n = end - start; - - boolean easy = true; - boolean altered = false; - int dir = DEFAULT_DIR; // XXX - - for (int i = 0; i < n; i++) { - if (chs[i] >= FIRST_RIGHT_TO_LEFT) { - easy = false; - break; - } - } + measured.setPara(source, paraStart, paraEnd, DIR_REQUEST_DEFAULT_LTR); + char[] chs = measured.mChars; + float[] widths = measured.mWidths; + byte[] chdirs = measured.mLevels; + int dir = measured.mDir; + boolean easy = measured.mEasy; - // Ensure that none of the underlying characters are treated - // as viable breakpoints, and that the entire run gets the - // same bidi direction. - - if (source instanceof Spanned) { - Spanned sp = (Spanned) source; - ReplacementSpan[] spans = sp.getSpans(start, end, ReplacementSpan.class); - - for (int y = 0; y < spans.length; y++) { - int a = sp.getSpanStart(spans[y]); - int b = sp.getSpanEnd(spans[y]); - - for (int x = a; x < b; x++) { - chs[x - start] = '\uFFFC'; - } - } - } - - if (!easy) { - // XXX put override flags, etc. into chdirs - dir = bidi(dir, chs, chdirs, n, false); - - // Do mirroring for right-to-left segments - - for (int i = 0; i < n; i++) { - if (chdirs[i] == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { - int j; - - for (j = i; j < n; j++) { - if (chdirs[j] != - Character.DIRECTIONALITY_RIGHT_TO_LEFT) - break; - } - - if (AndroidCharacter.mirror(chs, i, j - i)) - altered = true; - - i = j - 1; - } - } - } - - CharSequence sub; - - if (altered) { - if (alter == null) - alter = AlteredCharSequence.make(source, chs, start, end); - else - alter.update(chs, start, end); - - sub = alter; - } else { - sub = source; - } + CharSequence sub = source; int width = firstwidth; float w = 0; - int here = start; + int here = paraStart; - int ok = start; + int ok = paraStart; float okwidth = w; int okascent = 0, okdescent = 0, oktop = 0, okbottom = 0; - int fit = start; + int fit = paraStart; float fitwidth = w; int fitascent = 0, fitdescent = 0, fittop = 0, fitbottom = 0; boolean tab = false; - int next; - for (int i = start; i < end; i = next) { + int spanEnd; + for (int spanStart = paraStart; spanStart < paraEnd; spanStart = spanEnd) { if (spanned == null) - next = end; + spanEnd = paraEnd; else - next = spanned.nextSpanTransition(i, end, - MetricAffectingSpan. - class); + spanEnd = spanned.nextSpanTransition(spanStart, paraEnd, + MetricAffectingSpan.class); + + int spanLen = spanEnd - spanStart; + int startInPara = spanStart - paraStart; + int endInPara = spanEnd - paraStart; if (spanned == null) { - paint.getTextWidths(sub, i, next, widths); - System.arraycopy(widths, 0, widths, - end - start + (i - start), next - i); - - paint.getFontMetricsInt(fm); + measured.addStyleRun(paint, spanLen, fm); } else { - mWorkPaint.baselineShift = 0; - - Styled.getTextWidths(paint, mWorkPaint, - spanned, i, next, - widths, fm); - System.arraycopy(widths, 0, widths, - end - start + (i - start), next - i); - - if (mWorkPaint.baselineShift < 0) { - fm.ascent += mWorkPaint.baselineShift; - fm.top += mWorkPaint.baselineShift; - } else { - fm.descent += mWorkPaint.baselineShift; - fm.bottom += mWorkPaint.baselineShift; - } + MetricAffectingSpan[] spans = + spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class); + measured.addStyleRun(paint, spans, spanLen, fm); } int fmtop = fm.top; @@ -341,27 +241,17 @@ extends Layout int fmascent = fm.ascent; int fmdescent = fm.descent; - if (false) { - StringBuilder sb = new StringBuilder(); - for (int j = i; j < next; j++) { - sb.append(widths[j - start + (end - start)]); - sb.append(' '); - } - - Log.e("text", sb.toString()); - } - - for (int j = i; j < next; j++) { - char c = chs[j - start]; + for (int j = spanStart; j < spanEnd; j++) { + char c = chs[j - paraStart]; float before = w; if (c == '\n') { ; } else if (c == '\t') { - w = Layout.nextTab(sub, start, end, w, null); + w = Layout.nextTab(sub, paraStart, paraEnd, w, null); tab = true; - } else if (c >= 0xD800 && c <= 0xDFFF && j + 1 < next) { - int emoji = Character.codePointAt(chs, j - start); + } else if (c >= 0xD800 && c <= 0xDFFF && j + 1 < spanEnd) { + int emoji = Character.codePointAt(chs, j - paraStart); if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) { Bitmap bm = EMOJI_FACTORY. @@ -376,7 +266,7 @@ extends Layout whichPaint = mWorkPaint; } - float wid = (float) bm.getWidth() * + float wid = bm.getWidth() * -whichPaint.ascent() / bm.getHeight(); @@ -384,13 +274,13 @@ extends Layout tab = true; j++; } else { - w += widths[j - start + (end - start)]; + w += widths[j - paraStart]; } } else { - w += widths[j - start + (end - start)]; + w += widths[j - paraStart]; } } else { - w += widths[j - start + (end - start)]; + w += widths[j - paraStart]; } // Log.e("text", "was " + before + " now " + w + " after " + c + " within " + width); @@ -411,7 +301,7 @@ extends Layout /* * From the Unicode Line Breaking Algorithm: * (at least approximately) - * + * * .,:; are class IS: breakpoints * except when adjacent to digits * / is class SY: a breakpoint @@ -426,12 +316,12 @@ extends Layout if (c == ' ' || c == '\t' || ((c == '.' || c == ',' || c == ':' || c == ';') && - (j - 1 < here || !Character.isDigit(chs[j - 1 - start])) && - (j + 1 >= next || !Character.isDigit(chs[j + 1 - start]))) || + (j - 1 < here || !Character.isDigit(chs[j - 1 - paraStart])) && + (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) || ((c == '/' || c == '-') && - (j + 1 >= next || !Character.isDigit(chs[j + 1 - start]))) || + (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) || (c >= FIRST_CJK && isIdeographic(c, true) && - j + 1 < next && isIdeographic(chs[j + 1 - start], false))) { + j + 1 < spanEnd && isIdeographic(chs[j + 1 - paraStart], false))) { okwidth = w; ok = j + 1; @@ -448,7 +338,7 @@ extends Layout if (ok != here) { // Log.e("text", "output ok " + here + " to " +ok); - while (ok < next && chs[ok - start] == ' ') { + while (ok < spanEnd && chs[ok - paraStart] == ' ') { ok++; } @@ -458,9 +348,9 @@ extends Layout v, spacingmult, spacingadd, chooseht, choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + needMultiply, paraStart, chdirs, dir, easy, ok == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, okwidth, paint); @@ -484,7 +374,7 @@ extends Layout if (ok != here) { // Log.e("text", "output ok " + here + " to " +ok); - while (ok < next && chs[ok - start] == ' ') { + while (ok < spanEnd && chs[ok - paraStart] == ' ') { ok++; } @@ -494,9 +384,9 @@ extends Layout v, spacingmult, spacingadd, chooseht, choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + needMultiply, paraStart, chdirs, dir, easy, ok == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, okwidth, paint); @@ -510,18 +400,19 @@ extends Layout v, spacingmult, spacingadd, chooseht, choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + needMultiply, paraStart, chdirs, dir, easy, fit == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, fitwidth, paint); here = fit; } else { // Log.e("text", "output one " + here + " to " +(here + 1)); - measureText(paint, mWorkPaint, - source, here, here + 1, fm, tab, - null); + // XXX not sure why the existing fm wasn't ok. + // measureText(paint, mWorkPaint, + // source, here, here + 1, fm, tab, + // null); v = out(source, here, here+1, @@ -530,18 +421,18 @@ extends Layout v, spacingmult, spacingadd, chooseht, choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, + needMultiply, paraStart, chdirs, dir, easy, here + 1 == bufend, includepad, trackpad, - widths, start, end - start, + chs, widths, here - paraStart, where, ellipsizedWidth, - widths[here - start], paint); + widths[here - paraStart], paint); here = here + 1; } - if (here < i) { - j = next = here; // must remeasure + if (here < spanStart) { + j = spanEnd = here; // must remeasure } else { j = here - 1; // continue looping } @@ -558,7 +449,7 @@ extends Layout } } - if (end != here) { + if (paraEnd != here) { if ((fittop | fitbottom | fitdescent | fitascent) == 0) { paint.getFontMetricsInt(fm); @@ -571,20 +462,20 @@ extends Layout // Log.e("text", "output rest " + here + " to " + end); v = out(source, - here, end, fitascent, fitdescent, + here, paraEnd, fitascent, fitdescent, fittop, fitbottom, v, spacingmult, spacingadd, chooseht, choosehtv, fm, tab, - needMultiply, start, chdirs, dir, easy, - end == bufend, includepad, trackpad, - widths, start, end - start, + needMultiply, paraStart, chdirs, dir, easy, + paraEnd == bufend, includepad, trackpad, + chs, widths, here - paraStart, where, ellipsizedWidth, w, paint); } - start = end; + paraStart = paraEnd; - if (end == bufend) + if (paraEnd == bufend) break; } @@ -599,246 +490,13 @@ extends Layout v, spacingmult, spacingadd, null, null, fm, false, - needMultiply, bufend, chdirs, DEFAULT_DIR, true, + needMultiply, bufend, null, DEFAULT_DIR, true, true, includepad, trackpad, - widths, bufstart, 0, + null, null, bufstart, where, ellipsizedWidth, 0, paint); } } - /** - * Runs the unicode bidi algorithm on the first n chars in chs, returning - * the char dirs in chInfo and the base line direction of the first - * paragraph. - * - * XXX change result from dirs to levels - * - * @param dir the direction flag, either DIR_REQUEST_LTR, - * DIR_REQUEST_RTL, DIR_REQUEST_DEFAULT_LTR, or DIR_REQUEST_DEFAULT_RTL. - * @param chs the text to examine - * @param chInfo on input, if hasInfo is true, override and other flags - * representing out-of-band embedding information. On output, the generated - * dirs of the text. - * @param n the length of the text/information in chs and chInfo - * @param hasInfo true if chInfo has input information, otherwise the - * input data in chInfo is ignored. - * @return the resolved direction level of the first paragraph, either - * DIR_LEFT_TO_RIGHT or DIR_RIGHT_TO_LEFT. - */ - /* package */ static int bidi(int dir, char[] chs, byte[] chInfo, int n, - boolean hasInfo) { - - AndroidCharacter.getDirectionalities(chs, chInfo, n); - - /* - * Determine primary paragraph direction if not specified - */ - if (dir != DIR_REQUEST_LTR && dir != DIR_REQUEST_RTL) { - // set up default - dir = dir >= 0 ? DIR_LEFT_TO_RIGHT : DIR_RIGHT_TO_LEFT; - for (int j = 0; j < n; j++) { - int d = chInfo[j]; - - if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT) { - dir = DIR_LEFT_TO_RIGHT; - break; - } - if (d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { - dir = DIR_RIGHT_TO_LEFT; - break; - } - } - } - - final byte SOR = dir == DIR_LEFT_TO_RIGHT ? - Character.DIRECTIONALITY_LEFT_TO_RIGHT : - Character.DIRECTIONALITY_RIGHT_TO_LEFT; - - /* - * XXX Explicit overrides should go here - */ - - /* - * Weak type resolution - */ - - // dump(chdirs, n, "initial"); - - // W1 non spacing marks - for (int j = 0; j < n; j++) { - if (chInfo[j] == Character.NON_SPACING_MARK) { - if (j == 0) - chInfo[j] = SOR; - else - chInfo[j] = chInfo[j - 1]; - } - } - - // dump(chdirs, n, "W1"); - - // W2 european numbers - byte cur = SOR; - for (int j = 0; j < n; j++) { - byte d = chInfo[j]; - - if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT || - d == Character.DIRECTIONALITY_RIGHT_TO_LEFT || - d == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) - cur = d; - else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) { - if (cur == - Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) - chInfo[j] = Character.DIRECTIONALITY_ARABIC_NUMBER; - } - } - - // dump(chdirs, n, "W2"); - - // W3 arabic letters - for (int j = 0; j < n; j++) { - if (chInfo[j] == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) - chInfo[j] = Character.DIRECTIONALITY_RIGHT_TO_LEFT; - } - - // dump(chdirs, n, "W3"); - - // W4 single separator between numbers - for (int j = 1; j < n - 1; j++) { - byte d = chInfo[j]; - byte prev = chInfo[j - 1]; - byte next = chInfo[j + 1]; - - if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR) { - if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER && - next == Character.DIRECTIONALITY_EUROPEAN_NUMBER) - chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; - } else if (d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR) { - if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER && - next == Character.DIRECTIONALITY_EUROPEAN_NUMBER) - chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; - if (prev == Character.DIRECTIONALITY_ARABIC_NUMBER && - next == Character.DIRECTIONALITY_ARABIC_NUMBER) - chInfo[j] = Character.DIRECTIONALITY_ARABIC_NUMBER; - } - } - - // dump(chdirs, n, "W4"); - - // W5 european number terminators - boolean adjacent = false; - for (int j = 0; j < n; j++) { - byte d = chInfo[j]; - - if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) - adjacent = true; - else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR && adjacent) - chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; - else - adjacent = false; - } - - //dump(chdirs, n, "W5"); - - // W5 european number terminators part 2, - // W6 separators and terminators - adjacent = false; - for (int j = n - 1; j >= 0; j--) { - byte d = chInfo[j]; - - if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) - adjacent = true; - else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR) { - if (adjacent) - chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; - else - chInfo[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS; - } - else { - adjacent = false; - - if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR || - d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR || - d == Character.DIRECTIONALITY_PARAGRAPH_SEPARATOR || - d == Character.DIRECTIONALITY_SEGMENT_SEPARATOR) - chInfo[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS; - } - } - - // dump(chdirs, n, "W6"); - - // W7 strong direction of european numbers - cur = SOR; - for (int j = 0; j < n; j++) { - byte d = chInfo[j]; - - if (d == SOR || - d == Character.DIRECTIONALITY_LEFT_TO_RIGHT || - d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) - cur = d; - - if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) - chInfo[j] = cur; - } - - // dump(chdirs, n, "W7"); - - // N1, N2 neutrals - cur = SOR; - for (int j = 0; j < n; j++) { - byte d = chInfo[j]; - - if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT || - d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { - cur = d; - } else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER || - d == Character.DIRECTIONALITY_ARABIC_NUMBER) { - cur = Character.DIRECTIONALITY_RIGHT_TO_LEFT; - } else { - byte dd = SOR; - int k; - - for (k = j + 1; k < n; k++) { - dd = chInfo[k]; - - if (dd == Character.DIRECTIONALITY_LEFT_TO_RIGHT || - dd == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { - break; - } - if (dd == Character.DIRECTIONALITY_EUROPEAN_NUMBER || - dd == Character.DIRECTIONALITY_ARABIC_NUMBER) { - dd = Character.DIRECTIONALITY_RIGHT_TO_LEFT; - break; - } - } - - for (int y = j; y < k; y++) { - if (dd == cur) - chInfo[y] = cur; - else - chInfo[y] = SOR; - } - - j = k - 1; - } - } - - // dump(chdirs, n, "final"); - - // extra: enforce that all tabs and surrogate characters go the - // primary direction - // TODO: actually do directions right for surrogates - - for (int j = 0; j < n; j++) { - char c = chs[j]; - - if (c == '\t' || (c >= 0xD800 && c <= 0xDFFF)) { - chInfo[j] = SOR; - } - } - - return dir; - } - private static final char FIRST_CJK = '\u2E80'; /** * Returns true if the specified character is one of those specified @@ -944,28 +602,6 @@ extends Layout } */ - private static int getFit(TextPaint paint, - TextPaint workPaint, - CharSequence text, int start, int end, - float wid) { - int high = end + 1, low = start - 1, guess; - - while (high - low > 1) { - guess = (high + low) / 2; - - if (measureText(paint, workPaint, - text, start, guess, null, true, null) > wid) - high = guess; - else - low = guess; - } - - if (low < start) - return start; - else - return low; - } - private int out(CharSequence text, int start, int end, int above, int below, int top, int bottom, int v, float spacingmult, float spacingadd, @@ -974,7 +610,7 @@ extends Layout boolean needMultiply, int pstart, byte[] chdirs, int dir, boolean easy, boolean last, boolean includepad, boolean trackpad, - float[] widths, int widstart, int widoff, + char[] chs, float[] widths, int widstart, TextUtils.TruncateAt ellipsize, float ellipsiswidth, float textwidth, TextPaint paint) { int j = mLineCount; @@ -982,8 +618,6 @@ extends Layout int want = off + mColumns + TOP; int[] lines = mLines; - // Log.e("text", "line " + start + " to " + end + (last ? "===" : "")); - if (want >= lines.length) { int nlen = ArrayUtils.idealIntArraySize(want + 1); int[] grow = new int[nlen]; @@ -1062,56 +696,20 @@ extends Layout if (tab) lines[off + TAB] |= TAB_MASK; - { - lines[off + DIR] |= dir << DIR_SHIFT; - - int cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT; - int count = 0; - - if (!easy) { - for (int k = start; k < end; k++) { - if (chdirs[k - pstart] != cur) { - count++; - cur = chdirs[k - pstart]; - } - } - } - - Directions linedirs; - - if (count == 0) { - linedirs = DIRS_ALL_LEFT_TO_RIGHT; - } else { - short[] ld = new short[count + 1]; - - cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT; - count = 0; - int here = start; - - for (int k = start; k < end; k++) { - if (chdirs[k - pstart] != cur) { - // XXX check to make sure we don't - // overflow short - ld[count++] = (short) (k - here); - cur = chdirs[k - pstart]; - here = k; - } - } - - ld[count] = (short) (end - here); - - if (count == 1 && ld[0] == 0) { - linedirs = DIRS_ALL_RIGHT_TO_LEFT; - } else { - linedirs = new Directions(ld); - } - } - + lines[off + DIR] |= dir << DIR_SHIFT; + Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT; + // easy means all chars < the first RTL, so no emoji, no nothing + // XXX a run with no text or all spaces is easy but might be an empty + // RTL paragraph. Make sure easy is false if this is the case. + if (easy) { mLineDirections[j] = linedirs; + } else { + mLineDirections[j] = AndroidBidi.directions(dir, chdirs, widstart, chs, + widstart, end - start); // If ellipsize is in marquee mode, do not apply ellipsis on the first line if (ellipsize != null && (ellipsize != TextUtils.TruncateAt.MARQUEE || j != 0)) { - calculateEllipsis(start, end, widths, widstart, widoff, + calculateEllipsis(start, end, widths, widstart, ellipsiswidth, ellipsize, j, textwidth, paint); } @@ -1122,7 +720,7 @@ extends Layout } private void calculateEllipsis(int linestart, int lineend, - float[] widths, int widstart, int widoff, + float[] widths, int widstart, float avail, TextUtils.TruncateAt where, int line, float textwidth, TextPaint paint) { int len = lineend - linestart; @@ -1142,7 +740,7 @@ extends Layout int i; for (i = len; i >= 0; i--) { - float w = widths[i - 1 + linestart - widstart + widoff]; + float w = widths[i - 1 + linestart - widstart]; if (w + sum + ellipsiswid > avail) { break; @@ -1158,7 +756,7 @@ extends Layout int i; for (i = 0; i < len; i++) { - float w = widths[i + linestart - widstart + widoff]; + float w = widths[i + linestart - widstart]; if (w + sum + ellipsiswid > avail) { break; @@ -1175,7 +773,7 @@ extends Layout float ravail = (avail - ellipsiswid) / 2; for (right = len; right >= 0; right--) { - float w = widths[right - 1 + linestart - widstart + widoff]; + float w = widths[right - 1 + linestart - widstart]; if (w + rsum > ravail) { break; @@ -1186,7 +784,7 @@ extends Layout float lavail = avail - ellipsiswid - rsum; for (left = 0; left < right; left++) { - float w = widths[left + linestart - widstart + widoff]; + float w = widths[left + linestart - widstart]; if (w + lsum > lavail) { break; @@ -1203,7 +801,7 @@ extends Layout mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount; } - // Override the baseclass so we can directly access our members, + // Override the base class so we can directly access our members, // rather than relying on member functions. // The logic mirrors that of Layout.getLineForVertical // FIXME: It may be faster to do a linear search for layouts without many lines. @@ -1232,11 +830,11 @@ extends Layout } public int getLineTop(int line) { - return mLines[mColumns * line + TOP]; + return mLines[mColumns * line + TOP]; } public int getLineDescent(int line) { - return mLines[mColumns * line + DESCENT]; + return mLines[mColumns * line + DESCENT]; } public int getLineStart(int line) { @@ -1312,10 +910,8 @@ extends Layout private static final char FIRST_RIGHT_TO_LEFT = '\u0590'; /* - * These are reused across calls to generate() + * This is reused across calls to generate() */ - private byte[] mChdirs; - private char[] mChs; - private float[] mWidths; + private MeasuredText mMeasured; private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); } diff --git a/core/java/android/text/Styled.java b/core/java/android/text/Styled.java deleted file mode 100644 index 513b2cd..0000000 --- a/core/java/android/text/Styled.java +++ /dev/null @@ -1,434 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package android.text; - -import android.graphics.Canvas; -import android.graphics.Paint; -import android.text.style.CharacterStyle; -import android.text.style.MetricAffectingSpan; -import android.text.style.ReplacementSpan; - -/** - * This class provides static methods for drawing and measuring styled text, - * like {@link android.text.Spanned} object with - * {@link android.text.style.ReplacementSpan}. - * - * @hide - */ -public class Styled -{ - /** - * Draws and/or measures a uniform run of text on a single line. No span of - * interest should start or end in the middle of this run (if not - * drawing, character spans that don't affect metrics can be ignored). - * Neither should the run direction change in the middle of the run. - * - * <p>The x position is the leading edge of the text. In a right-to-left - * paragraph, this will be to the right of the text to be drawn. Paint - * should not have an Align value other than LEFT or positioning will get - * confused. - * - * <p>On return, workPaint will reflect the original paint plus any - * modifications made by character styles on the run. - * - * <p>The returned width is signed and will be < 0 if the paragraph - * direction is right-to-left. - */ - private static float drawUniformRun(Canvas canvas, - Spanned text, int start, int end, - int dir, boolean runIsRtl, - float x, int top, int y, int bottom, - Paint.FontMetricsInt fmi, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - - boolean haveWidth = false; - float ret = 0; - CharacterStyle[] spans = text.getSpans(start, end, CharacterStyle.class); - - ReplacementSpan replacement = null; - - // XXX: This shouldn't be modifying paint, only workPaint. - // However, the members belonging to TextPaint should have default - // values anyway. Better to ensure this in the Layout constructor. - paint.bgColor = 0; - paint.baselineShift = 0; - workPaint.set(paint); - - if (spans.length > 0) { - for (int i = 0; i < spans.length; i++) { - CharacterStyle span = spans[i]; - - if (span instanceof ReplacementSpan) { - replacement = (ReplacementSpan)span; - } - else { - span.updateDrawState(workPaint); - } - } - } - - if (replacement == null) { - CharSequence tmp; - int tmpstart, tmpend; - - if (runIsRtl) { - tmp = TextUtils.getReverse(text, start, end); - tmpstart = 0; - // XXX: assumes getReverse doesn't change the length of the text - tmpend = end - start; - } else { - tmp = text; - tmpstart = start; - tmpend = end; - } - - if (fmi != null) { - workPaint.getFontMetricsInt(fmi); - } - - if (canvas != null) { - if (workPaint.bgColor != 0) { - int c = workPaint.getColor(); - Paint.Style s = workPaint.getStyle(); - workPaint.setColor(workPaint.bgColor); - workPaint.setStyle(Paint.Style.FILL); - - if (!haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - - if (dir == Layout.DIR_RIGHT_TO_LEFT) - canvas.drawRect(x - ret, top, x, bottom, workPaint); - else - canvas.drawRect(x, top, x + ret, bottom, workPaint); - - workPaint.setStyle(s); - workPaint.setColor(c); - } - - if (dir == Layout.DIR_RIGHT_TO_LEFT) { - if (!haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - - canvas.drawText(tmp, tmpstart, tmpend, - x - ret, y + workPaint.baselineShift, workPaint); - } else { - if (needWidth) { - if (!haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - } - - canvas.drawText(tmp, tmpstart, tmpend, - x, y + workPaint.baselineShift, workPaint); - } - } else { - if (needWidth && !haveWidth) { - ret = workPaint.measureText(tmp, tmpstart, tmpend); - haveWidth = true; - } - } - } else { - ret = replacement.getSize(workPaint, text, start, end, fmi); - - if (canvas != null) { - if (dir == Layout.DIR_RIGHT_TO_LEFT) - replacement.draw(canvas, text, start, end, - x - ret, top, y, bottom, workPaint); - else - replacement.draw(canvas, text, start, end, - x, top, y, bottom, workPaint); - } - } - - if (dir == Layout.DIR_RIGHT_TO_LEFT) - return -ret; - else - return ret; - } - - /** - * Returns the advance widths for a uniform left-to-right run of text with - * no style changes in the middle of the run. If any style is replacement - * text, the first character will get the width of the replacement and the - * remaining characters will get a width of 0. - * - * @param paint the paint, will not be modified - * @param workPaint a paint to modify; on return will reflect the original - * paint plus the effect of all spans on the run - * @param text the text - * @param start the start of the run - * @param end the limit of the run - * @param widths array to receive the advance widths of the characters. Must - * be at least a large as (end - start). - * @param fmi FontMetrics information; can be null - * @return the actual number of widths returned - */ - public static int getTextWidths(TextPaint paint, - TextPaint workPaint, - Spanned text, int start, int end, - float[] widths, Paint.FontMetricsInt fmi) { - MetricAffectingSpan[] spans = - text.getSpans(start, end, MetricAffectingSpan.class); - - ReplacementSpan replacement = null; - workPaint.set(paint); - - for (int i = 0; i < spans.length; i++) { - MetricAffectingSpan span = spans[i]; - if (span instanceof ReplacementSpan) { - replacement = (ReplacementSpan)span; - } - else { - span.updateMeasureState(workPaint); - } - } - - if (replacement == null) { - workPaint.getFontMetricsInt(fmi); - workPaint.getTextWidths(text, start, end, widths); - } else { - int wid = replacement.getSize(workPaint, text, start, end, fmi); - - if (end > start) { - widths[0] = wid; - for (int i = start + 1; i < end; i++) - widths[i - start] = 0; - } - } - return end - start; - } - - /** - * Renders and/or measures a directional run of text on a single line. - * Unlike {@link #drawUniformRun}, this can render runs that cross style - * boundaries. Returns the signed advance width, if requested. - * - * <p>The x position is the leading edge of the text. In a right-to-left - * paragraph, this will be to the right of the text to be drawn. Paint - * should not have an Align value other than LEFT or positioning will get - * confused. - * - * <p>This optimizes for unstyled text and so workPaint might not be - * modified by this call. - * - * <p>The returned advance width will be < 0 if the paragraph - * direction is right-to-left. - */ - private static float drawDirectionalRun(Canvas canvas, - CharSequence text, int start, int end, - int dir, boolean runIsRtl, - float x, int top, int y, int bottom, - Paint.FontMetricsInt fmi, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - - // XXX: It looks like all calls to this API match dir and runIsRtl, so - // having both parameters is redundant and confusing. - - // fast path for unstyled text - if (!(text instanceof Spanned)) { - float ret = 0; - - if (runIsRtl) { - CharSequence tmp = TextUtils.getReverse(text, start, end); - // XXX: this assumes getReverse doesn't tweak the length of - // the text - int tmpend = end - start; - - if (canvas != null || needWidth) - ret = paint.measureText(tmp, 0, tmpend); - - if (canvas != null) - canvas.drawText(tmp, 0, tmpend, - x - ret, y, paint); - } else { - if (needWidth) - ret = paint.measureText(text, start, end); - - if (canvas != null) - canvas.drawText(text, start, end, x, y, paint); - } - - if (fmi != null) { - paint.getFontMetricsInt(fmi); - } - - return ret * dir; // Layout.DIR_RIGHT_TO_LEFT == -1 - } - - float ox = x; - int minAscent = 0, maxDescent = 0, minTop = 0, maxBottom = 0; - - Spanned sp = (Spanned) text; - Class<?> division; - - if (canvas == null) - division = MetricAffectingSpan.class; - else - division = CharacterStyle.class; - - int next; - for (int i = start; i < end; i = next) { - next = sp.nextSpanTransition(i, end, division); - - // XXX: if dir and runIsRtl were not the same, this would draw - // spans in the wrong order, but no one appears to call it this - // way. - x += drawUniformRun(canvas, sp, i, next, dir, runIsRtl, - x, top, y, bottom, fmi, paint, workPaint, - needWidth || next != end); - - if (fmi != null) { - if (fmi.ascent < minAscent) - minAscent = fmi.ascent; - if (fmi.descent > maxDescent) - maxDescent = fmi.descent; - - if (fmi.top < minTop) - minTop = fmi.top; - if (fmi.bottom > maxBottom) - maxBottom = fmi.bottom; - } - } - - if (fmi != null) { - if (start == end) { - paint.getFontMetricsInt(fmi); - } else { - fmi.ascent = minAscent; - fmi.descent = maxDescent; - fmi.top = minTop; - fmi.bottom = maxBottom; - } - } - - return x - ox; - } - - /** - * Draws a unidirectional run of text on a single line, and optionally - * returns the signed advance. Unlike drawDirectionalRun, the paragraph - * direction and run direction can be different. - */ - /* package */ static float drawText(Canvas canvas, - CharSequence text, int start, int end, - int dir, boolean runIsRtl, - float x, int top, int y, int bottom, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - // XXX this logic is (dir == DIR_LEFT_TO_RIGHT) == runIsRtl - if ((dir == Layout.DIR_RIGHT_TO_LEFT && !runIsRtl) || - (runIsRtl && dir == Layout.DIR_LEFT_TO_RIGHT)) { - // TODO: this needs the real direction - float ch = drawDirectionalRun(null, text, start, end, - Layout.DIR_LEFT_TO_RIGHT, false, 0, 0, 0, 0, null, paint, - workPaint, true); - - ch *= dir; // DIR_RIGHT_TO_LEFT == -1 - drawDirectionalRun(canvas, text, start, end, -dir, - runIsRtl, x + ch, top, y, bottom, null, paint, - workPaint, true); - - return ch; - } - - return drawDirectionalRun(canvas, text, start, end, dir, runIsRtl, - x, top, y, bottom, null, paint, workPaint, - needWidth); - } - - /** - * Draws a run of text on a single line, with its - * origin at (x,y), in the specified Paint. The origin is interpreted based - * on the Align setting in the Paint. - * - * This method considers style information in the text (e.g. even when text - * is an instance of {@link android.text.Spanned}, this method correctly - * draws the text). See also - * {@link android.graphics.Canvas#drawText(CharSequence, int, int, float, - * float, Paint)} and - * {@link android.graphics.Canvas#drawRect(float, float, float, float, - * Paint)}. - * - * @param canvas The target canvas - * @param text The text to be drawn - * @param start The index of the first character in text to draw - * @param end (end - 1) is the index of the last character in text to draw - * @param direction The direction of the text. This must be - * {@link android.text.Layout#DIR_LEFT_TO_RIGHT} or - * {@link android.text.Layout#DIR_RIGHT_TO_LEFT}. - * @param x The x-coordinate of origin for where to draw the text - * @param top The top side of the rectangle to be drawn - * @param y The y-coordinate of origin for where to draw the text - * @param bottom The bottom side of the rectangle to be drawn - * @param paint The main {@link TextPaint} object. - * @param workPaint The {@link TextPaint} object used for temporal - * workspace. - * @param needWidth If true, this method returns the width of drawn text - * @return Width of the drawn text if needWidth is true - */ - public static float drawText(Canvas canvas, - CharSequence text, int start, int end, - int direction, - float x, int top, int y, int bottom, - TextPaint paint, - TextPaint workPaint, - boolean needWidth) { - // For safety. - direction = direction >= 0 ? Layout.DIR_LEFT_TO_RIGHT - : Layout.DIR_RIGHT_TO_LEFT; - - // Hide runIsRtl parameter since it is meaningless for external - // developers. - // XXX: the runIsRtl probably ought to be the same as direction, then - // this could draw rtl text. - return drawText(canvas, text, start, end, direction, false, - x, top, y, bottom, paint, workPaint, needWidth); - } - - /** - * Returns the width of a run of left-to-right text on a single line, - * considering style information in the text (e.g. even when text is an - * instance of {@link android.text.Spanned}, this method correctly measures - * the width of the text). - * - * @param paint the main {@link TextPaint} object; will not be modified - * @param workPaint the {@link TextPaint} object available for modification; - * will not necessarily be used - * @param text the text to measure - * @param start the index of the first character to start measuring - * @param end 1 beyond the index of the last character to measure - * @param fmi FontMetrics information; can be null - * @return The width of the text - */ - public static float measureText(TextPaint paint, - TextPaint workPaint, - CharSequence text, int start, int end, - Paint.FontMetricsInt fmi) { - return drawDirectionalRun(null, text, start, end, - Layout.DIR_LEFT_TO_RIGHT, false, - 0, 0, 0, 0, fmi, paint, workPaint, true); - } -} diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java new file mode 100644 index 0000000..fae3fc3 --- /dev/null +++ b/core/java/android/text/TextLine.java @@ -0,0 +1,1016 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import com.android.internal.util.ArrayUtils; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Paint.FontMetricsInt; +import android.icu.text.ArabicShaping; +import android.text.Layout.Directions; +import android.text.style.CharacterStyle; +import android.text.style.MetricAffectingSpan; +import android.text.style.ReplacementSpan; +import android.text.style.TabStopSpan; +import android.util.Log; + +/** + * Represents a line of styled text, for measuring in visual order and + * for rendering. + * + * <p>Get a new instance using obtain(), and when finished with it, return it + * to the pool using recycle(). + * + * <p>Call set to prepare the instance for use, then either draw, measure, + * metrics, or caretToLeftRightOf. + * + * @hide + */ +class TextLine { + private TextPaint mPaint; + private CharSequence mText; + private int mStart; + private int mLen; + private int mDir; + private Directions mDirections; + private boolean mHasTabs; + private TabStopSpan[] mTabs; + + private char[] mChars; + private boolean mCharsValid; + private Spanned mSpanned; + private TextPaint mWorkPaint = new TextPaint(); + private int mPreppedIndex; + private int mPreppedLimit; + + private static TextLine[] cached = new TextLine[3]; + + /** + * Returns a new TextLine from the shared pool. + * + * @return an uninitialized TextLine + */ + static TextLine obtain() { + TextLine tl; + synchronized (cached) { + for (int i = cached.length; --i >= 0;) { + if (cached[i] != null) { + tl = cached[i]; + cached[i] = null; + return tl; + } + } + } + tl = new TextLine(); + Log.e("TLINE", "new: " + tl); + return tl; + } + + /** + * Puts a TextLine back into the shared pool. Do not use this TextLine once + * it has been returned. + * @param tl the textLine + * @return null, as a convenience from clearing references to the provided + * TextLine + */ + static TextLine recycle(TextLine tl) { + tl.mText = null; + tl.mPaint = null; + tl.mDirections = null; + if (tl.mLen < 250) { + synchronized(cached) { + for (int i = 0; i < cached.length; ++i) { + if (cached[i] == null) { + cached[i] = tl; + break; + } + } + } + } + return null; + } + + /** + * Initializes a TextLine and prepares it for use. + * + * @param paint the base paint for the line + * @param text the text, can be Styled + * @param start the start of the line relative to the text + * @param limit the limit of the line relative to the text + * @param dir the paragraph direction of this line + * @param directions the directions information of this line + * @param hasTabs true if the line might contain tabs or emoji + * @param spans array of paragraph-level spans, of which only TabStopSpans + * are used. Can be null. + */ + void set(TextPaint paint, CharSequence text, int start, int limit, int dir, + Directions directions, boolean hasTabs, Object[] spans) { + mPaint = paint; + mText = text; + mStart = start; + mLen = limit - start; + mDir = dir; + mDirections = directions; + mHasTabs = hasTabs; + mSpanned = null; + mPreppedIndex = 0; + mPreppedLimit = 0; + + boolean hasReplacement = false; + if (text instanceof Spanned) { + mSpanned = (Spanned) text; + hasReplacement = mSpanned.getSpans(start, limit, + ReplacementSpan.class).length > 0; + } + + mCharsValid = hasReplacement || hasTabs || + directions != Layout.DIRS_ALL_LEFT_TO_RIGHT; + + if (mCharsValid) { + if (mChars == null || mChars.length < mLen) { + mChars = new char[ArrayUtils.idealCharArraySize(mLen)]; + } + TextUtils.getChars(text, start, limit, mChars, 0); + + if (hasTabs) { + TabStopSpan[] tabs = mTabs; + int tabLen = 0; + if (mSpanned != null && spans == null) { + TabStopSpan[] newTabs = mSpanned.getSpans(start, limit, + TabStopSpan.class); + if (tabs == null || tabs.length < newTabs.length) { + tabs = newTabs; + } else { + for (int i = 0; i < newTabs.length; ++i) { + tabs[i] = newTabs[i]; + } + } + tabLen = newTabs.length; + } else if (spans != null) { + if (tabs == null || tabs.length < spans.length) { + tabs = new TabStopSpan[spans.length]; + } + for (int i = 0; i < spans.length; ++i) { + if (spans[i] instanceof TabStopSpan) { + tabs[tabLen++] = (TabStopSpan) spans[i]; + } + } + } + + if (tabs != null && tabLen < tabs.length){ + tabs[tabLen] = null; + } + mTabs = tabs; + } + } + } + + /** + * Renders the TextLine. + * + * @param c the canvas to render on + * @param x the leading margin position + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + */ + void draw(Canvas c, float x, int top, int y, int bottom) { + if (!mHasTabs) { + if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) { + drawRun(c, 0, 0, mLen, false, x, top, y, bottom, false); + return; + } + if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) { + drawRun(c, 0, 0, mLen, true, x, top, y, bottom, false); + return; + } + } + + float h = 0; + int[] runs = mDirections.mDirections; + RectF emojiRect = null; + + int lastRunIndex = runs.length - 2; + for (int i = 0; i < runs.length; i += 2) { + int runStart = runs[i]; + int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK); + if (runLimit > mLen) { + runLimit = mLen; + } + boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; + + int segstart = runStart; + char[] chars = mChars; + for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { + int codept = 0; + Bitmap bm = null; + + if (mHasTabs && j < runLimit) { + codept = mChars[j]; + if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) { + codept = Character.codePointAt(mChars, j); + if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) { + bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept); + } else if (codept > 0xffff) { + ++j; + continue; + } + } + } + + if (j == runLimit || codept == '\t' || bm != null) { + h += drawRun(c, i, segstart, j, runIsRtl, x+h, top, y, bottom, + i != lastRunIndex || j != mLen); + + if (codept == '\t') { + h = mDir * nextTab(h * mDir); + } else if (bm != null) { + float bmAscent = ascent(j); + float bitmapHeight = bm.getHeight(); + float scale = -bmAscent / bitmapHeight; + float width = bm.getWidth() * scale; + + if (emojiRect == null) { + emojiRect = new RectF(); + } + emojiRect.set(x + h, y + bmAscent, + x + h + width, y); + c.drawBitmap(bm, null, emojiRect, mPaint); + h += width; + j++; + } + segstart = j + 1; + } + } + } + } + + /** + * Returns metrics information for the entire line. + * + * @param fmi receives font metrics information, can be null + * @return the signed width of the line + */ + float metrics(FontMetricsInt fmi) { + return measure(mLen, false, fmi); + } + + /** + * Returns information about a position on the line. + * + * @param offset the line-relative character offset, between 0 and the + * line length, inclusive + * @param trailing true to measure the trailing edge of the character + * before offset, false to measure the leading edge of the character + * at offset. + * @param fmi receives metrics information about the requested + * character, can be null. + * @return the signed offset from the leading margin to the requested + * character edge. + */ + float measure(int offset, boolean trailing, FontMetricsInt fmi) { + int target = trailing ? offset - 1 : offset; + if (target < 0) { + return 0; + } + + float h = 0; + + if (!mHasTabs) { + if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) { + return measureRun( 0, 0, offset, mLen, false, fmi); + } + if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) { + return measureRun(0, 0, offset, mLen, true, fmi); + } + } + + char[] chars = mChars; + int[] runs = mDirections.mDirections; + for (int i = 0; i < runs.length; i += 2) { + int runStart = runs[i]; + int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK); + if (runLimit > mLen) { + runLimit = mLen; + } + boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; + + int segstart = runStart; + for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { + int codept = 0; + Bitmap bm = null; + + if (mHasTabs && j < runLimit) { + codept = chars[j]; + if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) { + codept = Character.codePointAt(chars, j); + if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) { + bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept); + } else if (codept > 0xffff) { + ++j; + continue; + } + } + } + + if (j == runLimit || codept == '\t' || bm != null) { + boolean inSegment = target >= segstart && target < j; + + boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; + if (inSegment && advance) { + return h += measureRun(i, segstart, offset, j, runIsRtl, fmi); + } + + float w = measureRun(i, segstart, j, j, runIsRtl, fmi); + h += advance ? w : -w; + + if (inSegment) { + return h += measureRun(i, segstart, offset, j, runIsRtl, null); + } + + if (codept == '\t') { + if (offset == j) { + return h; + } + h = mDir * nextTab(h * mDir); + if (target == j) { + return h; + } + } + + if (bm != null) { + float bmAscent = ascent(j); + float wid = bm.getWidth() * -bmAscent / bm.getHeight(); + h += mDir * wid; + j++; + } + + segstart = j + 1; + } + } + } + + return h; + } + + /** + * Draws a unidirectional (but possibly multi-styled) run of text. + * + * @param c the canvas to draw on + * @param runIndex the index of this directional run + * @param start the line-relative start + * @param limit the line-relative limit + * @param runIsRtl true if the run is right-to-left + * @param x the position of the run that is closest to the leading margin + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + * @param needWidth true if the width value is required. + * @return the signed width of the run, based on the paragraph direction. + * Only valid if needWidth is true. + */ + private float drawRun(Canvas c, int runIndex, int start, + int limit, boolean runIsRtl, float x, int top, int y, int bottom, + boolean needWidth) { + + if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { + float w = -measureRun(runIndex, start, limit, limit, runIsRtl, null); + handleRun(runIndex, start, limit, limit, runIsRtl, c, x + w, top, + y, bottom, null, false, PREP_NONE); + return w; + } + + return handleRun(runIndex, start, limit, limit, runIsRtl, c, x, top, + y, bottom, null, needWidth, PREP_NEEDED); + } + + /** + * Measures a unidirectional (but possibly multi-styled) run of text. + * + * @param runIndex the run index + * @param start the line-relative start of the run + * @param offset the offset to measure to, between start and limit inclusive + * @param limit the line-relative limit of the run + * @param runIsRtl true if the run is right-to-left + * @param fmi receives metrics information about the requested + * run, can be null. + * @return the signed width from the start of the run to the leading edge + * of the character at offset, based on the run (not paragraph) direction + */ + private float measureRun(int runIndex, int start, + int offset, int limit, boolean runIsRtl, FontMetricsInt fmi) { + return handleRun(runIndex, start, offset, limit, runIsRtl, null, + 0, 0, 0, 0, fmi, true, PREP_NEEDED); + } + + /** + * Prepares a run for measurement or rendering. This ensures that any + * required shaping of the text in the run has been performed so that + * measurements reflect the shaped text. + * + * @param runIndex the run index + * @param start the line-relative start of the run + * @param limit the line-relative limit of the run + * @param runIsRtl true if the run is right-to-left + */ + private void prepRun(int runIndex, int start, int limit, + boolean runIsRtl) { + handleRun(runIndex, start, limit, limit, runIsRtl, null, 0, 0, 0, + 0, null, false, PREP_ONLY); + } + + /** + * Walk the cursor through this line, skipping conjuncts and + * zero-width characters. + * + * <p>This function cannot properly walk the cursor off the ends of the line + * since it does not know about any shaping on the previous/following line + * that might affect the cursor position. Callers must either avoid these + * situations or handle the result specially. + * + * <p>The paint is required because the region around the cursor might not + * have been formatted yet, and the valid positions can depend on the glyphs + * used to render the text, which in turn depends on the paint. + * + * @param paint the base paint of the line + * @param cursor the starting position of the cursor, between 0 and the + * length of the line, inclusive + * @param toLeft true if the caret is moving to the left. + * @return the new offset. If it is less than 0 or greater than the length + * of the line, the previous/following line should be examined to get the + * actual offset. + */ + int getOffsetToLeftRightOf(int cursor, boolean toLeft) { + // 1) The caret marks the leading edge of a character. The character + // logically before it might be on a different level, and the active caret + // position is on the character at the lower level. If that character + // was the previous character, the caret is on its trailing edge. + // 2) Take this character/edge and move it in the indicated direction. + // This gives you a new character and a new edge. + // 3) This position is between two visually adjacent characters. One of + // these might be at a lower level. The active position is on the + // character at the lower level. + // 4) If the active position is on the trailing edge of the character, + // the new caret position is the following logical character, else it + // is the character. + + int lineStart = 0; + int lineEnd = mLen; + boolean paraIsRtl = mDir == -1; + int[] runs = mDirections.mDirections; + + int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1; + boolean trailing = false; + + if (cursor == lineStart) { + runIndex = -2; + } else if (cursor == lineEnd) { + runIndex = runs.length; + } else { + // First, get information about the run containing the character with + // the active caret. + for (runIndex = 0; runIndex < runs.length; runIndex += 2) { + runStart = lineStart + runs[runIndex]; + if (cursor >= runStart) { + runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK); + if (runLimit > lineEnd) { + runLimit = lineEnd; + } + if (cursor < runLimit) { + runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) & + Layout.RUN_LEVEL_MASK; + if (cursor == runStart) { + // The caret is on a run boundary, see if we should + // use the position on the trailing edge of the previous + // logical character instead. + int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit; + int pos = cursor - 1; + for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) { + prevRunStart = lineStart + runs[prevRunIndex]; + if (pos >= prevRunStart) { + prevRunLimit = prevRunStart + + (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK); + if (prevRunLimit > lineEnd) { + prevRunLimit = lineEnd; + } + if (pos < prevRunLimit) { + prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) + & Layout.RUN_LEVEL_MASK; + if (prevRunLevel < runLevel) { + // Start from logically previous character. + runIndex = prevRunIndex; + runLevel = prevRunLevel; + runStart = prevRunStart; + runLimit = prevRunLimit; + trailing = true; + break; + } + } + } + } + } + break; + } + } + } + + // caret might be == lineEnd. This is generally a space or paragraph + // separator and has an associated run, but might be the end of + // text, in which case it doesn't. If that happens, we ran off the + // end of the run list, and runIndex == runs.length. In this case, + // we are at a run boundary so we skip the below test. + if (runIndex != runs.length) { + boolean runIsRtl = (runLevel & 0x1) != 0; + boolean advance = toLeft == runIsRtl; + if (cursor != (advance ? runLimit : runStart) || advance != trailing) { + // Moving within or into the run, so we can move logically. + prepRun(runIndex, runStart, runLimit, runIsRtl); + newCaret = getOffsetBeforeAfter(runIndex, cursor, advance); + // If the new position is internal to the run, we're at the strong + // position already so we're finished. + if (newCaret != (advance ? runLimit : runStart)) { + return newCaret; + } + } + } + } + + // If newCaret is -1, we're starting at a run boundary and crossing + // into another run. Otherwise we've arrived at a run boundary, and + // need to figure out which character to attach to. Note we might + // need to run this twice, if we cross a run boundary and end up at + // another run boundary. + while (true) { + boolean advance = toLeft == paraIsRtl; + int otherRunIndex = runIndex + (advance ? 2 : -2); + if (otherRunIndex >= 0 && otherRunIndex < runs.length) { + int otherRunStart = lineStart + runs[otherRunIndex]; + int otherRunLimit = otherRunStart + + (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK); + if (otherRunLimit > lineEnd) { + otherRunLimit = lineEnd; + } + int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) & + Layout.RUN_LEVEL_MASK; + boolean otherRunIsRtl = (otherRunLevel & 1) != 0; + + advance = toLeft == otherRunIsRtl; + if (newCaret == -1) { + prepRun(otherRunIndex, otherRunStart, otherRunLimit, + otherRunIsRtl); + newCaret = getOffsetBeforeAfter(otherRunIndex, + advance ? otherRunStart : otherRunLimit, advance); + if (newCaret == (advance ? otherRunLimit : otherRunStart)) { + // Crossed and ended up at a new boundary, + // repeat a second and final time. + runIndex = otherRunIndex; + runLevel = otherRunLevel; + continue; + } + break; + } + + // The new caret is at a boundary. + if (otherRunLevel < runLevel) { + // The strong character is in the other run. + newCaret = advance ? otherRunStart : otherRunLimit; + } + break; + } + + if (newCaret == -1) { + // We're walking off the end of the line. The paragraph + // level is always equal to or lower than any internal level, so + // the boundaries get the strong caret. + newCaret = getOffsetBeforeAfter(-1, cursor, advance); + break; + } + + // Else we've arrived at the end of the line. That's a strong position. + // We might have arrived here by crossing over a run with no internal + // breaks and dropping out of the above loop before advancing one final + // time, so reset the caret. + // Note, we use '<=' below to handle a situation where the only run + // on the line is a counter-directional run. If we're not advancing, + // we can end up at the 'lineEnd' position but the caret we want is at + // the lineStart. + if (newCaret <= lineEnd) { + newCaret = advance ? lineEnd : lineStart; + } + break; + } + + return newCaret; + } + + /** + * Returns the next valid offset within this directional run, skipping + * conjuncts and zero-width characters. This should not be called to walk + * off the end of the run. + * + * @param runIndex the run index + * @param offset the offset + * @param after true if the new offset should logically follow the provided + * offset + * @return the new offset + */ + private int getOffsetBeforeAfter(int runIndex, int offset, boolean after) { + // XXX note currently there is no special handling of zero-width + // combining marks, since the only analysis involves mock shaping. + + boolean offEnd = offset == (after ? mLen : 0); + if (runIndex >= 0 && !offEnd && mCharsValid) { + char[] chars = mChars; + if (after) { + int cp = Character.codePointAt(chars, offset, mLen); + if (cp >= 0x10000) { + ++offset; + } + while (++offset < mLen && chars[offset] == '\ufeff'){} + } else { + while (--offset >= 0 && chars[offset] == '\ufeff'){} + int cp = Character.codePointBefore(chars, offset + 1); + if (cp >= 0x10000) { + --offset; + } + } + return offset; + } + + if (after) { + return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart; + } + return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart; + } + + /** + * Utility function for measuring and rendering text. The text must + * not include a tab or emoji. + * + * @param wp the working paint + * @param start the start of the text + * @param limit the limit of the text + * @param runIsRtl true if the run is right-to-left + * @param c the canvas, can be null if rendering is not needed + * @param x the edge of the run closest to the leading margin + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + * @param fmi receives metrics information, can be null + * @param needWidth true if the width of the run is needed + * @return the signed width of the run based on the run direction; only + * valid if needWidth is true + */ + private float handleText(TextPaint wp, int start, int limit, + boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, + FontMetricsInt fmi, boolean needWidth) { + + float ret = 0; + + int runLen = limit - start; + if (needWidth || (c != null && (wp.bgColor != 0 || runIsRtl))) { + if (mCharsValid) { + ret = wp.measureText(mChars, start, runLen); + } else { + ret = wp.measureText(mText, mStart + start, + mStart + start + runLen); + } + } + + if (fmi != null) { + wp.getFontMetricsInt(fmi); + } + + if (c != null) { + if (runIsRtl) { + x -= ret; + } + + if (wp.bgColor != 0) { + int color = wp.getColor(); + Paint.Style s = wp.getStyle(); + wp.setColor(wp.bgColor); + wp.setStyle(Paint.Style.FILL); + + c.drawRect(x, top, x + ret, bottom, wp); + + wp.setStyle(s); + wp.setColor(color); + } + + drawTextRun(c, wp, start, limit, runIsRtl, x, y + wp.baselineShift); + } + + return runIsRtl ? -ret : ret; + } + + /** + * Utility function for measuring and rendering a replacement. + * + * @param replacement the replacement + * @param wp the work paint + * @param runIndex the run index + * @param start the start of the run + * @param limit the limit of the run + * @param runIsRtl true if the run is right-to-left + * @param c the canvas, can be null if not rendering + * @param x the edge of the replacement closest to the leading margin + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + * @param fmi receives metrics information, can be null + * @param needWidth true if the width of the replacement is needed + * @param prepFlags one of PREP_NONE, PREP_REQUIRED, or PREP_ONLY + * @return the signed width of the run based on the run direction; only + * valid if needWidth is true + */ + private float handleReplacement(ReplacementSpan replacement, TextPaint wp, + int runIndex, int start, int limit, boolean runIsRtl, Canvas c, + float x, int top, int y, int bottom, FontMetricsInt fmi, + boolean needWidth, int prepFlags) { + + float ret = 0; + + // Preparation replaces the first character of the series with the + // object-replacement character and the remainder with zero width + // non-break space aka BOM. Cursor movement code skips over the BOMs + // so that the replacement character is the only character 'seen'. + if (prepFlags != PREP_NONE && limit > start && + (runIndex > mPreppedIndex || + (runIndex == mPreppedIndex && start >= mPreppedLimit))) { + char[] chars = mChars; + chars[start] = '\ufffc'; + for (int i = start + 1; i < limit; ++i) { + chars[i] = '\ufeff'; // used as ZWNBS, marks positions to skip + } + mPreppedIndex = runIndex; + mPreppedLimit = limit; + } + + if (prepFlags != PREP_ONLY) { + int textStart = mStart + start; + int textLimit = mStart + limit; + + if (needWidth || (c != null && runIsRtl)) { + ret = replacement.getSize(wp, mText, textStart, textLimit, fmi); + } + + if (c != null) { + if (runIsRtl) { + x -= ret; + } + replacement.draw(c, mText, textStart, textLimit, + x, top, y, bottom, wp); + } + } + + return runIsRtl ? -ret : ret; + } + + /** + * Utility function for handling a unidirectional run. The run must not + * contain tabs or emoji but can contain styles. + * + * @param p the base paint + * @param runIndex the run index + * @param start the line-relative start of the run + * @param offset the offset to measure to, between start and limit inclusive + * @param limit the limit of the run + * @param runIsRtl true if the run is right-to-left + * @param c the canvas, can be null + * @param x the end of the run closest to the leading margin + * @param top the top of the line + * @param y the baseline + * @param bottom the bottom of the line + * @param fmi receives metrics information, can be null + * @param needWidth true if the width is required + * @param prepFlags one of PREP_NONE, PREP_REQUIRED, or PREP_ONLY + * @return the signed width of the run based on the run direction; only + * valid if needWidth is true + */ + private float handleRun(int runIndex, int start, int offset, + int limit, boolean runIsRtl, Canvas c, float x, int top, int y, + int bottom, FontMetricsInt fmi, boolean needWidth, int prepFlags) { + + // Shaping needs to take into account context up to metric boundaries, + // but rendering needs to take into account character style boundaries. + // So we iterate through metric runs, shape using the initial + // paint (the same typeface is used up to the next metric boundary), + // then within each metric run iterate through character style runs. + float ox = x; + for (int i = start, inext; i < offset; i = inext) { + TextPaint wp = mWorkPaint; + wp.set(mPaint); + + int mnext; + if (mSpanned == null) { + inext = limit; + mnext = offset; + } else { + inext = mSpanned.nextSpanTransition(mStart + i, mStart + limit, + MetricAffectingSpan.class) - mStart; + + mnext = inext < offset ? inext : offset; + MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + i, + mStart + mnext, MetricAffectingSpan.class); + + if (spans.length > 0) { + ReplacementSpan replacement = null; + for (int j = 0; j < spans.length; j++) { + MetricAffectingSpan span = spans[j]; + if (span instanceof ReplacementSpan) { + replacement = (ReplacementSpan)span; + } else { + span.updateDrawState(wp); // XXX or measureState? + } + } + + if (replacement != null) { + x += handleReplacement(replacement, wp, runIndex, i, + mnext, runIsRtl, c, x, top, y, bottom, fmi, + needWidth || mnext < offset, prepFlags); + continue; + } + } + } + + if (prepFlags != PREP_NONE) { + handlePrep(wp, runIndex, i, inext, runIsRtl); + } + + if (prepFlags != PREP_ONLY) { + if (mSpanned == null || c == null) { + x += handleText(wp, i, mnext, runIsRtl, c, x, top, + y, bottom, fmi, needWidth || mnext < offset); + } else { + for (int j = i, jnext; j < mnext; j = jnext) { + jnext = mSpanned.nextSpanTransition(mStart + j, + mStart + mnext, CharacterStyle.class) - mStart; + + CharacterStyle[] spans = mSpanned.getSpans(mStart + j, + mStart + jnext, CharacterStyle.class); + + wp.set(mPaint); + for (int k = 0; k < spans.length; k++) { + CharacterStyle span = spans[k]; + span.updateDrawState(wp); + } + + x += handleText(wp, j, jnext, runIsRtl, c, x, + top, y, bottom, fmi, needWidth || jnext < offset); + } + } + } + } + + return x - ox; + } + + private static final int PREP_NONE = 0; + private static final int PREP_NEEDED = 1; + private static final int PREP_ONLY = 2; + + /** + * Prepares text for measuring or rendering. + * + * @param paint the paint used to shape the text + * @param runIndex the run index + * @param start the start of the text to prepare + * @param limit the limit of the text to prepare + * @param runIsRtl true if the run is right-to-left + */ + private void handlePrep(TextPaint paint, int runIndex, int start, int limit, + boolean runIsRtl) { + + // The current implementation 'prepares' text by manipulating the + // character array. In order to keep track of what ranges have + // already been prepared, it uses the runIndex and the limit of + // the prepared text within that run. This index is required + // since operations that prepare the text always proceed in visual + // order and the limit itself does not let us know which runs have + // been processed and which have not. + // + // This bookkeeping is an attempt to let us process a line partially, + // for example, by only shaping up to the cursor position. This may + // not make sense if we can reuse the line, say by caching repeated + // accesses to the same line for both measuring and drawing, since in + // those cases we'd always prepare the entire line. At the + // opposite extreme, we might shape and then immediately discard only + // the run of text we're working with at the moment, instead of retaining + // the results of shaping (as the chars array is). In this case as well + // we would not need to do the index/limit bookkeeping. + // + // Technically, the only reason for bookkeeping is so that we don't + // re-mirror already-mirrored glyphs, since the shaping and object + // replacement operations will not change already-processed text. + + if (runIndex > mPreppedIndex || + (runIndex == mPreppedIndex && start >= mPreppedLimit)) { + if (runIsRtl) { + int runLen = limit - start; + AndroidCharacter.mirror(mChars, start, runLen); + ArabicShaping.SHAPER.shape(mChars, start, runLen); + + // Note: tweaked MockShaper to put '\ufeff' in place of + // alef when it forms lam-alef ligatures, so no extra + // processing is necessary here. + } + mPreppedIndex = runIndex; + mPreppedLimit = limit; + } + } + + /** + * Render a text run with the set-up paint. + * + * @param c the canvas + * @param wp the paint used to render the text + * @param start the run start + * @param limit the run limit + * @param runIsRtl true if the run is right-to-left + * @param x the x position of the left edge of the run + * @param y the baseline of the run + */ + private void drawTextRun(Canvas c, TextPaint wp, int start, int limit, + boolean runIsRtl, float x, int y) { + + int flags = runIsRtl ? Canvas.DIRECTION_RTL : Canvas.DIRECTION_LTR; + if (mCharsValid) { + c.drawTextRun(mChars, start, limit - start, x, y, flags, wp); + } else { + c.drawTextRun(mText, mStart + start, mStart + limit, x, y, flags, wp); + } + } + + /** + * Returns the ascent of the text at start. This is used for scaling + * emoji. + * + * @param pos the line-relative position + * @return the ascent of the text at start + */ + float ascent(int pos) { + if (mSpanned == null) { + return mPaint.ascent(); + } + + pos += mStart; + MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, + MetricAffectingSpan.class); + if (spans.length == 0) { + return mPaint.ascent(); + } + + TextPaint wp = mWorkPaint; + wp.set(mPaint); + for (MetricAffectingSpan span : spans) { + span.updateMeasureState(wp); + } + return wp.ascent(); + } + + /** + * Returns the next tab position. + * + * @param h the (unsigned) offset from the leading margin + * @return the (unsigned) tab position after this offset + */ + float nextTab(float h) { + float nh = Float.MAX_VALUE; + boolean alltabs = false; + + if (mHasTabs && mTabs != null) { + TabStopSpan[] tabs = mTabs; + for (int i = 0; i < tabs.length && tabs[i] != null; ++i) { + int where = tabs[i].getTabStop(); + if (where < nh && where > h) { + nh = where; + } + } + if (nh != Float.MAX_VALUE) { + return nh; + } + } + + return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT; + } + + private static final int TAB_INCREMENT = 20; +} diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 9589bf3..2d6c7b6 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -17,12 +17,11 @@ package android.text; import com.android.internal.R; +import com.android.internal.util.ArrayUtils; -import android.content.res.ColorStateList; import android.content.res.Resources; import android.os.Parcel; import android.os.Parcelable; -import android.text.method.TextKeyListener.Capitalize; import android.text.style.AbsoluteSizeSpan; import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; @@ -45,10 +44,8 @@ import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import android.util.Printer; -import com.android.internal.util.ArrayUtils; - -import java.util.regex.Pattern; import java.util.Iterator; +import java.util.regex.Pattern; public class TextUtils { private TextUtils() { /* cannot be instantiated */ } @@ -983,7 +980,7 @@ public class TextUtils { /** * Returns the original text if it fits in the specified width * given the properties of the specified Paint, - * or, if it does not fit, a copy with ellipsis character added + * or, if it does not fit, a copy with ellipsis character added * at the specified edge or center. * If <code>preserveLength</code> is specified, the returned copy * will be padded with zero-width spaces to preserve the original @@ -992,7 +989,7 @@ public class TextUtils { * report the start and end of the ellipsized range. */ public static CharSequence ellipsize(CharSequence text, - TextPaint p, + TextPaint paint, float avail, TruncateAt where, boolean preserveLength, EllipsizeCallback callback) { @@ -1003,13 +1000,12 @@ public class TextUtils { int len = text.length(); - // Use Paint.breakText() for the non-Spanned case to avoid having - // to allocate memory and accumulate the character widths ourselves. - - if (!(text instanceof Spanned)) { - float wid = p.measureText(text, 0, len); + MeasuredText mt = MeasuredText.obtain(); + try { + float width = setPara(mt, paint, text, 0, text.length(), + Layout.DIR_REQUEST_DEFAULT_LTR); - if (wid <= avail) { + if (width <= avail) { if (callback != null) { callback.ellipsized(0, 0); } @@ -1017,252 +1013,71 @@ public class TextUtils { return text; } - float ellipsiswid = p.measureText(sEllipsis); - - if (ellipsiswid > avail) { - if (callback != null) { - callback.ellipsized(0, len); - } - - if (preserveLength) { - char[] buf = obtain(len); - for (int i = 0; i < len; i++) { - buf[i] = '\uFEFF'; - } - String ret = new String(buf, 0, len); - recycle(buf); - return ret; - } else { - return ""; - } - } - - if (where == TruncateAt.START) { - int fit = p.breakText(text, 0, len, false, - avail - ellipsiswid, null); - - if (callback != null) { - callback.ellipsized(0, len - fit); - } - - if (preserveLength) { - return blank(text, 0, len - fit); - } else { - return sEllipsis + text.toString().substring(len - fit, len); - } + // XXX assumes ellipsis string does not require shaping and + // is unaffected by style + float ellipsiswid = paint.measureText(sEllipsis); + avail -= ellipsiswid; + + int left = 0; + int right = len; + if (avail < 0) { + // it all goes + } else if (where == TruncateAt.START) { + right = len - mt.breakText(0, len, false, avail); } else if (where == TruncateAt.END) { - int fit = p.breakText(text, 0, len, true, - avail - ellipsiswid, null); - - if (callback != null) { - callback.ellipsized(fit, len); - } - - if (preserveLength) { - return blank(text, fit, len); - } else { - return text.toString().substring(0, fit) + sEllipsis; - } - } else /* where == TruncateAt.MIDDLE */ { - int right = p.breakText(text, 0, len, false, - (avail - ellipsiswid) / 2, null); - float used = p.measureText(text, len - right, len); - int left = p.breakText(text, 0, len - right, true, - avail - ellipsiswid - used, null); - - if (callback != null) { - callback.ellipsized(left, len - right); - } - - if (preserveLength) { - return blank(text, left, len - right); - } else { - String s = text.toString(); - return s.substring(0, left) + sEllipsis + - s.substring(len - right, len); - } + left = mt.breakText(0, len, true, avail); + } else { + right = len - mt.breakText(0, len, false, avail / 2); + avail -= mt.measure(right, len); + left = mt.breakText(0, right, true, avail); } - } - - // But do the Spanned cases by hand, because it's such a pain - // to iterate the span transitions backwards and getTextWidths() - // will give us the information we need. - - // getTextWidths() always writes into the start of the array, - // so measure each span into the first half and then copy the - // results into the second half to use later. - - float[] wid = new float[len * 2]; - TextPaint temppaint = new TextPaint(); - Spanned sp = (Spanned) text; - - int next; - for (int i = 0; i < len; i = next) { - next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class); - - Styled.getTextWidths(p, temppaint, sp, i, next, wid, null); - System.arraycopy(wid, 0, wid, len + i, next - i); - } - - float sum = 0; - for (int i = 0; i < len; i++) { - sum += wid[len + i]; - } - if (sum <= avail) { if (callback != null) { - callback.ellipsized(0, 0); + callback.ellipsized(left, right); } - return text; - } - - float ellipsiswid = p.measureText(sEllipsis); - - if (ellipsiswid > avail) { - if (callback != null) { - callback.ellipsized(0, len); - } + char[] buf = mt.mChars; + Spanned sp = text instanceof Spanned ? (Spanned) text : null; + int remaining = len - (right - left); if (preserveLength) { - char[] buf = obtain(len); - for (int i = 0; i < len; i++) { + if (remaining > 0) { // else eliminate the ellipsis too + buf[left++] = '\u2026'; + } + for (int i = left; i < right; i++) { buf[i] = '\uFEFF'; } - SpannableString ss = new SpannableString(new String(buf, 0, len)); - recycle(buf); - copySpansFrom(sp, 0, len, Object.class, ss, 0); - return ss; - } else { - return ""; - } - } - - if (where == TruncateAt.START) { - sum = 0; - int i; - - for (i = len; i >= 0; i--) { - float w = wid[len + i - 1]; - - if (w + sum + ellipsiswid > avail) { - break; + String s = new String(buf, 0, len); + if (sp == null) { + return s; } - - sum += w; - } - - if (callback != null) { - callback.ellipsized(0, i); - } - - if (preserveLength) { - SpannableString ss = new SpannableString(blank(text, 0, i)); - copySpansFrom(sp, 0, len, Object.class, ss, 0); - return ss; - } else { - SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis); - out.insert(1, text, i, len); - - return out; - } - } else if (where == TruncateAt.END) { - sum = 0; - int i; - - for (i = 0; i < len; i++) { - float w = wid[len + i]; - - if (w + sum + ellipsiswid > avail) { - break; - } - - sum += w; - } - - if (callback != null) { - callback.ellipsized(i, len); - } - - if (preserveLength) { - SpannableString ss = new SpannableString(blank(text, i, len)); + SpannableString ss = new SpannableString(s); copySpansFrom(sp, 0, len, Object.class, ss, 0); return ss; - } else { - SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis); - out.insert(0, text, 0, i); - - return out; - } - } else /* where = TruncateAt.MIDDLE */ { - float lsum = 0, rsum = 0; - int left = 0, right = len; - - float ravail = (avail - ellipsiswid) / 2; - for (right = len; right >= 0; right--) { - float w = wid[len + right - 1]; - - if (w + rsum > ravail) { - break; - } - - rsum += w; } - float lavail = avail - ellipsiswid - rsum; - for (left = 0; left < right; left++) { - float w = wid[len + left]; - - if (w + lsum > lavail) { - break; - } - - lsum += w; + if (remaining == 0) { + return ""; } - if (callback != null) { - callback.ellipsized(left, right); + if (sp == null) { + StringBuilder sb = new StringBuilder(remaining + sEllipsis.length()); + sb.append(buf, 0, left); + sb.append(sEllipsis); + sb.append(buf, right, len - right); + return sb.toString(); } - if (preserveLength) { - SpannableString ss = new SpannableString(blank(text, left, right)); - copySpansFrom(sp, 0, len, Object.class, ss, 0); - return ss; - } else { - SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis); - out.insert(0, text, 0, left); - out.insert(out.length(), text, right, len); - - return out; - } + SpannableStringBuilder ssb = new SpannableStringBuilder(); + ssb.append(text, 0, left); + ssb.append(sEllipsis); + ssb.append(text, right, len); + return ssb; + } finally { + MeasuredText.recycle(mt); } } - private static String blank(CharSequence source, int start, int end) { - int len = source.length(); - char[] buf = obtain(len); - - if (start != 0) { - getChars(source, 0, start, buf, 0); - } - if (end != len) { - getChars(source, end, len, buf, end); - } - - if (start != end) { - buf[start] = '\u2026'; - - for (int i = start + 1; i < end; i++) { - buf[i] = '\uFEFF'; - } - } - - String ret = new String(buf, 0, len); - recycle(buf); - - return ret; - } - /** * Converts a CharSequence of the comma-separated form "Andy, Bob, * Charles, David" that is too wide to fit into the specified width @@ -1278,80 +1093,121 @@ public class TextUtils { TextPaint p, float avail, String oneMore, String more) { - int len = text.length(); - char[] buf = new char[len]; - TextUtils.getChars(text, 0, len, buf, 0); - int commaCount = 0; - for (int i = 0; i < len; i++) { - if (buf[i] == ',') { - commaCount++; + MeasuredText mt = MeasuredText.obtain(); + try { + int len = text.length(); + float width = setPara(mt, p, text, 0, len, Layout.DIR_REQUEST_DEFAULT_LTR); + if (width <= avail) { + return text; } - } - - float[] wid; - if (text instanceof Spanned) { - Spanned sp = (Spanned) text; - TextPaint temppaint = new TextPaint(); - wid = new float[len * 2]; + char[] buf = mt.mChars; - int next; - for (int i = 0; i < len; i = next) { - next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class); - - Styled.getTextWidths(p, temppaint, sp, i, next, wid, null); - System.arraycopy(wid, 0, wid, len + i, next - i); + int commaCount = 0; + for (int i = 0; i < len; i++) { + if (buf[i] == ',') { + commaCount++; + } } - System.arraycopy(wid, len, wid, 0, len); - } else { - wid = new float[len]; - p.getTextWidths(text, 0, len, wid); - } + int remaining = commaCount + 1; - int ok = 0; - int okRemaining = commaCount + 1; - String okFormat = ""; + int ok = 0; + int okRemaining = remaining; + String okFormat = ""; - int w = 0; - int count = 0; + int w = 0; + int count = 0; + float[] widths = mt.mWidths; - for (int i = 0; i < len; i++) { - w += wid[i]; + int request = mt.mDir == 1 ? Layout.DIR_REQUEST_LTR : + Layout.DIR_REQUEST_RTL; - if (buf[i] == ',') { - count++; + MeasuredText tempMt = MeasuredText.obtain(); + for (int i = 0; i < len; i++) { + w += widths[i]; - int remaining = commaCount - count + 1; - float moreWid; - String format; + if (buf[i] == ',') { + count++; - if (remaining == 1) { - format = " " + oneMore; - } else { - format = " " + String.format(more, remaining); - } + String format; + // XXX should not insert spaces, should be part of string + // XXX should use plural rules and not assume English plurals + if (--remaining == 1) { + format = " " + oneMore; + } else { + format = " " + String.format(more, remaining); + } - moreWid = p.measureText(format); + // XXX this is probably ok, but need to look at it more + tempMt.setPara(format, 0, format.length(), request); + float moreWid = mt.addStyleRun(p, mt.mLen, null); - if (w + moreWid <= avail) { - ok = i + 1; - okRemaining = remaining; - okFormat = format; + if (w + moreWid <= avail) { + ok = i + 1; + okRemaining = remaining; + okFormat = format; + } } } - } + MeasuredText.recycle(tempMt); - if (w <= avail) { - return text; - } else { SpannableStringBuilder out = new SpannableStringBuilder(okFormat); out.insert(0, text, 0, ok); return out; + } finally { + MeasuredText.recycle(mt); } } + private static float setPara(MeasuredText mt, TextPaint paint, + CharSequence text, int start, int end, int bidiRequest) { + + mt.setPara(text, start, end, bidiRequest); + + float width; + Spanned sp = text instanceof Spanned ? (Spanned) text : null; + int len = end - start; + if (sp == null) { + width = mt.addStyleRun(paint, len, null); + } else { + width = 0; + int spanEnd; + for (int spanStart = 0; spanStart < len; spanStart = spanEnd) { + spanEnd = sp.nextSpanTransition(spanStart, len, + MetricAffectingSpan.class); + MetricAffectingSpan[] spans = sp.getSpans( + spanStart, spanEnd, MetricAffectingSpan.class); + width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null); + } + } + + return width; + } + + private static final char FIRST_RIGHT_TO_LEFT = '\u0590'; + + /* package */ + static boolean doesNotNeedBidi(CharSequence s, int start, int end) { + for (int i = start; i < end; i++) { + if (s.charAt(i) >= FIRST_RIGHT_TO_LEFT) { + return false; + } + } + return true; + } + + /* package */ + static boolean doesNotNeedBidi(char[] text, int start, int len) { + for (int i = start, e = i + len; i < e; i++) { + if (text[i] >= FIRST_RIGHT_TO_LEFT) { + return false; + } + } + return true; + } + /* package */ static char[] obtain(int len) { char[] buf; @@ -1529,7 +1385,7 @@ public class TextUtils { */ public static final int CAP_MODE_CHARACTERS = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; - + /** * Capitalization mode for {@link #getCapsMode}: capitalize the first * character of all words. This value is explicitly defined to be the same as @@ -1537,7 +1393,7 @@ public class TextUtils { */ public static final int CAP_MODE_WORDS = InputType.TYPE_TEXT_FLAG_CAP_WORDS; - + /** * Capitalization mode for {@link #getCapsMode}: capitalize the first * character of each sentence. This value is explicitly defined to be the same as @@ -1545,13 +1401,13 @@ public class TextUtils { */ public static final int CAP_MODE_SENTENCES = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; - + /** * Determine what caps mode should be in effect at the current offset in * the text. Only the mode bits set in <var>reqModes</var> will be * checked. Note that the caps mode flags here are explicitly defined * to match those in {@link InputType}. - * + * * @param cs The text that should be checked for caps modes. * @param off Location in the text at which to check. * @param reqModes The modes to be checked: may be any combination of @@ -1651,7 +1507,7 @@ public class TextUtils { return mode; } - + private static Object sLock = new Object(); private static char[] sTemp = null; } diff --git a/core/java/android/util/Patterns.java b/core/java/android/util/Patterns.java index 5cbfd29..3bcd266 100644 --- a/core/java/android/util/Patterns.java +++ b/core/java/android/util/Patterns.java @@ -25,7 +25,7 @@ import java.util.regex.Pattern; public class Patterns { /** * Regular expression to match all IANA top-level domains. - * List accurate as of 2010/02/05. List taken from: + * List accurate as of 2010/05/06. List taken from: * http://data.iana.org/TLD/tlds-alpha-by-domain.txt * This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py */ @@ -53,8 +53,8 @@ public class Patterns { + "|u[agksyz]" + "|v[aceginu]" + "|w[fs]" - + "|(xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-zckzah)" - + "|y[etu]" + + "|(xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-mgbaam7a8h|xn\\-\\-mgberp4a5d4ar|xn\\-\\-wgbh1c|xn\\-\\-zckzah)" + + "|y[et]" + "|z[amw])"; /** @@ -65,7 +65,7 @@ public class Patterns { /** * Regular expression to match all IANA top-level domains for WEB_URL. - * List accurate as of 2010/02/05. List taken from: + * List accurate as of 2010/05/06. List taken from: * http://data.iana.org/TLD/tlds-alpha-by-domain.txt * This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py */ @@ -94,8 +94,8 @@ public class Patterns { + "|u[agksyz]" + "|v[aceginu]" + "|w[fs]" - + "|(?:xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-zckzah)" - + "|y[etu]" + + "|(?:xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-mgbaam7a8h|xn\\-\\-mgberp4a5d4ar|xn\\-\\-wgbh1c|xn\\-\\-zckzah)" + + "|y[et]" + "|z[amw]))"; /** diff --git a/core/java/android/view/ActionBarView.java b/core/java/android/view/ActionBarView.java new file mode 100644 index 0000000..311274c --- /dev/null +++ b/core/java/android/view/ActionBarView.java @@ -0,0 +1,659 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import java.util.ArrayList; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.ActionBar.Callback; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.TypedArray; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.SparseArray; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.internal.R; +import com.android.internal.view.menu.ActionMenu; +import com.android.internal.view.menu.ActionMenuItem; + +/** + * @hide + */ +public class ActionBarView extends ViewGroup { + private static final String TAG = "ActionBarView"; + + // TODO: This must be defined in the default theme + private static final int CONTENT_HEIGHT_DIP = 50; + private static final int CONTENT_PADDING_DIP = 3; + private static final int CONTENT_SPACING_DIP = 6; + private static final int CONTENT_ACTION_SPACING_DIP = 12; + + /** + * Display options applied by default + */ + public static final int DISPLAY_DEFAULT = 0; + + /** + * Display options that require re-layout as opposed to a simple invalidate + */ + private static final int DISPLAY_RELAYOUT_MASK = + ActionBar.DISPLAY_HIDE_HOME | + ActionBar.DISPLAY_USE_LOGO; + + private final int mContentHeight; + + private int mNavigationMode; + private int mDisplayOptions; + private int mSpacing; + private int mActionSpacing; + private CharSequence mTitle; + private CharSequence mSubtitle; + private Drawable mIcon; + private Drawable mLogo; + private Drawable mDivider; + + private ImageView mIconView; + private ImageView mLogoView; + private TextView mTitleView; + private TextView mSubtitleView; + private View mNavigationView; + + private boolean mShowMenu; + + private ActionMenuItem mLogoNavItem; + private ActionMenu mNavMenu; + private ActionMenu mActionMenu; + private ActionMenu mOptionsMenu; + + private SparseArray<ActionMenu> mContextMenus; + + private Callback mCallback; + + private final ArrayList<ActionView> mActions = new ArrayList<ActionView>(); + private final OnClickListener mActionClickHandler = new OnClickListener() { + public void onClick(View v) { + ActionView av = (ActionView) v; + ActionMenuItem item = (ActionMenuItem) av.menuItem; + + if (!mCallback.onActionItemSelected(item)) { + item.invoke(); + } + } + }; + + private OnClickListener mHomeClickListener = null; + + public ActionBarView(Context context, AttributeSet attrs) { + super(context, attrs); + + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + mContentHeight = (int) (CONTENT_HEIGHT_DIP * metrics.density + 0.5f); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ActionBar); + + final int colorFilter = a.getColor(R.styleable.ActionBar_colorFilter, 0); + + if (colorFilter != 0) { + final Drawable d = getBackground(); + d.setDither(true); + d.setColorFilter(new PorterDuffColorFilter(colorFilter, PorterDuff.Mode.OVERLAY)); + } + + ApplicationInfo info = context.getApplicationInfo(); + PackageManager pm = context.getPackageManager(); + mNavigationMode = a.getInt(R.styleable.ActionBar_navigationMode, ActionBar.NAVIGATION_MODE_NORMAL); + mTitle = a.getText(R.styleable.ActionBar_title); + mSubtitle = a.getText(R.styleable.ActionBar_subtitle); + mDisplayOptions = a.getInt(R.styleable.ActionBar_displayOptions, DISPLAY_DEFAULT); + + mLogo = a.getDrawable(R.styleable.ActionBar_logo); + if (mLogo == null) { + mLogo = info.loadLogo(pm); + } + mIcon = a.getDrawable(R.styleable.ActionBar_icon); + if (mIcon == null) { + mIcon = info.loadIcon(pm); + } + mDivider = a.getDrawable(R.styleable.ActionBar_divider); + + Drawable background = a.getDrawable(R.styleable.ActionBar_background); + if (background != null) { + setBackgroundDrawable(background); + } + + final int customNavId = a.getResourceId(R.styleable.ActionBar_customNavigationLayout, 0); + if (customNavId != 0) { + LayoutInflater inflater = LayoutInflater.from(context); + mNavigationView = (View) inflater.inflate(customNavId, null); + mNavigationMode = ActionBar.NAVIGATION_MODE_CUSTOM; + } + + a.recycle(); + + // TODO: Set this in the theme + int padding = (int) (CONTENT_PADDING_DIP * metrics.density + 0.5f); + setPadding(padding, padding, padding, padding); + + mSpacing = (int) (CONTENT_SPACING_DIP * metrics.density + 0.5f); + mActionSpacing = (int) (CONTENT_ACTION_SPACING_DIP * metrics.density + 0.5f); + + if (mLogo != null || mIcon != null || mTitle != null) { + mLogoNavItem = new ActionMenuItem(context, 0, android.R.id.home, 0, 0, mTitle); + mHomeClickListener = new OnClickListener() { + public void onClick(View v) { + if (mCallback != null) { + mCallback.onActionItemSelected(mLogoNavItem); + } + } + }; + } + + mContextMenus = new SparseArray<ActionMenu>(); + } + + private boolean initOptionsMenu() { + final Context context = getContext(); + if (!(context instanceof Activity)) { + return false; + } + + final Activity activity = (Activity) context; + ActionMenu optionsMenu = new ActionMenu(context); + if (activity.onCreateOptionsMenu(optionsMenu)) { + mOptionsMenu = optionsMenu; + return true; + } + + return false; + } + + public void setCallback(Callback callback) { + final Context context = getContext(); + mCallback = callback; + + ActionMenu actionMenu = new ActionMenu(context); + if (callback.onCreateActionMenu(actionMenu)) { + mActionMenu = actionMenu; + performUpdateActionMenu(); + } + } + + public void setCustomNavigationView(View view) { + mNavigationView = view; + if (view != null) { + setNavigationMode(ActionBar.NAVIGATION_MODE_CUSTOM); + } + } + + public void setDividerDrawable(Drawable d) { + mDivider = d; + } + + public CharSequence getTitle() { + return mTitle; + } + + public void setTitle(CharSequence title) { + mTitle = title; + if (mTitleView != null) { + mTitleView.setText(title); + } + if (mLogoNavItem != null) { + mLogoNavItem.setTitle(title); + } + } + + public CharSequence getSubtitle() { + return mSubtitle; + } + + public void setSubtitle(CharSequence subtitle) { + mSubtitle = subtitle; + if (mSubtitleView != null) { + mSubtitleView.setText(subtitle); + } + } + + public void setDisplayOptions(int options) { + final int flagsChanged = options ^ mDisplayOptions; + mDisplayOptions = options; + if ((flagsChanged & DISPLAY_RELAYOUT_MASK) != 0) { + final int vis = (options & ActionBar.DISPLAY_HIDE_HOME) != 0 ? GONE : VISIBLE; + if (mLogoView != null) { + mLogoView.setVisibility(vis); + } + if (mIconView != null) { + mIconView.setVisibility(vis); + } + + requestLayout(); + } else { + invalidate(); + } + } + + public void setNavigationMode(int mode) { + final int oldMode = mNavigationMode; + if (mode != oldMode) { + switch (oldMode) { + case ActionBar.NAVIGATION_MODE_NORMAL: + if (mTitleView != null) { + removeView(mTitleView); + mTitleView = null; + } + break; + case ActionBar.NAVIGATION_MODE_CUSTOM: + if (mNavigationView != null) { + removeView(mNavigationView); + mNavigationView = null; + } + } + + switch (mode) { + case ActionBar.NAVIGATION_MODE_NORMAL: + initTitle(); + break; + case ActionBar.NAVIGATION_MODE_CUSTOM: + addView(mNavigationView); + break; + } + mNavigationMode = mode; + requestLayout(); + } + } + + public View getCustomNavigationView() { + return mNavigationView; + } + + public int getNavigationMode() { + return mNavigationMode; + } + + public int getDisplayOptions() { + return mDisplayOptions; + } + + private ActionView findActionViewForItem(MenuItem item) { + final ArrayList<ActionView> actions = mActions; + final int actionCount = actions.size(); + for (int i = 0; i < actionCount; i++) { + ActionView av = actions.get(i); + if (av.menuItem.equals(item)) { + return av; + } + } + return null; + } + + public void setContextMode(int mode) { + Callback callback = mCallback; + if (callback == null) { + throw new IllegalStateException( + "Attempted to set ActionBar context mode with no callback"); + } + + ActionMenu menu = mContextMenus.get(mode); + if (menu == null) { + // Initialize the new mode + menu = new ActionMenu(getContext()); + + if (!callback.onCreateContextMode(mode, menu)) { + throw new IllegalArgumentException( + "ActionBar callback does not know how to create context mode " + mode); + } + mContextMenus.put(mode, menu); + } + + if (callback.onPrepareContextMode(mode, menu)) { + // TODO Set mode, animate, etc. + } + } + + public void exitContextMode() { + // TODO Turn off context mode; go back to normal. + } + + public void updateActionMenu() { + final ActionMenu menu = mActionMenu; + if (menu == null || mCallback == null || !mCallback.onUpdateActionMenu(menu)) { + return; + } + performUpdateActionMenu(); + } + + private void performUpdateActionMenu() { + final ActionMenu menu = mActionMenu; + if (menu == null) { + return; + } + final Context context = getContext(); + + int childCount = getChildCount(); + int childIndex = 0; + while (childIndex < childCount) { + View v = getChildAt(childIndex); + if (v instanceof ActionView) { + detachViewFromParent(childIndex); + childCount--; + } else { + childIndex++; + } + } + + ArrayList<ActionView> detachedViews = new ArrayList<ActionView>(mActions); + final int itemCount = menu.size(); + for (int i = 0; i < itemCount; i++) { + final MenuItem item = menu.getItem(i); + + boolean newView = false; + ActionView actionView = findActionViewForItem(item); + if (actionView == null) { + actionView = new ActionView(context); + newView = true; + } + actionView.actionId = item.getItemId(); + actionView.menuItem = item; + actionView.actionLabel = item.getTitle(); + actionView.setAdjustViewBounds(true); + actionView.setImageDrawable(item.getIcon()); + actionView.setFocusable(true); + actionView.setOnClickListener(mActionClickHandler); + + LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT, LayoutParams.ITEM_TYPE_ACTION); + actionView.setLayoutParams(layoutParams); + + if (newView) { + addView(actionView); + mActions.add(actionView); + } else { + attachViewToParent(actionView, -1, layoutParams); + detachedViews.remove(actionView); + actionView.invalidate(); + } + } + + final int detachedCount = detachedViews.size(); + for (int i = 0; i < detachedCount; i++) { + removeDetachedView(detachedViews.get(i), false); + } + + requestLayout(); + } + + public void addAction(int id, Drawable icon, CharSequence label, OnActionListener listener) { + ActionView actionView = new ActionView(getContext()); + actionView.actionId = id; + actionView.actionLabel = label; + actionView.actionListener = listener; + actionView.setAdjustViewBounds(true); + actionView.setImageDrawable(icon); + actionView.setOnClickListener(mActionClickHandler); + + actionView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT, LayoutParams.ITEM_TYPE_ACTION)); + + addView(actionView); + mActions.add(actionView); + + requestLayout(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + if ((mDisplayOptions & ActionBar.DISPLAY_HIDE_HOME) == 0) { + if (mLogo != null && (mDisplayOptions & ActionBar.DISPLAY_USE_LOGO) != 0) { + mLogoView = new ImageView(getContext()); + mLogoView.setAdjustViewBounds(true); + mLogoView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT, LayoutParams.ITEM_TYPE_ICON)); + mLogoView.setImageDrawable(mLogo); + mLogoView.setClickable(true); + mLogoView.setFocusable(true); + mLogoView.setOnClickListener(mHomeClickListener); + addView(mLogoView); + } else if (mIcon != null) { + mIconView = new ImageView(getContext()); + mIconView.setAdjustViewBounds(true); + mIconView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT, LayoutParams.ITEM_TYPE_ICON)); + mIconView.setImageDrawable(mIcon); + mIconView.setClickable(true); + mIconView.setFocusable(true); + mIconView.setOnClickListener(mHomeClickListener); + addView(mIconView); + } + } + + switch (mNavigationMode) { + case ActionBar.NAVIGATION_MODE_NORMAL: + if (mLogoView == null) { + initTitle(); + } + break; + + case ActionBar.NAVIGATION_MODE_DROPDOWN_LIST: + throw new UnsupportedOperationException( + "Dropdown list navigation isn't supported yet!"); + + case ActionBar.NAVIGATION_MODE_TABS: + throw new UnsupportedOperationException( + "Tab navigation isn't supported yet!"); + + case ActionBar.NAVIGATION_MODE_CUSTOM: + if (mNavigationView != null) { + addView(mNavigationView); + } + break; + } + } + + private void initTitle() { + LayoutInflater inflater = LayoutInflater.from(getContext()); + mTitleView = (TextView) inflater.inflate(R.layout.action_bar_title_item, null); + mTitleView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT, LayoutParams.ITEM_TYPE_TITLE)); + if (mTitle != null) { + mTitleView.setText(mTitle); + } + addView(mTitleView); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + if (widthMode != MeasureSpec.EXACTLY) { + throw new IllegalStateException(getClass().getSimpleName() + " can only be used " + + "with android:layout_width=\"match_parent\" (or fill_parent)"); + } + + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode != MeasureSpec.AT_MOST) { + throw new IllegalStateException(getClass().getSimpleName() + " can only be used " + + "with android:layout_height=\"wrap_content\""); + } + + int contentWidth = MeasureSpec.getSize(widthMeasureSpec); + + int availableWidth = contentWidth - getPaddingLeft() - getPaddingRight(); + final int height = mContentHeight - getPaddingTop() - getPaddingBottom(); + final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); + + if (mLogoView != null && mLogoView.getVisibility() != GONE) { + availableWidth = measureChildView(mLogoView, availableWidth, childSpecHeight, mSpacing); + } + if (mIconView != null && mIconView.getVisibility() != GONE) { + availableWidth = measureChildView(mIconView, availableWidth, childSpecHeight, mSpacing); + } + + final ArrayList<ActionView> actions = mActions; + final int actionCount = actions.size(); + for (int i = 0; i < actionCount; i++) { + ActionView action = actions.get(i); + availableWidth = measureChildView(action, availableWidth, + childSpecHeight, mActionSpacing); + } + + switch (mNavigationMode) { + case ActionBar.NAVIGATION_MODE_NORMAL: + if (mTitleView != null) { + measureChildView(mTitleView, availableWidth, childSpecHeight, mSpacing); + } + break; + case ActionBar.NAVIGATION_MODE_CUSTOM: + if (mNavigationView != null) { + mNavigationView.measure( + MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + } + break; + } + + setMeasuredDimension(contentWidth, mContentHeight); + } + + private int measureChildView(View child, int availableWidth, int childSpecHeight, int spacing) { + child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), + childSpecHeight); + + availableWidth -= child.getMeasuredWidth(); + availableWidth -= spacing; + + return availableWidth; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int x = getPaddingLeft(); + final int y = getPaddingTop(); + final int contentHeight = b - t - getPaddingTop() - getPaddingBottom(); + + if (mLogoView != null && mLogoView.getVisibility() != GONE) { + x += positionChild(mLogoView, x, y, contentHeight) + mSpacing; + } + if (mIconView != null && mIconView.getVisibility() != GONE) { + x += positionChild(mIconView, x, y, contentHeight) + mSpacing; + } + + switch (mNavigationMode) { + case ActionBar.NAVIGATION_MODE_NORMAL: + if (mTitleView != null) { + x += positionChild(mTitleView, x, y, contentHeight) + mSpacing; + } + break; + + case ActionBar.NAVIGATION_MODE_CUSTOM: + if (mNavigationView != null) { + x += positionChild(mNavigationView, x, y, contentHeight) + mSpacing; + } + break; + } + + x = r - l - getPaddingRight(); + + final int count = mActions.size(); + for (int i = count - 1; i >= 0; i--) { + ActionView action = mActions.get(i); + x -= (positionChildInverse(action, x, y, contentHeight) + mActionSpacing); + } + } + + private int positionChild(View child, int x, int y, int contentHeight) { + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + int childTop = y + (contentHeight - childHeight) / 2; + + child.layout(x, childTop, x + childWidth, childTop + childHeight); + + return childWidth; + } + + private int positionChildInverse(View child, int x, int y, int contentHeight) { + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + int childTop = y + (contentHeight - childHeight) / 2; + + child.layout(x - childWidth, childTop, x, childTop + childHeight); + + return childWidth; + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new ViewGroup.LayoutParams(getContext(), attrs); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p != null && p instanceof LayoutParams; + } + + private static class LayoutParams extends ViewGroup.LayoutParams { + static final int ITEM_TYPE_UNKNOWN = -1; + static final int ITEM_TYPE_ICON = 0; + static final int ITEM_TYPE_TITLE = 1; + static final int ITEM_TYPE_CUSTOM_NAV = 2; + static final int ITEM_TYPE_ACTION = 3; + static final int ITEM_TYPE_MORE = 4; + + int type = ITEM_TYPE_UNKNOWN; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(int width, int height, int type) { + this(width, height); + this.type = type; + } + + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + } + + public interface OnActionListener { + void onAction(int id); + } + + private static class ActionView extends ImageView { + int actionId; + CharSequence actionLabel; + OnActionListener actionListener; + MenuItem menuItem; + + public ActionView(Context context) { + super(context); + } + } +} diff --git a/core/java/android/view/MenuInflater.java b/core/java/android/view/MenuInflater.java index 46c805c..a37d83d 100644 --- a/core/java/android/view/MenuInflater.java +++ b/core/java/android/view/MenuInflater.java @@ -16,9 +16,8 @@ package android.view; -import com.android.internal.view.menu.MenuItemImpl; - import java.io.IOException; +import java.lang.reflect.Method; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -30,6 +29,8 @@ import android.content.res.XmlResourceParser; import android.util.AttributeSet; import android.util.Xml; +import com.android.internal.view.menu.MenuItemImpl; + /** * This class is used to instantiate menu XML files into Menu objects. * <p> @@ -166,6 +167,41 @@ public class MenuInflater { } } + private static class InflatedOnMenuItemClickListener + implements MenuItem.OnMenuItemClickListener { + private static final Class[] PARAM_TYPES = new Class[] { MenuItem.class }; + + private Context mContext; + private Method mMethod; + + public InflatedOnMenuItemClickListener(Context context, String methodName) { + mContext = context; + Class c = context.getClass(); + try { + mMethod = c.getMethod(methodName, PARAM_TYPES); + } catch (Exception e) { + InflateException ex = new InflateException( + "Couldn't resolve menu item onClick handler " + methodName + + " in class " + c.getName()); + ex.initCause(e); + throw ex; + } + } + + public boolean onMenuItemClick(MenuItem item) { + try { + if (mMethod.getReturnType() == Boolean.TYPE) { + return (Boolean) mMethod.invoke(mContext, item); + } else { + mMethod.invoke(mContext, item); + return true; + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + /** * State for the current menu. * <p> @@ -205,6 +241,8 @@ public class MenuInflater { private boolean itemVisible; private boolean itemEnabled; + private String itemListenerMethodName; + private static final int defaultGroupId = NO_ID; private static final int defaultItemId = NO_ID; private static final int defaultItemCategory = 0; @@ -276,6 +314,7 @@ public class MenuInflater { itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked); itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible); itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled); + itemListenerMethodName = a.getString(com.android.internal.R.styleable.MenuItem_onClick); a.recycle(); @@ -299,8 +338,13 @@ public class MenuInflater { .setIcon(itemIconResId) .setAlphabeticShortcut(itemAlphabeticShortcut) .setNumericShortcut(itemNumericShortcut); + + if (itemListenerMethodName != null) { + item.setOnMenuItemClickListener( + new InflatedOnMenuItemClickListener(mContext, itemListenerMethodName)); + } - if (itemCheckable >= 2) { + if (itemCheckable >= 2 && item instanceof MenuItemImpl) { ((MenuItemImpl) item).setExclusiveCheckable(true); } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 11e5ad1..527b4f4 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -887,6 +887,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility public static final int HAPTIC_FEEDBACK_ENABLED = 0x10000000; /** + * <p>Indicates that the view hierarchy should stop saving state when + * it reaches this view. If state saving is initiated immediately at + * the view, it will be allowed. + * {@hide} + */ + static final int PARENT_SAVE_DISABLED = 0x20000000; + + /** + * <p>Mask for use with setFlags indicating bits used for PARENT_SAVE_DISABLED.</p> + * {@hide} + */ + static final int PARENT_SAVE_DISABLED_MASK = 0x20000000; + + /** * View flag indicating whether {@link #addFocusables(ArrayList, int, int)} * should add all focusable Views regardless if they are focusable in touch mode. */ @@ -3342,6 +3356,38 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** + * Indicates whether the entire hierarchy under this view will save its + * state when a state saving traversal occurs from its parent. The default + * is true; if false, these views will not be saved unless + * {@link #saveHierarchyState(SparseArray)} is called directly on this view. + * + * @return Returns true if the view state saving from parent is enabled, else false. + * + * @see #setSaveFromParentEnabled(boolean) + */ + public boolean isSaveFromParentEnabled() { + return (mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED; + } + + /** + * Controls whether the entire hierarchy under this view will save its + * state when a state saving traversal occurs from its parent. The default + * is true; if false, these views will not be saved unless + * {@link #saveHierarchyState(SparseArray)} is called directly on this view. + * + * @param enabled Set to false to <em>disable</em> state saving, or true + * (the default) to allow it. + * + * @see #isSaveFromParentEnabled() + * @see #setId(int) + * @see #onSaveInstanceState() + */ + public void setSaveFromParentEnabled(boolean enabled) { + setFlags(enabled ? 0 : PARENT_SAVE_DISABLED, PARENT_SAVE_DISABLED_MASK); + } + + + /** * Returns whether this View is able to take focus. * * @return True if this view can take focus, or false otherwise. diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index eca583f..9329d94 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -1181,7 +1181,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int count = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < count; i++) { - children[i].dispatchSaveInstanceState(container); + View c = children[i]; + if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) { + c.dispatchSaveInstanceState(container); + } } } @@ -1206,7 +1209,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int count = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < count; i++) { - children[i].dispatchRestoreInstanceState(container); + View c = children[i]; + if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) { + c.dispatchRestoreInstanceState(container); + } } } @@ -2869,7 +2875,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ @ViewDebug.ExportedProperty(mapping = { @ViewDebug.IntToString(from = PERSISTENT_NO_CACHE, to = "NONE"), - @ViewDebug.IntToString(from = PERSISTENT_ALL_CACHES, to = "ANIMATION"), + @ViewDebug.IntToString(from = PERSISTENT_ANIMATION_CACHE, to = "ANIMATION"), @ViewDebug.IntToString(from = PERSISTENT_SCROLLING_CACHE, to = "SCROLLING"), @ViewDebug.IntToString(from = PERSISTENT_ALL_CACHES, to = "ALL") }) diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 234deba..bbd9f04 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -61,6 +61,13 @@ public abstract class Window { @hide */ public static final int FEATURE_OPENGL = 8; + /** + * Flag for enabling the Action Bar. + * This is enabled by default for some devices. The Action Bar + * replaces the title bar and provides an alternate location + * for an on-screen menu button on some devices. + */ + public static final int FEATURE_ACTION_BAR = 9; /** Flag for setting the progress bar's visibility to VISIBLE */ public static final int PROGRESS_VISIBILITY_ON = -1; /** Flag for setting the progress bar's visibility to GONE */ @@ -989,6 +996,16 @@ public abstract class Window { { return mFeatures; } + + /** + * Query for the availability of a certain feature. + * + * @param feature The feature ID to check + * @return true if the feature is enabled, false otherwise. + */ + public boolean hasFeature(int feature) { + return (getFeatures() & (1 << feature)) != 0; + } /** * Return the feature bits that are being implemented by this Window. diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index c22f991..fc61700 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -622,6 +622,7 @@ public final class AccessibilityEvent implements Parcelable { mPackageName = null; mContentDescription = null; mBeforeText = null; + mParcelableData = null; mText.clear(); } diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 0186270..f406da9 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -94,7 +94,9 @@ public final class AccessibilityManager { public static AccessibilityManager getInstance(Context context) { synchronized (sInstanceSync) { if (sInstance == null) { - sInstance = new AccessibilityManager(context); + IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE); + IAccessibilityManager service = IAccessibilityManager.Stub.asInterface(iBinder); + sInstance = new AccessibilityManager(context, service); } } return sInstance; @@ -104,13 +106,16 @@ public final class AccessibilityManager { * Create an instance. * * @param context A {@link Context}. + * @param service An interface to the backing service. + * + * @hide */ - private AccessibilityManager(Context context) { + public AccessibilityManager(Context context, IAccessibilityManager service) { mHandler = new MyHandler(context.getMainLooper()); - IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE); - mService = IAccessibilityManager.Stub.asInterface(iBinder); + mService = service; + try { - mService.addClient(mClient); + mIsEnabled = mService.addClient(mClient); } catch (RemoteException re) { Log.e(LOG_TAG, "AccessibilityManagerService is dead", re); } @@ -128,6 +133,18 @@ public final class AccessibilityManager { } /** + * Returns the client interface this instance registers in + * the centralized accessibility manager service. + * + * @return The client. + * + * @hide + */ + public IAccessibilityManagerClient getClient() { + return (IAccessibilityManagerClient) mClient.asBinder(); + } + + /** * Sends an {@link AccessibilityEvent}. If this {@link AccessibilityManager} is not * enabled the call is a NOOP. * diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 32788be..7633569 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -29,7 +29,7 @@ import android.content.pm.ServiceInfo; */ interface IAccessibilityManager { - void addClient(IAccessibilityManagerClient client); + boolean addClient(IAccessibilityManagerClient client); boolean sendAccessibilityEvent(in AccessibilityEvent uiEvent); diff --git a/core/java/android/webkit/AccessibilityInjector.java b/core/java/android/webkit/AccessibilityInjector.java new file mode 100644 index 0000000..49ddc19 --- /dev/null +++ b/core/java/android/webkit/AccessibilityInjector.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.view.KeyEvent; +import android.view.accessibility.AccessibilityEvent; +import android.webkit.WebViewCore.EventHub; + +/** + * This class injects accessibility into WebViews with disabled JavaScript or + * WebViews with enabled JavaScript but for which we have no accessibility + * script to inject. + */ +class AccessibilityInjector { + + // Handle to the WebView this injector is associated with. + private final WebView mWebView; + + /** + * Creates a new injector associated with a given VwebView. + * + * @param webView The associated WebView. + */ + public AccessibilityInjector(WebView webView) { + mWebView = webView; + } + + /** + * Processes a key down <code>event</code>. + * + * @return True if the event was processed. + */ + public boolean onKeyEvent(KeyEvent event) { + + // as a proof of concept let us do the simplest example + + if (event.getAction() != KeyEvent.ACTION_UP) { + return false; + } + + int keyCode = event.getKeyCode(); + + switch (keyCode) { + case KeyEvent.KEYCODE_N: + modifySelection("extend", "forward", "sentence"); + break; + case KeyEvent.KEYCODE_P: + modifySelection("extend", "backward", "sentence"); + break; + } + + return false; + } + + /** + * Called when the <code>selectionString</code> has changed. + */ + public void onSelectionStringChange(String selectionString) { + // put the selection string in an AccessibilityEvent and send it + AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); + event.getText().add(selectionString); + mWebView.sendAccessibilityEventUnchecked(event); + } + + /** + * Modifies the current selection. + * + * @param alter Specifies how to alter the selection. + * @param direction The direction in which to alter the selection. + * @param granularity The granularity of the selection modification. + */ + private void modifySelection(String alter, String direction, String granularity) { + WebViewCore webViewCore = mWebView.getWebViewCore(); + + if (webViewCore == null) { + return; + } + + WebViewCore.ModifySelectionData data = new WebViewCore.ModifySelectionData(); + data.mAlter = alter; + data.mDirection = direction; + data.mGranularity = granularity; + webViewCore.sendMessage(EventHub.MODIFY_SELECTION, data); + } +} diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index 0e0e032..9521b49 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -1087,10 +1087,15 @@ class CallbackProxy extends Handler { public void onProgressChanged(int newProgress) { // Synchronize so that mLatestProgress is up-to-date. synchronized (this) { - if (mWebChromeClient == null || mLatestProgress == newProgress) { + // update mLatestProgress even mWebChromeClient is null as + // WebView.getProgress() needs it + if (mLatestProgress == newProgress) { return; } mLatestProgress = newProgress; + if (mWebChromeClient == null) { + return; + } if (!mProgressUpdatePending) { sendEmptyMessage(PROGRESS); mProgressUpdatePending = true; diff --git a/core/java/android/webkit/GeolocationService.java b/core/java/android/webkit/GeolocationService.java index 24306f4..183b3c0 100755 --- a/core/java/android/webkit/GeolocationService.java +++ b/core/java/android/webkit/GeolocationService.java @@ -62,9 +62,10 @@ final class GeolocationService implements LocationListener { /** * Start listening for location updates. */ - public void start() { + public boolean start() { registerForLocationUpdates(); mIsRunning = true; + return mIsNetworkProviderAvailable || mIsGpsProviderAvailable; } /** @@ -87,6 +88,8 @@ final class GeolocationService implements LocationListener { // only unregister from all, then reregister with all but the GPS. unregisterFromLocationUpdates(); registerForLocationUpdates(); + // Check that the providers are still available after we re-register. + maybeReportError("The last location provider is no longer available"); } } } @@ -156,11 +159,16 @@ final class GeolocationService implements LocationListener { */ private void registerForLocationUpdates() { try { - mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, this); - mIsNetworkProviderAvailable = true; + // Registration may fail if providers are not present on the device. + try { + mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, this); + mIsNetworkProviderAvailable = true; + } catch(IllegalArgumentException e) { } if (mIsGpsEnabled) { - mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this); - mIsGpsProviderAvailable = true; + try { + mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this); + mIsGpsProviderAvailable = true; + } catch(IllegalArgumentException e) { } } } catch(SecurityException e) { Log.e(TAG, "Caught security exception registering for location updates from system. " + @@ -173,6 +181,8 @@ final class GeolocationService implements LocationListener { */ private void unregisterFromLocationUpdates() { mLocationManager.removeUpdates(this); + mIsNetworkProviderAvailable = false; + mIsGpsProviderAvailable = false; } /** diff --git a/core/java/android/webkit/HTML5Audio.java b/core/java/android/webkit/HTML5Audio.java new file mode 100644 index 0000000..d292881 --- /dev/null +++ b/core/java/android/webkit/HTML5Audio.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnBufferingUpdateListener; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.media.MediaPlayer.OnPreparedListener; +import android.media.MediaPlayer.OnSeekCompleteListener; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import java.io.IOException; +import java.util.Timer; +import java.util.TimerTask; + +/** + * <p>HTML5 support class for Audio. + */ +class HTML5Audio extends Handler + implements MediaPlayer.OnBufferingUpdateListener, + MediaPlayer.OnCompletionListener, + MediaPlayer.OnErrorListener, + MediaPlayer.OnPreparedListener, + MediaPlayer.OnSeekCompleteListener { + // Logging tag. + private static final String LOGTAG = "HTML5Audio"; + + private MediaPlayer mMediaPlayer; + + // The C++ MediaPlayerPrivateAndroid object. + private int mNativePointer; + + private static int IDLE = 0; + private static int INITIALIZED = 1; + private static int PREPARED = 2; + private static int STARTED = 4; + private static int COMPLETE = 5; + private static int PAUSED = 6; + private static int STOPPED = -2; + private static int ERROR = -1; + + private int mState = IDLE; + + private String mUrl; + private boolean mAskToPlay = false; + + // Timer thread -> UI thread + private static final int TIMEUPDATE = 100; + + // The spec says the timer should fire every 250 ms or less. + private static final int TIMEUPDATE_PERIOD = 250; // ms + // The timer for timeupate events. + // See http://www.whatwg.org/specs/web-apps/current-work/#event-media-timeupdate + private Timer mTimer; + private final class TimeupdateTask extends TimerTask { + public void run() { + HTML5Audio.this.obtainMessage(TIMEUPDATE).sendToTarget(); + } + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case TIMEUPDATE: { + try { + if (mState != ERROR && mMediaPlayer.isPlaying()) { + int position = mMediaPlayer.getCurrentPosition(); + nativeOnTimeupdate(position, mNativePointer); + } + } catch (IllegalStateException e) { + mState = ERROR; + } + } + } + } + + // event listeners for MediaPlayer + // Those are called from the same thread we created the MediaPlayer + // (i.e. the webviewcore thread here) + + // MediaPlayer.OnBufferingUpdateListener + public void onBufferingUpdate(MediaPlayer mp, int percent) { + nativeOnBuffering(percent, mNativePointer); + } + + // MediaPlayer.OnCompletionListener; + public void onCompletion(MediaPlayer mp) { + resetMediaPlayer(); + mState = IDLE; + nativeOnEnded(mNativePointer); + } + + // MediaPlayer.OnErrorListener + public boolean onError(MediaPlayer mp, int what, int extra) { + mState = ERROR; + resetMediaPlayer(); + mState = IDLE; + return false; + } + + // MediaPlayer.OnPreparedListener + public void onPrepared(MediaPlayer mp) { + mState = PREPARED; + if (mTimer != null) { + mTimer.schedule(new TimeupdateTask(), + TIMEUPDATE_PERIOD, TIMEUPDATE_PERIOD); + } + nativeOnPrepared(mp.getDuration(), 0, 0, mNativePointer); + if (mAskToPlay) { + mAskToPlay = false; + play(); + } + } + + // MediaPlayer.OnSeekCompleteListener + public void onSeekComplete(MediaPlayer mp) { + nativeOnTimeupdate(mp.getCurrentPosition(), mNativePointer); + } + + + /** + * @param nativePtr is the C++ pointer to the MediaPlayerPrivate object. + */ + public HTML5Audio(int nativePtr) { + // Save the native ptr + mNativePointer = nativePtr; + resetMediaPlayer(); + } + + private void resetMediaPlayer() { + if (mMediaPlayer == null) { + mMediaPlayer = new MediaPlayer(); + } else { + mMediaPlayer.reset(); + } + mMediaPlayer.setOnBufferingUpdateListener(this); + mMediaPlayer.setOnCompletionListener(this); + mMediaPlayer.setOnErrorListener(this); + mMediaPlayer.setOnPreparedListener(this); + mMediaPlayer.setOnSeekCompleteListener(this); + + if (mTimer != null) { + mTimer.cancel(); + } + mTimer = new Timer(); + mState = IDLE; + } + + private void setDataSource(String url) { + mUrl = url; + try { + if (mState != IDLE) { + resetMediaPlayer(); + } + mMediaPlayer.setDataSource(url); + mState = INITIALIZED; + mMediaPlayer.prepareAsync(); + } catch (IOException e) { + Log.e(LOGTAG, "couldn't load the resource: " + url + " exc: " + e); + resetMediaPlayer(); + } + } + + private void play() { + if ((mState == ERROR || mState == IDLE) && mUrl != null) { + resetMediaPlayer(); + setDataSource(mUrl); + mAskToPlay = true; + } + + if (mState >= PREPARED) { + mMediaPlayer.start(); + mState = STARTED; + } + } + + private void pause() { + if (mState == STARTED) { + if (mTimer != null) { + mTimer.purge(); + } + mMediaPlayer.pause(); + mState = PAUSED; + } + } + + private void seek(int msec) { + if (mState >= PREPARED) { + mMediaPlayer.seekTo(msec); + } + } + + private void teardown() { + mMediaPlayer.release(); + mState = ERROR; + mNativePointer = 0; + } + + private float getMaxTimeSeekable() { + return mMediaPlayer.getDuration() / 1000.0f; + } + + private native void nativeOnBuffering(int percent, int nativePointer); + private native void nativeOnEnded(int nativePointer); + private native void nativeOnPrepared(int duration, int width, int height, int nativePointer); + private native void nativeOnTimeupdate(int position, int nativePointer); +} diff --git a/core/java/android/webkit/Network.java b/core/java/android/webkit/Network.java index 598f20d..0f03258 100644 --- a/core/java/android/webkit/Network.java +++ b/core/java/android/webkit/Network.java @@ -16,7 +16,12 @@ package android.webkit; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.net.http.*; import android.os.*; import android.util.Log; @@ -76,6 +81,19 @@ class Network { */ private HttpAuthHandler mHttpAuthHandler; + private Context mContext; + + /** + * True if the currently used network connection is a roaming phone + * connection. + */ + private boolean mRoaming; + + /** + * Tracks if we are roaming. + */ + private RoamingMonitor mRoamingMonitor; + /** * @return The singleton instance of the network. */ @@ -107,6 +125,7 @@ class Network { if (++sPlatformNotificationEnableRefCount == 1) { if (sNetwork != null) { sNetwork.mRequestQueue.enablePlatformNotifications(); + sNetwork.monitorRoaming(); } else { sPlatformNotifications = true; } @@ -121,6 +140,7 @@ class Network { if (--sPlatformNotificationEnableRefCount == 0) { if (sNetwork != null) { sNetwork.mRequestQueue.disablePlatformNotifications(); + sNetwork.stopMonitoringRoaming(); } else { sPlatformNotifications = false; } @@ -136,12 +156,39 @@ class Network { Assert.assertTrue(Thread.currentThread(). getName().equals(WebViewCore.THREAD_NAME)); } + mContext = context; mSslErrorHandler = new SslErrorHandler(); mHttpAuthHandler = new HttpAuthHandler(this); mRequestQueue = new RequestQueue(context); } + private class RoamingMonitor extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (!ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) + return; + + NetworkInfo info = (NetworkInfo)intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO); + if (info != null) + mRoaming = info.isRoaming(); + }; + }; + + private void monitorRoaming() { + mRoamingMonitor = new RoamingMonitor(); + IntentFilter filter = new IntentFilter(); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + mContext.registerReceiver(sNetwork.mRoamingMonitor, filter); + } + + private void stopMonitoringRoaming() { + if (mRoamingMonitor != null) { + mContext.unregisterReceiver(mRoamingMonitor); + mRoamingMonitor = null; + } + } + /** * Request a url from either the network or the file system. * @param url The url to load. @@ -170,6 +217,11 @@ class Network { return false; } + // If this is a prefetch, abort it if we're roaming. + if (mRoaming && headers.containsKey("X-Moz") && "prefetch".equals(headers.get("X-Moz"))) { + return false; + } + /* FIXME: this is lame. Pass an InputStream in, rather than making this lame one here */ InputStream bodyProvider = null; diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index b767f11..6958f5c 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -296,13 +296,13 @@ public class WebSettings { "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us)" + " AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0" + " Safari/530.17"; - private static final String IPHONE_USERAGENT = + private static final String IPHONE_USERAGENT = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us)" + " AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0" + " Mobile/7A341 Safari/528.16"; private static Locale sLocale; private static Object sLockForLocaleSettings; - + /** * Package constructor to prevent clients from creating a new settings * instance. @@ -327,6 +327,8 @@ public class WebSettings { android.os.Process.myUid()) != PackageManager.PERMISSION_GRANTED; } + private static final String ACCEPT_LANG_FOR_US_LOCALE = "en-US"; + /** * Looks at sLocale and returns current AcceptLanguage String. * @return Current AcceptLanguage String. @@ -336,32 +338,53 @@ public class WebSettings { synchronized(sLockForLocaleSettings) { locale = sLocale; } - StringBuffer buffer = new StringBuffer(); - final String language = locale.getLanguage(); - if (language != null) { - buffer.append(language); - final String country = locale.getCountry(); - if (country != null) { - buffer.append("-"); - buffer.append(country); - } - } - if (!locale.equals(Locale.US)) { - buffer.append(", "); - java.util.Locale us = Locale.US; - if (us.getLanguage() != null) { - buffer.append(us.getLanguage()); - final String country = us.getCountry(); - if (country != null) { - buffer.append("-"); - buffer.append(country); - } + StringBuilder buffer = new StringBuilder(); + addLocaleToHttpAcceptLanguage(buffer, locale); + + if (!Locale.US.equals(locale)) { + if (buffer.length() > 0) { + buffer.append(", "); } + buffer.append(ACCEPT_LANG_FOR_US_LOCALE); } return buffer.toString(); } - + + /** + * Convert obsolete language codes, including Hebrew/Indonesian/Yiddish, + * to new standard. + */ + private static String convertObsoleteLanguageCodeToNew(String langCode) { + if (langCode == null) { + return null; + } + if ("iw".equals(langCode)) { + // Hebrew + return "he"; + } else if ("in".equals(langCode)) { + // Indonesian + return "id"; + } else if ("ji".equals(langCode)) { + // Yiddish + return "yi"; + } + return langCode; + } + + private static void addLocaleToHttpAcceptLanguage(StringBuilder builder, + Locale locale) { + String language = convertObsoleteLanguageCodeToNew(locale.getLanguage()); + if (language != null) { + builder.append(language); + String country = locale.getCountry(); + if (country != null) { + builder.append("-"); + builder.append(country); + } + } + } + /** * Looks at sLocale and mContext and returns current UserAgent String. * @return Current UserAgent String. @@ -379,11 +402,11 @@ public class WebSettings { } else { // default to "1.0" buffer.append("1.0"); - } + } buffer.append("; "); final String language = locale.getLanguage(); if (language != null) { - buffer.append(language.toLowerCase()); + buffer.append(convertObsoleteLanguageCodeToNew(language)); final String country = locale.getCountry(); if (country != null) { buffer.append("-"); diff --git a/core/java/android/webkit/WebTextView.java b/core/java/android/webkit/WebTextView.java index 19abec1..c809b5a 100644 --- a/core/java/android/webkit/WebTextView.java +++ b/core/java/android/webkit/WebTextView.java @@ -300,6 +300,33 @@ import java.util.ArrayList; return connection; } + /** + * In general, TextView makes a call to InputMethodManager.updateSelection + * in onDraw. However, in the general case of WebTextView, we do not draw. + * This method is called by WebView.onDraw to take care of the part that + * needs to be called. + */ + /* package */ void onDrawSubstitute() { + if (!willNotDraw()) { + // If the WebTextView is set to draw, such as in the case of a + // password, onDraw calls updateSelection(), so this code path is + // unnecessary. + return; + } + // This code is copied from TextView.onDraw(). That code does not get + // executed, however, because the WebTextView does not draw, allowing + // webkit's drawing to show through. + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null && imm.isActive(this)) { + Spannable sp = (Spannable) getText(); + int selStart = Selection.getSelectionStart(sp); + int selEnd = Selection.getSelectionEnd(sp); + int candStart = EditableInputConnection.getComposingSpanStart(sp); + int candEnd = EditableInputConnection.getComposingSpanEnd(sp); + imm.updateSelection(this, selStart, selEnd, candStart, candEnd); + } + } + @Override protected void onDraw(Canvas canvas) { // onDraw should only be called for password fields. If WebTextView is @@ -360,19 +387,8 @@ import java.util.ArrayList; @Override protected void onSelectionChanged(int selStart, int selEnd) { - if (mInSetTextAndKeepSelection) return; - // This code is copied from TextView.onDraw(). That code does not get - // executed, however, because the WebTextView does not draw, allowing - // webkit's drawing to show through. - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null && imm.isActive(this)) { - Spannable sp = (Spannable) getText(); - int candStart = EditableInputConnection.getComposingSpanStart(sp); - int candEnd = EditableInputConnection.getComposingSpanEnd(sp); - imm.updateSelection(this, selStart, selEnd, candStart, candEnd); - } if (!mFromWebKit && !mFromFocusChange && !mFromSetInputType - && mWebView != null) { + && mWebView != null && !mInSetTextAndKeepSelection) { if (DebugFlags.WEB_TEXT_VIEW) { Log.v(LOGTAG, "onSelectionChanged selStart=" + selStart + " selEnd=" + selEnd); @@ -667,6 +683,7 @@ import java.util.ArrayList; } else { Selection.setSelection(text, selection, selection); } + if (mWebView != null) mWebView.incrementTextGeneration(); } /** diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 921d0f5..ebcf5c5 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -25,19 +25,13 @@ import android.content.DialogInterface.OnCancelListener; import android.content.pm.PackageManager; import android.database.DataSetObserver; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Interpolator; -import android.graphics.Matrix; -import android.graphics.Paint; import android.graphics.Picture; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; -import android.graphics.Region; -import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.net.Uri; import android.net.http.SslCertificate; @@ -46,9 +40,11 @@ import android.os.Handler; import android.os.Message; import android.os.ServiceManager; import android.os.SystemClock; +import android.speech.tts.TextToSpeech; import android.text.IClipboard; import android.text.Selection; import android.text.Spannable; +import android.text.TextUtils; import android.util.AttributeSet; import android.util.EventLog; import android.util.Log; @@ -64,6 +60,7 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.view.accessibility.AccessibilityManager; import android.view.animation.AlphaAnimation; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; @@ -76,20 +73,17 @@ import android.widget.Adapter; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.CheckedTextView; -import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.Scroller; import android.widget.Toast; import android.widget.ZoomButtonsController; -import android.widget.ZoomControls; import android.widget.AdapterView.OnItemClickListener; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; -import java.io.IOException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; @@ -310,49 +304,7 @@ public class WebView extends AbsoluteLayout static final String LOGTAG = "webview"; - private static class ExtendedZoomControls extends FrameLayout { - public ExtendedZoomControls(Context context, AttributeSet attrs) { - super(context, attrs); - LayoutInflater inflater = (LayoutInflater) - context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(com.android.internal.R.layout.zoom_magnify, this, true); - mPlusMinusZoomControls = (ZoomControls) findViewById( - com.android.internal.R.id.zoomControls); - findViewById(com.android.internal.R.id.zoomMagnify).setVisibility( - View.GONE); - } - - public void show(boolean showZoom, boolean canZoomOut) { - mPlusMinusZoomControls.setVisibility( - showZoom ? View.VISIBLE : View.GONE); - fade(View.VISIBLE, 0.0f, 1.0f); - } - - public void hide() { - fade(View.GONE, 1.0f, 0.0f); - } - - private void fade(int visibility, float startAlpha, float endAlpha) { - AlphaAnimation anim = new AlphaAnimation(startAlpha, endAlpha); - anim.setDuration(500); - startAnimation(anim); - setVisibility(visibility); - } - - public boolean hasFocus() { - return mPlusMinusZoomControls.hasFocus(); - } - - public void setOnZoomInClickListener(OnClickListener listener) { - mPlusMinusZoomControls.setOnZoomInClickListener(listener); - } - - public void setOnZoomOutClickListener(OnClickListener listener) { - mPlusMinusZoomControls.setOnZoomOutClickListener(listener); - } - - ZoomControls mPlusMinusZoomControls; - } + private ZoomManager mZoomManager; /** * Transportation object for returning WebView across thread boundaries. @@ -398,6 +350,8 @@ public class WebView extends AbsoluteLayout // more key events. private int mTextGeneration; + /* package */ void incrementTextGeneration() { mTextGeneration++; } + // Used by WebViewCore to create child views. /* package */ final ViewManager mViewManager; @@ -445,6 +399,10 @@ public class WebView extends AbsoluteLayout private float mLastVelX; private float mLastVelY; + // only trigger accelerated fling if the new velocity is at least + // MINIMUM_VELOCITY_RATIO_FOR_ACCELERATION times of the previous velocity + private static final float MINIMUM_VELOCITY_RATIO_FOR_ACCELERATION = 0.2f; + /** * Touch mode */ @@ -521,9 +479,6 @@ public class WebView extends AbsoluteLayout private static final int MIN_FLING_TIME = 250; // draw unfiltered after drag is held without movement private static final int MOTIONLESS_TIME = 100; - // The time that the Zoom Controls are visible before fading away - private static final long ZOOM_CONTROLS_TIMEOUT = - ViewConfiguration.getZoomControlsTimeout(); // The amount of content to overlap between two screens when going through // pages with the space bar, in pixels. private static final int PAGE_SCROLL_OVERLAP = 24; @@ -569,6 +524,10 @@ public class WebView extends AbsoluteLayout // use the framework's ScaleGestureDetector to handle multi-touch private ScaleGestureDetector mScaleDetector; + // An instance for injecting accessibility in WebViews with disabled + // JavaScript or ones for which no accessibility script exists + private AccessibilityInjector mAccessibilityInjector; + // the anchor point in the document space where VIEW_SIZE_CHANGED should // apply to private int mAnchorX; @@ -606,7 +565,7 @@ public class WebView extends AbsoluteLayout static final int WEBCORE_INITIALIZED_MSG_ID = 107; static final int UPDATE_TEXTFIELD_TEXT_MSG_ID = 108; static final int UPDATE_ZOOM_RANGE = 109; - static final int MOVE_OUT_OF_PLUGIN = 110; + static final int UNHANDLED_NAV_KEY = 110; static final int CLEAR_TEXT_ENTRY = 111; static final int UPDATE_TEXT_SELECTION_MSG_ID = 112; static final int SHOW_RECT_MSG_ID = 113; @@ -627,6 +586,7 @@ public class WebView extends AbsoluteLayout static final int CENTER_FIT_RECT = 127; static final int REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID = 128; static final int SET_SCROLLBAR_MODES = 129; + static final int SELECTION_STRING_CHANGED = 130; private static final int FIRST_PACKAGE_MSG_ID = SCROLL_TO_MSG_ID; private static final int LAST_PACKAGE_MSG_ID = SET_SCROLLBAR_MODES; @@ -654,7 +614,7 @@ public class WebView extends AbsoluteLayout "WEBCORE_INITIALIZED_MSG_ID", // = 107; "UPDATE_TEXTFIELD_TEXT_MSG_ID", // = 108; "UPDATE_ZOOM_RANGE", // = 109; - "MOVE_OUT_OF_PLUGIN", // = 110; + "UNHANDLED_NAV_KEY", // = 110; "CLEAR_TEXT_ENTRY", // = 111; "UPDATE_TEXT_SELECTION_MSG_ID", // = 112; "SHOW_RECT_MSG_ID", // = 113; @@ -686,23 +646,9 @@ public class WebView extends AbsoluteLayout // the minimum preferred width is huge, an upper limit is needed. static int sMaxViewportWidth = DEFAULT_VIEWPORT_WIDTH; - // default scale limit. Depending on the display density - private static float DEFAULT_MAX_ZOOM_SCALE; - private static float DEFAULT_MIN_ZOOM_SCALE; - // scale limit, which can be set through viewport meta tag in the web page - private float mMaxZoomScale; - private float mMinZoomScale; - private boolean mMinZoomScaleFixed = true; - // initial scale in percent. 0 means using default. private int mInitialScaleInPercent = 0; - // while in the zoom overview mode, the page's width is fully fit to the - // current window. The page is alive, in another words, you can click to - // follow the links. Double tap will toggle between zoom overview mode and - // the last zoom scale. - boolean mInZoomOverview = false; - // ideally mZoomOverviewWidth should be mContentWidth. But sites like espn, // engadget always have wider mContentWidth no matter what viewport size is. int mZoomOverviewWidth = DEFAULT_VIEWPORT_WIDTH; @@ -752,6 +698,19 @@ public class WebView extends AbsoluteLayout private int mHorizontalScrollBarMode = SCROLLBAR_AUTO; private int mVerticalScrollBarMode = SCROLLBAR_AUTO; + // the alias via which accessibility JavaScript interface is exposed + private static final String ALIAS_ACCESSIBILITY_JS_INTERFACE = "accessibility"; + + // JavaScript to inject the script chooser which will + // pick the right script for the current URL + private static final String ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT = + "javascript:(function() {" + + " var chooser = document.createElement('script');" + + " chooser.type = 'text/javascript';" + + " chooser.src = 'https://ssl.gstatic.com/accessibility/javascript/android/AndroidScriptChooser.user.js';" + + " document.getElementsByTagName('head')[0].appendChild(chooser);" + + " })();"; + // Used to match key downs and key ups private boolean mGotKeyDown; @@ -856,43 +815,11 @@ public class WebView extends AbsoluteLayout } } - // The View containing the zoom controls - private ExtendedZoomControls mZoomControls; - private Runnable mZoomControlRunnable; - - // mZoomButtonsController will be lazy initialized in - // getZoomButtonsController() to get better performance. - private ZoomButtonsController mZoomButtonsController; - // These keep track of the center point of the zoom. They are used to // determine the point around which we should zoom. private float mZoomCenterX; private float mZoomCenterY; - private ZoomButtonsController.OnZoomListener mZoomListener = - new ZoomButtonsController.OnZoomListener() { - - public void onVisibilityChanged(boolean visible) { - if (visible) { - switchOutDrawHistory(); - // Bring back the hidden zoom controls. - mZoomButtonsController.getZoomControls().setVisibility( - View.VISIBLE); - updateZoomButtonsEnabled(); - } - } - - public void onZoom(boolean zoomIn) { - if (zoomIn) { - zoomIn(); - } else { - zoomOut(); - } - - updateZoomButtonsEnabled(); - } - }; - /** * Construct a new WebView with a Context object. * @param context A Context object used to access application assets. @@ -928,21 +855,32 @@ public class WebView extends AbsoluteLayout * @param context A Context object used to access application assets. * @param attrs An AttributeSet passed to our parent. * @param defStyle The default style resource ID. - * @param javascriptInterfaces is a Map of intareface names, as keys, and + * @param javascriptInterfaces is a Map of interface names, as keys, and * object implementing those interfaces, as values. * @hide pending API council approval. */ protected WebView(Context context, AttributeSet attrs, int defStyle, Map<String, Object> javascriptInterfaces) { super(context, attrs, defStyle); - init(); + + if (AccessibilityManager.getInstance(context).isEnabled()) { + if (javascriptInterfaces == null) { + javascriptInterfaces = new HashMap<String, Object>(); + } + exposeAccessibilityJavaScriptApi(javascriptInterfaces); + } mCallbackProxy = new CallbackProxy(context, this); mViewManager = new ViewManager(this); mWebViewCore = new WebViewCore(context, this, mCallbackProxy, javascriptInterfaces); mDatabase = WebViewDatabase.getInstance(context); mScroller = new Scroller(context); + mZoomManager = new ZoomManager(this); + /* The init method must follow the creation of certain member variables, + * such as the mZoomManager. + */ + init(); updateMultiTouchSupport(context); } @@ -959,22 +897,6 @@ public class WebView extends AbsoluteLayout } } - private void updateZoomButtonsEnabled() { - if (mZoomButtonsController == null) return; - boolean canZoomIn = mActualScale < mMaxZoomScale; - boolean canZoomOut = mActualScale > mMinZoomScale && !mInZoomOverview; - if (!canZoomIn && !canZoomOut) { - // Hide the zoom in and out buttons, as well as the fit to page - // button, if the page cannot zoom - mZoomButtonsController.getZoomControls().setVisibility(View.GONE); - } else { - // Set each one individually, as a page may be able to zoom in - // or out. - mZoomButtonsController.setZoomInEnabled(canZoomIn); - mZoomButtonsController.setZoomOutEnabled(canZoomOut); - } - } - private void init() { setWillNotDraw(false); setFocusable(true); @@ -998,13 +920,30 @@ public class WebView extends AbsoluteLayout mActualScale = density; mInvActualScale = 1 / density; mTextWrapScale = density; - DEFAULT_MAX_ZOOM_SCALE = 4.0f * density; - DEFAULT_MIN_ZOOM_SCALE = 0.25f * density; - mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; - mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; + mZoomManager.init(density); mMaximumFling = configuration.getScaledMaximumFlingVelocity(); } + /** + * Exposes accessibility APIs to JavaScript by appending them to the JavaScript + * interfaces map provided by the WebView client. In case of conflicting + * alias with the one of the accessibility API the user specified one wins. + * + * @param javascriptInterfaces A map with interfaces to be exposed to JavaScript. + */ + private void exposeAccessibilityJavaScriptApi(Map<String, Object> javascriptInterfaces) { + if (javascriptInterfaces.containsKey(ALIAS_ACCESSIBILITY_JS_INTERFACE)) { + Log.w(LOGTAG, "JavaScript interface mapped to \"" + ALIAS_ACCESSIBILITY_JS_INTERFACE + + "\" overrides the accessibility API JavaScript interface. No accessibility" + + "API will be exposed to JavaScript!"); + return; + } + + // expose the TTS for now ... + javascriptInterfaces.put(ALIAS_ACCESSIBILITY_JS_INTERFACE, + new TextToSpeech(getContext(), null)); + } + /* package */void updateDefaultZoomDensity(int zoomDensity) { final float density = getContext().getResources().getDisplayMetrics().density * 100 / zoomDensity; @@ -1013,11 +952,11 @@ public class WebView extends AbsoluteLayout // adjust the limits mNavSlop = (int) (16 * density); DEFAULT_SCALE_PERCENT = (int) (100 * density); - DEFAULT_MAX_ZOOM_SCALE = 4.0f * density; - DEFAULT_MIN_ZOOM_SCALE = 0.25f * density; + mZoomManager.DEFAULT_MAX_ZOOM_SCALE = 4.0f * density; + mZoomManager.DEFAULT_MIN_ZOOM_SCALE = 0.25f * density; mDefaultScale = density; - mMaxZoomScale *= scaleFactor; - mMinZoomScale *= scaleFactor; + mZoomManager.mMaxZoomScale *= scaleFactor; + mZoomManager.mMinZoomScale *= scaleFactor; setNewZoomScale(mActualScale * scaleFactor, true, false); } } @@ -1399,7 +1338,7 @@ public class WebView extends AbsoluteLayout b.putInt("scrollY", mScrollY); b.putFloat("scale", mActualScale); b.putFloat("textwrapScale", mTextWrapScale); - b.putBoolean("overview", mInZoomOverview); + b.putBoolean("overview", mZoomManager.mInZoomOverview); return true; } @@ -1419,7 +1358,7 @@ public class WebView extends AbsoluteLayout mActualScale = scale; mInvActualScale = 1 / scale; mTextWrapScale = b.getFloat("textwrapScale", scale); - mInZoomOverview = b.getBoolean("overview"); + mZoomManager.mInZoomOverview = b.getBoolean("overview"); invalidate(); } @@ -1865,13 +1804,7 @@ public class WebView extends AbsoluteLayout return; } clearTextEntry(false); - if (getSettings().getBuiltInZoomControls()) { - getZoomButtonsController().setVisible(true); - } else { - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - mPrivateHandler.postDelayed(mZoomControlRunnable, - ZOOM_CONTROLS_TIMEOUT); - } + mZoomManager.invokeZoomPicker(); } /** @@ -2213,12 +2146,14 @@ public class WebView extends AbsoluteLayout private void setNewZoomScale(float scale, boolean updateTextWrapScale, boolean force) { - if (scale < mMinZoomScale) { - scale = mMinZoomScale; + if (scale < mZoomManager.mMinZoomScale) { + scale = mZoomManager.mMinZoomScale; // set mInZoomOverview for non mobile sites - if (scale < mDefaultScale) mInZoomOverview = true; - } else if (scale > mMaxZoomScale) { - scale = mMaxZoomScale; + if (scale < mDefaultScale) { + mZoomManager.mInZoomOverview = true; + } + } else if (scale > mZoomManager.mMaxZoomScale) { + scale = mZoomManager.mMaxZoomScale; } if (updateTextWrapScale) { mTextWrapScale = scale; @@ -2314,9 +2249,6 @@ public class WebView extends AbsoluteLayout Point p = new Point(); getGlobalVisibleRect(r, p); r.offset(-p.x, -p.y); - if (mFindIsUp) { - r.bottom -= mFindHeight; - } } // Sets r to be our visible rectangle in content coordinates @@ -2408,7 +2340,7 @@ public class WebView extends AbsoluteLayout if (mDrawHistory) { return mHistoryWidth; } else if (mHorizontalScrollBarMode == SCROLLBAR_ALWAYSOFF - && (mActualScale - mMinZoomScale <= MINIMUM_SCALE_INCREMENT)) { + && (mActualScale - mZoomManager.mMinZoomScale <= MINIMUM_SCALE_INCREMENT)) { // only honor the scrollbar mode when it is at minimum zoom level return computeHorizontalScrollExtent(); } else { @@ -2422,7 +2354,7 @@ public class WebView extends AbsoluteLayout if (mDrawHistory) { return mHistoryHeight; } else if (mVerticalScrollBarMode == SCROLLBAR_ALWAYSOFF - && (mActualScale - mMinZoomScale <= MINIMUM_SCALE_INCREMENT)) { + && (mActualScale - mZoomManager.mMinZoomScale <= MINIMUM_SCALE_INCREMENT)) { // only honor the scrollbar mode when it is at minimum zoom level return computeVerticalScrollExtent(); } else { @@ -2672,19 +2604,27 @@ public class WebView extends AbsoluteLayout */ public void setFindIsUp(boolean isUp) { mFindIsUp = isUp; - if (isUp) { - recordNewContentSize(mContentWidth, mContentHeight + mFindHeight, - false); - } if (0 == mNativeClass) return; // client isn't initialized nativeSetFindIsUp(isUp); } + /** + * @hide + */ + public int findIndex() { + if (0 == mNativeClass) return -1; + return nativeFindIndex(); + } + + /** + * @hide + */ + public boolean getFindIsUp() { return mFindIsUp; } + // Used to know whether the find dialog is open. Affects whether // or not we draw the highlights for matches. private boolean mFindIsUp; - private int mFindHeight; // Keep track of the last string sent, so we can search again after an // orientation change or the dismissal of the soft keyboard. private String mLastFind; @@ -2759,8 +2699,6 @@ public class WebView extends AbsoluteLayout } clearMatches(); setFindIsUp(false); - recordNewContentSize(mContentWidth, mContentHeight - mFindHeight, - false); // Now that the dialog has been removed, ensure that we scroll to a // location that is not beyond the end of the page. pinScrollTo(mScrollX, mScrollY, false, 0); @@ -2768,16 +2706,6 @@ public class WebView extends AbsoluteLayout } /** - * @hide - */ - public void setFindDialogHeight(int height) { - if (DebugFlags.WEB_VIEW) { - Log.v(LOGTAG, "setFindDialogHeight height=" + height); - } - mFindHeight = height; - } - - /** * Query the document to see if it contains any image references. The * message object will be dispatched with arg1 being set to 1 if images * were found and 0 if the document does not reference any images. @@ -2800,6 +2728,11 @@ public class WebView extends AbsoluteLayout postInvalidate(); // So we draw again if (oldX != mScrollX || oldY != mScrollY) { onScrollChanged(mScrollX, mScrollY, oldX, oldY); + } else { + abortAnimation(); + mPrivateHandler.removeMessages(RESUME_WEBCORE_PRIORITY); + WebViewCore.resumePriority(); + WebViewCore.resumeUpdatePicture(mWebViewCore); } } else { super.computeScroll(); @@ -2888,6 +2821,29 @@ public class WebView extends AbsoluteLayout } mPageThatNeedsToSlideTitleBarOffScreen = null; } + + injectAccessibilityForUrl(url); + } + + /** + * This method injects accessibility in the loaded document if accessibility + * is enabled. If JavaScript is enabled we try to inject a URL specific script. + * If no URL specific script is found or JavaScript is disabled we fallback to + * the default {@link AccessibilityInjector} implementation. + * + * @param url The URL loaded by this {@link WebView}. + */ + private void injectAccessibilityForUrl(String url) { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + if (getSettings().getJavaScriptEnabled()) { + loadUrl(ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT); + } else if (mAccessibilityInjector == null) { + mAccessibilityInjector = new AccessibilityInjector(this); + } + } else { + // it is possible that accessibility was turned off between reloads + mAccessibilityInjector = null; + } } /** @@ -3134,7 +3090,7 @@ public class WebView extends AbsoluteLayout * settings. */ public WebSettings getSettings() { - return mWebViewCore.getSettings(); + return (mWebViewCore != null) ? mWebViewCore.getSettings() : null; } /** @@ -3275,6 +3231,7 @@ public class WebView extends AbsoluteLayout if (AUTO_REDRAW_HACK && mAutoRedraw) { invalidate(); } + if (inEditingMode()) mWebTextView.onDrawSubstitute(); mWebViewCore.signalRepaintDone(); } @@ -3296,6 +3253,8 @@ public class WebView extends AbsoluteLayout // Send the click so that the textfield is in focus centerKeyPressOnTextField(); rebuildWebTextView(); + } else { + clearTextEntry(true); } if (inEditingMode()) { return mWebTextView.performLongClick(); @@ -3457,7 +3416,8 @@ public class WebView extends AbsoluteLayout if (!animateScroll) { extras = DRAW_EXTRAS_FIND; } - } else if (mShiftIsPressed && !nativeFocusIsPlugin()) { + } else if (mShiftIsPressed + && !nativePageShouldHandleShiftAndArrows()) { if (!animateZoom && !mPreviewZoomOnly) { extras = DRAW_EXTRAS_SELECTION; nativeSetSelectionRegion(mTouchSelection || mExtendSelection); @@ -3577,7 +3537,7 @@ public class WebView extends AbsoluteLayout // bring it back to the default scale so that user can enter text boolean zoom = mActualScale < mDefaultScale; if (zoom) { - mInZoomOverview = false; + mZoomManager.mInZoomOverview = false; mZoomCenterX = mLastTouchX; mZoomCenterY = mLastTouchY; // do not change text wrap scale so that there is no reflow @@ -3807,14 +3767,16 @@ public class WebView extends AbsoluteLayout // Bubble up the key event if // 1. it is a system key; or // 2. the host application wants to handle it; + // 3. the accessibility injector is present and wants to handle it; if (event.isSystem() - || mCallbackProxy.uiOverrideKeyEvent(event)) { + || mCallbackProxy.uiOverrideKeyEvent(event) + || (mAccessibilityInjector != null && mAccessibilityInjector.onKeyEvent(event))) { return false; } if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { - if (nativeFocusIsPlugin()) { + if (nativePageShouldHandleShiftAndArrows()) { mShiftIsPressed = true; } else if (!nativeCursorWantsKeyEvents() && !mShiftIsPressed) { setUpSelectXY(); @@ -3824,8 +3786,8 @@ public class WebView extends AbsoluteLayout if (keyCode >= KeyEvent.KEYCODE_DPAD_UP && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { switchOutDrawHistory(); - if (nativeFocusIsPlugin()) { - letPluginHandleNavKey(keyCode, event.getEventTime(), true); + if (nativePageShouldHandleShiftAndArrows()) { + letPageHandleNavKey(keyCode, event.getEventTime(), true); return true; } if (mShiftIsPressed) { @@ -3848,7 +3810,8 @@ public class WebView extends AbsoluteLayout if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { switchOutDrawHistory(); if (event.getRepeatCount() == 0) { - if (mShiftIsPressed && !nativeFocusIsPlugin()) { + if (mShiftIsPressed + && !nativePageShouldHandleShiftAndArrows()) { return true; // discard press if copy in progress } mGotCenterDown = true; @@ -3951,13 +3914,16 @@ public class WebView extends AbsoluteLayout // Bubble up the key event if // 1. it is a system key; or // 2. the host application wants to handle it; - if (event.isSystem() || mCallbackProxy.uiOverrideKeyEvent(event)) { + // 3. the accessibility injector is present and wants to handle it; + if (event.isSystem() + || mCallbackProxy.uiOverrideKeyEvent(event) + || (mAccessibilityInjector != null && mAccessibilityInjector.onKeyEvent(event))) { return false; } if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { - if (nativeFocusIsPlugin()) { + if (nativePageShouldHandleShiftAndArrows()) { mShiftIsPressed = false; } else if (commitCopy()) { return true; @@ -3966,8 +3932,8 @@ public class WebView extends AbsoluteLayout if (keyCode >= KeyEvent.KEYCODE_DPAD_UP && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { - if (nativeFocusIsPlugin()) { - letPluginHandleNavKey(keyCode, event.getEventTime(), false); + if (nativePageShouldHandleShiftAndArrows()) { + letPageHandleNavKey(keyCode, event.getEventTime(), false); return true; } // always handle the navigation keys in the UI thread @@ -3980,7 +3946,7 @@ public class WebView extends AbsoluteLayout mPrivateHandler.removeMessages(LONG_PRESS_CENTER); mGotCenterDown = false; - if (mShiftIsPressed && !nativeFocusIsPlugin()) { + if (mShiftIsPressed && !nativePageShouldHandleShiftAndArrows()) { if (mExtendSelection) { commitCopy(); } else { @@ -4094,11 +4060,19 @@ public class WebView extends AbsoluteLayout @Override protected void onDetachedFromWindow() { clearTextEntry(false); - dismissZoomControl(); + mZoomManager.dismissZoomPicker(); if (hasWindowFocus()) setActive(false); super.onDetachedFromWindow(); } + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + if (visibility != View.VISIBLE) { + mZoomManager.dismissZoomPicker(); + } + } + /** * @deprecated WebView no longer needs to implement * ViewGroup.OnHierarchyChangeListener. This method does nothing now. @@ -4143,17 +4117,14 @@ public class WebView extends AbsoluteLayout // false for the first parameter } } else { - if (mWebViewCore != null && getSettings().getBuiltInZoomControls() - && (mZoomButtonsController == null || - !mZoomButtonsController.isVisible())) { + if (!mZoomManager.isZoomPickerVisible()) { /* - * The zoom controls come in their own window, so our window - * loses focus. Our policy is to not draw the cursor ring if - * our window is not focused, but this is an exception since + * The external zoom controls come in their own window, so our + * window loses focus. Our policy is to not draw the cursor ring + * if our window is not focused, but this is an exception since * the user can still navigate the web page with the zoom * controls showing. */ - // If our window has lost focus, stop drawing the cursor ring mDrawCursorRing = false; } mGotKeyDown = false; @@ -4263,9 +4234,7 @@ public class WebView extends AbsoluteLayout mWebView.setNewZoomScale(mWebView.mActualScale, mUpdateTextWrap, true); // update the zoom buttons as the scale can be changed - if (mWebView.getSettings().getBuiltInZoomControls()) { - mWebView.updateZoomButtonsEnabled(); - } + mWebView.mZoomManager.updateZoomPicker(); } } } @@ -4285,30 +4254,30 @@ public class WebView extends AbsoluteLayout // adjust the max viewport width depending on the view dimensions. This // is to ensure the scaling is not going insane. So do not shrink it if // the view size is temporarily smaller, e.g. when soft keyboard is up. - int newMaxViewportWidth = (int) (Math.max(w, h) / DEFAULT_MIN_ZOOM_SCALE); + int newMaxViewportWidth = (int) (Math.max(w, h) / mZoomManager.DEFAULT_MIN_ZOOM_SCALE); if (newMaxViewportWidth > sMaxViewportWidth) { sMaxViewportWidth = newMaxViewportWidth; } // update mMinZoomScale if the minimum zoom scale is not fixed - if (!mMinZoomScaleFixed) { + if (!mZoomManager.mMinZoomScaleFixed) { // when change from narrow screen to wide screen, the new viewWidth // can be wider than the old content width. We limit the minimum // scale to 1.0f. The proper minimum scale will be calculated when // the new picture shows up. - mMinZoomScale = Math.min(1.0f, (float) getViewWidth() + mZoomManager.mMinZoomScale = Math.min(1.0f, (float) getViewWidth() / (mDrawHistory ? mHistoryPicture.getWidth() : mZoomOverviewWidth)); if (mInitialScaleInPercent > 0) { // limit the minZoomScale to the initialScale if it is set float initialScale = mInitialScaleInPercent / 100.0f; - if (mMinZoomScale > initialScale) { - mMinZoomScale = initialScale; + if (mZoomManager.mMinZoomScale > initialScale) { + mZoomManager.mMinZoomScale = initialScale; } } } - dismissZoomControl(); + mZoomManager.dismissZoomPicker(); // onSizeChanged() is called during WebView layout. And any // requestLayout() is blocked during layout. As setNewZoomScale() will @@ -4335,9 +4304,11 @@ public class WebView extends AbsoluteLayout public boolean dispatchKeyEvent(KeyEvent event) { boolean dispatch = true; - // Textfields and plugins need to receive the shift up key even if - // another key was released while the shift key was held down. - if (!inEditingMode() && (mNativeClass == 0 || !nativeFocusIsPlugin())) { + // Textfields, plugins, and contentEditable nodes need to receive the + // shift up key even if another key was released while the shift key + // was held down. + if (!inEditingMode() && (mNativeClass == 0 + || !nativePageShouldHandleShiftAndArrows())) { if (event.getAction() == KeyEvent.ACTION_DOWN) { mGotKeyDown = true; } else { @@ -4577,9 +4548,9 @@ public class WebView extends AbsoluteLayout public boolean onScaleBegin(ScaleGestureDetector detector) { // cancel the single touch handling cancelTouch(); - dismissZoomControl(); + mZoomManager.dismissZoomPicker(); // reset the zoom overview mode so that the page won't auto grow - mInZoomOverview = false; + mZoomManager.mInZoomOverview = false; // If it is in password mode, turn it off so it does not draw // misplaced. if (inEditingMode() && nativeFocusCandidateIsPassword()) { @@ -4598,7 +4569,7 @@ public class WebView extends AbsoluteLayout mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY); // don't reflow when zoom in; when zoom out, do reflow if the // new scale is almost minimum scale; - boolean reflowNow = (mActualScale - mMinZoomScale + boolean reflowNow = (mActualScale - mZoomManager.mMinZoomScale <= MINIMUM_SCALE_INCREMENT) || ((mActualScale <= 0.8 * mTextWrapScale)); // force zoom after mPreviewZoomOnly is set to false so that the @@ -4685,7 +4656,7 @@ public class WebView extends AbsoluteLayout // FIXME: we may consider to give WebKit an option to handle multi-touch // events later. if (mSupportMultiTouch && ev.getPointerCount() > 1) { - if (mMinZoomScale < mMaxZoomScale) { + if (mZoomManager.mMinZoomScale < mZoomManager.mMaxZoomScale) { mScaleDetector.onTouchEvent(ev); if (mScaleDetector.isInProgress()) { mLastTouchTime = eventTime; @@ -4804,16 +4775,13 @@ public class WebView extends AbsoluteLayout ted.mY = contentY; ted.mMetaState = ev.getMetaState(); ted.mReprocess = mDeferTouchProcess; + mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); if (mDeferTouchProcess) { // still needs to set them for compute deltaX/Y mLastTouchX = x; mLastTouchY = y; - ted.mViewX = x; - ted.mViewY = y; - mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); break; } - mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); if (!inFullScreenMode()) { mPrivateHandler.sendMessageDelayed(mPrivateHandler .obtainMessage(PREVENT_DEFAULT_TIMEOUT, @@ -4839,20 +4807,17 @@ public class WebView extends AbsoluteLayout // pass the touch events from UI thread to WebCore thread if (shouldForwardTouchEvent() && mConfirmMove && (firstMove || eventTime - mLastSentTouchTime > mCurrentTouchInterval)) { - mLastSentTouchTime = eventTime; TouchEventData ted = new TouchEventData(); ted.mAction = action; ted.mX = contentX; ted.mY = contentY; ted.mMetaState = ev.getMetaState(); ted.mReprocess = mDeferTouchProcess; + mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); + mLastSentTouchTime = eventTime; if (mDeferTouchProcess) { - ted.mViewX = x; - ted.mViewY = y; - mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); break; } - mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); if (firstMove && !inFullScreenMode()) { mPrivateHandler.sendMessageDelayed(mPrivateHandler .obtainMessage(PREVENT_DEFAULT_TIMEOUT, @@ -4883,9 +4848,11 @@ public class WebView extends AbsoluteLayout invalidate(); break; } + if (!mConfirmMove) { break; } + if (mPreventDefault == PREVENT_DEFAULT_MAYBE_YES || mPreventDefault == PREVENT_DEFAULT_NO_FROM_TOUCH_DOWN) { // track mLastTouchTime as we may need to do fling at @@ -5013,6 +4980,7 @@ public class WebView extends AbsoluteLayout break; } case MotionEvent.ACTION_UP: { + if (!isFocused()) requestFocus(); // pass the touch events from UI thread to WebCore thread if (shouldForwardTouchEvent()) { TouchEventData ted = new TouchEventData(); @@ -5021,10 +4989,6 @@ public class WebView extends AbsoluteLayout ted.mY = contentY; ted.mMetaState = ev.getMetaState(); ted.mReprocess = mDeferTouchProcess; - if (mDeferTouchProcess) { - ted.mViewX = x; - ted.mViewY = y; - } mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); } mLastTouchUpTime = eventTime; @@ -5039,10 +5003,6 @@ public class WebView extends AbsoluteLayout ted.mY = contentY; ted.mMetaState = ev.getMetaState(); ted.mReprocess = mDeferTouchProcess; - if (mDeferTouchProcess) { - ted.mViewX = x; - ted.mViewY = y; - } mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); } else if (mPreventDefault != PREVENT_DEFAULT_YES){ doDoubleTap(); @@ -5064,9 +5024,17 @@ public class WebView extends AbsoluteLayout if (mPreventDefault != PREVENT_DEFAULT_YES && (computeMaxScrollX() > 0 || computeMaxScrollY() > 0)) { - // UI takes control back, cancel WebCore touch - cancelWebCoreTouchEvent(contentX, contentY, - true); + // If the user has performed a very quick touch + // sequence it is possible that we may get here + // before WebCore has had a chance to process the events. + // In this case, any call to preventDefault in the + // JS touch handler will not have been executed yet. + // Hence we will see both the UI (now) and WebCore + // (when context switches) handling the event, + // regardless of whether the web developer actually + // doeses preventDefault in their touch handler. This + // is the nature of our asynchronous touch model. + // we will not rewrite drag code here, but we // will try fling if it applies. WebViewCore.reducePriority(); @@ -5174,21 +5142,10 @@ public class WebView extends AbsoluteLayout if (!mDragFromTextInput) { nativeHideCursor(); } - WebSettings settings = getSettings(); - if (settings.supportZoom() - && settings.getBuiltInZoomControls() - && !getZoomButtonsController().isVisible() - && mMinZoomScale < mMaxZoomScale - && (mHorizontalScrollBarMode != SCROLLBAR_ALWAYSOFF - || mVerticalScrollBarMode != SCROLLBAR_ALWAYSOFF)) { - mZoomButtonsController.setVisible(true); - int count = settings.getDoubleTapToastCount(); - if (mInZoomOverview && count > 0) { - settings.setDoubleTapToastCount(--count); - Toast.makeText(mContext, - com.android.internal.R.string.double_tap_toast, - Toast.LENGTH_LONG).show(); - } + + if (mHorizontalScrollBarMode != SCROLLBAR_ALWAYSOFF + || mVerticalScrollBarMode != SCROLLBAR_ALWAYSOFF) { + mZoomManager.invokeZoomPicker(); } } @@ -5196,18 +5153,7 @@ public class WebView extends AbsoluteLayout if ((deltaX | deltaY) != 0) { scrollBy(deltaX, deltaY); } - if (!getSettings().getBuiltInZoomControls()) { - boolean showPlusMinus = mMinZoomScale < mMaxZoomScale; - if (mZoomControls != null && showPlusMinus) { - if (mZoomControls.getVisibility() == View.VISIBLE) { - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - } else { - mZoomControls.show(showPlusMinus, false); - } - mPrivateHandler.postDelayed(mZoomControlRunnable, - ZOOM_CONTROLS_TIMEOUT); - } - } + mZoomManager.keepZoomPickerVisible(); } private void stopTouch() { @@ -5294,7 +5240,7 @@ public class WebView extends AbsoluteLayout return true; } boolean shiftPressed = mShiftIsPressed && (mNativeClass == 0 - || !nativeFocusIsPlugin()); + || !nativePageShouldHandleShiftAndArrows()); if (ev.getAction() == MotionEvent.ACTION_DOWN) { if (shiftPressed) { return true; // discard press if copy in progress @@ -5459,7 +5405,8 @@ public class WebView extends AbsoluteLayout float yRate = mTrackballRemainsY * 1000 / elapsed; int viewWidth = getViewWidth(); int viewHeight = getViewHeight(); - if (mShiftIsPressed && (mNativeClass == 0 || !nativeFocusIsPlugin())) { + if (mShiftIsPressed && (mNativeClass == 0 + || !nativePageShouldHandleShiftAndArrows())) { moveSelection(scaleTrackballX(xRate, viewWidth), scaleTrackballY(yRate, viewHeight)); mTrackballRemainsX = mTrackballRemainsY = 0; @@ -5497,11 +5444,11 @@ public class WebView extends AbsoluteLayout + " mTrackballRemainsX=" + mTrackballRemainsX + " mTrackballRemainsY=" + mTrackballRemainsY); } - if (mNativeClass != 0 && nativeFocusIsPlugin()) { + if (mNativeClass != 0 && nativePageShouldHandleShiftAndArrows()) { for (int i = 0; i < count; i++) { - letPluginHandleNavKey(selectKeyCode, time, true); + letPageHandleNavKey(selectKeyCode, time, true); } - letPluginHandleNavKey(selectKeyCode, time, false); + letPageHandleNavKey(selectKeyCode, time, false); } else if (navHandledKey(selectKeyCode, count, false, time)) { playSoundEffect(keyCodeToSoundsEffect(selectKeyCode)); } @@ -5575,13 +5522,16 @@ public class WebView extends AbsoluteLayout return; } float currentVelocity = mScroller.getCurrVelocity(); - if (mLastVelocity > 0 && currentVelocity > 0) { + float velocity = (float) Math.hypot(vx, vy); + if (mLastVelocity > 0 && currentVelocity > 0 && velocity + > mLastVelocity * MINIMUM_VELOCITY_RATIO_FOR_ACCELERATION) { float deltaR = (float) (Math.abs(Math.atan2(mLastVelY, mLastVelX) - Math.atan2(vy, vx))); final float circle = (float) (Math.PI) * 2.0f; if (deltaR > circle * 0.9f || deltaR < circle * 0.1f) { vx += currentVelocity * mLastVelX / mLastVelocity; vy += currentVelocity * mLastVelY / mLastVelocity; + velocity = (float) Math.hypot(vx, vy); if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "doFling vx= " + vx + " vy=" + vy); } @@ -5597,7 +5547,7 @@ public class WebView extends AbsoluteLayout } mLastVelX = vx; mLastVelY = vy; - mLastVelocity = (float) Math.hypot(vx, vy); + mLastVelocity = velocity; mScroller.fling(mScrollX, mScrollY, -vx, -vy, 0, maxX, 0, maxY); // TODO: duration is calculated based on velocity, if the range is @@ -5655,81 +5605,11 @@ public class WebView extends AbsoluteLayout Log.w(LOGTAG, "This WebView doesn't support zoom."); return null; } - if (mZoomControls == null) { - mZoomControls = createZoomControls(); - - /* - * need to be set to VISIBLE first so that getMeasuredHeight() in - * {@link #onSizeChanged()} can return the measured value for proper - * layout. - */ - mZoomControls.setVisibility(View.VISIBLE); - mZoomControlRunnable = new Runnable() { - public void run() { - - /* Don't dismiss the controls if the user has - * focus on them. Wait and check again later. - */ - if (!mZoomControls.hasFocus()) { - mZoomControls.hide(); - } else { - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - mPrivateHandler.postDelayed(mZoomControlRunnable, - ZOOM_CONTROLS_TIMEOUT); - } - } - }; - } - return mZoomControls; - } - - private ExtendedZoomControls createZoomControls() { - ExtendedZoomControls zoomControls = new ExtendedZoomControls(mContext - , null); - zoomControls.setOnZoomInClickListener(new OnClickListener() { - public void onClick(View v) { - // reset time out - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - mPrivateHandler.postDelayed(mZoomControlRunnable, - ZOOM_CONTROLS_TIMEOUT); - zoomIn(); - } - }); - zoomControls.setOnZoomOutClickListener(new OnClickListener() { - public void onClick(View v) { - // reset time out - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - mPrivateHandler.postDelayed(mZoomControlRunnable, - ZOOM_CONTROLS_TIMEOUT); - zoomOut(); - } - }); - return zoomControls; + return mZoomManager.getExternalZoomPicker(); } - /** - * Gets the {@link ZoomButtonsController} which can be used to add - * additional buttons to the zoom controls window. - * - * @return The instance of {@link ZoomButtonsController} used by this class, - * or null if it is unavailable. - * @hide - */ - public ZoomButtonsController getZoomButtonsController() { - if (mZoomButtonsController == null) { - mZoomButtonsController = new ZoomButtonsController(this); - mZoomButtonsController.setOnZoomListener(mZoomListener); - // ZoomButtonsController positions the buttons at the bottom, but in - // the middle. Change their layout parameters so they appear on the - // right. - View controls = mZoomButtonsController.getZoomControls(); - ViewGroup.LayoutParams params = controls.getLayoutParams(); - if (params instanceof FrameLayout.LayoutParams) { - FrameLayout.LayoutParams frameParams = (FrameLayout.LayoutParams) params; - frameParams.gravity = Gravity.RIGHT; - } - } - return mZoomButtonsController; + void dismissZoomControl() { + mZoomManager.dismissZoomPicker(); } /** @@ -5739,7 +5619,7 @@ public class WebView extends AbsoluteLayout public boolean zoomIn() { // TODO: alternatively we can disallow this during draw history mode switchOutDrawHistory(); - mInZoomOverview = false; + mZoomManager.mInZoomOverview = false; // Center zooming to the center of the screen. mZoomCenterX = getViewWidth() * .5f; mZoomCenterY = getViewHeight() * .5f; @@ -5894,10 +5774,10 @@ public class WebView extends AbsoluteLayout int viewHeight = getViewHeightWithTitle(); float scale = Math.min((float) viewWidth / view.width, (float) viewHeight / view.height); - if (scale < mMinZoomScale) { - scale = mMinZoomScale; - } else if (scale > mMaxZoomScale) { - scale = mMaxZoomScale; + if (scale < mZoomManager.mMinZoomScale) { + scale = mZoomManager.mMinZoomScale; + } else if (scale > mZoomManager.mMaxZoomScale) { + scale = mZoomManager.mMaxZoomScale; } if (Math.abs(scale - mActualScale) < MINIMUM_SCALE_INCREMENT) { if (contentToViewX(view.x) >= mScrollX @@ -5923,10 +5803,10 @@ public class WebView extends AbsoluteLayout int viewHeight = getViewHeightWithTitle(); float scale = Math.min((float) viewWidth / docWidth, (float) viewHeight / docHeight); - if (scale < mMinZoomScale) { - scale = mMinZoomScale; - } else if (scale > mMaxZoomScale) { - scale = mMaxZoomScale; + if (scale < mZoomManager.mMinZoomScale) { + scale = mZoomManager.mMinZoomScale; + } else if (scale > mZoomManager.mMaxZoomScale) { + scale = mZoomManager.mMaxZoomScale; } if (Math.abs(scale - mActualScale) < MINIMUM_SCALE_INCREMENT) { pinScrollTo(contentToViewX(docX + docWidth / 2) - viewWidth / 2, @@ -5964,33 +5844,6 @@ public class WebView extends AbsoluteLayout } } - void dismissZoomControl() { - if (mWebViewCore == null) { - // maybe called after WebView's destroy(). As we can't get settings, - // just hide zoom control for both styles. - if (mZoomButtonsController != null) { - mZoomButtonsController.setVisible(false); - } - if (mZoomControls != null) { - mZoomControls.hide(); - } - return; - } - WebSettings settings = getSettings(); - if (settings.getBuiltInZoomControls()) { - if (mZoomButtonsController != null) { - mZoomButtonsController.setVisible(false); - } - } else { - if (mZoomControlRunnable != null) { - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - } - if (mZoomControls != null) { - mZoomControls.hide(); - } - } - } - // Rule for double tap: // 1. if the current scale is not same as the text wrap scale and layout // algorithm is NARROW_COLUMNS, fit to column; @@ -6007,17 +5860,17 @@ public class WebView extends AbsoluteLayout WebSettings settings = getSettings(); settings.setDoubleTapToastCount(0); // remove the zoom control after double tap - dismissZoomControl(); + mZoomManager.dismissZoomPicker(); ViewManager.ChildView plugin = mViewManager.hitTest(mAnchorX, mAnchorY); if (plugin != null) { if (isPluginFitOnScreen(plugin)) { - mInZoomOverview = true; + mZoomManager.mInZoomOverview = true; // Force the titlebar fully reveal in overview mode if (mScrollY < getTitleHeight()) mScrollY = 0; zoomWithPreview((float) getViewWidth() / mZoomOverviewWidth, true); } else { - mInZoomOverview = false; + mZoomManager.mInZoomOverview = false; centerFitRect(plugin.x, plugin.y, plugin.width, plugin.height); } return; @@ -6028,12 +5881,12 @@ public class WebView extends AbsoluteLayout setNewZoomScale(mActualScale, true, true); float overviewScale = (float) getViewWidth() / mZoomOverviewWidth; if (Math.abs(mActualScale - overviewScale) < MINIMUM_SCALE_INCREMENT) { - mInZoomOverview = true; + mZoomManager.mInZoomOverview = true; } - } else if (!mInZoomOverview) { + } else if (!mZoomManager.mInZoomOverview) { float newScale = (float) getViewWidth() / mZoomOverviewWidth; if (Math.abs(mActualScale - newScale) >= MINIMUM_SCALE_INCREMENT) { - mInZoomOverview = true; + mZoomManager.mInZoomOverview = true; // Force the titlebar fully reveal in overview mode if (mScrollY < getTitleHeight()) mScrollY = 0; zoomWithPreview(newScale, true); @@ -6044,7 +5897,7 @@ public class WebView extends AbsoluteLayout zoomToDefault = true; } if (zoomToDefault) { - mInZoomOverview = false; + mZoomManager.mInZoomOverview = false; int left = nativeGetBlockLeftEdge(mAnchorX, mAnchorY, mActualScale); if (left != NO_LEFTEDGE) { // add a 5pt padding to the left edge. @@ -6072,6 +5925,9 @@ public class WebView extends AbsoluteLayout @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { + // FIXME: If a subwindow is showing find, and the user touches the + // background window, it can steal focus. + if (mFindIsUp) return false; boolean result = false; if (inEditingMode()) { result = mWebTextView.requestFocus(direction, @@ -6326,15 +6182,10 @@ public class WebView extends AbsoluteLayout // simplicity for now, we don't set it. ted.mMetaState = 0; ted.mReprocess = mDeferTouchProcess; - if (mDeferTouchProcess) { - ted.mViewX = mLastTouchX; - ted.mViewY = mLastTouchY; - } mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); } else if (mPreventDefault != PREVENT_DEFAULT_YES) { mTouchMode = TOUCH_DONE_MODE; performLongClick(); - rebuildWebTextView(); } break; } @@ -6394,7 +6245,7 @@ public class WebView extends AbsoluteLayout updateZoomRange(restoreState, viewSize.x, draw.mMinPrefWidth, true); if (!mDrawHistory) { - mInZoomOverview = false; + mZoomManager.mInZoomOverview = false; if (mInitialScaleInPercent > 0) { setNewZoomScale(mInitialScaleInPercent / 100.0f, @@ -6405,10 +6256,10 @@ public class WebView extends AbsoluteLayout setNewZoomScale(restoreState.mViewScale, false, false); } else { - mInZoomOverview = useWideViewport + mZoomManager.mInZoomOverview = useWideViewport && settings.getLoadWithOverviewMode(); float scale; - if (mInZoomOverview) { + if (mZoomManager.mInZoomOverview) { scale = (float) viewWidth / DEFAULT_VIEWPORT_WIDTH; } else { @@ -6426,9 +6277,7 @@ public class WebView extends AbsoluteLayout // the WebTextView was visible. clearTextEntry(false); // update the zoom buttons as the scale can be changed - if (getSettings().getBuiltInZoomControls()) { - updateZoomButtonsEnabled(); - } + mZoomManager.updateZoomPicker(); } } // We update the layout (i.e. request a layout from the @@ -6438,8 +6287,7 @@ public class WebView extends AbsoluteLayout final boolean updateLayout = viewSize.x == mLastWidthSent && viewSize.y == mLastHeightSent; recordNewContentSize(draw.mWidthHeight.x, - draw.mWidthHeight.y - + (mFindIsUp ? mFindHeight : 0), updateLayout); + draw.mWidthHeight.y, updateLayout); if (DebugFlags.WEB_VIEW) { Rect b = draw.mInvalRegion.getBounds(); Log.v(LOGTAG, "NEW_PICTURE_MSG_ID {" + @@ -6459,10 +6307,10 @@ public class WebView extends AbsoluteLayout .max(draw.mMinPrefWidth, draw.mViewPoint.x))); } - if (!mMinZoomScaleFixed) { - mMinZoomScale = (float) viewWidth / mZoomOverviewWidth; + if (!mZoomManager.mMinZoomScaleFixed) { + mZoomManager.mMinZoomScale = (float) viewWidth / mZoomOverviewWidth; } - if (!mDrawHistory && mInZoomOverview) { + if (!mDrawHistory && mZoomManager.mInZoomOverview) { // fit the content width to the current view. Ignore // the rounding error case. if (Math.abs((viewWidth * mInvActualScale) @@ -6510,14 +6358,8 @@ public class WebView extends AbsoluteLayout break; case REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID: displaySoftKeyboard(true); - updateTextSelectionFromMessage(msg.arg1, msg.arg2, - (WebViewCore.TextSelectionData) msg.obj); - break; + // fall through to UPDATE_TEXT_SELECTION_MSG_ID case UPDATE_TEXT_SELECTION_MSG_ID: - // If no textfield was in focus, and the user touched one, - // causing it to send this message, then WebTextView has not - // been set up yet. Rebuild it so it can set its selection. - rebuildWebTextView(); updateTextSelectionFromMessage(msg.arg1, msg.arg2, (WebViewCore.TextSelectionData) msg.obj); break; @@ -6535,7 +6377,7 @@ public class WebView extends AbsoluteLayout } } break; - case MOVE_OUT_OF_PLUGIN: + case UNHANDLED_NAV_KEY: navHandledKey(msg.arg1, 1, false, 0); break; case UPDATE_TEXT_ENTRY_MSG_ID: @@ -6626,27 +6468,31 @@ public class WebView extends AbsoluteLayout TouchEventData ted = (TouchEventData) msg.obj; switch (ted.mAction) { case MotionEvent.ACTION_DOWN: - mLastDeferTouchX = ted.mViewX; - mLastDeferTouchY = ted.mViewY; + mLastDeferTouchX = contentToViewX(ted.mX) + - mScrollX; + mLastDeferTouchY = contentToViewY(ted.mY) + - mScrollY; mDeferTouchMode = TOUCH_INIT_MODE; break; case MotionEvent.ACTION_MOVE: { // no snapping in defer process + int x = contentToViewX(ted.mX) - mScrollX; + int y = contentToViewY(ted.mY) - mScrollY; if (mDeferTouchMode != TOUCH_DRAG_MODE) { mDeferTouchMode = TOUCH_DRAG_MODE; - mLastDeferTouchX = ted.mViewX; - mLastDeferTouchY = ted.mViewY; + mLastDeferTouchX = x; + mLastDeferTouchY = y; startDrag(); } int deltaX = pinLocX((int) (mScrollX - + mLastDeferTouchX - ted.mViewX)) + + mLastDeferTouchX - x)) - mScrollX; int deltaY = pinLocY((int) (mScrollY - + mLastDeferTouchY - ted.mViewY)) + + mLastDeferTouchY - y)) - mScrollY; doDrag(deltaX, deltaY); - if (deltaX != 0) mLastDeferTouchX = ted.mViewX; - if (deltaY != 0) mLastDeferTouchY = ted.mViewY; + if (deltaX != 0) mLastDeferTouchX = x; + if (deltaY != 0) mLastDeferTouchY = y; break; } case MotionEvent.ACTION_UP: @@ -6660,8 +6506,8 @@ public class WebView extends AbsoluteLayout break; case WebViewCore.ACTION_DOUBLETAP: // doDoubleTap() needs mLastTouchX/Y as anchor - mLastTouchX = ted.mViewX; - mLastTouchY = ted.mViewY; + mLastTouchX = contentToViewX(ted.mX) - mScrollX; + mLastTouchY = contentToViewY(ted.mY) - mScrollY; doDoubleTap(); mDeferTouchMode = TOUCH_DONE_MODE; break; @@ -6670,7 +6516,6 @@ public class WebView extends AbsoluteLayout if (hitTest != null && hitTest.mType != HitTestResult.UNKNOWN_TYPE) { performLongClick(); - rebuildWebTextView(); } mDeferTouchMode = TOUCH_DONE_MODE; break; @@ -6794,7 +6639,7 @@ public class WebView extends AbsoluteLayout case CENTER_FIT_RECT: Rect r = (Rect)msg.obj; - mInZoomOverview = false; + mZoomManager.mInZoomOverview = false; centerFitRect(r.left, r.top, r.width(), r.height()); break; @@ -6803,6 +6648,13 @@ public class WebView extends AbsoluteLayout mVerticalScrollBarMode = msg.arg2; break; + case SELECTION_STRING_CHANGED: + if (mAccessibilityInjector != null) { + String selectionString = (String) msg.obj; + mAccessibilityInjector.onSelectionStringChange(selectionString); + } + break; + default: super.handleMessage(msg); break; @@ -7115,29 +6967,29 @@ public class WebView extends AbsoluteLayout if (restoreState.mMinScale == 0) { if (restoreState.mMobileSite) { if (minPrefWidth > Math.max(0, viewWidth)) { - mMinZoomScale = (float) viewWidth / minPrefWidth; - mMinZoomScaleFixed = false; + mZoomManager.mMinZoomScale = (float) viewWidth / minPrefWidth; + mZoomManager.mMinZoomScaleFixed = false; if (updateZoomOverview) { WebSettings settings = getSettings(); - mInZoomOverview = settings.getUseWideViewPort() && + mZoomManager.mInZoomOverview = settings.getUseWideViewPort() && settings.getLoadWithOverviewMode(); } } else { - mMinZoomScale = restoreState.mDefaultScale; - mMinZoomScaleFixed = true; + mZoomManager.mMinZoomScale = restoreState.mDefaultScale; + mZoomManager.mMinZoomScaleFixed = true; } } else { - mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; - mMinZoomScaleFixed = false; + mZoomManager.mMinZoomScale = mZoomManager.DEFAULT_MIN_ZOOM_SCALE; + mZoomManager.mMinZoomScaleFixed = false; } } else { - mMinZoomScale = restoreState.mMinScale; - mMinZoomScaleFixed = true; + mZoomManager.mMinZoomScale = restoreState.mMinScale; + mZoomManager.mMinZoomScaleFixed = true; } if (restoreState.mMaxScale == 0) { - mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; + mZoomManager.mMaxZoomScale = mZoomManager.DEFAULT_MAX_ZOOM_SCALE; } else { - mMaxZoomScale = restoreState.mMaxScale; + mZoomManager.mMaxZoomScale = restoreState.mMaxScale; } } @@ -7234,10 +7086,10 @@ public class WebView extends AbsoluteLayout } /** - * Pass the key to the plugin. This assumes that nativeFocusIsPlugin() - * returned true. + * Pass the key directly to the page. This assumes that + * nativePageShouldHandleShiftAndArrows() returned true. */ - private void letPluginHandleNavKey(int keyCode, long time, boolean down) { + private void letPageHandleNavKey(int keyCode, long time, boolean down) { int keyEventAction; int eventHubAction; if (down) { @@ -7418,12 +7270,21 @@ public class WebView extends AbsoluteLayout private native int nativeMoveGeneration(); private native void nativeMoveSelection(int x, int y, boolean extendSelection); + /** + * @return true if the page should get the shift and arrow keys, rather + * than select text/navigation. + * + * If the focus is a plugin, or if the focus and cursor match and are + * a contentEditable element, then the page should handle these keys. + */ + private native boolean nativePageShouldHandleShiftAndArrows(); private native boolean nativePointInNavCache(int x, int y, int slop); // Like many other of our native methods, you must make sure that // mNativeClass is not null before calling this method. private native void nativeRecordButtons(boolean focused, boolean pressed, boolean invalidate); private native void nativeSelectBestAt(Rect rect); + private native int nativeFindIndex(); private native void nativeSetFindIsEmpty(); private native void nativeSetFindIsUp(boolean isUp); private native void nativeSetFollowedLink(boolean followed); diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index 4118119..f67819e 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -17,7 +17,6 @@ package android.webkit; import android.content.Context; -import android.content.Intent; import android.content.pm.PackageManager.NameNotFoundException; import android.database.Cursor; import android.graphics.Canvas; @@ -33,12 +32,10 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Process; -import android.provider.Browser; import android.provider.OpenableColumns; import android.util.Log; import android.util.SparseBooleanArray; import android.view.KeyEvent; -import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; @@ -576,7 +573,18 @@ final class WebViewCore { /** * Provide WebCore with the previously visted links from the history database */ - private native void nativeProvideVisitedHistory(String[] history); + private native void nativeProvideVisitedHistory(String[] history); + + /** + * Modifies the current selection. + * + * @param alter Specifies how to alter the selection. + * @param direction The direction in which to alter the selection. + * @param granularity The granularity of the selection modification. + * + * @return The selection string. + */ + private native String nativeModifySelection(String alter, String direction, String granularity); // EventHub for processing messages private final EventHub mEventHub; @@ -708,8 +716,6 @@ final class WebViewCore { int mY; int mMetaState; boolean mReprocess; - float mViewX; - float mViewY; } static class GeolocationPermissionsData { @@ -718,7 +724,11 @@ final class WebViewCore { boolean mRemember; } - + static class ModifySelectionData { + String mAlter; + String mDirection; + String mGranularity; + } static final String[] HandlerDebugString = { "REQUEST_LABEL", // 97 @@ -865,6 +875,9 @@ final class WebViewCore { static final int ADD_PACKAGE_NAME = 185; static final int REMOVE_PACKAGE_NAME = 186; + // accessibility support + static final int MODIFY_SELECTION = 190; + // private message ids private static final int DESTROY = 200; @@ -1238,6 +1251,19 @@ final class WebViewCore { nativeSetSelection(msg.arg1, msg.arg2); break; + case MODIFY_SELECTION: + ModifySelectionData modifySelectionData = + (ModifySelectionData) msg.obj; + String selectionString = nativeModifySelection( + modifySelectionData.mAlter, + modifySelectionData.mDirection, + modifySelectionData.mGranularity); + + mWebView.mPrivateHandler.obtainMessage( + WebView.SELECTION_STRING_CHANGED, selectionString) + .sendToTarget(); + break; + case LISTBOX_CHOICES: SparseBooleanArray choices = (SparseBooleanArray) msg.obj; @@ -1575,11 +1601,12 @@ final class WebViewCore { if (keyCode >= KeyEvent.KEYCODE_DPAD_UP && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { if (DebugFlags.WEB_VIEW_CORE) { - Log.v(LOGTAG, "key: arrow unused by plugin: " + keyCode); + Log.v(LOGTAG, "key: arrow unused by page: " + keyCode); } if (mWebView != null && evt.isDown()) { Message.obtain(mWebView.mPrivateHandler, - WebView.MOVE_OUT_OF_PLUGIN, keyCode).sendToTarget(); + WebView.UNHANDLED_NAV_KEY, keyCode, + 0).sendToTarget(); } return; } diff --git a/core/java/android/webkit/ZoomControlBase.java b/core/java/android/webkit/ZoomControlBase.java new file mode 100644 index 0000000..be9e8f3 --- /dev/null +++ b/core/java/android/webkit/ZoomControlBase.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.webkit; + +interface ZoomControlBase { + + /** + * Causes the on-screen zoom control to be made visible + */ + public void show(); + + /** + * Causes the on-screen zoom control to disappear + */ + public void hide(); + + /** + * Enables the control to update its state if necessary in response to a + * change in the pages zoom level. For example, if the max zoom level is + * reached then the control can disable the button for zooming in. + */ + public void update(); + + /** + * Checks to see if the control is currently visible to the user. + */ + public boolean isVisible(); +} diff --git a/core/java/android/webkit/ZoomControlEmbedded.java b/core/java/android/webkit/ZoomControlEmbedded.java new file mode 100644 index 0000000..cd02c00 --- /dev/null +++ b/core/java/android/webkit/ZoomControlEmbedded.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.webkit; + +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.Toast; +import android.widget.ZoomButtonsController; + +class ZoomControlEmbedded implements ZoomControlBase { + + private final ZoomManager mZoomManager; + private final WebView mWebView; + + // The controller is lazily initialized in getControls() for performance. + private ZoomButtonsController mZoomButtonsController; + + public ZoomControlEmbedded(ZoomManager zoomManager, WebView webView) { + mZoomManager = zoomManager; + mWebView = webView; + } + + public void show() { + if (!getControls().isVisible() + && mZoomManager.mMinZoomScale < mZoomManager.mMaxZoomScale) { + + mZoomButtonsController.setVisible(true); + + WebSettings settings = mWebView.getSettings(); + int count = settings.getDoubleTapToastCount(); + if (mZoomManager.mInZoomOverview && count > 0) { + settings.setDoubleTapToastCount(--count); + Toast.makeText(mWebView.getContext(), + com.android.internal.R.string.double_tap_toast, + Toast.LENGTH_LONG).show(); + } + } + } + + public void hide() { + if (mZoomButtonsController != null) { + mZoomButtonsController.setVisible(false); + } + } + + public boolean isVisible() { + return mZoomButtonsController != null && mZoomButtonsController.isVisible(); + } + + public void update() { + if (mZoomButtonsController == null) { + return; + } + + boolean canZoomIn = mWebView.getScale() < mZoomManager.mMaxZoomScale; + boolean canZoomOut = mWebView.getScale() > mZoomManager.mMinZoomScale && + !mZoomManager.mInZoomOverview; + if (!canZoomIn && !canZoomOut) { + // Hide the zoom in and out buttons if the page cannot zoom + mZoomButtonsController.getZoomControls().setVisibility(View.GONE); + } else { + // Set each one individually, as a page may be able to zoom in or out + mZoomButtonsController.setZoomInEnabled(canZoomIn); + mZoomButtonsController.setZoomOutEnabled(canZoomOut); + } + } + + private ZoomButtonsController getControls() { + if (mZoomButtonsController == null) { + mZoomButtonsController = new ZoomButtonsController(mWebView); + mZoomButtonsController.setOnZoomListener(new ZoomListener()); + // ZoomButtonsController positions the buttons at the bottom, but in + // the middle. Change their layout parameters so they appear on the + // right. + View controls = mZoomButtonsController.getZoomControls(); + ViewGroup.LayoutParams params = controls.getLayoutParams(); + if (params instanceof FrameLayout.LayoutParams) { + ((FrameLayout.LayoutParams) params).gravity = Gravity.RIGHT; + } + } + return mZoomButtonsController; + } + + private class ZoomListener implements ZoomButtonsController.OnZoomListener { + + public void onVisibilityChanged(boolean visible) { + if (visible) { + mWebView.switchOutDrawHistory(); + // Bring back the hidden zoom controls. + mZoomButtonsController.getZoomControls().setVisibility(View.VISIBLE); + update(); + } + } + + public void onZoom(boolean zoomIn) { + if (zoomIn) { + mWebView.zoomIn(); + } else { + mWebView.zoomOut(); + } + update(); + } + } +} diff --git a/core/java/android/webkit/ZoomControlExternal.java b/core/java/android/webkit/ZoomControlExternal.java new file mode 100644 index 0000000..d75313e --- /dev/null +++ b/core/java/android/webkit/ZoomControlExternal.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.webkit; + +import android.content.Context; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.View.OnClickListener; +import android.view.animation.AlphaAnimation; +import android.widget.FrameLayout; + +@Deprecated +class ZoomControlExternal implements ZoomControlBase { + + // The time that the external controls are visible before fading away + private static final long ZOOM_CONTROLS_TIMEOUT = + ViewConfiguration.getZoomControlsTimeout(); + // The view containing the external zoom controls + private ExtendedZoomControls mZoomControls; + private Runnable mZoomControlRunnable; + private final Handler mPrivateHandler = new Handler(); + + private final WebView mWebView; + + public ZoomControlExternal(WebView webView) { + mWebView = webView; + } + + public void show() { + if(mZoomControlRunnable != null) { + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + } + getControls().show(true); + mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT); + } + + public void hide() { + if (mZoomControlRunnable != null) { + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + } + if (mZoomControls != null) { + mZoomControls.hide(); + } + } + + public boolean isVisible() { + return mZoomControls != null && mZoomControls.isShown(); + } + + public void update() { } + + public ExtendedZoomControls getControls() { + if (mZoomControls == null) { + mZoomControls = createZoomControls(); + + /* + * need to be set to VISIBLE first so that getMeasuredHeight() in + * {@link #onSizeChanged()} can return the measured value for proper + * layout. + */ + mZoomControls.setVisibility(View.VISIBLE); + mZoomControlRunnable = new Runnable() { + public void run() { + /* Don't dismiss the controls if the user has + * focus on them. Wait and check again later. + */ + if (!mZoomControls.hasFocus()) { + mZoomControls.hide(); + } else { + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + mPrivateHandler.postDelayed(mZoomControlRunnable, + ZOOM_CONTROLS_TIMEOUT); + } + } + }; + } + return mZoomControls; + } + + private ExtendedZoomControls createZoomControls() { + ExtendedZoomControls zoomControls = new ExtendedZoomControls(mWebView.getContext()); + zoomControls.setOnZoomInClickListener(new OnClickListener() { + public void onClick(View v) { + // reset time out + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT); + mWebView.zoomIn(); + } + }); + zoomControls.setOnZoomOutClickListener(new OnClickListener() { + public void onClick(View v) { + // reset time out + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT); + mWebView.zoomOut(); + } + }); + return zoomControls; + } + + private static class ExtendedZoomControls extends FrameLayout { + + private android.widget.ZoomControls mPlusMinusZoomControls; + + public ExtendedZoomControls(Context context) { + super(context, null); + LayoutInflater inflater = (LayoutInflater) + context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(com.android.internal.R.layout.zoom_magnify, this, true); + mPlusMinusZoomControls = (android.widget.ZoomControls) findViewById( + com.android.internal.R.id.zoomControls); + findViewById(com.android.internal.R.id.zoomMagnify).setVisibility( + View.GONE); + } + + public void show(boolean showZoom) { + mPlusMinusZoomControls.setVisibility(showZoom ? View.VISIBLE : View.GONE); + fade(View.VISIBLE, 0.0f, 1.0f); + } + + public void hide() { + fade(View.GONE, 1.0f, 0.0f); + } + + private void fade(int visibility, float startAlpha, float endAlpha) { + AlphaAnimation anim = new AlphaAnimation(startAlpha, endAlpha); + anim.setDuration(500); + startAnimation(anim); + setVisibility(visibility); + } + + public boolean hasFocus() { + return mPlusMinusZoomControls.hasFocus(); + } + + public void setOnZoomInClickListener(OnClickListener listener) { + mPlusMinusZoomControls.setOnZoomInClickListener(listener); + } + + public void setOnZoomOutClickListener(OnClickListener listener) { + mPlusMinusZoomControls.setOnZoomOutClickListener(listener); + } + } +} diff --git a/core/java/android/webkit/ZoomManager.java b/core/java/android/webkit/ZoomManager.java new file mode 100644 index 0000000..8ec771f --- /dev/null +++ b/core/java/android/webkit/ZoomManager.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.view.View; + +class ZoomManager { + + static final String LOGTAG = "webviewZoom"; + + private final WebView mWebView; + + // manages the on-screen zoom functions of the WebView + private ZoomControlEmbedded mEmbeddedZoomControl; + + private ZoomControlExternal mExternalZoomControl; + + /* + * TODO: clean up the visibility of the class variables when the zoom + * refactoring is complete + */ + + // default scale limits, which are dependent on the display density + static float DEFAULT_MAX_ZOOM_SCALE; + static float DEFAULT_MIN_ZOOM_SCALE; + + // actual scale limits, which can be set through a webpage viewport meta tag + float mMaxZoomScale; + float mMinZoomScale; + + // locks the minimum ZoomScale to the value currently set in mMinZoomScale + boolean mMinZoomScaleFixed = true; + + // while in the zoom overview mode, the page's width is fully fit to the + // current window. The page is alive, in another words, you can click to + // follow the links. Double tap will toggle between zoom overview mode and + // the last zoom scale. + boolean mInZoomOverview = false; + + public ZoomManager(WebView webView) { + mWebView = webView; + } + + public void init(float density) { + DEFAULT_MAX_ZOOM_SCALE = 4.0f * density; + DEFAULT_MIN_ZOOM_SCALE = 0.25f * density; + mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; + mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; + } + + private ZoomControlBase getCurrentZoomControl() { + if (mWebView.getSettings() != null && mWebView.getSettings().supportZoom()) { + if (mWebView.getSettings().getBuiltInZoomControls()) { + if (mEmbeddedZoomControl == null) { + mEmbeddedZoomControl = new ZoomControlEmbedded(this, mWebView); + } + return mEmbeddedZoomControl; + } else { + if (mExternalZoomControl == null) { + mExternalZoomControl = new ZoomControlExternal(mWebView); + } + return mExternalZoomControl; + } + } + return null; + } + + public void invokeZoomPicker() { + ZoomControlBase control = getCurrentZoomControl(); + if (control != null) { + control.show(); + } + } + + public void dismissZoomPicker() { + ZoomControlBase control = getCurrentZoomControl(); + if (control != null) { + control.hide(); + } + } + + public boolean isZoomPickerVisible() { + ZoomControlBase control = getCurrentZoomControl(); + return (control != null) ? control.isVisible() : false; + } + + public void updateZoomPicker() { + ZoomControlBase control = getCurrentZoomControl(); + if (control != null) { + control.update(); + } + } + + /** + * The embedded zoom control intercepts touch events and automatically stays + * visible. The external control needs to constantly refresh its internal + * timer to stay visible. + */ + public void keepZoomPickerVisible() { + ZoomControlBase control = getCurrentZoomControl(); + if (control != null && control == mExternalZoomControl) { + control.show(); + } + } + + public View getExternalZoomPicker() { + ZoomControlBase control = getCurrentZoomControl(); + if (control != null && control == mExternalZoomControl) { + return mExternalZoomControl.getControls(); + } else { + return null; + } + } +} diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 48e7f79..78bbbce 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -559,6 +559,16 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te boolean smoothScrollbar = a.getBoolean(R.styleable.AbsListView_smoothScrollbar, true); setSmoothScrollbarEnabled(smoothScrollbar); + + final int adapterId = a.getResourceId(R.styleable.AbsListView_adapter, 0); + if (adapterId != 0) { + final Context c = context; + post(new Runnable() { + public void run() { + setAdapter(Adapters.loadAdapter(c, adapterId)); + } + }); + } a.recycle(); } @@ -1575,6 +1585,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te treeObserver.addOnGlobalLayoutListener(this); } } + + if (mAdapter != null && mDataSetObserver == null) { + mDataSetObserver = new AdapterDataSetObserver(); + mAdapter.registerDataSetObserver(mDataSetObserver); + } } @Override @@ -1595,6 +1610,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mGlobalLayoutListenerAddedFilter = false; } } + + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mDataSetObserver); + mDataSetObserver = null; + } } @Override diff --git a/core/java/android/widget/Adapters.java b/core/java/android/widget/Adapters.java new file mode 100644 index 0000000..05e501a --- /dev/null +++ b/core/java/android/widget/Adapters.java @@ -0,0 +1,1191 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.widget; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.AsyncTask; +import android.util.AttributeSet; +import android.util.Xml; +import android.view.View; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.HashMap; + +import static com.android.internal.R.*; + +/** + * <p>This class can be used to load {@link android.widget.Adapter adapters} defined in + * XML resources. XML-defined adapters can be used to easily create adapters in your + * own application or to pass adapters to other processes.</p> + * + * <h2>Types of adapters</h2> + * <p>Adapters defined using XML resources can only be one of the following supported + * types. Arbitrary adapters are not supported to guarantee the safety of the loaded + * code when adapters are loaded across packages.</p> + * <ul> + * <li><a href="#xml-cursor-adapter">Cursor adapter</a>: a cursor adapter can be used + * to display the content of a cursor, most often coming from a content provider</li> + * </ul> + * <p>The complete XML format definition of each adapter type is available below.</p> + * + * <a name="xml-cursor-adapter" /> + * <h2>Cursor adapter</h2> + * <p>A cursor adapter XML definition starts with the + * <a href="#xml-cursor-adapter-tag"><code><cursor-adapter /></code></a> + * tag and may contain one or more instances of the following tags:</p> + * <ul> + * <li><a href="#xml-cursor-adapter-select-tag"><code><select /></code></a></li> + * <li><a href="#xml-cursor-adapter-bind-tag"><code><bind /></code></a></li> + * </ul> + * + * <a name="xml-cursor-adapter-tag" /> + * <h3><cursor-adapter /></h3> + * <p>The <code><cursor-adapter /></code> element defines the beginning of the + * document and supports the following attributes:</p> + * <ul> + * <li><code>android:layout</code>: Reference to the XML layout to be inflated for + * each item of the adapter. This attribute is mandatory.</li> + * <li><code>android:selection</code>: Selection expression, used when the + * <code>android:uri</code> attribute is defined or when the adapter is loaded with + * {@link android.widget.Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}. + * This attribute is optional.</li> + * <li><code>android:sortOrder</code>: Sort expression, used when the + * <code>android:uri</code> attribute is defined or when the adapter is loaded with + * {@link android.widget.Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}. + * This attribute is optional.</li> + * <li><code>android:uri</code>: URI of the content provider to query to retrieve a cursor. + * Specifying this attribute is equivalent to calling {@link android.widget.Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}. + * If you call this method, the value of the XML attribute is ignored. This attribute is + * optional.</li> + * </ul> + * <p>In addition, you can specify one or more instances of + * <a href="#xml-cursor-adapter-select-tag"><code><select /></code></a> and + * <a href="#xml-cursor-adapter-bind-tag"><code><bind /></code></a> tags as children + * of <code><cursor-adapter /></code>.</p> + * + * <a name="xml-cursor-adapter-select-tag" /> + * <h3><select /></h3> + * <p>The <code><select /></code> tag is used to select columns from the cursor + * when doing the query. This can be very useful when using transformations in the + * <code><bind /></code> elements. It can also be very useful if you are providing + * your own <a href="#xml-cursor-adapter-bind-data-types">binder</a> or + * <a href="#xml-cursor-adapter-bind-data-types">transformation</a> classes. + * <code><select /></code> elements are ignored if you supply the cursor yourself.</p> + * <p>The <code><select /></code> supports the following attributes:</p> + * <ul> + * <li><code>android:column</code>: Name of the column to select in the cursor during the + * query operation</li> + * </ul> + * <p><strong>Note:</strong> The column named <code>_id</code> is always implicitely + * selected.</p> + * + * <a name="xml-cursor-adapter-bind-tag" /> + * <h3><bind /></h3> + * <p>The <code><bind /></code> tag is used to bind a column from the cursor to + * a {@link android.view.View}. A column bound using this tag is automatically selected + * during the query and a matching + * <a href="#xml-cursor-adapter-select-tag"><code><select /></code> tag is therefore + * not required.</p> + * + * <p>Each binding is declared as a one to one matching but + * custom binder classes or special + * <a href="#xml-cursor-adapter-bind-data-transformation">data transformations</a> can + * allow you to bind several columns to a single view. In this case you must use the + * <a href="#xml-cursor-adapter-select-tag"><code><select /></code> tag to make + * sure any required column is part of the query.</p> + * + * <p>The <code><bind /></code> tag supports the following attributes:</p> + * <ul> + * <li><code>android:from</code>: The name of the column to bind from. + * This attribute is mandatory.</li> + * <li><code>android:to</code>: The id of the view to bind to. This attribute is mandatory.</li> + * <li><code>android:as</code>: The <a href="#xml-cursor-adapter-bind-data-types">data type</a> + * of the binding. This attribute is mandatory.</li> + * </ul> + * + * <p>In addition, a <code><bind /></code> can contain zero or more instances of + * <a href="#xml-cursor-adapter-bind-data-transformation">data transformations</a> chilren + * tags.</p> + * + * <a name="xml-cursor-adapter-bind-data-types" /> + * <h4>Binding data types</h4> + * <p>For a binding to occur the data type of the bound column/view pair must be specified. + * The following data types are currently supported:</p> + * <ul> + * <li><code>string</code>: The content of the column is interpreted as a string and must be + * bound to a {@link android.widget.TextView}</li> + * <li><code>image</code>: The content of the column is interpreted as a blob describing an + * image and must be bound to an {@link android.widget.ImageView}</li> + * <li><code>image-uri</code>: The content of the column is interpreted as a URI to an image + * and must be bound to an {@link android.widget.ImageView}</li> + * <li><code>drawable</code>: The content of the column is interpreted as a resource id to a + * drawable and must be bound to an {@link android.widget.ImageView}</li> + * <li>A fully qualified class name: The name of a class corresponding to an implementation of + * {@link android.widget.Adapters.CursorBinder}. Cursor binders can be used to provide + * bindings not supported by default. Custom binders cannot be used with + * {@link android.content.Context#isRestricted() restricted contexts}, for instance in an + * app widget</li> + * </ul> + * + * <a name="xml-cursor-adapter-bind-transformation" /> + * <h4>Binding transformations</h4> + * <p>When defining a data binding you can specify an optional transformation by using one + * of the following tags as a child of a <code><bind /></code> elements:</p> + * <ul> + * <li><code><map /></code>: Maps a constant string to a string or a resource. Use + * one instance of this tag per value you want to map</li> + * <li><code><transform /></code>: Transforms a column's value using an expression + * or an instance of {@link android.widget.Adapters.CursorTransformation}</li> + * </ul> + * <p>While several <code><map /></code> tags can be used at the same time, you cannot + * mix <code><map /></code> and <code><transform /></code> tags. If several + * <code><transform /></code> tags are specified, only the last one is retained.</p> + * + * <a name="xml-cursor-adapter-bind-transformation-map" /> + * <p><strong><map /></strong></p> + * <p>A map element simply specifies a value to match from and a value to match to. When + * a column's value equals the value to match from, it is replaced with the value to match + * to. The following attributes are supported:</p> + * <ul> + * <li><code>android:fromValue</code>: The value to match from. This attribute is mandatory</li> + * <li><code>android:toValue</code>: The value to match to. This value can be either a string + * or a resource identifier. This value is interpreted as a resource identifier when the + * data binding is of type <code>drawable</code>. This attribute is mandatory</li> + * </ul> + * + * <a name="xml-cursor-adapter-bind-transformation-transform" /> + * <p><strong><transform /></strong></p> + * <p>A simple transform that occurs either by calling a specified class or by performing + * simple text substitution. The following attributes are supported:</p> + * <ul> + * <li><code>android:withExpression</code>: The transformation expression. The expression is + * a string containing column names surrounded with curly braces { and }. During the + * transformation each column name is replaced by its value. All columns must have been + * selected in the query. An example of expression is <code>"First name: {first_name}, + * last name: {last_name}"</code>. This attribute is mandatory + * if <code>android:withClass</code> is not specified and ignored if <code>android:withClass</code> + * is specified</li> + * <li><code>android:withClass</code>: A fully qualified class name corresponding to an + * implementation of {@link android.widget.Adapters.CursorTransformation}. Custom + * transformationscannot be used with + * {@link android.content.Context#isRestricted() restricted contexts}, for instance in + * an app widget This attribute is mandatory if <code>android:withExpression</code> is + * not specified</li> + * </ul> + * + * <h3>Example</h3> + * <p>The following example defines a cursor adapter that queries all the contacts with + * a phone number using the contacts content provider. Each contact is displayed with + * its display name, its favorite status and its photo. To display photos, a custom data + * binder is declared:</p> + * + * <pre class="prettyprint"> + * <cursor-adapter xmlns:android="http://schemas.android.com/apk/res/android" + * android:uri="content://com.android.contacts/contacts" + * android:selection="has_phone_number=1" + * android:layout="@layout/contact_item"> + * + * <bind android:from="display_name" android:to="@id/name" android:as="string" /> + * <bind android:from="starred" android:to="@id/star" android:as="drawable"> + * <map android:fromValue="0" android:toValue="@android:drawable/star_big_off" /> + * <map android:fromValue="1" android:toValue="@android:drawable/star_big_on" /> + * </bind> + * <bind android:from="_id" android:to="@id/name" + * android:as="com.google.android.test.adapters.ContactPhotoBinder" /> + * + * </cursor-adapter> + * </pre> + * + * <h3>Related APIs</h3> + * <ul> + * <li>{@link android.widget.Adapters#loadAdapter(android.content.Context, int, Object[])}</li> + * <li>{@link android.widget.Adapters#loadCursorAdapter(android.content.Context, int, android.database.Cursor, Object[])}</li> + * <li>{@link android.widget.Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}</li> + * <li>{@link android.widget.Adapters.CursorBinder}</li> + * <li>{@link android.widget.Adapters.CursorTransformation}</li> + * <li>{@link android.widget.CursorAdapter}</li> + * </ul> + * + * @see android.widget.Adapter + * @see android.content.ContentProvider + * + * @attr ref android.R.styleable#CursorAdapter_layout + * @attr ref android.R.styleable#CursorAdapter_selection + * @attr ref android.R.styleable#CursorAdapter_sortOrder + * @attr ref android.R.styleable#CursorAdapter_uri + * @attr ref android.R.styleable#CursorAdapter_BindItem_as + * @attr ref android.R.styleable#CursorAdapter_BindItem_from + * @attr ref android.R.styleable#CursorAdapter_BindItem_to + * @attr ref android.R.styleable#CursorAdapter_MapItem_fromValue + * @attr ref android.R.styleable#CursorAdapter_MapItem_toValue + * @attr ref android.R.styleable#CursorAdapter_SelectItem_column + * @attr ref android.R.styleable#CursorAdapter_TransformItem_withClass + * @attr ref android.R.styleable#CursorAdapter_TransformItem_withExpression + */ +@SuppressWarnings({"JavadocReference"}) +public class Adapters { + private static final String ADAPTER_CURSOR = "cursor-adapter"; + + /** + * <p>Interface used to bind a {@link android.database.Cursor} column to a View. This + * interface can be used to provide bindings for data types not supported by the + * standard implementation of {@link android.widget.Adapters}.</p> + * + * <p>A binder is provided with a cursor transformation which may or may not be used + * to transform the value retrieved from the cursor. The transformation is guaranteed + * to never be null so it's always safe to apply the transformation.</p> + * + * <p>The binder is associated with a Context but can be re-used with multiple cursors. + * As such, the implementation should make no assumption about the Cursor in use.</p> + * + * @see android.view.View + * @see android.database.Cursor + * @see android.widget.Adapters.CursorTransformation + */ + public static abstract class CursorBinder { + /** + * <p>The context associated with this binder.</p> + */ + protected final Context mContext; + + /** + * <p>The transformation associated with this binder. This transformation is never + * null and may or may not be applied to the Cursor data during the + * {@link #bind(android.view.View, android.database.Cursor, int)} operation.</p> + * + * @see #bind(android.view.View, android.database.Cursor, int) + */ + protected final CursorTransformation mTransformation; + + /** + * <p>Creates a new Cursor binder.</p> + * + * @param context The context associated with this binder. + * @param transformation The transformation associated with this binder. This + * transformation may or may not be applied by the binder and is guaranteed + * to not be null. + */ + public CursorBinder(Context context, CursorTransformation transformation) { + mContext = context; + mTransformation = transformation; + } + + /** + * <p>Binds the specified Cursor column to the supplied View. The binding operation + * can query other Cursor columns as needed. During the binding operation, values + * retrieved from the Cursor may or may not be transformed using this binder's + * cursor transformation.</p> + * + * @param view The view to bind data to. + * @param cursor The cursor to bind data from. + * @param columnIndex The column index in the cursor where the data to bind resides. + * + * @see #mTransformation + * + * @return True if the column was successfully bound to the View, false otherwise. + */ + public abstract boolean bind(View view, Cursor cursor, int columnIndex); + } + + /** + * <p>Interface used to transform data coming out of a {@link android.database.Cursor} + * before it is bound to a {@link android.view.View}.</p> + * + * <p>Transformations are used to transform text-based data (in the form of a String), + * or to transform data into a resource identifier. A default implementation is provided + * to generate resource identifiers.</p> + * + * @see android.database.Cursor + * @see android.widget.Adapters.CursorBinder + */ + public static abstract class CursorTransformation { + /** + * <p>The context associated with this transformation.</p> + */ + protected final Context mContext; + + /** + * <p>Creates a new Cursor transformation.</p> + * + * @param context The context associated with this transformation. + */ + public CursorTransformation(Context context) { + mContext = context; + } + + /** + * <p>Transforms the specified Cursor column into a String. The transformation + * can simply return the content of the column as a String (this is known + * as the identity transformation) or manipulate the content. For instance, + * a transformation can perform text substitutions or concatenate other + * columns with the specified column.</p> + * + * @param cursor The cursor that contains the data to transform. + * @param columnIndex The index of the column to transform. + * + * @return A String containing the transformed value of the column. + */ + public abstract String transform(Cursor cursor, int columnIndex); + + /** + * <p>Transforms the specified Cursor column into a resource identifier. + * The default implementation simply interprets the content of the column + * as an integer.</p> + * + * @param cursor The cursor that contains the data to transform. + * @param columnIndex The index of the column to transform. + * + * @return A resource identifier. + */ + public int transformToResource(Cursor cursor, int columnIndex) { + return cursor.getInt(columnIndex); + } + } + + /** + * <p>Loads the {@link android.widget.CursorAdapter} defined in the specified + * XML resource. The content of the adapter is loaded from the content provider + * identified by the supplied URI.</p> + * + * <p><strong>Note:</strong> If the supplied {@link android.content.Context} is + * an {@link android.app.Activity}, the cursor returned by the content provider + * will be automatically managed. Otherwise, you are responsible for managing the + * cursor yourself.</p> + * + * <p>The format of the XML definition of the cursor adapter is documented at + * the top of this page.</p> + * + * @param context The context to load the XML resource from. + * @param id The identifier of the XML resource declaring the adapter. + * @param uri The URI of the content provider. + * @param parameters Optional parameters to pass to the CursorAdapter, used + * to substitute values in the selection expression. + * + * @return A {@link android.widget.CursorAdapter} + * + * @throws IllegalArgumentException If the XML resource does not contain + * a valid <cursor-adapter /> definition. + * + * @see android.content.ContentProvider + * @see android.widget.CursorAdapter + * @see #loadAdapter(android.content.Context, int, Object[]) + */ + public static CursorAdapter loadCursorAdapter(Context context, int id, String uri, + Object... parameters) { + + XmlCursorAdapter adapter = (XmlCursorAdapter) loadAdapter(context, id, ADAPTER_CURSOR, + parameters); + + if (uri != null) { + adapter.setUri(uri); + } + adapter.load(); + + return adapter; + } + + /** + * <p>Loads the {@link android.widget.CursorAdapter} defined in the specified + * XML resource. The content of the adapter is loaded from the specified cursor. + * You are responsible for managing the supplied cursor.</p> + * + * <p>The format of the XML definition of the cursor adapter is documented at + * the top of this page.</p> + * + * @param context The context to load the XML resource from. + * @param id The identifier of the XML resource declaring the adapter. + * @param cursor The cursor containing the data for the adapter. + * @param parameters Optional parameters to pass to the CursorAdapter, used + * to substitute values in the selection expression. + * + * @return A {@link android.widget.CursorAdapter} + * + * @throws IllegalArgumentException If the XML resource does not contain + * a valid <cursor-adapter /> definition. + * + * @see android.content.ContentProvider + * @see android.widget.CursorAdapter + * @see android.database.Cursor + * @see #loadAdapter(android.content.Context, int, Object[]) + */ + public static CursorAdapter loadCursorAdapter(Context context, int id, Cursor cursor, + Object... parameters) { + + XmlCursorAdapter adapter = (XmlCursorAdapter) loadAdapter(context, id, ADAPTER_CURSOR, + parameters); + + if (cursor != null) { + adapter.changeCursor(cursor); + } + + return adapter; + } + + /** + * <p>Loads the adapter defined in the specified XML resource. The XML definition of + * the adapter must follow the format definition of one of the supported adapter + * types described at the top of this page.</p> + * + * <p><strong>Note:</strong> If the loaded adapter is a {@link android.widget.CursorAdapter} + * and the supplied {@link android.content.Context} is an {@link android.app.Activity}, + * the cursor returned by the content provider will be automatically managed. Otherwise, + * you are responsible for managing the cursor yourself.</p> + * + * @param context The context to load the XML resource from. + * @param id The identifier of the XML resource declaring the adapter. + * @param parameters Optional parameters to pass to the adapter. + * + * @return An adapter instance. + * + * @see #loadCursorAdapter(android.content.Context, int, android.database.Cursor, Object[]) + * @see #loadCursorAdapter(android.content.Context, int, String, Object[]) + */ + public static BaseAdapter loadAdapter(Context context, int id, Object... parameters) { + final BaseAdapter adapter = loadAdapter(context, id, null, parameters); + if (adapter instanceof ManagedAdapter) { + ((ManagedAdapter) adapter).load(); + } + return adapter; + } + + /** + * Loads an adapter from the specified XML resource. The optional assertName can + * be used to exit early if the adapter defined in the XML resource is not of the + * expected type. + * + * @param context The context to associate with the adapter. + * @param id The resource id of the XML document defining the adapter. + * @param assertName The mandatory name of the adapter in the XML document. + * Ignored if null. + * @param parameters Optional parameters passed to the adapter. + * + * @return An instance of {@link android.widget.BaseAdapter}. + */ + private static BaseAdapter loadAdapter(Context context, int id, String assertName, + Object... parameters) { + + XmlResourceParser parser = null; + try { + parser = context.getResources().getXml(id); + return createAdapterFromXml(context, parser, Xml.asAttributeSet(parser), + id, parameters, assertName); + } catch (XmlPullParserException ex) { + Resources.NotFoundException rnf = new Resources.NotFoundException( + "Can't load adapter resource ID " + + context.getResources().getResourceEntryName(id)); + rnf.initCause(ex); + throw rnf; + } catch (IOException ex) { + Resources.NotFoundException rnf = new Resources.NotFoundException( + "Can't load adapter resource ID " + + context.getResources().getResourceEntryName(id)); + rnf.initCause(ex); + throw rnf; + } finally { + if (parser != null) parser.close(); + } + } + + /** + * Generates an adapter using the specified XML parser. This method is responsible + * for choosing the type of the adapter to create based on the content of the + * XML parser. + * + * This method will generate an {@link IllegalArgumentException} if + * <code>assertName</code> is not null and does not match the root tag of the XML + * document. + */ + private static BaseAdapter createAdapterFromXml(Context c, + XmlPullParser parser, AttributeSet attrs, int id, Object[] parameters, + String assertName) throws XmlPullParserException, IOException { + + BaseAdapter adapter = null; + + // Make sure we are on a start tag. + int type; + int depth = parser.getDepth(); + + while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && + type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG) { + continue; + } + + String name = parser.getName(); + if (assertName != null && !assertName.equals(name)) { + throw new IllegalArgumentException("The adapter defined in " + + c.getResources().getResourceEntryName(id) + " must be a <" + name + " />"); + } + + if (ADAPTER_CURSOR.equals(name)) { + adapter = createCursorAdapter(c, parser, attrs, id, parameters); + } else { + throw new IllegalArgumentException("Unknown adapter name " + parser.getName() + + " in " + c.getResources().getResourceEntryName(id)); + } + } + + return adapter; + + } + + /** + * Creates an XmlCursorAdapter using an XmlCursorAdapterParser. + */ + private static XmlCursorAdapter createCursorAdapter(Context c, XmlPullParser parser, + AttributeSet attrs, int id, Object[] parameters) + throws IOException, XmlPullParserException { + + return new XmlCursorAdapterParser(c, parser, attrs, id).parse(parameters); + } + + /** + * Parser that can generate XmlCursorAdapter instances. This parser is responsible for + * handling all the attributes and child nodes for a <cursor-adapter />. + */ + private static class XmlCursorAdapterParser { + private static final String ADAPTER_CURSOR_BIND = "bind"; + private static final String ADAPTER_CURSOR_SELECT = "select"; + private static final String ADAPTER_CURSOR_AS_STRING = "string"; + private static final String ADAPTER_CURSOR_AS_IMAGE = "image"; + private static final String ADAPTER_CURSOR_AS_IMAGE_URI = "image-uri"; + private static final String ADAPTER_CURSOR_AS_DRAWABLE = "drawable"; + private static final String ADAPTER_CURSOR_MAP = "map"; + private static final String ADAPTER_CURSOR_TRANSFORM = "transform"; + + private final Context mContext; + private final XmlPullParser mParser; + private final AttributeSet mAttrs; + private final int mId; + + private final HashMap<String, CursorBinder> mBinders; + private final ArrayList<String> mFrom; + private final ArrayList<Integer> mTo; + private final CursorTransformation mIdentity; + private final Resources mResources; + + public XmlCursorAdapterParser(Context c, XmlPullParser parser, AttributeSet attrs, int id) { + mContext = c; + mParser = parser; + mAttrs = attrs; + mId = id; + + mResources = mContext.getResources(); + mBinders = new HashMap<String, CursorBinder>(); + mFrom = new ArrayList<String>(); + mTo = new ArrayList<Integer>(); + mIdentity = new IdentityTransformation(mContext); + } + + public XmlCursorAdapter parse(Object[] parameters) + throws IOException, XmlPullParserException { + + Resources resources = mResources; + TypedArray a = resources.obtainAttributes(mAttrs, styleable.CursorAdapter); + + String uri = a.getString(styleable.CursorAdapter_uri); + String selection = a.getString(styleable.CursorAdapter_selection); + String sortOrder = a.getString(styleable.CursorAdapter_sortOrder); + int layout = a.getResourceId(styleable.CursorAdapter_layout, 0); + if (layout == 0) { + throw new IllegalArgumentException("The layout specified in " + + resources.getResourceEntryName(mId) + " does not exist"); + } + + a.recycle(); + + XmlPullParser parser = mParser; + int type; + int depth = parser.getDepth(); + + while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && + type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG) { + continue; + } + + String name = parser.getName(); + + if (ADAPTER_CURSOR_BIND.equals(name)) { + parseBindTag(); + } else if (ADAPTER_CURSOR_SELECT.equals(name)) { + parseSelectTag(); + } else { + throw new RuntimeException("Unknown tag name " + parser.getName() + " in " + + resources.getResourceEntryName(mId)); + } + } + + String[] fromArray = mFrom.toArray(new String[mFrom.size()]); + int[] toArray = new int[mTo.size()]; + for (int i = 0; i < toArray.length; i++) { + toArray[i] = mTo.get(i); + } + + String[] selectionArgs = null; + if (parameters != null) { + selectionArgs = new String[parameters.length]; + for (int i = 0; i < selectionArgs.length; i++) { + selectionArgs[i] = (String) parameters[i]; + } + } + + return new XmlCursorAdapter(mContext, layout, uri, fromArray, toArray, selection, + selectionArgs, sortOrder, mBinders); + } + + private void parseSelectTag() { + TypedArray a = mResources.obtainAttributes(mAttrs, styleable.CursorAdapter_SelectItem); + + String fromName = a.getString(styleable.CursorAdapter_SelectItem_column); + if (fromName == null) { + throw new IllegalArgumentException("A select item in " + + mResources.getResourceEntryName(mId) + " does not have a 'column' attribute"); + } + + a.recycle(); + + mFrom.add(fromName); + mTo.add(View.NO_ID); + } + + private void parseBindTag() throws IOException, XmlPullParserException { + Resources resources = mResources; + TypedArray a = resources.obtainAttributes(mAttrs, styleable.CursorAdapter_BindItem); + + String fromName = a.getString(styleable.CursorAdapter_BindItem_from); + if (fromName == null) { + throw new IllegalArgumentException("A bind item in " + + resources.getResourceEntryName(mId) + " does not have a 'from' attribute"); + } + + int toName = a.getResourceId(styleable.CursorAdapter_BindItem_to, 0); + if (toName == 0) { + throw new IllegalArgumentException("A bind item in " + + resources.getResourceEntryName(mId) + " does not have a 'to' attribute"); + } + + String asType = a.getString(styleable.CursorAdapter_BindItem_as); + if (asType == null) { + throw new IllegalArgumentException("A bind item in " + + resources.getResourceEntryName(mId) + " does not have an 'as' attribute"); + } + + mFrom.add(fromName); + mTo.add(toName); + mBinders.put(fromName, findBinder(asType)); + + a.recycle(); + } + + private CursorBinder findBinder(String type) throws IOException, XmlPullParserException { + final XmlPullParser parser = mParser; + final Context context = mContext; + CursorTransformation transformation = mIdentity; + + int tagType; + int depth = parser.getDepth(); + + final boolean isDrawable = ADAPTER_CURSOR_AS_DRAWABLE.equals(type); + + while (((tagType = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) + && tagType != XmlPullParser.END_DOCUMENT) { + + if (tagType != XmlPullParser.START_TAG) { + continue; + } + + String name = parser.getName(); + + if (ADAPTER_CURSOR_TRANSFORM.equals(name)) { + transformation = findTransformation(); + } else if (ADAPTER_CURSOR_MAP.equals(name)) { + if (!(transformation instanceof MapTransformation)) { + transformation = new MapTransformation(context); + } + findMap(((MapTransformation) transformation), isDrawable); + } else { + throw new RuntimeException("Unknown tag name " + parser.getName() + " in " + + context.getResources().getResourceEntryName(mId)); + } + } + + if (ADAPTER_CURSOR_AS_STRING.equals(type)) { + return new StringBinder(context, transformation); + } else if (ADAPTER_CURSOR_AS_IMAGE.equals(type)) { + return new ImageBinder(context, transformation); + } else if (ADAPTER_CURSOR_AS_IMAGE_URI.equals(type)) { + return new ImageUriBinder(context, transformation); + } else if (isDrawable) { + return new DrawableBinder(context, transformation); + } else { + return createBinder(type, transformation); + } + } + + private CursorBinder createBinder(String type, CursorTransformation transformation) { + if (mContext.isRestricted()) return null; + + try { + final Class<?> klass = Class.forName(type, true, mContext.getClassLoader()); + if (CursorBinder.class.isAssignableFrom(klass)) { + final Constructor<?> c = klass.getDeclaredConstructor( + Context.class, CursorTransformation.class); + return (CursorBinder) c.newInstance(mContext, transformation); + } + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Cannot instanciate binder type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + type, e); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("Cannot instanciate binder type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + type, e); + } catch (InvocationTargetException e) { + throw new IllegalArgumentException("Cannot instanciate binder type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + type, e); + } catch (InstantiationException e) { + throw new IllegalArgumentException("Cannot instanciate binder type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + type, e); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Cannot instanciate binder type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + type, e); + } + + return null; + } + + private void findMap(MapTransformation transformation, boolean drawable) { + Resources resources = mResources; + + TypedArray a = resources.obtainAttributes(mAttrs, styleable.CursorAdapter_MapItem); + + String from = a.getString(styleable.CursorAdapter_MapItem_fromValue); + if (from == null) { + throw new IllegalArgumentException("A map item in " + + resources.getResourceEntryName(mId) + " does not have a 'fromValue' attribute"); + } + + if (!drawable) { + String to = a.getString(styleable.CursorAdapter_MapItem_toValue); + if (to == null) { + throw new IllegalArgumentException("A map item in " + + resources.getResourceEntryName(mId) + " does not have a 'toValue' attribute"); + } + transformation.addStringMapping(from, to); + } else { + int to = a.getResourceId(styleable.CursorAdapter_MapItem_toValue, 0); + if (to == 0) { + throw new IllegalArgumentException("A map item in " + + resources.getResourceEntryName(mId) + " does not have a 'toValue' attribute"); + } + transformation.addResourceMapping(from, to); + } + + a.recycle(); + } + + private CursorTransformation findTransformation() { + Resources resources = mResources; + CursorTransformation transformation = null; + TypedArray a = resources.obtainAttributes(mAttrs, styleable.CursorAdapter_TransformItem); + + String className = a.getString(styleable.CursorAdapter_TransformItem_withClass); + if (className == null) { + String expression = a.getString( + styleable.CursorAdapter_TransformItem_withExpression); + transformation = createExpressionTransformation(expression); + } else if (!mContext.isRestricted()) { + try { + final Class<?> klass = Class.forName(className, true, mContext.getClassLoader()); + if (CursorTransformation.class.isAssignableFrom(klass)) { + final Constructor<?> c = klass.getDeclaredConstructor(Context.class); + transformation = (CursorTransformation) c.newInstance(mContext); + } + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Cannot instanciate transform type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + className, e); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("Cannot instanciate transform type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + className, e); + } catch (InvocationTargetException e) { + throw new IllegalArgumentException("Cannot instanciate transform type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + className, e); + } catch (InstantiationException e) { + throw new IllegalArgumentException("Cannot instanciate transform type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + className, e); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Cannot instanciate transform type in " + + mContext.getResources().getResourceEntryName(mId) + ": " + className, e); + } + } + + a.recycle(); + + if (transformation == null) { + throw new IllegalArgumentException("A transform item in " + + resources.getResourceEntryName(mId) + " must have a 'withClass' or " + + "'withExpression' attribute"); + } + + return transformation; + } + + private CursorTransformation createExpressionTransformation(String expression) { + return new ExpressionTransformation(mContext, expression); + } + } + + /** + * Interface used by adapters that require to be loaded after creation. + */ + private static interface ManagedAdapter { + /** + * Loads the content of the adapter, asynchronously. + */ + void load(); + } + + /** + * Implementation of a Cursor adapter defined in XML. This class is a thin wrapper + * of a SimpleCursorAdapter. The main difference is the ability to handle CursorBinders. + */ + private static class XmlCursorAdapter extends SimpleCursorAdapter implements ManagedAdapter { + private final Context mContext; + private String mUri; + private final String mSelection; + private final String[] mSelectionArgs; + private final String mSortOrder; + private final String[] mColumns; + private final CursorBinder[] mBinders; + private AsyncTask<Void,Void,Cursor> mLoadTask; + + XmlCursorAdapter(Context context, int layout, String uri, String[] from, int[] to, + String selection, String[] selectionArgs, String sortOrder, + HashMap<String, CursorBinder> binders) { + + super(context, layout, null, from, to); + mContext = context; + mUri = uri; + mSelection = selection; + mSelectionArgs = selectionArgs; + mSortOrder = sortOrder; + mColumns = new String[from.length + 1]; + // This is mandatory in CursorAdapter + mColumns[0] = "_id"; + System.arraycopy(from, 0, mColumns, 1, from.length); + + CursorBinder basic = new StringBinder(context, new IdentityTransformation(context)); + final int count = from.length; + mBinders = new CursorBinder[count]; + + for (int i = 0; i < count; i++) { + CursorBinder binder = binders.get(from[i]); + if (binder == null) binder = basic; + mBinders[i] = binder; + } + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + final int count = mTo.length; + final int[] from = mFrom; + final int[] to = mTo; + final CursorBinder[] binders = mBinders; + + for (int i = 0; i < count; i++) { + final View v = view.findViewById(to[i]); + if (v != null) { + binders[i].bind(v, cursor, from[i]); + } + } + } + + public void load() { + if (mUri != null) { + mLoadTask = new QueryTask().execute(); + } + } + + void setUri(String uri) { + mUri = uri; + } + + @Override + public void changeCursor(Cursor c) { + if (mLoadTask != null && mLoadTask.getStatus() != QueryTask.Status.FINISHED) { + mLoadTask.cancel(true); + mLoadTask = null; + } + super.changeCursor(c); + } + + class QueryTask extends AsyncTask<Void, Void, Cursor> { + @Override + protected Cursor doInBackground(Void... params) { + if (mContext instanceof Activity) { + return ((Activity) mContext).managedQuery( + Uri.parse(mUri), mColumns, mSelection, mSelectionArgs, mSortOrder); + } else { + return mContext.getContentResolver().query( + Uri.parse(mUri), mColumns, mSelection, mSelectionArgs, mSortOrder); + } + } + + @Override + protected void onPostExecute(Cursor cursor) { + if (!isCancelled()) { + XmlCursorAdapter.super.changeCursor(cursor); + } + } + } + } + + /** + * Identity transformation, returns the content of the specified column as a String, + * without performing any manipulation. This is used when no transformation is specified. + */ + private static class IdentityTransformation extends CursorTransformation { + public IdentityTransformation(Context context) { + super(context); + } + + @Override + public String transform(Cursor cursor, int columnIndex) { + return cursor.getString(columnIndex); + } + } + + /** + * An expression transformation is a simple template based replacement utility. + * In an expression, each segment of the form <code>{(^[}]+)}</code> is replaced + * with the value of the column of name $1. + */ + private static class ExpressionTransformation extends CursorTransformation { + private final ExpressionNode mFirstNode = new ConstantExpressionNode(""); + private final StringBuilder mBuilder = new StringBuilder(); + + public ExpressionTransformation(Context context, String expression) { + super(context); + + parse(expression); + } + + private void parse(String expression) { + ExpressionNode node = mFirstNode; + int segmentStart; + int count = expression.length(); + + for (int i = 0; i < count; i++) { + char c = expression.charAt(i); + // Start a column name segment + segmentStart = i; + if (c == '{') { + while (i < count && (c = expression.charAt(i)) != '}') { + i++; + } + // We've reached the end, but the expression didn't close + if (c != '}') { + throw new IllegalStateException("The transform expression contains a " + + "non-closed column name: " + + expression.substring(segmentStart + 1, i)); + } + node.next = new ColumnExpressionNode(expression.substring(segmentStart + 1, i)); + } else { + while (i < count && (c = expression.charAt(i)) != '{') { + i++; + } + node.next = new ConstantExpressionNode(expression.substring(segmentStart, i)); + // Rewind if we've reached a column expression + if (c == '{') i--; + } + node = node.next; + } + } + + @Override + public String transform(Cursor cursor, int columnIndex) { + final StringBuilder builder = mBuilder; + builder.delete(0, builder.length()); + + ExpressionNode node = mFirstNode; + // Skip the first node + while ((node = node.next) != null) { + builder.append(node.asString(cursor)); + } + + return builder.toString(); + } + + static abstract class ExpressionNode { + public ExpressionNode next; + + public abstract String asString(Cursor cursor); + } + + static class ConstantExpressionNode extends ExpressionNode { + private final String mConstant; + + ConstantExpressionNode(String constant) { + mConstant = constant; + } + + @Override + public String asString(Cursor cursor) { + return mConstant; + } + } + + static class ColumnExpressionNode extends ExpressionNode { + private final String mColumnName; + private Cursor mSignature; + private int mColumnIndex = -1; + + ColumnExpressionNode(String columnName) { + mColumnName = columnName; + } + + @Override + public String asString(Cursor cursor) { + if (cursor != mSignature || mColumnIndex == -1) { + mColumnIndex = cursor.getColumnIndex(mColumnName); + mSignature = cursor; + } + + return cursor.getString(mColumnIndex); + } + } + } + + /** + * A map transformation offers a simple mapping between specified String values + * to Strings or integers. + */ + private static class MapTransformation extends CursorTransformation { + private final HashMap<String, String> mStringMappings; + private final HashMap<String, Integer> mResourceMappings; + + public MapTransformation(Context context) { + super(context); + mStringMappings = new HashMap<String, String>(); + mResourceMappings = new HashMap<String, Integer>(); + } + + void addStringMapping(String from, String to) { + mStringMappings.put(from, to); + } + + void addResourceMapping(String from, int to) { + mResourceMappings.put(from, to); + } + + @Override + public String transform(Cursor cursor, int columnIndex) { + final String value = cursor.getString(columnIndex); + final String transformed = mStringMappings.get(value); + return transformed == null ? value : transformed; + } + + @Override + public int transformToResource(Cursor cursor, int columnIndex) { + final String value = cursor.getString(columnIndex); + final Integer transformed = mResourceMappings.get(value); + try { + return transformed == null ? Integer.parseInt(value) : transformed; + } catch (NumberFormatException e) { + return 0; + } + } + } + + /** + * Binds a String to a TextView. + */ + private static class StringBinder extends CursorBinder { + public StringBinder(Context context, CursorTransformation transformation) { + super(context, transformation); + } + + @Override + public boolean bind(View view, Cursor cursor, int columnIndex) { + ((TextView) view).setText(mTransformation.transform(cursor, columnIndex)); + return true; + } + } + + /** + * Binds an image blob to an ImageView. + */ + private static class ImageBinder extends CursorBinder { + public ImageBinder(Context context, CursorTransformation transformation) { + super(context, transformation); + } + + @Override + public boolean bind(View view, Cursor cursor, int columnIndex) { + final byte[] data = cursor.getBlob(columnIndex); + ((ImageView) view).setImageBitmap(BitmapFactory.decodeByteArray(data, 0, data.length)); + return true; + } + } + + /** + * Binds an image URI to an ImageView. + */ + private static class ImageUriBinder extends CursorBinder { + public ImageUriBinder(Context context, CursorTransformation transformation) { + super(context, transformation); + } + + @Override + public boolean bind(View view, Cursor cursor, int columnIndex) { + ((ImageView) view).setImageURI(Uri.parse( + mTransformation.transform(cursor, columnIndex))); + return true; + } + } + + /** + * Binds a drawable resource identifier to an ImageView. + */ + private static class DrawableBinder extends CursorBinder { + public DrawableBinder(Context context, CursorTransformation transformation) { + super(context, transformation); + } + + @Override + public boolean bind(View view, Cursor cursor, int columnIndex) { + final int resource = mTransformation.transformToResource(cursor, columnIndex); + if (resource == 0) return false; + + ((ImageView) view).setImageResource(resource); + return true; + } + } +} diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java index e15a520..8611901 100644 --- a/core/java/android/widget/AutoCompleteTextView.java +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -913,10 +913,10 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe if (mItemClickListener != null) { final DropDownListView list = mDropDownList; - // Note that we don't have a View here, so we will need to - // supply null. Hopefully no existing apps crash... - mItemClickListener.onItemClick(list, null, completion.getPosition(), - completion.getId()); + final int position = completion.getPosition(); + mItemClickListener.onItemClick(list, + list.getChildAt(position - list.getFirstVisiblePosition()), + position, completion.getId()); } } } diff --git a/core/java/android/widget/CursorAdapter.java b/core/java/android/widget/CursorAdapter.java index baa6833..4cf8785 100644 --- a/core/java/android/widget/CursorAdapter.java +++ b/core/java/android/widget/CursorAdapter.java @@ -80,6 +80,18 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, protected FilterQueryProvider mFilterQueryProvider; /** + * If set the adapter will call requery() on the cursor whenever a content change + * notification is delivered. Implies {@link #FLAG_REGISTER_CONTENT_OBSERVER} + */ + public static final int FLAG_AUTO_REQUERY = 0x01; + + /** + * If set the adapter will register a content observer on the cursor and will call + * {@link #onContentChanged()} when a notification comes in. + */ + public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02; + + /** * Constructor. The adapter will call requery() on the cursor whenever * it changes so that the most recent data is always displayed. * @@ -87,7 +99,7 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, * @param context The context */ public CursorAdapter(Context context, Cursor c) { - init(context, c, true); + init(context, c, FLAG_AUTO_REQUERY); } /** @@ -99,19 +111,43 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, * data is always displayed. */ public CursorAdapter(Context context, Cursor c, boolean autoRequery) { - init(context, c, autoRequery); + init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER); + } + + /** + * Constructor + * @param c The cursor from which to get the data. + * @param context The context + * @param flags flags used to determine the behavior of the adapter + */ + public CursorAdapter(Context context, Cursor c, int flags) { + init(context, c, flags); } protected void init(Context context, Cursor c, boolean autoRequery) { + init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER); + } + + protected void init(Context context, Cursor c, int flags) { + if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) { + flags |= FLAG_REGISTER_CONTENT_OBSERVER; + mAutoRequery = true; + } else { + mAutoRequery = false; + } boolean cursorPresent = c != null; - mAutoRequery = autoRequery; mCursor = c; mDataValid = cursorPresent; mContext = context; mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1; - mChangeObserver = new ChangeObserver(); + if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) { + mChangeObserver = new ChangeObserver(); + } else { + mChangeObserver = null; + } + if (cursorPresent) { - c.registerContentObserver(mChangeObserver); + if (mChangeObserver != null) c.registerContentObserver(mChangeObserver); c.registerDataSetObserver(mDataSetObserver); } } @@ -246,13 +282,13 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, return; } if (mCursor != null) { - mCursor.unregisterContentObserver(mChangeObserver); + if (mChangeObserver != null) mCursor.unregisterContentObserver(mChangeObserver); mCursor.unregisterDataSetObserver(mDataSetObserver); mCursor.close(); } mCursor = cursor; if (cursor != null) { - cursor.registerContentObserver(mChangeObserver); + if (mChangeObserver != null) cursor.registerContentObserver(mChangeObserver); cursor.registerDataSetObserver(mDataSetObserver); mRowIDColumn = cursor.getColumnIndexOrThrow("_id"); mDataValid = true; diff --git a/core/java/android/widget/Gallery.java b/core/java/android/widget/Gallery.java index 1ed6b16..c47292f 100644 --- a/core/java/android/widget/Gallery.java +++ b/core/java/android/widget/Gallery.java @@ -1207,7 +1207,7 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList // We unfocus the old child down here so the above hasFocus check // returns true - if (oldSelectedChild != null) { + if (oldSelectedChild != null && oldSelectedChild != child) { // Make sure its drawable state doesn't contain 'selected' oldSelectedChild.setSelected(false); @@ -1263,6 +1263,7 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList */ if (gainFocus && mSelectedChild != null) { mSelectedChild.requestFocus(direction); + mSelectedChild.setSelected(true); } } diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index d2829db..363e0a2 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -23,6 +23,7 @@ import android.util.AttributeSet; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; +import android.view.ViewDebug; import android.view.ViewGroup; import android.view.SoundEffectConstants; import android.view.animation.GridLayoutAnimationController; @@ -1774,6 +1775,19 @@ public class GridView extends AbsListView { requestLayoutIfNecessary(); } } + + /** + * Get the number of columns in the grid. + * Returns {@link #AUTO_FIT} if the Grid has never been laid out. + * + * @attr ref android.R.styleable#GridView_numColumns + * + * @see #setNumColumns(int) + */ + @ViewDebug.ExportedProperty + public int getNumColumns() { + return mNumColumns; + } /** * Make sure views are touching the top or bottom edge, as appropriate for diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 892c44a..5382894 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -415,7 +415,7 @@ public class ListView extends AbsListView { */ @Override public void setAdapter(ListAdapter adapter) { - if (null != mAdapter) { + if (mAdapter != null) { mAdapter.unregisterDataSetObserver(mDataSetObserver); } diff --git a/core/java/android/widget/QuickContactBadge.java b/core/java/android/widget/QuickContactBadge.java index 07c3e4b..50fbb6b 100644 --- a/core/java/android/widget/QuickContactBadge.java +++ b/core/java/android/widget/QuickContactBadge.java @@ -48,6 +48,7 @@ public class QuickContactBadge extends ImageView implements OnClickListener { private QueryHandler mQueryHandler; private Drawable mBadgeBackground; private Drawable mNoBadgeBackground; + private Drawable mDefaultAvatar; protected String[] mExcludeMimes = null; @@ -117,6 +118,16 @@ public class QuickContactBadge extends ImageView implements OnClickListener { public void setMode(int size) { mMode = size; } + + /** + * Resets the contact photo to the default state. + */ + public void setImageToDefault() { + if (mDefaultAvatar == null) { + mDefaultAvatar = getResources().getDrawable(R.drawable.ic_contact_picture); + } + setImageDrawable(mDefaultAvatar); + } /** * Assign the contact uri that this QuickContactBadge should be associated diff --git a/core/java/android/widget/SimpleCursorAdapter.java b/core/java/android/widget/SimpleCursorAdapter.java index 7d3459e..d1c2270 100644 --- a/core/java/android/widget/SimpleCursorAdapter.java +++ b/core/java/android/widget/SimpleCursorAdapter.java @@ -62,7 +62,8 @@ public class SimpleCursorAdapter extends ResourceCursorAdapter { private int mStringConversionColumn = -1; private CursorToStringConverter mCursorToStringConverter; private ViewBinder mViewBinder; - private String[] mOriginalFrom; + + String[] mOriginalFrom; /** * Constructor. diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 950012c..3466c17 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -2781,6 +2781,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener c.drawText(mChars, start + mStart, end - start, x, y, p); } + public void drawTextRun(Canvas c, int start, int end, + float x, float y, int flags, Paint p) { + c.drawTextRun(mChars, start + mStart, end - start, x, y, flags, p); + } + public float measureText(int start, int end, Paint p) { return p.measureText(mChars, start + mStart, end - start); } diff --git a/core/java/android/widget/ZoomButtonsController.java b/core/java/android/widget/ZoomButtonsController.java index 3df419a..450c966 100644 --- a/core/java/android/widget/ZoomButtonsController.java +++ b/core/java/android/widget/ZoomButtonsController.java @@ -66,8 +66,9 @@ import android.view.WindowManager.LayoutParams; * {@link #setZoomInEnabled(boolean)} and {@link #setZoomOutEnabled(boolean)}. * <p> * If you are using this with a custom View, please call - * {@link #setVisible(boolean) setVisible(false)} from the - * {@link View#onDetachedFromWindow}. + * {@link #setVisible(boolean) setVisible(false)} from + * {@link View#onDetachedFromWindow} and from {@link View#onVisibilityChanged} + * when <code>visibility != View.VISIBLE</code>. * */ public class ZoomButtonsController implements View.OnTouchListener { diff --git a/core/java/com/android/internal/app/SplitActionBar.java b/core/java/com/android/internal/app/SplitActionBar.java new file mode 100644 index 0000000..9204c00 --- /dev/null +++ b/core/java/com/android/internal/app/SplitActionBar.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app; + +import android.app.ActionBar; +import android.graphics.drawable.Drawable; +import android.view.ActionBarView; +import android.view.View; +import android.widget.LinearLayout; + +/** + * SplitActionBar is the ActionBar implementation used + * by small-screen devices. It expects to split contextual + * modes across both the ActionBarView at the top of the screen + * and a horizontal LinearLayout at the bottom which is normally + * hidden. + */ +public class SplitActionBar extends ActionBar { + private ActionBarView mActionView; + private LinearLayout mContextView; + + public SplitActionBar(ActionBarView view, LinearLayout contextView) { + mActionView = view; + mContextView = contextView; + } + + public void setCallback(Callback callback) { + mActionView.setCallback(callback); + } + + public void setCustomNavigationView(View view) { + mActionView.setCustomNavigationView(view); + } + + public void setTitle(CharSequence title) { + mActionView.setTitle(title); + } + + public void setSubtitle(CharSequence subtitle) { + mActionView.setSubtitle(subtitle); + } + + public void setNavigationMode(int mode) { + mActionView.setNavigationMode(mode); + } + + public void setDisplayOptions(int options) { + mActionView.setDisplayOptions(options); + } + + public void setDisplayOptions(int options, int mask) { + final int current = mActionView.getDisplayOptions(); + mActionView.setDisplayOptions((options & mask) | (current & ~mask)); + } + + public void setBackgroundDrawable(Drawable d) { + mActionView.setBackgroundDrawable(d); + } + + public void setDividerDrawable(Drawable d) { + mActionView.setDividerDrawable(d); + } + + public View getCustomNavigationView() { + return mActionView.getCustomNavigationView(); + } + + public CharSequence getTitle() { + return mActionView.getTitle(); + } + + public CharSequence getSubtitle() { + return mActionView.getSubtitle(); + } + + public int getNavigationMode() { + return mActionView.getNavigationMode(); + } + + public int getDisplayOptions() { + return mActionView.getDisplayOptions(); + } + + public void updateActionMenu() { + mActionView.updateActionMenu(); + } + +} diff --git a/core/java/com/android/internal/database/SortCursor.java b/core/java/com/android/internal/database/SortCursor.java index 99410bc..12248a2 100644 --- a/core/java/com/android/internal/database/SortCursor.java +++ b/core/java/com/android/internal/database/SortCursor.java @@ -182,24 +182,6 @@ public class SortCursor extends AbstractCursor } @Override - public boolean deleteRow() - { - return mCursor.deleteRow(); - } - - @Override - public boolean commitUpdates() { - int length = mCursors.length; - for (int i = 0 ; i < length ; i++) { - if (mCursors[i] != null) { - mCursors[i].commitUpdates(); - } - } - onChange(true); - return true; - } - - @Override public String getString(int column) { return mCursor.getString(column); diff --git a/core/java/com/android/internal/util/XmlUtils.java b/core/java/com/android/internal/util/XmlUtils.java index 8d8df16..e00a853 100644 --- a/core/java/com/android/internal/util/XmlUtils.java +++ b/core/java/com/android/internal/util/XmlUtils.java @@ -26,6 +26,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -284,6 +285,26 @@ public class XmlUtils out.endTag(null, "list"); } + + public static final void writeSetXml(Set val, String name, XmlSerializer out) + throws XmlPullParserException, java.io.IOException { + if (val == null) { + out.startTag(null, "null"); + out.endTag(null, "null"); + return; + } + + out.startTag(null, "set"); + if (name != null) { + out.attribute(null, "name", name); + } + + for (Object v : val) { + writeValueXml(v, null, out); + } + + out.endTag(null, "set"); + } /** * Flatten a byte[] into an XmlSerializer. The list can later be read back @@ -426,6 +447,9 @@ public class XmlUtils } else if (v instanceof List) { writeListXml((List)v, name, out); return; + } else if (v instanceof Set) { + writeSetXml((Set)v, name, out); + return; } else if (v instanceof CharSequence) { // XXX This is to allow us to at least write something if // we encounter styled text... but it means we will drop all @@ -476,7 +500,7 @@ public class XmlUtils * * @param in The InputStream from which to read. * - * @return HashMap The resulting list. + * @return ArrayList The resulting list. * * @see #readMapXml * @see #readValueXml @@ -490,6 +514,29 @@ public class XmlUtils parser.setInput(in, null); return (ArrayList)readValueXml(parser, new String[1]); } + + + /** + * Read a HashSet from an InputStream containing XML. The stream can + * previously have been written by writeSetXml(). + * + * @param in The InputStream from which to read. + * + * @return HashSet The resulting set. + * + * @throws XmlPullParserException + * @throws java.io.IOException + * + * @see #readValueXml + * @see #readThisSetXml + * @see #writeSetXml + */ + public static final HashSet readSetXml(InputStream in) + throws XmlPullParserException, java.io.IOException { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(in, null); + return (HashSet) readValueXml(parser, new String[1]); + } /** * Read a HashMap object from an XmlPullParser. The XML data could @@ -573,6 +620,47 @@ public class XmlUtils throw new XmlPullParserException( "Document ended before " + endTag + " end tag"); } + + /** + * Read a HashSet object from an XmlPullParser. The XML data could previously + * have been generated by writeSetXml(). The XmlPullParser must be positioned + * <em>after</em> the tag that begins the set. + * + * @param parser The XmlPullParser from which to read the set data. + * @param endTag Name of the tag that will end the set, usually "set". + * @param name An array of one string, used to return the name attribute + * of the set's tag. + * + * @return HashSet The newly generated set. + * + * @throws XmlPullParserException + * @throws java.io.IOException + * + * @see #readSetXml + */ + public static final HashSet readThisSetXml(XmlPullParser parser, String endTag, String[] name) + throws XmlPullParserException, java.io.IOException { + HashSet set = new HashSet(); + + int eventType = parser.getEventType(); + do { + if (eventType == parser.START_TAG) { + Object val = readThisValueXml(parser, name); + set.add(val); + //System.out.println("Adding to set: " + val); + } else if (eventType == parser.END_TAG) { + if (parser.getName().equals(endTag)) { + return set; + } + throw new XmlPullParserException( + "Expected " + endTag + " end tag at: " + parser.getName()); + } + eventType = parser.next(); + } while (eventType != parser.END_DOCUMENT); + + throw new XmlPullParserException( + "Document ended before " + endTag + " end tag"); + } /** * Read an int[] object from an XmlPullParser. The XML data could @@ -740,6 +828,12 @@ public class XmlUtils name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + res); return res; + } else if (tagName.equals("set")) { + parser.next(); + res = readThisSetXml(parser, "set", name); + name[0] = valueName; + //System.out.println("Returning value for " + valueName + ": " + res); + return res; } else { throw new XmlPullParserException( "Unknown tag: " + tagName); diff --git a/core/java/com/android/internal/view/menu/ActionMenu.java b/core/java/com/android/internal/view/menu/ActionMenu.java new file mode 100644 index 0000000..3d44ebc --- /dev/null +++ b/core/java/com/android/internal/view/menu/ActionMenu.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view.menu; + +import java.util.ArrayList; +import java.util.List; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.SubMenu; + +/** + * @hide + */ +public class ActionMenu implements Menu { + private Context mContext; + + private boolean mIsQwerty; + + private ArrayList<ActionMenuItem> mItems; + + public ActionMenu(Context context) { + mContext = context; + mItems = new ArrayList<ActionMenuItem>(); + } + + public Context getContext() { + return mContext; + } + + public MenuItem add(CharSequence title) { + return add(0, 0, 0, title); + } + + public MenuItem add(int titleRes) { + return add(0, 0, 0, titleRes); + } + + public MenuItem add(int groupId, int itemId, int order, int titleRes) { + return add(groupId, itemId, order, mContext.getResources().getString(titleRes)); + } + + public MenuItem add(int groupId, int itemId, int order, CharSequence title) { + ActionMenuItem item = new ActionMenuItem(getContext(), + groupId, itemId, 0, order, title); + mItems.add(order, item); + return item; + } + + public int addIntentOptions(int groupId, int itemId, int order, + ComponentName caller, Intent[] specifics, Intent intent, int flags, + MenuItem[] outSpecificItems) { + PackageManager pm = mContext.getPackageManager(); + final List<ResolveInfo> lri = + pm.queryIntentActivityOptions(caller, specifics, intent, 0); + final int N = lri != null ? lri.size() : 0; + + if ((flags & FLAG_APPEND_TO_GROUP) == 0) { + removeGroup(groupId); + } + + for (int i=0; i<N; i++) { + final ResolveInfo ri = lri.get(i); + Intent rintent = new Intent( + ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]); + rintent.setComponent(new ComponentName( + ri.activityInfo.applicationInfo.packageName, + ri.activityInfo.name)); + final MenuItem item = add(groupId, itemId, order, ri.loadLabel(pm)) + .setIcon(ri.loadIcon(pm)) + .setIntent(rintent); + if (outSpecificItems != null && ri.specificIndex >= 0) { + outSpecificItems[ri.specificIndex] = item; + } + } + + return N; + } + + public SubMenu addSubMenu(CharSequence title) { + // TODO Implement submenus + return null; + } + + public SubMenu addSubMenu(int titleRes) { + // TODO Implement submenus + return null; + } + + public SubMenu addSubMenu(int groupId, int itemId, int order, + CharSequence title) { + // TODO Implement submenus + return null; + } + + public SubMenu addSubMenu(int groupId, int itemId, int order, int titleRes) { + // TODO Implement submenus + return null; + } + + public void clear() { + mItems.clear(); + } + + public void close() { + } + + private int findItemIndex(int id) { + final ArrayList<ActionMenuItem> items = mItems; + final int itemCount = items.size(); + for (int i = 0; i < itemCount; i++) { + if (items.get(i).getItemId() == id) { + return i; + } + } + + return -1; + } + + public MenuItem findItem(int id) { + return mItems.get(findItemIndex(id)); + } + + public MenuItem getItem(int index) { + return mItems.get(index); + } + + public boolean hasVisibleItems() { + final ArrayList<ActionMenuItem> items = mItems; + final int itemCount = items.size(); + + for (int i = 0; i < itemCount; i++) { + if (items.get(i).isVisible()) { + return true; + } + } + + return false; + } + + private ActionMenuItem findItemWithShortcut(int keyCode, KeyEvent event) { + // TODO Make this smarter. + final boolean qwerty = mIsQwerty; + final ArrayList<ActionMenuItem> items = mItems; + final int itemCount = items.size(); + + for (int i = 0; i < itemCount; i++) { + ActionMenuItem item = items.get(i); + final char shortcut = qwerty ? item.getAlphabeticShortcut() : + item.getNumericShortcut(); + if (keyCode == shortcut) { + return item; + } + } + return null; + } + + public boolean isShortcutKey(int keyCode, KeyEvent event) { + return findItemWithShortcut(keyCode, event) != null; + } + + public boolean performIdentifierAction(int id, int flags) { + final int index = findItemIndex(id); + if (index < 0) { + return false; + } + + return mItems.get(index).invoke(); + } + + public boolean performShortcut(int keyCode, KeyEvent event, int flags) { + ActionMenuItem item = findItemWithShortcut(keyCode, event); + if (item == null) { + return false; + } + + return item.invoke(); + } + + public void removeGroup(int groupId) { + final ArrayList<ActionMenuItem> items = mItems; + int itemCount = items.size(); + int i = 0; + while (i < itemCount) { + if (items.get(i).getGroupId() == groupId) { + items.remove(i); + itemCount--; + } else { + i++; + } + } + } + + public void removeItem(int id) { + mItems.remove(findItemIndex(id)); + } + + public void setGroupCheckable(int group, boolean checkable, + boolean exclusive) { + final ArrayList<ActionMenuItem> items = mItems; + final int itemCount = items.size(); + + for (int i = 0; i < itemCount; i++) { + ActionMenuItem item = items.get(i); + if (item.getGroupId() == group) { + item.setCheckable(checkable); + item.setExclusiveCheckable(exclusive); + } + } + } + + public void setGroupEnabled(int group, boolean enabled) { + final ArrayList<ActionMenuItem> items = mItems; + final int itemCount = items.size(); + + for (int i = 0; i < itemCount; i++) { + ActionMenuItem item = items.get(i); + if (item.getGroupId() == group) { + item.setEnabled(enabled); + } + } + } + + public void setGroupVisible(int group, boolean visible) { + final ArrayList<ActionMenuItem> items = mItems; + final int itemCount = items.size(); + + for (int i = 0; i < itemCount; i++) { + ActionMenuItem item = items.get(i); + if (item.getGroupId() == group) { + item.setVisible(visible); + } + } + } + + public void setQwertyMode(boolean isQwerty) { + mIsQwerty = isQwerty; + } + + public int size() { + return mItems.size(); + } +} diff --git a/core/java/com/android/internal/view/menu/ActionMenuItem.java b/core/java/com/android/internal/view/menu/ActionMenuItem.java new file mode 100644 index 0000000..47d5fb9 --- /dev/null +++ b/core/java/com/android/internal/view/menu/ActionMenuItem.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view.menu; + +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.view.MenuItem; +import android.view.SubMenu; +import android.view.ContextMenu.ContextMenuInfo; + +/** + * @hide + */ +public class ActionMenuItem implements MenuItem { + private final int mId; + private final int mGroup; + private final int mCategoryOrder; + private final int mOrdering; + + private CharSequence mTitle; + private CharSequence mTitleCondensed; + private Intent mIntent; + private char mShortcutNumericChar; + private char mShortcutAlphabeticChar; + + private Drawable mIconDrawable; + private int mIconResId = NO_ICON; + + private Context mContext; + + private MenuItem.OnMenuItemClickListener mClickListener; + + private static final int NO_ICON = 0; + + private int mFlags = ENABLED; + private static final int CHECKABLE = 0x00000001; + private static final int CHECKED = 0x00000002; + private static final int EXCLUSIVE = 0x00000004; + private static final int HIDDEN = 0x00000008; + private static final int ENABLED = 0x00000010; + + public ActionMenuItem(Context context, int group, int id, int categoryOrder, int ordering, + CharSequence title) { + mContext = context; + mId = id; + mGroup = group; + mCategoryOrder = categoryOrder; + mOrdering = ordering; + mTitle = title; + } + + public char getAlphabeticShortcut() { + return mShortcutAlphabeticChar; + } + + public int getGroupId() { + return mGroup; + } + + public Drawable getIcon() { + return mIconDrawable; + } + + public Intent getIntent() { + return mIntent; + } + + public int getItemId() { + return mId; + } + + public ContextMenuInfo getMenuInfo() { + return null; + } + + public char getNumericShortcut() { + return mShortcutNumericChar; + } + + public int getOrder() { + return mOrdering; + } + + public SubMenu getSubMenu() { + return null; + } + + public CharSequence getTitle() { + return mTitle; + } + + public CharSequence getTitleCondensed() { + return mTitleCondensed; + } + + public boolean hasSubMenu() { + return false; + } + + public boolean isCheckable() { + return (mFlags & CHECKABLE) != 0; + } + + public boolean isChecked() { + return (mFlags & CHECKED) != 0; + } + + public boolean isEnabled() { + return (mFlags & ENABLED) != 0; + } + + public boolean isVisible() { + return (mFlags & HIDDEN) == 0; + } + + public MenuItem setAlphabeticShortcut(char alphaChar) { + mShortcutAlphabeticChar = alphaChar; + return this; + } + + public MenuItem setCheckable(boolean checkable) { + mFlags = (mFlags & ~CHECKABLE) | (checkable ? CHECKABLE : 0); + return this; + } + + public ActionMenuItem setExclusiveCheckable(boolean exclusive) { + mFlags = (mFlags & ~EXCLUSIVE) | (exclusive ? EXCLUSIVE : 0); + return this; + } + + public MenuItem setChecked(boolean checked) { + mFlags = (mFlags & ~CHECKED) | (checked ? CHECKED : 0); + return this; + } + + public MenuItem setEnabled(boolean enabled) { + mFlags = (mFlags & ~ENABLED) | (enabled ? ENABLED : 0); + return this; + } + + public MenuItem setIcon(Drawable icon) { + mIconDrawable = icon; + mIconResId = NO_ICON; + return this; + } + + public MenuItem setIcon(int iconRes) { + mIconResId = iconRes; + mIconDrawable = mContext.getResources().getDrawable(iconRes); + return this; + } + + public MenuItem setIntent(Intent intent) { + mIntent = intent; + return this; + } + + public MenuItem setNumericShortcut(char numericChar) { + mShortcutNumericChar = numericChar; + return this; + } + + public MenuItem setOnMenuItemClickListener(OnMenuItemClickListener menuItemClickListener) { + mClickListener = menuItemClickListener; + return this; + } + + public MenuItem setShortcut(char numericChar, char alphaChar) { + mShortcutNumericChar = numericChar; + mShortcutAlphabeticChar = alphaChar; + return this; + } + + public MenuItem setTitle(CharSequence title) { + mTitle = title; + return this; + } + + public MenuItem setTitle(int title) { + mTitle = mContext.getResources().getString(title); + return this; + } + + public MenuItem setTitleCondensed(CharSequence title) { + mTitleCondensed = title; + return this; + } + + public MenuItem setVisible(boolean visible) { + mFlags = (mFlags & HIDDEN) | (visible ? 0 : HIDDEN); + return this; + } + + public boolean invoke() { + if (mClickListener != null && mClickListener.onMenuItemClick(this)) { + return true; + } + + if (mIntent != null) { + mContext.startActivity(mIntent); + return true; + } + + return false; + } +} diff --git a/core/java/com/android/internal/widget/ContactHeaderWidget.java b/core/java/com/android/internal/widget/ContactHeaderWidget.java index f421466..a514089 100644 --- a/core/java/com/android/internal/widget/ContactHeaderWidget.java +++ b/core/java/com/android/internal/widget/ContactHeaderWidget.java @@ -55,7 +55,7 @@ import android.widget.TextView; /** * Header used across system for displaying a title bar with contact info. You * can bind specific values on the header, or use helper methods like - * {@link #bindFromContactId(long)} to populate asynchronously. + * {@link #bindFromContactLookupUri(Uri)} to populate asynchronously. * <p> * The parent must request the {@link Manifest.permission#READ_CONTACTS} * permission to access contact data. @@ -257,7 +257,7 @@ public class ContactHeaderWidget extends FrameLayout implements View.OnClickList if (photoBitmap == null) { photoBitmap = loadPlaceholderPhoto(null); } - mPhotoView.setImageBitmap(photoBitmap); + setPhoto(photoBitmap); if (cookie != null && cookie instanceof Uri) { mPhotoView.assignContactUri((Uri) cookie); } @@ -267,21 +267,13 @@ public class ContactHeaderWidget extends FrameLayout implements View.OnClickList case TOKEN_CONTACT_INFO: { if (cursor != null && cursor.moveToFirst()) { bindContactInfo(cursor); - Uri lookupUri = Contacts.getLookupUri(cursor.getLong(ContactQuery._ID), + final Uri lookupUri = Contacts.getLookupUri( + cursor.getLong(ContactQuery._ID), cursor.getString(ContactQuery.LOOKUP_KEY)); final long photoId = cursor.getLong(ContactQuery.PHOTO_ID); - if (photoId == 0) { - mPhotoView.setImageBitmap(loadPlaceholderPhoto(null)); - if (cookie != null && cookie instanceof Uri) { - mPhotoView.assignContactUri((Uri) cookie); - } - invalidate(); - } else { - startPhotoQuery(photoId, lookupUri, - false /* don't reset query handler */); - } + setPhotoId(photoId, lookupUri); } else { // shouldn't really happen setDisplayName(null, null); @@ -361,18 +353,40 @@ public class ContactHeaderWidget extends FrameLayout implements View.OnClickList } /** - * Manually set the contact uri + * Manually set the presence. If presence is null, it is hidden. + * This doesn't change the underlying {@link Contacts} value, only the UI state. + * @hide + */ + public void setPresence(Integer presence) { + if (presence == null) { + showPresence(false); + } else { + showPresence(true); + setPresence(presence.intValue()); + } + } + + /** + * Turn on/off showing the presence. + * @hide this is here for consistency with setStared/showStar and should be public + */ + public void showPresence(boolean showPresence) { + mPresenceView.setVisibility(showPresence ? View.VISIBLE : View.GONE); + } + + /** + * Manually set the contact uri without loading any data */ public void setContactUri(Uri uri) { setContactUri(uri, true); } /** - * Manually set the contact uri + * Manually set the contact uri without loading any data */ - public void setContactUri(Uri uri, boolean sendToFastrack) { + public void setContactUri(Uri uri, boolean sendToQuickContact) { mContactUri = uri; - if (sendToFastrack) { + if (sendToQuickContact) { mPhotoView.assignContactUri(uri); } } @@ -386,6 +400,22 @@ public class ContactHeaderWidget extends FrameLayout implements View.OnClickList } /** + * Manually set the photo given its id. If the id is 0, a placeholder picture will + * be loaded. For any other Id, an async query is started + * @hide + */ + public void setPhotoId(final long photoId, final Uri lookupUri) { + if (photoId == 0) { + setPhoto(loadPlaceholderPhoto(null)); + mPhotoView.assignContactUri(lookupUri); + invalidate(); + } else { + startPhotoQuery(photoId, lookupUri, + false /* don't reset query handler */); + } + } + + /** * Manually set the display name and phonetic name to show in the header. * This doesn't change the underlying {@link Contacts}, only the UI state. */ @@ -400,7 +430,8 @@ public class ContactHeaderWidget extends FrameLayout implements View.OnClickList } /** - * Manually set the social snippet text to display in the header. + * Manually set the social snippet text to display in the header. This doesn't change the + * underlying {@link Contacts}, only the UI state. */ public void setSocialSnippet(CharSequence snippet) { if (snippet == null) { @@ -413,6 +444,20 @@ public class ContactHeaderWidget extends FrameLayout implements View.OnClickList } /** + * Manually set the status attribution text to display in the header. + * This doesn't change the underlying {@link Contacts}, only the UI state. + * @hide + */ + public void setStatusAttribution(CharSequence attribution) { + if (attribution != null) { + mStatusAttributionView.setText(attribution); + mStatusAttributionView.setVisibility(View.VISIBLE); + } else { + mStatusAttributionView.setVisibility(View.GONE); + } + } + + /** * Set a list of specific MIME-types to exclude and not display. For * example, this can be used to hide the {@link Contacts#CONTENT_ITEM_TYPE} * profile icon. @@ -423,6 +468,88 @@ public class ContactHeaderWidget extends FrameLayout implements View.OnClickList } /** + * Manually set all the status values to display in the header. + * This doesn't change the underlying {@link Contacts}, only the UI state. + * @hide + * @param status The status of the contact. If this is either null or empty, + * the status is cleared and the other parameters are ignored. + * @param statusTimestamp The timestamp (retrieved via a call to + * {@link System#currentTimeMillis()}) of the last status update. + * This value can be null if it is not known. + * @param statusLabel The id of a resource string that specifies the current + * status. This value can be null if no Label should be used. + * @param statusResPackage The name of the resource package containing the resource string + * referenced in the parameter statusLabel. + */ + public void setStatus(final String status, final Long statusTimestamp, + final Integer statusLabel, final String statusResPackage) { + if (TextUtils.isEmpty(status)) { + setSocialSnippet(null); + return; + } + + setSocialSnippet(status); + + final CharSequence timestampDisplayValue; + + if (statusTimestamp != null) { + // Set the date/time field by mixing relative and absolute + // times. + int flags = DateUtils.FORMAT_ABBREV_RELATIVE; + + timestampDisplayValue = DateUtils.getRelativeTimeSpanString( + statusTimestamp.longValue(), System.currentTimeMillis(), + DateUtils.MINUTE_IN_MILLIS, flags); + } else { + timestampDisplayValue = null; + } + + + String labelDisplayValue = null; + + if (statusLabel != null) { + Resources resources; + if (TextUtils.isEmpty(statusResPackage)) { + resources = getResources(); + } else { + PackageManager pm = getContext().getPackageManager(); + try { + resources = pm.getResourcesForApplication(statusResPackage); + } catch (NameNotFoundException e) { + Log.w(TAG, "Contact status update resource package not found: " + + statusResPackage); + resources = null; + } + } + + if (resources != null) { + try { + labelDisplayValue = resources.getString(statusLabel.intValue()); + } catch (NotFoundException e) { + Log.w(TAG, "Contact status update resource not found: " + statusResPackage + "@" + + statusLabel.intValue()); + } + } + } + + final CharSequence attribution; + if (timestampDisplayValue != null && labelDisplayValue != null) { + attribution = getContext().getString( + R.string.contact_status_update_attribution_with_date, + timestampDisplayValue, labelDisplayValue); + } else if (timestampDisplayValue == null && labelDisplayValue != null) { + attribution = getContext().getString( + R.string.contact_status_update_attribution, + labelDisplayValue); + } else if (timestampDisplayValue != null) { + attribution = timestampDisplayValue; + } else { + attribution = null; + } + setStatusAttribution(attribution); + } + + /** * Convenience method for binding all available data from an existing * contact. * @@ -543,89 +670,28 @@ public class ContactHeaderWidget extends FrameLayout implements View.OnClickList this.setDisplayName(displayName, phoneticName); final boolean starred = c.getInt(ContactQuery.STARRED) != 0; - mStarredView.setChecked(starred); + setStared(starred); //Set the presence status if (!c.isNull(ContactQuery.CONTACT_PRESENCE_STATUS)) { int presence = c.getInt(ContactQuery.CONTACT_PRESENCE_STATUS); - mPresenceView.setImageResource(StatusUpdates.getPresenceIconResourceId(presence)); - mPresenceView.setVisibility(View.VISIBLE); + setPresence(presence); + showPresence(true); } else { - mPresenceView.setVisibility(View.GONE); + showPresence(false); } //Set the status update - String status = c.getString(ContactQuery.CONTACT_STATUS); - if (!TextUtils.isEmpty(status)) { - mStatusView.setText(status); - mStatusView.setVisibility(View.VISIBLE); - - CharSequence timestamp = null; - - if (!c.isNull(ContactQuery.CONTACT_STATUS_TIMESTAMP)) { - long date = c.getLong(ContactQuery.CONTACT_STATUS_TIMESTAMP); - - // Set the date/time field by mixing relative and absolute - // times. - int flags = DateUtils.FORMAT_ABBREV_RELATIVE; - - timestamp = DateUtils.getRelativeTimeSpanString(date, - System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, flags); - } - - String label = null; - - if (!c.isNull(ContactQuery.CONTACT_STATUS_LABEL)) { - String resPackage = c.getString(ContactQuery.CONTACT_STATUS_RES_PACKAGE); - int labelResource = c.getInt(ContactQuery.CONTACT_STATUS_LABEL); - Resources resources; - if (TextUtils.isEmpty(resPackage)) { - resources = getResources(); - } else { - PackageManager pm = getContext().getPackageManager(); - try { - resources = pm.getResourcesForApplication(resPackage); - } catch (NameNotFoundException e) { - Log.w(TAG, "Contact status update resource package not found: " - + resPackage); - resources = null; - } - } - - if (resources != null) { - try { - label = resources.getString(labelResource); - } catch (NotFoundException e) { - Log.w(TAG, "Contact status update resource not found: " + resPackage + "@" - + labelResource); - } - } - } - - CharSequence attribution; - if (timestamp != null && label != null) { - attribution = getContext().getString( - R.string.contact_status_update_attribution_with_date, - timestamp, label); - } else if (timestamp == null && label != null) { - attribution = getContext().getString( - R.string.contact_status_update_attribution, - label); - } else if (timestamp != null) { - attribution = timestamp; - } else { - attribution = null; - } - if (attribution != null) { - mStatusAttributionView.setText(attribution); - mStatusAttributionView.setVisibility(View.VISIBLE); - } else { - mStatusAttributionView.setVisibility(View.GONE); - } - } else { - mStatusView.setVisibility(View.GONE); - mStatusAttributionView.setVisibility(View.GONE); - } + final String status = c.getString(ContactQuery.CONTACT_STATUS); + final Long statusTimestamp = c.isNull(ContactQuery.CONTACT_STATUS_TIMESTAMP) + ? null + : c.getLong(ContactQuery.CONTACT_STATUS_TIMESTAMP); + final Integer statusLabel = c.isNull(ContactQuery.CONTACT_STATUS_LABEL) + ? null + : c.getInt(ContactQuery.CONTACT_STATUS_LABEL); + final String statusResPackage = c.getString(ContactQuery.CONTACT_STATUS_RES_PACKAGE); + + setStatus(status, statusTimestamp, statusLabel, statusResPackage); } public void onClick(View view) { diff --git a/core/java/com/android/internal/widget/DigitalClock.java b/core/java/com/android/internal/widget/DigitalClock.java index fa47ff6..23e2277 100644 --- a/core/java/com/android/internal/widget/DigitalClock.java +++ b/core/java/com/android/internal/widget/DigitalClock.java @@ -30,7 +30,7 @@ import android.provider.Settings; import android.text.format.DateFormat; import android.util.AttributeSet; import android.view.View; -import android.widget.LinearLayout; +import android.widget.RelativeLayout; import android.widget.TextView; import java.text.DateFormatSymbols; @@ -39,7 +39,7 @@ import java.util.Calendar; /** * Displays the time */ -public class DigitalClock extends LinearLayout { +public class DigitalClock extends RelativeLayout { private final static String M12 = "h:mm"; private final static String M24 = "kk:mm"; diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index dbbd286..56bc851 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -86,12 +86,20 @@ public class LockPatternUtils { */ public static final int MIN_PATTERN_REGISTER_FAIL = 3; + /** + * The number of previous password hashes to store. This is used to prevent + * the user from setting the same password as any of the stored ones. + */ + public static final int MAX_PASSWORD_HISTORY_LENGTH = 5; + private final static String LOCKOUT_PERMANENT_KEY = "lockscreen.lockedoutpermanently"; private final static String LOCKOUT_ATTEMPT_DEADLINE = "lockscreen.lockoutattemptdeadline"; private final static String PATTERN_EVER_CHOSEN_KEY = "lockscreen.patterneverchosen"; public final static String PASSWORD_TYPE_KEY = "lockscreen.password_type"; private final static String LOCK_PASSWORD_SALT_KEY = "lockscreen.password_salt"; + private final static String PASSWORD_HISTORY_KEY = "lockscreen.passwordhistory"; + private final Context mContext; private final ContentResolver mContentResolver; private DevicePolicyManager mDevicePolicyManager; @@ -202,8 +210,22 @@ public class LockPatternUtils { } /** - * Checks to see if the given file exists and contains any data. Returns true if it does, - * false otherwise. + * Check to see if a password matches any of the passwords stored in the + * password history. + * + * @param password The password to check. + * @return Whether the password matches any in the history. + */ + public boolean checkPasswordHistory(String password) { + String passwordHashString = new String(passwordToHash(password)); + String passwordHistory = getString(PASSWORD_HISTORY_KEY); + return passwordHistory != null && passwordHistory.contains(passwordHashString); + } + + /** + * Checks to see if the given file exists and contains any data. Returns + * true if it does, false otherwise. + * * @param filename * @return true if file exists and is non-empty. */ @@ -384,6 +406,20 @@ public class LockPatternUtils { dpm.setActivePasswordState( DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0); } + // Add the password to the password history. We assume all + // password + // hashes have the same length for simplicity of implementation. + String passwordHistory = getString(PASSWORD_HISTORY_KEY); + if (passwordHistory == null) { + passwordHistory = new String(); + } + passwordHistory = new String(hash) + "," + passwordHistory; + // Cut it to contain MAX_PASSWORD_HISTORY_LENGTH hashes + // and MAX_PASSWORD_HISTORY_LENGTH -1 commas. + passwordHistory = passwordHistory.substring(0, Math.min(hash.length + * MAX_PASSWORD_HISTORY_LENGTH + MAX_PASSWORD_HISTORY_LENGTH - 1, + passwordHistory.length())); + setString(PASSWORD_HISTORY_KEY, passwordHistory); } else { dpm.setActivePasswordState( DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0); @@ -650,6 +686,14 @@ public class LockPatternUtils { android.provider.Settings.Secure.putLong(mContentResolver, secureSettingKey, value); } + private String getString(String secureSettingKey) { + return android.provider.Settings.Secure.getString(mContentResolver, secureSettingKey); + } + + private void setString(String secureSettingKey, String value) { + android.provider.Settings.Secure.putString(mContentResolver, secureSettingKey, value); + } + public boolean isSecure() { long mode = getKeyguardStoredPasswordQuality(); final boolean isPattern = mode == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; |