diff options
Diffstat (limited to 'core/java')
380 files changed, 30096 insertions, 10483 deletions
diff --git a/core/java/android/accounts/AccountMonitor.java b/core/java/android/accounts/AccountMonitor.java index 9bcc1e7..f21385e 100644 --- a/core/java/android/accounts/AccountMonitor.java +++ b/core/java/android/accounts/AccountMonitor.java @@ -42,31 +42,42 @@ public class AccountMonitor extends BroadcastReceiver implements ServiceConnecti private final Context mContext; private final AccountMonitorListener mListener; private boolean mClosed = false; + private int pending = 0; // This thread runs in the background and runs the code to update accounts // in the listener. private class AccountUpdater extends Thread { private IBinder mService; - + public AccountUpdater(IBinder service) { mService = service; } - + @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); IAccountsService accountsService = IAccountsService.Stub.asInterface(mService); - String[] accounts; - try { - accounts = accountsService.getAccounts(); - } catch (RemoteException e) { - // if the service was killed then the system will restart it and when it does we - // will get another onServiceConnected, at which point we will do a notify. - Log.w("AccountMonitor", "Remote exception when getting accounts", e); - return; - } + String[] accounts = null; + do { + try { + accounts = accountsService.getAccounts(); + } catch (RemoteException e) { + // if the service was killed then the system will restart it and when it does we + // will get another onServiceConnected, at which point we will do a notify. + Log.w("AccountMonitor", "Remote exception when getting accounts", e); + return; + } + + synchronized (AccountMonitor.this) { + --pending; + if (pending == 0) { + break; + } + } + } while (true); + mContext.unbindService(AccountMonitor.this); - + try { mListener.onAccountsUpdated(accounts); } catch (SQLException e) { @@ -76,7 +87,7 @@ public class AccountMonitor extends BroadcastReceiver implements ServiceConnecti } } } - + /** * Initializes the AccountMonitor and initiates a bind to the * AccountsService to get the initial account list. For 1.0, @@ -93,7 +104,7 @@ public class AccountMonitor extends BroadcastReceiver implements ServiceConnecti mContext = context; mListener = listener; - // Register an intent receiver to monitor account changes + // Register a broadcast receiver to monitor account changes IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(AccountsServiceConstants.LOGIN_ACCOUNTS_CHANGED_ACTION); intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); // To recover from disk-full. @@ -116,14 +127,27 @@ public class AccountMonitor extends BroadcastReceiver implements ServiceConnecti public void onServiceDisconnected(ComponentName className) { } - private void notifyListener() { - // initiate the bind - if (!mContext.bindService(AccountsServiceConstants.SERVICE_INTENT, this, - Context.BIND_AUTO_CREATE)) { - // This is normal if GLS isn't part of this build. - Log.w("AccountMonitor", - "Couldn't connect to the accounts service (Missing service?)"); + private synchronized void notifyListener() { + if (pending == 0) { + // initiate the bind + if (!mContext.bindService(AccountsServiceConstants.SERVICE_INTENT, + this, Context.BIND_AUTO_CREATE)) { + // This is normal if GLS isn't part of this build. + Log.w("AccountMonitor", + "Couldn't connect to " + + AccountsServiceConstants.SERVICE_INTENT + + " (Missing service?)"); + } + } else { + // already bound. bindService will not trigger another + // call to onServiceConnected, so instead we make sure + // that the existing background thread will call + // getAccounts() after this function returns, by + // incrementing pending. + // + // Yes, this else clause contains only a comment. } + ++pending; } /** diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index fa310a5..eafb048 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -62,6 +62,7 @@ import android.widget.AdapterView; import com.android.internal.policy.PolicyManager; import java.util.ArrayList; +import java.util.HashMap; /** * An activity is a single, focused thing that the user can do. Almost all @@ -613,7 +614,8 @@ public class Activity extends ContextThemeWrapper private ComponentName mComponent; /*package*/ ActivityInfo mActivityInfo; /*package*/ ActivityThread mMainThread; - private Object mLastNonConfigurationInstance; + /*package*/ Object mLastNonConfigurationInstance; + /*package*/ HashMap<String,Object> mLastNonConfigurationChildInstances; Activity mParent; boolean mCalled; private boolean mResumed; @@ -1379,6 +1381,38 @@ public class Activity extends ContextThemeWrapper return null; } + /** + * Retrieve the non-configuration instance data that was previously + * returned by {@link #onRetainNonConfigurationChildInstances()}. This will + * be available from the initial {@link #onCreate} and + * {@link #onStart} calls to the new instance, allowing you to extract + * any useful dynamic state from the previous instance. + * + * <p>Note that the data you retrieve here should <em>only</em> be used + * as an optimization for handling configuration changes. You should always + * be able to handle getting a null pointer back, and an activity must + * still be able to restore itself to its previous state (through the + * normal {@link #onSaveInstanceState(Bundle)} mechanism) even if this + * function returns null. + * + * @return Returns the object previously returned by + * {@link #onRetainNonConfigurationChildInstances()} + */ + HashMap<String,Object> getLastNonConfigurationChildInstances() { + return mLastNonConfigurationChildInstances; + } + + /** + * This method is similar to {@link #onRetainNonConfigurationInstance()} except that + * it should return either a mapping from child activity id strings to arbitrary objects, + * or null. This method is intended to be used by Activity framework subclasses that control a + * set of child activities, such as ActivityGroup. The same guarantees and restrictions apply + * as for {@link #onRetainNonConfigurationInstance()}. The default implementation returns null. + */ + HashMap<String,Object> onRetainNonConfigurationChildInstances() { + return null; + } + public void onLowMemory() { mCalled = true; } @@ -1837,13 +1871,50 @@ public class Activity extends ContextThemeWrapper * Called when the current {@link Window} of the activity gains or loses * focus. This is the best indicator of whether this activity is visible * to the user. + * + * <p>Note that this provides information what global focus state, which + * is managed independently of activity lifecycles. As such, while focus + * changes will generally have some relation to lifecycle changes (an + * activity that is stopped will not generally get window focus), you + * should not rely on any particular order between the callbacks here and + * those in the other lifecycle methods such as {@link #onResume}. + * + * <p>As a general rule, however, a resumed activity will have window + * focus... unless it has displayed other dialogs or popups that take + * input focus, in which case the activity itself will not have focus + * when the other windows have it. Likewise, the system may display + * system-level windows (such as the status bar notification panel or + * a system alert) which will temporarily take window input focus without + * pausing the foreground activity. * * @param hasFocus Whether the window of this activity has focus. + * + * @see #hasWindowFocus() + * @see #onResume */ public void onWindowFocusChanged(boolean hasFocus) { } /** + * Returns true if this activity's <em>main</em> window currently has window focus. + * Note that this is not the same as the view itself having focus. + * + * @return True if this activity's main window currently has window focus. + * + * @see #onWindowAttributesChanged(android.view.WindowManager.LayoutParams) + */ + public boolean hasWindowFocus() { + Window w = getWindow(); + if (w != null) { + View d = w.getDecorView(); + if (d != null) { + return d.hasWindowFocus(); + } + } + return false; + } + + /** * Called to process key events. You can override this to intercept all * key events before they are dispatched to the window. Be sure to call * this implementation for key events that should be handled normally. @@ -2160,6 +2231,15 @@ public class Activity extends ContextThemeWrapper } /** + * Programmatically closes the most recently opened context menu, if showing. + * + * @hide pending API council + */ + public void closeContextMenu() { + mWindow.closePanel(Window.FEATURE_CONTEXT_MENU); + } + + /** * This hook is called whenever an item in a context menu is selected. The * default implementation simply returns false to have the normal processing * happen (calling the item's Runnable or sending a message to its Handler @@ -2910,6 +2990,7 @@ public class Activity extends ContextThemeWrapper * @param flags May be {@link PendingIntent#FLAG_ONE_SHOT PendingIntent.FLAG_ONE_SHOT}, * {@link PendingIntent#FLAG_NO_CREATE PendingIntent.FLAG_NO_CREATE}, * {@link PendingIntent#FLAG_CANCEL_CURRENT PendingIntent.FLAG_CANCEL_CURRENT}, + * {@link PendingIntent#FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT}, * or any of the flags as supported by * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts * of the intent that can be supplied when the actual send happens. @@ -3285,10 +3366,21 @@ public class Activity extends ContextThemeWrapper Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, Object lastNonConfigurationInstance, Configuration config) { + attach(context, aThread, instr, token, application, intent, info, title, parent, id, + lastNonConfigurationInstance, null, config); + } + + 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, + HashMap<String,Object> lastNonConfigurationChildInstances, Configuration config) { attachBaseContext(context); mWindow = PolicyManager.makeNewWindow(this); mWindow.setCallback(this); + if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) { + mWindow.setSoftInputMode(info.softInputMode); + } mUiThread = Thread.currentThread(); mMainThread = aThread; @@ -3302,6 +3394,7 @@ public class Activity extends ContextThemeWrapper mParent = parent; mEmbeddedID = id; mLastNonConfigurationInstance = lastNonConfigurationInstance; + mLastNonConfigurationChildInstances = lastNonConfigurationChildInstances; mWindow.setWindowManager(null, mToken, mComponent.flattenToString()); if (mParent != null) { @@ -3375,6 +3468,10 @@ public class Activity extends ContextThemeWrapper } } + final void performPause() { + onPause(); + } + final void performStop() { if (!mStopped) { if (mWindow != null) { diff --git a/core/java/android/app/ActivityGroup.java b/core/java/android/app/ActivityGroup.java index 96bb475..f1216f9 100644 --- a/core/java/android/app/ActivityGroup.java +++ b/core/java/android/app/ActivityGroup.java @@ -16,14 +16,19 @@ package android.app; +import java.util.HashMap; + import android.content.Intent; import android.os.Bundle; +import android.util.Log; /** * A screen that contains and runs multiple embedded activities. */ public class ActivityGroup extends Activity { + private static final String TAG = "ActivityGroup"; private static final String STATES_KEY = "android:states"; + static final String PARENT_NON_CONFIG_INSTANCE_KEY = "android:parent_non_config_instance"; /** * This field should be made private, so it is hidden from the SDK. @@ -80,6 +85,17 @@ public class ActivityGroup extends Activity { mLocalActivityManager.dispatchDestroy(isFinishing()); } + /** + * Returns a HashMap mapping from child activity ids to the return values + * from calls to their onRetainNonConfigurationInstance methods. + * + * {@hide} + */ + @Override + public HashMap<String,Object> onRetainNonConfigurationChildInstances() { + return mLocalActivityManager.dispatchRetainNonConfigurationInstance(); + } + public Activity getCurrentActivity() { return mLocalActivityManager.getCurrentActivity(); } diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 6eb1102..f9b9221 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -29,7 +29,6 @@ import android.os.Parcelable; import android.os.Parcelable.Creator; import android.text.TextUtils; import android.util.Log; - import java.util.List; /** @@ -601,4 +600,75 @@ public class ActivityManager { return null; } } + + /** + * Information you can retrieve about a running process. + */ + public static class RunningAppProcessInfo implements Parcelable { + /** + * The name of the process that this object is associated with + */ + public String processName; + + /** + * The pid of this process; 0 if none + */ + public int pid; + + public String pkgList[]; + + public RunningAppProcessInfo() { + } + + public RunningAppProcessInfo(String pProcessName, int pPid, String pArr[]) { + processName = pProcessName; + pid = pPid; + pkgList = pArr; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(processName); + dest.writeInt(pid); + dest.writeStringArray(pkgList); + } + + public void readFromParcel(Parcel source) { + processName = source.readString(); + pid = source.readInt(); + pkgList = source.readStringArray(); + } + + public static final Creator<RunningAppProcessInfo> CREATOR = + new Creator<RunningAppProcessInfo>() { + public RunningAppProcessInfo createFromParcel(Parcel source) { + return new RunningAppProcessInfo(source); + } + public RunningAppProcessInfo[] newArray(int size) { + return new RunningAppProcessInfo[size]; + } + }; + + private RunningAppProcessInfo(Parcel source) { + readFromParcel(source); + } + } + + /** + * Returns a list of application processes that are running on the device. + * + * @return Returns a list of RunningAppProcessInfo records, or null if there are no + * running processes (it will not return an empty list). This list ordering is not + * specified. + */ + public List<RunningAppProcessInfo> getRunningAppProcesses() { + try { + return ActivityManagerNative.getDefault().getRunningAppProcesses(); + } catch (RemoteException e) { + return null; + } + } } diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index e6f1b05..ae9f3bf 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -387,6 +387,14 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM reply.writeTypedList(list); return true; } + + case GET_RUNNING_APP_PROCESSES_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + List<ActivityManager.RunningAppProcessInfo> list = getRunningAppProcesses(); + reply.writeNoException(); + reply.writeTypedList(list); + return true; + } case MOVE_TASK_TO_FRONT_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); @@ -1314,6 +1322,19 @@ class ActivityManagerProxy implements IActivityManager reply.recycle(); return list; } + public List<ActivityManager.RunningAppProcessInfo> getRunningAppProcesses() + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + mRemote.transact(GET_RUNNING_APP_PROCESSES_TRANSACTION, data, reply, 0); + reply.readException(); + ArrayList<ActivityManager.RunningAppProcessInfo> list + = reply.createTypedArrayList(ActivityManager.RunningAppProcessInfo.CREATOR); + data.recycle(); + reply.recycle(); + return list; + } public void moveTaskToFront(int task) throws RemoteException { Parcel data = Parcel.obtain(); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index d03a76f..3d448a6 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -220,7 +220,8 @@ public final class ActivityThread { mApplicationInfo = aInfo; mPackageName = aInfo.packageName; mAppDir = aInfo.sourceDir; - mResDir = aInfo.publicSourceDir; + mResDir = aInfo.uid == Process.myUid() ? aInfo.sourceDir + : aInfo.publicSourceDir; mSharedLibraries = aInfo.sharedLibraryFiles; mDataDir = aInfo.dataDir; mDataDirFile = mDataDir != null ? new File(mDataDir) : null; @@ -1057,6 +1058,7 @@ public final class ActivityThread { Activity parent; String embeddedID; Object lastNonConfigurationInstance; + HashMap<String,Object> lastNonConfigurationChildInstances; boolean paused; boolean stopped; boolean hideForNow; @@ -1966,6 +1968,12 @@ public final class ActivityThread { public final Activity startActivityNow(Activity parent, String id, Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state) { + return startActivityNow(parent, id, intent, activityInfo, token, state, null); + } + + public final Activity startActivityNow(Activity parent, String id, + Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state, + Object lastNonConfigurationInstance) { ActivityRecord r = new ActivityRecord(); r.token = token; r.intent = intent; @@ -1973,6 +1981,7 @@ public final class ActivityThread { r.parent = parent; r.embeddedID = id; r.activityInfo = activityInfo; + r.lastNonConfigurationInstance = lastNonConfigurationInstance; if (localLOGV) { ComponentName compname = intent.getComponent(); String name; @@ -2090,9 +2099,11 @@ public final class ActivityThread { Configuration config = new Configuration(mConfiguration); activity.attach(appContext, this, getInstrumentation(), r.token, app, r.intent, r.activityInfo, title, r.parent, r.embeddedID, - r.lastNonConfigurationInstance, config); + r.lastNonConfigurationInstance, r.lastNonConfigurationChildInstances, + config); r.lastNonConfigurationInstance = null; + r.lastNonConfigurationChildInstances = null; activity.mStartedActivity = false; int theme = r.activityInfo.getThemeResource(); if (theme != 0) { @@ -2948,6 +2959,17 @@ 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 " + + r.intent.getComponent().toShortString() + + ": " + e.toString(), e); + } + } } try { @@ -3225,11 +3247,7 @@ public final class ActivityThread { Locale.setDefault(config.locale); } - if (mSystemContext != null) { - mSystemContext.getResources().updateConfiguration(config, null); - //Log.i(TAG, "Updated system resources " + mSystemContext.getResources() - // + ": " + mSystemContext.getResources().getConfiguration()); - } + Resources.updateSystemConfiguration(config, null); ApplicationContext.ApplicationPackageManager.configurationChanged(); //Log.i(TAG, "Configuration changed in " + currentPackageName()); diff --git a/core/java/android/app/AlarmManager.java b/core/java/android/app/AlarmManager.java index 35c6ac1..b4c0e31 100644 --- a/core/java/android/app/AlarmManager.java +++ b/core/java/android/app/AlarmManager.java @@ -71,17 +71,13 @@ public class AlarmManager */ public static final int ELAPSED_REALTIME = 3; - private static IAlarmManager mService; + private final IAlarmManager mService; - static { - mService = IAlarmManager.Stub.asInterface( - ServiceManager.getService(Context.ALARM_SERVICE)); - } - /** * package private on purpose */ - AlarmManager() { + AlarmManager(IAlarmManager service) { + mService = service; } /** @@ -97,7 +93,7 @@ public class AlarmManager * this one. * * <p> - * The alarm is an intent broadcast that goes to an intent receiver that + * The alarm is an intent broadcast that goes to a broadcast receiver that * you registered with {@link android.content.Context#registerReceiver} * or through the <receiver> tag in an AndroidManifest.xml file. * @@ -189,6 +185,72 @@ public class AlarmManager } /** + * Available inexact recurrence intervals recognized by + * {@link #setInexactRepeating(int, long, long, PendingIntent)} + */ + public static final long INTERVAL_FIFTEEN_MINUTES = 15 * 60 * 1000; + public static final long INTERVAL_HALF_HOUR = 2*INTERVAL_FIFTEEN_MINUTES; + public static final long INTERVAL_HOUR = 2*INTERVAL_HALF_HOUR; + public static final long INTERVAL_HALF_DAY = 12*INTERVAL_HOUR; + public static final long INTERVAL_DAY = 2*INTERVAL_HALF_DAY; + + /** + * Schedule a repeating alarm that has inexact trigger time requirements; + * for example, an alarm that repeats every hour, but not necessarily at + * the top of every hour. These alarms are more power-efficient than + * the strict recurrences supplied by {@link #setRepeating}, since the + * system can adjust alarms' phase to cause them to fire simultaneously, + * avoiding waking the device from sleep more than necessary. + * + * <p>Your alarm's first trigger will not be before the requested time, + * but it might not occur for almost a full interval after that time. In + * addition, while the overall period of the repeating alarm will be as + * requested, the time between any two successive firings of the alarm + * may vary. If your application demands very low jitter, use + * {@link #setRepeating} instead. + * + * @param type One of ELAPSED_REALTIME, ELAPSED_REALTIME_WAKEUP}, RTC or + * RTC_WAKEUP. + * @param triggerAtTime Time the alarm should first go off, using the + * appropriate clock (depending on the alarm type). This + * is inexact: the alarm will not fire before this time, + * but there may be a delay of almost an entire alarm + * interval before the first invocation of the alarm. + * @param interval Interval between subsequent repeats of the alarm. If + * this is one of INTERVAL_FIFTEEN_MINUTES, INTERVAL_HALF_HOUR, + * INTERVAL_HOUR, INTERVAL_HALF_DAY, or INTERVAL_DAY then the + * alarm will be phase-aligned with other alarms to reduce + * the number of wakeups. Otherwise, the alarm will be set + * as though the application had called {@link #setRepeating}. + * @param operation Action to perform when the alarm goes off; + * typically comes from {@link PendingIntent#getBroadcast + * IntentSender.getBroadcast()}. + * + * @see android.os.Handler + * @see #set + * @see #cancel + * @see android.content.Context#sendBroadcast + * @see android.content.Context#registerReceiver + * @see android.content.Intent#filterEquals + * @see #ELAPSED_REALTIME + * @see #ELAPSED_REALTIME_WAKEUP + * @see #RTC + * @see #RTC_WAKEUP + * @see #INTERVAL_FIFTEEN_MINUTES + * @see #INTERVAL_HALF_HOUR + * @see #INTERVAL_HOUR + * @see #INTERVAL_HALF_DAY + * @see #INTERVAL_DAY + */ + public void setInexactRepeating(int type, long triggerAtTime, long interval, + PendingIntent operation) { + try { + mService.setInexactRepeating(type, triggerAtTime, interval, operation); + } catch (RemoteException ex) { + } + } + + /** * Remove any alarms with a matching {@link Intent}. * Any alarm, of any type, whose Intent matches this one (as defined by * {@link Intent#filterEquals}), will be canceled. diff --git a/core/java/android/app/AlertDialog.java b/core/java/android/app/AlertDialog.java index cc80ba4..a6981a5 100644 --- a/core/java/android/app/AlertDialog.java +++ b/core/java/android/app/AlertDialog.java @@ -25,6 +25,7 @@ import android.os.Message; import android.view.KeyEvent; import android.view.View; import android.widget.AdapterView; +import android.widget.Button; import android.widget.ListAdapter; import android.widget.ListView; @@ -59,6 +60,29 @@ public class AlertDialog extends Dialog implements DialogInterface { setOnCancelListener(cancelListener); mAlert = new AlertController(context, this, getWindow()); } + + /** + * Gets one of the buttons used in the dialog. + * <p> + * If a button does not exist in the dialog, null will be returned. + * + * @param whichButton The identifier of the button that should be returned. + * For example, this can be + * {@link DialogInterface#BUTTON_POSITIVE}. + * @return The button from the dialog, or null if a button does not exist. + */ + public Button getButton(int whichButton) { + return mAlert.getButton(whichButton); + } + + /** + * Gets the list view used in the dialog. + * + * @return The {@link ListView} from the dialog. + */ + public ListView getListView() { + return mAlert.getListView(); + } @Override public void setTitle(CharSequence title) { @@ -83,44 +107,115 @@ public class AlertDialog extends Dialog implements DialogInterface { public void setView(View view) { mAlert.setView(view); } + + /** + * Set the view to display in that dialog, specifying the spacing to appear around that + * view. + * + * @param view The view to show in the content area of the dialog + * @param viewSpacingLeft Extra space to appear to the left of {@code view} + * @param viewSpacingTop Extra space to appear above {@code view} + * @param viewSpacingRight Extra space to appear to the right of {@code view} + * @param viewSpacingBottom Extra space to appear below {@code view} + */ + public void setView(View view, int viewSpacingLeft, int viewSpacingTop, int viewSpacingRight, + int viewSpacingBottom) { + mAlert.setView(view, viewSpacingLeft, viewSpacingTop, viewSpacingRight, viewSpacingBottom); + } - public void setButton(CharSequence text, Message msg) { - mAlert.setButton(text, msg); + /** + * Set a message to be sent when a button is pressed. + * + * @param whichButton Which button to set the message for, can be one of + * {@link DialogInterface#BUTTON_POSITIVE}, + * {@link DialogInterface#BUTTON_NEGATIVE}, or + * {@link DialogInterface#BUTTON_NEUTRAL} + * @param text The text to display in positive button. + * @param msg The {@link Message} to be sent when clicked. + */ + public void setButton(int whichButton, CharSequence text, Message msg) { + mAlert.setButton(whichButton, text, null, msg); + } + + /** + * Set a listener to be invoked when the positive button of the dialog is pressed. + * + * @param whichButton Which button to set the listener on, can be one of + * {@link DialogInterface#BUTTON_POSITIVE}, + * {@link DialogInterface#BUTTON_NEGATIVE}, or + * {@link DialogInterface#BUTTON_NEUTRAL} + * @param text The text to display in positive button. + * @param listener The {@link DialogInterface.OnClickListener} to use. + */ + public void setButton(int whichButton, CharSequence text, OnClickListener listener) { + mAlert.setButton(whichButton, text, listener, null); } + /** + * @deprecated Use {@link #setButton(int, CharSequence, Message)} with + * {@link DialogInterface#BUTTON_POSITIVE}. + */ + @Deprecated + public void setButton(CharSequence text, Message msg) { + setButton(BUTTON_POSITIVE, text, msg); + } + + /** + * @deprecated Use {@link #setButton(int, CharSequence, Message)} with + * {@link DialogInterface#BUTTON_NEGATIVE}. + */ + @Deprecated public void setButton2(CharSequence text, Message msg) { - mAlert.setButton2(text, msg); + setButton(BUTTON_NEGATIVE, text, msg); } + /** + * @deprecated Use {@link #setButton(int, CharSequence, Message)} with + * {@link DialogInterface#BUTTON_NEUTRAL}. + */ + @Deprecated public void setButton3(CharSequence text, Message msg) { - mAlert.setButton3(text, msg); + setButton(BUTTON_NEUTRAL, text, msg); } /** * Set a listener to be invoked when button 1 of the dialog is pressed. + * * @param text The text to display in button 1. * @param listener The {@link DialogInterface.OnClickListener} to use. + * @deprecated Use + * {@link #setButton(int, CharSequence, android.content.DialogInterface.OnClickListener)} + * with {@link DialogInterface#BUTTON_POSITIVE} */ + @Deprecated public void setButton(CharSequence text, final OnClickListener listener) { - mAlert.setButton(text, listener); + setButton(BUTTON_POSITIVE, text, listener); } /** * Set a listener to be invoked when button 2 of the dialog is pressed. * @param text The text to display in button 2. * @param listener The {@link DialogInterface.OnClickListener} to use. + * @deprecated Use + * {@link #setButton(int, CharSequence, android.content.DialogInterface.OnClickListener)} + * with {@link DialogInterface#BUTTON_NEGATIVE} */ + @Deprecated public void setButton2(CharSequence text, final OnClickListener listener) { - mAlert.setButton2(text, listener); + setButton(BUTTON_NEGATIVE, text, listener); } /** * Set a listener to be invoked when button 3 of the dialog is pressed. * @param text The text to display in button 3. * @param listener The {@link DialogInterface.OnClickListener} to use. + * @deprecated Use + * {@link #setButton(int, CharSequence, android.content.DialogInterface.OnClickListener)} + * with {@link DialogInterface#BUTTON_POSITIVE} */ + @Deprecated public void setButton3(CharSequence text, final OnClickListener listener) { - mAlert.setButton3(text, listener); + setButton(BUTTON_NEUTRAL, text, listener); } /** @@ -170,6 +265,8 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Set the title using the given resource id. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setTitle(int titleId) { P.mTitle = P.mContext.getText(titleId); @@ -178,6 +275,8 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Set the title displayed in the {@link Dialog}. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setTitle(CharSequence title) { P.mTitle = title; @@ -192,6 +291,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * via the other methods. * * @param customTitleView The custom view to use as the title. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setCustomTitle(View customTitleView) { P.mCustomTitleView = customTitleView; @@ -200,6 +301,8 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Set the message to display using the given resource id. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setMessage(int messageId) { P.mMessage = P.mContext.getText(messageId); @@ -208,6 +311,8 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Set the message to display. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setMessage(CharSequence message) { P.mMessage = message; @@ -216,6 +321,8 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Set the resource id of the {@link Drawable} to be used in the title. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setIcon(int iconId) { P.mIconId = iconId; @@ -224,6 +331,8 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Set the {@link Drawable} to be used in the title. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setIcon(Drawable icon) { P.mIcon = icon; @@ -234,6 +343,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * Set a listener to be invoked when the positive button of the dialog is pressed. * @param textId The resource id of the text to display in the positive button * @param listener The {@link DialogInterface.OnClickListener} to use. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setPositiveButton(int textId, final OnClickListener listener) { P.mPositiveButtonText = P.mContext.getText(textId); @@ -245,6 +356,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * Set a listener to be invoked when the positive button of the dialog is pressed. * @param text The text to display in the positive button * @param listener The {@link DialogInterface.OnClickListener} to use. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setPositiveButton(CharSequence text, final OnClickListener listener) { P.mPositiveButtonText = text; @@ -256,6 +369,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * Set a listener to be invoked when the negative button of the dialog is pressed. * @param textId The resource id of the text to display in the negative button * @param listener The {@link DialogInterface.OnClickListener} to use. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setNegativeButton(int textId, final OnClickListener listener) { P.mNegativeButtonText = P.mContext.getText(textId); @@ -267,6 +382,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * Set a listener to be invoked when the negative button of the dialog is pressed. * @param text The text to display in the negative button * @param listener The {@link DialogInterface.OnClickListener} to use. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setNegativeButton(CharSequence text, final OnClickListener listener) { P.mNegativeButtonText = text; @@ -278,6 +395,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * Set a listener to be invoked when the neutral button of the dialog is pressed. * @param textId The resource id of the text to display in the neutral button * @param listener The {@link DialogInterface.OnClickListener} to use. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setNeutralButton(int textId, final OnClickListener listener) { P.mNeutralButtonText = P.mContext.getText(textId); @@ -289,6 +408,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * Set a listener to be invoked when the neutral button of the dialog is pressed. * @param text The text to display in the neutral button * @param listener The {@link DialogInterface.OnClickListener} to use. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setNeutralButton(CharSequence text, final OnClickListener listener) { P.mNeutralButtonText = text; @@ -298,6 +419,8 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Sets whether the dialog is cancelable or not default is true. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setCancelable(boolean cancelable) { P.mCancelable = cancelable; @@ -307,6 +430,8 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Sets the callback that will be called if the dialog is canceled. * @see #setCancelable(boolean) + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setOnCancelListener(OnCancelListener onCancelListener) { P.mOnCancelListener = onCancelListener; @@ -315,6 +440,8 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Sets the callback that will be called if a key is dispatched to the dialog. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setOnKeyListener(OnKeyListener onKeyListener) { P.mOnKeyListener = onKeyListener; @@ -324,6 +451,8 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Set a list of items to be displayed in the dialog as the content, you will be notified of the * selected item via the supplied listener. This should be an array type i.e. R.array.foo + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setItems(int itemsId, final OnClickListener listener) { P.mItems = P.mContext.getResources().getTextArray(itemsId); @@ -334,6 +463,8 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Set a list of items to be displayed in the dialog as the content, you will be notified of the * selected item via the supplied listener. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setItems(CharSequence[] items, final OnClickListener listener) { P.mItems = items; @@ -348,6 +479,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * * @param adapter The {@link ListAdapter} to supply the list of items * @param listener The listener that will be called when an item is clicked. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setAdapter(final ListAdapter adapter, final OnClickListener listener) { P.mAdapter = adapter; @@ -364,6 +497,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * @param listener The listener that will be called when an item is clicked. * @param labelColumn The column name on the cursor containing the string to display * in the label. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setCursor(final Cursor cursor, final OnClickListener listener, String labelColumn) { @@ -388,6 +523,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * @param listener notified when an item on the list is clicked. The dialog will not be * dismissed when an item is clicked. It will only be dismissed if clicked on a * button, if no buttons are supplied it's up to the user to dismiss the dialog. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setMultiChoiceItems(int itemsId, boolean[] checkedItems, final OnMultiChoiceClickListener listener) { @@ -412,6 +549,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * @param listener notified when an item on the list is clicked. The dialog will not be * dismissed when an item is clicked. It will only be dismissed if clicked on a * button, if no buttons are supplied it's up to the user to dismiss the dialog. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setMultiChoiceItems(CharSequence[] items, boolean[] checkedItems, final OnMultiChoiceClickListener listener) { @@ -438,6 +577,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * @param listener notified when an item on the list is clicked. The dialog will not be * dismissed when an item is clicked. It will only be dismissed if clicked on a * button, if no buttons are supplied it's up to the user to dismiss the dialog. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setMultiChoiceItems(Cursor cursor, String isCheckedColumn, String labelColumn, final OnMultiChoiceClickListener listener) { @@ -461,6 +602,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * @param listener notified when an item on the list is clicked. The dialog will not be * dismissed when an item is clicked. It will only be dismissed if clicked on a * button, if no buttons are supplied it's up to the user to dismiss the dialog. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setSingleChoiceItems(int itemsId, int checkedItem, final OnClickListener listener) { @@ -484,6 +627,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * @param listener notified when an item on the list is clicked. The dialog will not be * dismissed when an item is clicked. It will only be dismissed if clicked on a * button, if no buttons are supplied it's up to the user to dismiss the dialog. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setSingleChoiceItems(Cursor cursor, int checkedItem, String labelColumn, final OnClickListener listener) { @@ -506,6 +651,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * @param listener notified when an item on the list is clicked. The dialog will not be * dismissed when an item is clicked. It will only be dismissed if clicked on a * button, if no buttons are supplied it's up to the user to dismiss the dialog. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setSingleChoiceItems(CharSequence[] items, int checkedItem, final OnClickListener listener) { P.mItems = items; @@ -526,6 +673,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * @param listener notified when an item on the list is clicked. The dialog will not be * dismissed when an item is clicked. It will only be dismissed if clicked on a * button, if no buttons are supplied it's up to the user to dismiss the dialog. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setSingleChoiceItems(ListAdapter adapter, int checkedItem, final OnClickListener listener) { P.mAdapter = adapter; @@ -540,6 +689,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * * @param listener The listener to be invoked. * @see AdapterView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener) + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setOnItemSelectedListener(final AdapterView.OnItemSelectedListener listener) { P.mOnItemSelectedListener = listener; @@ -549,9 +700,44 @@ public class AlertDialog extends Dialog implements DialogInterface { /** * Set a custom view to be the contents of the Dialog. If the supplied view is an instance * of a {@link ListView} the light background will be used. + * + * @param view The view to use as the contents of the Dialog. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setView(View view) { P.mView = view; + P.mViewSpacingSpecified = false; + return this; + } + + /** + * Set a custom view to be the contents of the Dialog, specifying the + * spacing to appear around that view. If the supplied view is an + * instance of a {@link ListView} the light background will be used. + * + * @param view The view to use as the contents of the Dialog. + * @param viewSpacingLeft Spacing between the left edge of the view and + * the dialog frame + * @param viewSpacingTop Spacing between the top edge of the view and + * the dialog frame + * @param viewSpacingRight Spacing between the right edge of the view + * and the dialog frame + * @param viewSpacingBottom Spacing between the bottom edge of the view + * and the dialog frame + * @return This Builder object to allow for chaining of calls to set + * methods + * + * @hide pending API review + */ + public Builder setView(View view, int viewSpacingLeft, int viewSpacingTop, + int viewSpacingRight, int viewSpacingBottom) { + P.mView = view; + P.mViewSpacingSpecified = true; + P.mViewSpacingLeft = viewSpacingLeft; + P.mViewSpacingTop = viewSpacingTop; + P.mViewSpacingRight = viewSpacingRight; + P.mViewSpacingBottom = viewSpacingBottom; return this; } @@ -560,7 +746,8 @@ public class AlertDialog extends Dialog implements DialogInterface { * contents is. * * @param useInverseBackground Whether to use the inverse background - * @return This Builder object to allow for chaining of sets. + * + * @return This Builder object to allow for chaining of calls to set methods */ public Builder setInverseBackgroundForced(boolean useInverseBackground) { P.mForceInverseBackground = useInverseBackground; diff --git a/core/java/android/app/ApplicationContext.java b/core/java/android/app/ApplicationContext.java index 342ffcf..0e41ae6 100644 --- a/core/java/android/app/ApplicationContext.java +++ b/core/java/android/app/ApplicationContext.java @@ -87,6 +87,7 @@ import android.util.Log; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.WindowManagerImpl; +import android.view.inputmethod.InputMethodManager; import com.android.internal.policy.PolicyManager; @@ -104,6 +105,8 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import org.xmlpull.v1.XmlPullParserException; + class ReceiverRestrictedContext extends ContextWrapper { ReceiverRestrictedContext(Context base) { super(base); @@ -139,12 +142,12 @@ class ReceiverRestrictedContext extends ContextWrapper { * Common implementation of Context API, which Activity and other application * classes inherit. */ -@SuppressWarnings({"EmptyCatchBlock"}) class ApplicationContext extends Context { private final static String TAG = "ApplicationContext"; private final static boolean DEBUG_ICONS = false; private static final Object sSync = new Object(); + private static AlarmManager sAlarmManager; private static PowerManager sPowerManager; private static ConnectivityManager sConnectivityManager; private static WifiManager sWifiManager; @@ -288,12 +291,15 @@ class ApplicationContext extends Context { } throw new RuntimeException("Not supported in system context"); } + + private static File makeBackupFile(File prefsFile) { + return new File(prefsFile.getPath() + ".bak"); + } @Override public SharedPreferences getSharedPreferences(String name, int mode) { - File f; - f = makeFilename(getPreferencesDir(), name + ".xml"); SharedPreferencesImpl sp; + File f = makeFilename(getPreferencesDir(), name + ".xml"); synchronized (sSharedPrefs) { sp = sSharedPrefs.get(f); if (sp != null && !sp.hasFileChanged()) { @@ -301,15 +307,27 @@ class ApplicationContext extends Context { return sp; } } + + FileInputStream str = null; + File backup = makeBackupFile(f); + if (backup.exists()) { + f.delete(); + backup.renameTo(f); + } Map map = null; - try { - FileInputStream str = new FileInputStream(f); - map = XmlUtils.readMapXml(str); - str.close(); - } catch (org.xmlpull.v1.XmlPullParserException e) { - } catch (java.io.FileNotFoundException e) { - } catch (java.io.IOException e) { + if (f.exists()) { + try { + str = new FileInputStream(f); + map = XmlUtils.readMapXml(str); + str.close(); + } catch (org.xmlpull.v1.XmlPullParserException e) { + Log.w(TAG, "getSharedPreferences", e); + } catch (FileNotFoundException e) { + Log.w(TAG, "getSharedPreferences", e); + } catch (IOException e) { + Log.w(TAG, "getSharedPreferences", e); + } } synchronized (sSharedPrefs) { @@ -350,7 +368,7 @@ class ApplicationContext extends Context { File f = makeFilename(getFilesDir(), name); try { FileOutputStream fos = new FileOutputStream(f, append); - setFilePermissionsFromMode(f.toString(), mode, 0); + setFilePermissionsFromMode(f.getPath(), mode, 0); return fos; } catch (FileNotFoundException e) { } @@ -358,11 +376,11 @@ class ApplicationContext extends Context { File parent = f.getParentFile(); parent.mkdir(); FileUtils.setPermissions( - parent.toString(), + parent.getPath(), FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, -1, -1); FileOutputStream fos = new FileOutputStream(f, append); - setFilePermissionsFromMode(f.toString(), mode, 0); + setFilePermissionsFromMode(f.getPath(), mode, 0); return fos; } @@ -378,6 +396,16 @@ class ApplicationContext extends Context { if (mFilesDir == null) { mFilesDir = new File(getDataDirFile(), "files"); } + if (!mFilesDir.exists()) { + if(!mFilesDir.mkdirs()) { + Log.w(TAG, "Unable to create files directory"); + return null; + } + FileUtils.setPermissions( + mFilesDir.getPath(), + FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, + -1, -1); + } return mFilesDir; } } @@ -394,7 +422,7 @@ class ApplicationContext extends Context { return null; } FileUtils.setPermissions( - mCacheDir.toString(), + mCacheDir.getPath(), FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, -1, -1); } @@ -418,14 +446,14 @@ class ApplicationContext extends Context { public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory) { File dir = getDatabasesDir(); if (!dir.isDirectory() && dir.mkdir()) { - FileUtils.setPermissions(dir.toString(), + FileUtils.setPermissions(dir.getPath(), FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, -1, -1); } File f = makeFilename(dir, name); SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(f, factory); - setFilePermissionsFromMode(f.toString(), mode, 0); + setFilePermissionsFromMode(f.getPath(), mode, 0); return db; } @@ -844,7 +872,7 @@ class ApplicationContext extends Context { } else if (ACTIVITY_SERVICE.equals(name)) { return getActivityManager(); } else if (ALARM_SERVICE.equals(name)) { - return new AlarmManager(); + return getAlarmManager(); } else if (POWER_SERVICE.equals(name)) { return getPowerManager(); } else if (CONNECTIVITY_SERVICE.equals(name)) { @@ -878,6 +906,8 @@ class ApplicationContext extends Context { return getTelephonyManager(); } else if (CLIPBOARD_SERVICE.equals(name)) { return getClipboardManager(); + } else if (INPUT_METHOD_SERVICE.equals(name)) { + return InputMethodManager.getInstance(this); } return null; @@ -893,6 +923,17 @@ class ApplicationContext extends Context { return mActivityManager; } + private AlarmManager getAlarmManager() { + synchronized (sSync) { + if (sAlarmManager == null) { + IBinder b = ServiceManager.getService(ALARM_SERVICE); + IAlarmManager service = IAlarmManager.Stub.asInterface(b); + sAlarmManager = new AlarmManager(service); + } + } + return sAlarmManager; + } + private PowerManager getPowerManager() { synchronized (sSync) { if (sPowerManager == null) { @@ -1299,7 +1340,7 @@ class ApplicationContext extends Context { File file = makeFilename(getDataDirFile(), name); if (!file.exists()) { file.mkdir(); - setFilePermissionsFromMode(file.toString(), mode, + setFilePermissionsFromMode(file.getPath(), mode, FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH); } return file; @@ -1636,6 +1677,20 @@ class ApplicationContext extends Context { throw new RuntimeException("Package manager has died", e); } } + + @Override + public int getUidForSharedUser(String sharedUserName) + throws NameNotFoundException { + try { + int uid = mPM.getUidForSharedUser(sharedUserName); + if(uid != -1) { + return uid; + } + } catch (RemoteException e) { + throw new RuntimeException("Package manager has died", e); + } + throw new NameNotFoundException("No shared userid for user:"+sharedUserName); + } @Override public List<PackageInfo> getInstalledPackages(int flags) { @@ -1899,7 +1954,9 @@ class ApplicationContext extends Context { if (app.packageName.equals("system")) { return mContext.mMainThread.getSystemContext().getResources(); } - Resources r = mContext.mMainThread.getTopLevelResources(app.publicSourceDir); + Resources r = mContext.mMainThread.getTopLevelResources( + app.uid == Process.myUid() ? app.sourceDir + : app.publicSourceDir); if (r != null) { return r; } @@ -2341,6 +2398,7 @@ class ApplicationContext extends Context { private static final class SharedPreferencesImpl implements SharedPreferences { private final File mFile; + private final File mBackupFile; private final int mMode; private Map mMap; private final FileStatus mFileStatus = new FileStatus(); @@ -2351,6 +2409,7 @@ class ApplicationContext extends Context { SharedPreferencesImpl( File file, int mode, Map initialContents) { mFile = file; + mBackupFile = makeBackupFile(file); mMode = mode; mMap = initialContents != null ? initialContents : new HashMap(); if (FileUtils.getFileStatus(file.getPath(), mFileStatus)) { @@ -2544,30 +2603,68 @@ class ApplicationContext extends Context { public Editor edit() { return new EditorImpl(); } + + private FileOutputStream createFileOutputStream(File file) { + FileOutputStream str = null; + try { + str = new FileOutputStream(file); + } catch (FileNotFoundException e) { + File parent = file.getParentFile(); + if (!parent.mkdir()) { + Log.e(TAG, "Couldn't create directory for SharedPreferences file " + file); + return null; + } + FileUtils.setPermissions( + parent.getPath(), + FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, + -1, -1); + try { + str = new FileOutputStream(file); + } catch (FileNotFoundException e2) { + Log.e(TAG, "Couldn't create SharedPreferences file " + file, e2); + } + } + return str; + } private boolean writeFileLocked() { + // Rename the current file so it may be used as a backup during the next read + if (mFile.exists()) { + if (!mFile.renameTo(mBackupFile)) { + Log.e(TAG, "Couldn't rename file " + mFile + " to backup file " + mBackupFile); + } + } + + // Attempt to write the file, delete the backup and return true as atomically as + // possible. If any exception occurs, delete the new file; next time we will restore + // from the backup. try { - FileOutputStream str; - try { - str = new FileOutputStream(mFile); - } catch (Exception e) { - File parent = mFile.getParentFile(); - parent.mkdir(); - FileUtils.setPermissions( - parent.toString(), - FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, - -1, -1); - str = new FileOutputStream(mFile); + FileOutputStream str = createFileOutputStream(mFile); + if (str == null) { + return false; } XmlUtils.writeMapXml(mMap, str); str.close(); - setFilePermissionsFromMode(mFile.toString(), mMode, 0); + setFilePermissionsFromMode(mFile.getPath(), mMode, 0); if (FileUtils.getFileStatus(mFile.getPath(), mFileStatus)) { mTimestamp = mFileStatus.mtime; } - } catch (org.xmlpull.v1.XmlPullParserException e) { - } catch (java.io.FileNotFoundException e) { - } catch (java.io.IOException e) { + + // Writing was successful, delete the backup file + if (!mBackupFile.delete()) { + Log.e(TAG, "Couldn't delete new backup file " + mBackupFile); + } + return true; + } catch (XmlPullParserException e) { + Log.w(TAG, "writeFileLocked: Got exception:", e); + } catch (IOException e) { + Log.w(TAG, "writeFileLocked: Got exception:", e); + } + // Clean up an unsuccessfully written file + if (mFile.exists()) { + if (!mFile.delete()) { + Log.e(TAG, "Couldn't clean up partially-written file " + mFile); + } } return false; } diff --git a/core/java/android/app/DatePickerDialog.java b/core/java/android/app/DatePickerDialog.java index 7450559..ee5e0d5 100644 --- a/core/java/android/app/DatePickerDialog.java +++ b/core/java/android/app/DatePickerDialog.java @@ -20,8 +20,8 @@ import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.os.Bundle; -import android.pim.DateFormat; import android.text.TextUtils.TruncateAt; +import android.text.format.DateFormat; import android.view.LayoutInflater; import android.view.View; import android.widget.DatePicker; @@ -107,7 +107,7 @@ public class DatePickerDialog extends AlertDialog implements OnClickListener, DateFormatSymbols symbols = new DateFormatSymbols(); mWeekDays = symbols.getShortWeekdays(); - mDateFormat = DateFormat.getLongDateFormat(context); + mDateFormat = DateFormat.getMediumDateFormat(context); mCalendar = Calendar.getInstance(); updateTitle(mInitialYear, mInitialMonth, mInitialDay); diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index 2de21ed..353500e 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -213,6 +213,9 @@ public interface IActivityManager extends IInterface { * SIGUSR1 is delivered. All others are ignored. */ public void signalPersistentProcesses(int signal) throws RemoteException; + // Retrieve running application processes in the system + public List<ActivityManager.RunningAppProcessInfo> getRunningAppProcesses() + throws RemoteException; /** Information you can retrieve about a particular application. */ public static class ContentProviderHolder implements Parcelable { @@ -350,4 +353,5 @@ public interface IActivityManager extends IInterface { int KILL_PIDS_FOR_MEMORY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+79; int GET_SERVICES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+80; int REPORT_PSS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+81; + int GET_RUNNING_APP_PROCESSES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+82; } diff --git a/core/java/android/app/IAlarmManager.aidl b/core/java/android/app/IAlarmManager.aidl index c7f20b9..cb42236 100755 --- a/core/java/android/app/IAlarmManager.aidl +++ b/core/java/android/app/IAlarmManager.aidl @@ -26,6 +26,7 @@ import android.app.PendingIntent; interface IAlarmManager { void set(int type, long triggerAtTime, in PendingIntent operation); void setRepeating(int type, long triggerAtTime, long interval, in PendingIntent operation); + void setInexactRepeating(int type, long triggerAtTime, long interval, in PendingIntent operation); void setTimeZone(String zone); void remove(in PendingIntent operation); } diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index f80c947..17618ff 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -40,6 +40,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.Window; +import android.view.inputmethod.InputMethodManager; import java.io.File; import java.util.ArrayList; @@ -1262,7 +1263,7 @@ public class Instrumentation { * @param activity The activity being paused. */ public void callActivityOnPause(Activity activity) { - activity.onPause(); + activity.performPause(); } /* @@ -1392,8 +1393,8 @@ public class Instrumentation { * if there was no Activity found to run the given Intent. * * @param who The Context from which the activity is being started. - * @param whoThread The main thread of 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 activity is perform the start (and thus receiving @@ -1416,8 +1417,9 @@ public class Instrumentation { * {@hide} */ public ActivityResult execStartActivity( - Context who, IApplicationThread whoThread, IBinder token, Activity target, + Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode) { + IApplicationThread whoThread = (IApplicationThread) contextThread; if (mActivityMonitors != null) { synchronized (mSync) { final int N = mActivityMonitors.size(); diff --git a/core/java/android/app/LocalActivityManager.java b/core/java/android/app/LocalActivityManager.java index 12e70e3..a24fcae 100644 --- a/core/java/android/app/LocalActivityManager.java +++ b/core/java/android/app/LocalActivityManager.java @@ -22,9 +22,6 @@ import android.os.Binder; import android.os.Bundle; import android.util.Config; import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewParent; import android.view.Window; import java.util.ArrayList; @@ -114,13 +111,21 @@ public class LocalActivityManager { } if (r.curState == INITIALIZING) { + // Get the lastNonConfigurationInstance for the activity + HashMap<String,Object> lastNonConfigurationInstances = + mParent.getLastNonConfigurationChildInstances(); + Object instance = null; + if (lastNonConfigurationInstances != null) { + instance = lastNonConfigurationInstances.get(r.id); + } + // We need to have always created the activity. if (localLOGV) Log.v(TAG, r.id + ": starting " + r.intent); if (r.activityInfo == null) { r.activityInfo = mActivityThread.resolveActivityInfo(r.intent); } r.activity = mActivityThread.startActivityNow( - mParent, r.id, r.intent, r.activityInfo, r, r.instanceState); + mParent, r.id, r.intent, r.activityInfo, r, r.instanceState, instance); if (r.activity == null) { return; } @@ -288,7 +293,6 @@ public class LocalActivityManager { // It's a brand new world. mActivities.put(id, r); mActivityArray.add(r); - } else if (r.activityInfo != null) { // If the new activity is the same as the current one, then // we may be able to reuse it. @@ -568,6 +572,32 @@ public class LocalActivityManager { moveToState(r, CREATED); } } + + /** + * Call onRetainNonConfigurationInstance on each child activity and store the + * results in a HashMap by id. Only construct the HashMap if there is a non-null + * object to store. Note that this does not support nested ActivityGroups. + * + * {@hide} + */ + public HashMap<String,Object> dispatchRetainNonConfigurationInstance() { + HashMap<String,Object> instanceMap = null; + + final int N = mActivityArray.size(); + for (int i=0; i<N; i++) { + LocalActivityRecord r = mActivityArray.get(i); + if ((r != null) && (r.activity != null)) { + Object instance = r.activity.onRetainNonConfigurationInstance(); + if (instance != null) { + if (instanceMap == null) { + instanceMap = new HashMap<String,Object>(); + } + instanceMap.put(r.id, instance); + } + } + } + return instanceMap; + } /** * Remove all activities from this LocalActivityManager, performing an diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index cc56385..ea67cdb 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -25,9 +25,9 @@ import android.media.AudioManager; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; -import android.pim.DateFormat; -import android.pim.DateUtils; import android.text.TextUtils; +import android.text.format.DateFormat; +import android.text.format.DateUtils; import android.widget.RemoteViews; /** diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java index ba84903..b59e9dc 100644 --- a/core/java/android/app/PendingIntent.java +++ b/core/java/android/app/PendingIntent.java @@ -73,9 +73,22 @@ public final class PendingIntent implements Parcelable { * {@link #getService}: if the described PendingIntent already exists, * the current one is canceled before generating a new one. You can use * this to retrieve a new PendingIntent when you are only changing the - * extra data in the Intent. + * extra data in the Intent; by canceling the previous pending intent, + * this ensures that only entities given the new data will be able to + * launch it. If this assurance is not an issue, consider + * {@link #FLAG_UPDATE_CURRENT}. */ public static final int FLAG_CANCEL_CURRENT = 1<<28; + /** + * Flag for use with {@link #getActivity}, {@link #getBroadcast}, and + * {@link #getService}: if the described PendingIntent already exists, + * then keep it but its replace its extra data with what is in this new + * Intent. This can be used if you are creating intents where only the + * extras change, and don't care that any entities that received your + * previous PendingIntent will be able to launch it with your new + * extras even if they are not explicitly given to it. + */ + public static final int FLAG_UPDATE_CURRENT = 1<<27; /** * Exception thrown when trying to send through a PendingIntent that @@ -161,7 +174,8 @@ public final class PendingIntent implements Parcelable { * not used). * @param intent Intent of the activity to be launched. * @param flags May be {@link #FLAG_ONE_SHOT}, {@link #FLAG_NO_CREATE}, - * {@link #FLAG_CANCEL_CURRENT}, or any of the flags as supported by + * {@link #FLAG_CANCEL_CURRENT}, {@link #FLAG_UPDATE_CURRENT}, + * or any of the flags as supported by * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts * of the intent that can be supplied when the actual send happens. * @@ -195,7 +209,8 @@ public final class PendingIntent implements Parcelable { * not used). * @param intent The Intent to be broadcast. * @param flags May be {@link #FLAG_ONE_SHOT}, {@link #FLAG_NO_CREATE}, - * {@link #FLAG_CANCEL_CURRENT}, or any of the flags as supported by + * {@link #FLAG_CANCEL_CURRENT}, {@link #FLAG_UPDATE_CURRENT}, + * or any of the flags as supported by * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts * of the intent that can be supplied when the actual send happens. * @@ -230,7 +245,8 @@ public final class PendingIntent implements Parcelable { * not used). * @param intent An Intent describing the service to be started. * @param flags May be {@link #FLAG_ONE_SHOT}, {@link #FLAG_NO_CREATE}, - * {@link #FLAG_CANCEL_CURRENT}, or any of the flags as supported by + * {@link #FLAG_CANCEL_CURRENT}, {@link #FLAG_UPDATE_CURRENT}, + * or any of the flags as supported by * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts * of the intent that can be supplied when the actual send happens. * diff --git a/core/java/android/app/ProgressDialog.java b/core/java/android/app/ProgressDialog.java index 8b60cfa..c87e398 100644 --- a/core/java/android/app/ProgressDialog.java +++ b/core/java/android/app/ProgressDialog.java @@ -262,7 +262,7 @@ public class ProgressDialog extends AlertDialog { } public void setIndeterminate(boolean indeterminate) { - if (mHasStarted && (isIndeterminate() != indeterminate)) { + if (mProgress != null) { mProgress.setIndeterminate(indeterminate); } else { mIndeterminate = indeterminate; diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index 5f3f9ef..2ce2db9 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -16,11 +16,14 @@ package android.app; +import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; @@ -48,10 +51,12 @@ import android.view.WindowManager; import android.view.View.OnFocusChangeListener; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.EditorInfo; import android.widget.AdapterView; -import android.widget.Button; import android.widget.CursorAdapter; import android.widget.EditText; +import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListAdapter; @@ -62,6 +67,7 @@ import android.widget.WrapperListAdapter; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemSelectedListener; +import java.util.List; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicLong; @@ -100,7 +106,7 @@ public class SearchDialog extends Dialog { private TextView mBadgeLabel; private LinearLayout mSearchEditLayout; private EditText mSearchTextField; - private Button mGoButton; + private ImageButton mGoButton; private ListView mSuggestionsList; private ViewTreeObserver mViewTreeObserver = null; @@ -130,14 +136,14 @@ public class SearchDialog extends Dialog { private String mSuggestionAction = null; private Uri mSuggestionData = null; private String mSuggestionQuery = null; - + /** * Constructor - fires it up and makes it look like the search UI. * * @param context Application Context we can use for system acess */ public SearchDialog(Context context) { - super(context, com.android.internal.R.style.Theme_Translucent); + super(context, com.android.internal.R.style.Theme_SearchBar); } /** @@ -149,21 +155,15 @@ public class SearchDialog extends Dialog { super.onCreate(savedInstanceState); Window theWindow = getWindow(); - theWindow.requestFeature(Window.FEATURE_NO_TITLE); - theWindow.setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND, - WindowManager.LayoutParams.FLAG_DIM_BEHIND); theWindow.setGravity(Gravity.TOP|Gravity.FILL_HORIZONTAL); setContentView(com.android.internal.R.layout.search_bar); - // Note: theWindow.setBackgroundDrawable(null) does not work here - you get blackness - theWindow.setBackgroundDrawableResource(android.R.color.transparent); - theWindow.setLayout(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); WindowManager.LayoutParams lp = theWindow.getAttributes(); - lp.dimAmount = 0.5f; lp.setTitle("Search Dialog"); + lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE; theWindow.setAttributes(lp); // get the view elements for local access @@ -171,14 +171,14 @@ public class SearchDialog extends Dialog { mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge); mSearchEditLayout = (LinearLayout)findViewById(com.android.internal.R.id.search_edit_frame); mSearchTextField = (EditText) findViewById(com.android.internal.R.id.search_src_text); - mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn); + mGoButton = (ImageButton) findViewById(com.android.internal.R.id.search_go_btn); mSuggestionsList = (ListView) findViewById(com.android.internal.R.id.search_suggest_list); // attach listeners mSearchTextField.addTextChangedListener(mTextWatcher); mSearchTextField.setOnKeyListener(mTextKeyListener); mGoButton.setOnClickListener(mGoButtonClickListener); - mGoButton.setOnKeyListener(mGoButtonKeyListener); + mGoButton.setOnKeyListener(mButtonsKeyListener); mSuggestionsList.setOnItemClickListener(mSuggestionsListItemClickListener); mSuggestionsList.setOnKeyListener(mSuggestionsKeyListener); mSuggestionsList.setOnFocusChangeListener(mSuggestFocusListener); @@ -241,6 +241,7 @@ public class SearchDialog extends Dialog { if (mSuggestionsList != null) { mSuggestionsList.setVisibility(View.GONE); // prevent any flicker if was visible } + super.show(); setupSearchableInfo(); @@ -266,6 +267,17 @@ public class SearchDialog extends Dialog { initialQuery = ""; // This forces the preload to happen, triggering suggestions } mSearchTextField.setText(initialQuery); + + // If it is not for global search, that means the search dialog is + // launched to input a web address. + if (!globalSearch) { + mSearchTextField.setRawInputType(EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_URI); + } else { + mSearchTextField.setRawInputType(EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_NORMAL); + } + if (selectInitialQuery) { mSearchTextField.selectAll(); } else { @@ -424,7 +436,6 @@ public class SearchDialog extends Dialog { public void onConfigurationChanged(Configuration newConfig) { if (isShowing()) { // Redraw (resources may have changed) - updateSearchButton(); updateSearchBadge(); updateQueryHint(); } @@ -439,7 +450,6 @@ public class SearchDialog extends Dialog { mActivityContext = mSearchable.getActivityContext(getContext()); mProviderContext = mSearchable.getProviderContext(getContext(), mActivityContext); - updateSearchButton(); updateSearchBadge(); updateQueryHint(); } @@ -459,18 +469,6 @@ public class SearchDialog extends Dialog { } /** - * Update the text in the search button - */ - private void updateSearchButton() { - int textId = mSearchable.getSearchButtonText(); - if (textId == 0) { - textId = com.android.internal.R.string.search_go; - } - String goText = mActivityContext.getResources().getString(textId); - mGoButton.setText(goText); - } - - /** * Setup the search "Badge" if request by mode flags. */ private void updateSearchBadge() { @@ -1031,9 +1029,10 @@ public class SearchDialog extends Dialog { } /** - * React to typing in the GO button by refocusing to EditText. Continue typing the query. + * React to typing in the GO search button by refocusing to EditText. + * Continue typing the query. */ - View.OnKeyListener mGoButtonKeyListener = new View.OnKeyListener() { + View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() { public boolean onKey(View v, int keyCode, KeyEvent event) { // also guard against possible race conditions (late arrival after dismiss) if (mSearchable != null) { @@ -1054,7 +1053,7 @@ public class SearchDialog extends Dialog { } } }; - + /** * React to the user typing "enter" or other hardwired keys while typing in the search box. * This handles these special keys while the edit box has focus. diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index 01babc4..5f25b90 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -48,6 +48,7 @@ import android.view.KeyEvent; * <li><a href="#ActionKeys">Action Keys</a> * <li><a href="#SearchabilityMetadata">Searchability Metadata</a> * <li><a href="#PassingSearchContext">Passing Search Context</a> + * <li><a href="#ProtectingUserPrivacy">Protecting User Privacy</a> * </ol> * * <a name="DeveloperGuide"></a> @@ -578,7 +579,7 @@ import android.view.KeyEvent; * file. Each element defines one of the keycodes you are interested in, * defines the conditions under which they are sent, and provides details * on how to communicate the action key event back to your searchable activity.</li> - * <li>In your intent receiver, if you wish, you can check for action keys by checking the + * <li>In your broadcast receiver, if you wish, you can check for action keys by checking the * extras field of the {@link android.content.Intent Intent}.</li> * </ul> * @@ -974,6 +975,36 @@ import android.view.KeyEvent; * appData.get...(); * appData.get...(); * }</pre> + * + * <a name="ProtectingUserPrivacy"></a> + * <h3>Protecting User Privacy</h3> + * + * <p>Many users consider their activities on the phone, including searches, to be private + * information. Applications that implement search should take steps to protect users' privacy + * wherever possible. This section covers two areas of concern, but you should consider your search + * design carefully and take any additional steps necessary. + * + * <p><b>Don't send personal information to servers, and if you do, don't log it.</b> + * "Personal information" is information that can personally identify your users, such as name, + * email address or billing information, or other data which can be reasonably linked to such + * information. If your application implements search with the assistance of a server, try to + * avoid sending personal information with your searches. For example, if you are searching for + * businesses near a zip code, you don't need to send the user ID as well - just send the zip code + * to the server. If you do need to send personal information, you should take steps to avoid + * logging it. If you must log it, you should protect that data very carefully, and erase it as + * soon as possible. + * + * <p><b>Provide the user with a way to clear their search history.</b> The Search Manager helps + * your application provide context-specific suggestions. Sometimes these suggestions are based + * on previous searches, or other actions taken by the user in an earlier session. A user may not + * wish for previous searches to be revealed to other users, for instance if they share their phone + * with a friend. If your application provides suggestions that can reveal previous activities, + * you should implement a "Clear History" menu, preference, or button. If you are using + * {@link android.provider.SearchRecentSuggestions}, you can simply call its + * {@link android.provider.SearchRecentSuggestions#clearHistory() clearHistory()} method from + * your "Clear History" UI. If you are implementing your own form of recent suggestions, you'll + * need to provide a similar a "clear history" API in your provider, and call it from your + * "Clear History" UI. */ public class SearchManager implements DialogInterface.OnDismissListener, DialogInterface.OnCancelListener @@ -1008,6 +1039,16 @@ public class SearchManager * activity that launched the search. */ public final static String APP_DATA = "app_data"; + + /** + * Intent app_data bundle key: Use this key with the bundle from + * {@link android.content.Intent#getBundleExtra + * content.Intent.getBundleExtra(APP_DATA)} to obtain the source identifier + * set by the activity that launched the search. + * + * @hide + */ + public final static String SOURCE = "source"; /** * Intent extra data key: Use this key with Intent.ACTION_SEARCH and diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index 28b0615..6c08e75 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -163,7 +163,6 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac /** * Called by the system when the service is first created. Do not call this method directly. - * If you override this method, be sure to call super.onCreate(). */ public void onCreate() { } @@ -172,7 +171,6 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac * Called by the system every time a client explicitly starts the service by calling * {@link android.content.Context#startService}, providing the arguments it supplied and a * unique integer token representing the start request. Do not call this method directly. - * If you override this method, be sure to call super.onStart(). * * @param intent The Intent supplied to {@link android.content.Context#startService}, * as given. @@ -189,7 +187,6 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac * service should clean up an resources it holds (threads, registered * receivers, etc) at this point. Upon return, there will be no more calls * in to this Service object and it is effectively dead. Do not call this method directly. - * If you override this method, be sure to call super.onDestroy(). */ public void onDestroy() { } @@ -375,4 +372,3 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac private Application mApplication = null; private IActivityManager mActivityManager = null; } - diff --git a/core/java/android/app/TimePickerDialog.java b/core/java/android/app/TimePickerDialog.java index 107532e..002b01f 100644 --- a/core/java/android/app/TimePickerDialog.java +++ b/core/java/android/app/TimePickerDialog.java @@ -20,7 +20,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.os.Bundle; -import android.pim.DateFormat; +import android.text.format.DateFormat; import android.view.LayoutInflater; import android.view.View; import android.widget.TimePicker; diff --git a/core/java/android/bluetooth/BluetoothA2dp.java b/core/java/android/bluetooth/BluetoothA2dp.java new file mode 100644 index 0000000..d6ea889 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothA2dp.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.server.BluetoothA2dpService; +import android.content.Context; +import android.os.ServiceManager; +import android.os.RemoteException; +import android.os.IBinder; +import android.util.Log; + +import java.util.List; + +/** + * Public API for controlling the Bluetooth A2DP Profile Service. + * + * BluetoothA2dp is a proxy object for controlling the Bluetooth A2DP + * Service via IPC. + * + * Creating a BluetoothA2dp object will initiate a binding with the + * BluetoothHeadset service. Users of this object should call close() when they + * are finished, so that this proxy object can unbind from the service. + * + * Currently the BluetoothA2dp service runs in the system server and this + * proxy object will be immediately bound to the service on construction. + * However this may change in future releases, and error codes such as + * BluetoothError.ERROR_IPC_NOT_READY will be returned from this API when the + * proxy object is not yet attached. + * + * Currently this class provides methods to connect to A2DP audio sinks. + * + * @hide + */ +public class BluetoothA2dp { + private static final String TAG = "BluetoothA2dp"; + + /** int extra for SINK_STATE_CHANGED_ACTION */ + public static final String SINK_STATE = + "android.bluetooth.a2dp.intent.SINK_STATE"; + /** int extra for SINK_STATE_CHANGED_ACTION */ + public static final String SINK_PREVIOUS_STATE = + "android.bluetooth.a2dp.intent.SINK_PREVIOUS_STATE"; + + /** Indicates the state of an A2DP audio sink has changed. + * This intent will always contain SINK_STATE, SINK_PREVIOUS_STATE and + * BluetoothIntent.ADDRESS extras. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SINK_STATE_CHANGED_ACTION = + "android.bluetooth.a2dp.intent.action.SINK_STATE_CHANGED"; + + public static final int STATE_DISCONNECTED = 0; + public static final int STATE_CONNECTING = 1; + public static final int STATE_CONNECTED = 2; + public static final int STATE_DISCONNECTING = 3; + /** Playing implies connected */ + public static final int STATE_PLAYING = 4; + + private final IBluetoothA2dp mService; + private final Context mContext; + + /** + * Create a BluetoothA2dp proxy object for interacting with the local + * Bluetooth A2DP service. + * @param c Context + */ + public BluetoothA2dp(Context c) { + mContext = c; + IBinder b = ServiceManager.getService(BluetoothA2dpService.BLUETOOTH_A2DP_SERVICE); + if (b == null) { + throw new RuntimeException("Bluetooth A2DP service not available!"); + } + mService = IBluetoothA2dp.Stub.asInterface(b); + } + + /** Initiate a connection to an A2DP sink. + * Listen for A2DP_SINK_STATE_CHANGED_ACTION to find out when the + * connection is completed. + * @param address Remote BT address. + * @return Result code, negative indicates an immediate error. + * @hide + */ + public int connectSink(String address) { + try { + return mService.connectSink(address); + } catch (RemoteException e) { + Log.w(TAG, "", e); + return BluetoothError.ERROR_IPC; + } + } + + /** Initiate disconnect from an A2DP sink. + * Listen for A2DP_SINK_STATE_CHANGED_ACTION to find out when + * disconnect is completed. + * @param address Remote BT address. + * @return Result code, negative indicates an immediate error. + * @hide + */ + public int disconnectSink(String address) { + try { + return mService.disconnectSink(address); + } catch (RemoteException e) { + Log.w(TAG, "", e); + return BluetoothError.ERROR_IPC; + } + } + + /** Check if a specified A2DP sink is connected. + * @param address Remote BT address. + * @return True if connected (or playing), false otherwise and on error. + * @hide + */ + public boolean isSinkConnected(String address) { + int state = getSinkState(address); + return state == STATE_CONNECTED || state == STATE_PLAYING; + } + + /** Check if any A2DP sink is connected. + * @return a List of connected A2DP sinks, or null on error. + * @hide + */ + public List<String> listConnectedSinks() { + try { + return mService.listConnectedSinks(); + } catch (RemoteException e) { + Log.w(TAG, "", e); + return null; + } + } + + /** Get the state of an A2DP sink + * @param address Remote BT address. + * @return State code, or negative on error + * @hide + */ + public int getSinkState(String address) { + try { + return mService.getSinkState(address); + } catch (RemoteException e) { + Log.w(TAG, "", e); + return BluetoothError.ERROR_IPC; + } + } + + /** Helper for converting a state to a string. + * For debug use only - strings are not internationalized. + * @hide + */ + public static String stateToString(int state) { + switch (state) { + case STATE_DISCONNECTED: + return "disconnected"; + case STATE_CONNECTING: + return "connecting"; + case STATE_CONNECTED: + return "connected"; + case STATE_DISCONNECTING: + return "disconnecting"; + case STATE_PLAYING: + return "playing"; + default: + return "<unknown state " + state + ">"; + } + } +} diff --git a/core/java/android/bluetooth/BluetoothClass.java b/core/java/android/bluetooth/BluetoothClass.java new file mode 100644 index 0000000..88ce18b --- /dev/null +++ b/core/java/android/bluetooth/BluetoothClass.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +/** + * The Android Bluetooth API is not finalized, and *will* change. Use at your + * own risk. + * + * Static helper methods and constants to decode the device class bit vector + * returned by the Bluetooth API. + * + * The Android Bluetooth API returns a 32-bit integer to represent the class. + * The format of these bits is defined at + * http://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm + * (login required). This class provides static helper methods and constants to + * determine what Service Class(es) and Device Class are encoded in the 32-bit + * class. + * + * Devices typically have zero or more service classes, and exactly one device + * class. The device class is encoded as a major and minor device class, the + * minor being a subset of the major. + * + * Class is useful to describe a device (for example to show an icon), + * but does not reliably describe what profiles a device supports. To determine + * profile support you usually need to perform SDP queries. + * + * Each of these helper methods takes the 32-bit integer class as an argument. + * + * @hide + */ +public class BluetoothClass { + /** Indicates the Bluetooth API could not retrieve the class */ + public static final int ERROR = 0xFF000000; + + /** Every Bluetooth device has zero or more service classes */ + public static class Service { + public static final int BITMASK = 0xFFE000; + + public static final int LIMITED_DISCOVERABILITY = 0x002000; + public static final int POSITIONING = 0x010000; + public static final int NETWORKING = 0x020000; + public static final int RENDER = 0x040000; + public static final int CAPTURE = 0x080000; + public static final int OBJECT_TRANSFER = 0x100000; + public static final int AUDIO = 0x200000; + public static final int TELEPHONY = 0x400000; + public static final int INFORMATION = 0x800000; + + /** Returns true if the given class supports the given Service Class. + * A bluetooth device can claim to support zero or more service classes. + * @param btClass The bluetooth class. + * @param serviceClass The service class constant to test for. For + * example, Service.AUDIO. Must be one of the + * Service.FOO constants. + * @return True if the service class is supported. + */ + public static boolean hasService(int btClass, int serviceClass) { + if (btClass == ERROR) { + return false; + } + return ((btClass & Service.BITMASK & serviceClass) != 0); + } + } + + /** Every Bluetooth device has exactly one device class, comprimised of + * major and minor components. We have not included the minor classes for + * major classes: NETWORKING, PERIPHERAL and IMAGING yet because they work + * a little differently. */ + public static class Device { + public static final int BITMASK = 0x1FFC; + + public static class Major { + public static final int BITMASK = 0x1F00; + + public static final int MISC = 0x0000; + public static final int COMPUTER = 0x0100; + public static final int PHONE = 0x0200; + public static final int NETWORKING = 0x0300; + public static final int AUDIO_VIDEO = 0x0400; + public static final int PERIPHERAL = 0x0500; + public static final int IMAGING = 0x0600; + public static final int WEARABLE = 0x0700; + public static final int TOY = 0x0800; + public static final int HEALTH = 0x0900; + public static final int UNCATEGORIZED = 0x1F00; + + /** Returns the Major Device Class component of a bluetooth class. + * Values returned from this function can be compared with the constants + * Device.Major.FOO. A bluetooth device can only be associated + * with one major class. + */ + public static int getDeviceMajor(int btClass) { + if (btClass == ERROR) { + return ERROR; + } + return (btClass & Device.Major.BITMASK); + } + } + + // Devices in the COMPUTER major class + public static final int COMPUTER_UNCATEGORIZED = 0x0100; + public static final int COMPUTER_DESKTOP = 0x0104; + public static final int COMPUTER_SERVER = 0x0108; + public static final int COMPUTER_LAPTOP = 0x010C; + public static final int COMPUTER_HANDHELD_PC_PDA = 0x0110; + public static final int COMPUTER_PALM_SIZE_PC_PDA = 0x0114; + public static final int COMPUTER_WEARABLE = 0x0118; + + // Devices in the PHONE major class + public static final int PHONE_UNCATEGORIZED = 0x0200; + public static final int PHONE_CELLULAR = 0x0204; + public static final int PHONE_CORDLESS = 0x0208; + public static final int PHONE_SMART = 0x020C; + public static final int PHONE_MODEM_OR_GATEWAY = 0x0210; + public static final int PHONE_ISDN = 0x0214; + + // Minor classes for the AUDIO_VIDEO major class + public static final int AUDIO_VIDEO_UNCATEGORIZED = 0x0400; + public static final int AUDIO_VIDEO_WEARABLE_HEADSET = 0x0404; + public static final int AUDIO_VIDEO_HANDSFREE = 0x0408; + //public static final int AUDIO_VIDEO_RESERVED = 0x040C; + public static final int AUDIO_VIDEO_MICROPHONE = 0x0410; + public static final int AUDIO_VIDEO_LOUDSPEAKER = 0x0414; + public static final int AUDIO_VIDEO_HEADPHONES = 0x0418; + public static final int AUDIO_VIDEO_PORTABLE_AUDIO = 0x041C; + public static final int AUDIO_VIDEO_CAR_AUDIO = 0x0420; + public static final int AUDIO_VIDEO_SET_TOP_BOX = 0x0424; + public static final int AUDIO_VIDEO_HIFI_AUDIO = 0x0428; + public static final int AUDIO_VIDEO_VCR = 0x042C; + public static final int AUDIO_VIDEO_VIDEO_CAMERA = 0x0430; + public static final int AUDIO_VIDEO_CAMCORDER = 0x0434; + public static final int AUDIO_VIDEO_VIDEO_MONITOR = 0x0438; + public static final int AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER = 0x043C; + public static final int AUDIO_VIDEO_VIDEO_CONFERENCING = 0x0440; + //public static final int AUDIO_VIDEO_RESERVED = 0x0444; + public static final int AUDIO_VIDEO_VIDEO_GAMING_TOY = 0x0448; + + // Devices in the WEARABLE major class + public static final int WEARABLE_UNCATEGORIZED = 0x0700; + public static final int WEARABLE_WRIST_WATCH = 0x0704; + public static final int WEARABLE_PAGER = 0x0708; + public static final int WEARABLE_JACKET = 0x070C; + public static final int WEARABLE_HELMET = 0x0710; + public static final int WEARABLE_GLASSES = 0x0714; + + // Devices in the TOY major class + public static final int TOY_UNCATEGORIZED = 0x0800; + public static final int TOY_ROBOT = 0x0804; + public static final int TOY_VEHICLE = 0x0808; + public static final int TOY_DOLL_ACTION_FIGURE = 0x080C; + public static final int TOY_CONTROLLER = 0x0810; + public static final int TOY_GAME = 0x0814; + + // Devices in the HEALTH major class + public static final int HEALTH_UNCATEGORIZED = 0x0900; + public static final int HEALTH_BLOOD_PRESSURE = 0x0904; + public static final int HEALTH_THERMOMETER = 0x0908; + public static final int HEALTH_WEIGHING = 0x090C; + public static final int HEALTH_GLUCOSE = 0x0910; + public static final int HEALTH_PULSE_OXIMETER = 0x0914; + public static final int HEALTH_PULSE_RATE = 0x0918; + public static final int HEALTH_DATA_DISPLAY = 0x091C; + + /** Returns the Device Class component of a bluetooth class. This includes + * both the major and minor device components. Values returned from this + * function can be compared with the constants Device.FOO. A bluetooth + * device can only be associated with one device class. + */ + public static int getDevice(int btClass) { + if (btClass == ERROR) { + return ERROR; + } + return (btClass & Device.BITMASK); + } + } +} + diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java index 0b24db6..d1f71c5 100644 --- a/core/java/android/bluetooth/BluetoothDevice.java +++ b/core/java/android/bluetooth/BluetoothDevice.java @@ -514,7 +514,7 @@ public class BluetoothDevice { try { return mService.getRemoteClass(address); } catch (RemoteException e) {Log.e(TAG, "", e);} - return DeviceClass.CLASS_UNKNOWN; + return BluetoothClass.ERROR; } public byte[] getRemoteFeatures(String address) { try { diff --git a/core/java/android/bluetooth/BluetoothError.java b/core/java/android/bluetooth/BluetoothError.java new file mode 100644 index 0000000..2554bea --- /dev/null +++ b/core/java/android/bluetooth/BluetoothError.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +/** + * Bluetooth API error codes. + * + * Errors are always negative. + * + * @hide + */ +public class BluetoothError { + /** No error */ + public static final int SUCCESS = 0; + + /** Generic error */ + public static final int ERROR = -1000; + + /** Bluetooth currently disabled */ + public static final int ERROR_DISABLED = -1001; + + /** IPC is not ready, for example service is not yet bound */ + public static final int ERROR_IPC_NOT_READY = -1011; + + /** Some other IPC error, for example a RemoteException */ + public static final int ERROR_IPC = -1012; + +} diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java index 90db39b..905173e 100644 --- a/core/java/android/bluetooth/BluetoothHeadset.java +++ b/core/java/android/bluetooth/BluetoothHeadset.java @@ -28,35 +28,36 @@ import android.util.Log; * The Android Bluetooth API is not finalized, and *will* change. Use at your * own risk. * - * Public API for controlling the Bluetooth Headset Service. + * Public API for controlling the Bluetooth Headset Service. This includes both + * Bluetooth Headset and Handsfree (v1.5) profiles. The Headset service will + * attempt a handsfree connection first, and fall back to headset. * * BluetoothHeadset is a proxy object for controlling the Bluetooth Headset - * Service. + * Service via IPC. * * Creating a BluetoothHeadset object will create a binding with the * BluetoothHeadset service. Users of this object should call close() when they * are finished with the BluetoothHeadset, so that this proxy object can unbind * from the service. * - * BlueoothHeadset objects are not guarenteed to be connected to the - * BluetoothHeadsetService at all times. Calls on this object while not - * connected to the service will result in default error return values. Even - * after object construction, there is a short delay (~10ms) before this proxy - * object is actually connected to the Service. + * This BluetoothHeadset object is not immediately bound to the + * BluetoothHeadset service. Use the ServiceListener interface to obtain a + * notification when it is bound, this is especially important if you wish to + * immediately call methods on BluetootHeadset after construction. * * Android only supports one connected Bluetooth Headset at a time. * - * Note that in this context, Headset includes both Bluetooth Headset's and - * Handsfree devices. - * * @hide */ public class BluetoothHeadset { - private final static String TAG = "BluetoothHeadset"; + private static final String TAG = "BluetoothHeadset"; + private static final boolean DBG = false; - private final Context mContext; private IBluetoothHeadset mService; + private final Context mContext; + private final ServiceListener mServiceListener; + private ConnectHeadsetCallback mConnectHeadsetCallback; /** There was an error trying to obtain the state */ public static final int STATE_ERROR = -1; @@ -72,25 +73,44 @@ public class BluetoothHeadset { /** Connection cancelled before completetion. */ public static final int RESULT_CANCELLED = 2; - private ServiceConnection mConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, IBinder service) { - mService = IBluetoothHeadset.Stub.asInterface(service); - Log.i(TAG, "Proxy object is now connected to Bluetooth Headset Service"); - } - public void onServiceDisconnected(ComponentName className) { - mService = null; - } - }; + /** + * An interface for notifying BluetoothHeadset IPC clients when they have + * been connected to the BluetoothHeadset service. + */ + public interface ServiceListener { + /** + * Called to notify the client when this proxy object has been + * connected to the BluetoothHeadset service. Clients must wait for + * this callback before making IPC calls on the BluetoothHeadset + * service. + */ + public void onServiceConnected(); + + /** + * Called to notify the client that this proxy object has been + * disconnected from the BluetoothHeadset service. Clients must not + * make IPC calls on the BluetoothHeadset service after this callback. + * This callback will currently only occur if the application hosting + * the BluetoothHeadset service, but may be called more often in future. + */ + public void onServiceDisconnected(); + } + + /** + * Interface for connectHeadset() callback. + * This callback can occur in the Binder thread. + */ + public interface ConnectHeadsetCallback { + public void onConnectHeadsetResult(String address, int resultCode); + } /** * Create a BluetoothHeadset proxy object. - * Remeber to call close() when you are done with this object, so that it - * can unbind from the BluetoothHeadsetService. */ - public BluetoothHeadset(Context context) { + public BluetoothHeadset(Context context, ServiceListener l) { mContext = context; - if (!context.bindService( - new Intent(IBluetoothHeadset.class.getName()), mConnection, 0)) { + mServiceListener = l; + if (!context.bindService(new Intent(IBluetoothHeadset.class.getName()), mConnection, 0)) { Log.e(TAG, "Could not bind to Bluetooth Headset Service"); } } @@ -126,6 +146,9 @@ public class BluetoothHeadset { try { return mService.getState(); } catch (RemoteException e) {Log.e(TAG, e.toString());} + } else { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); } return BluetoothHeadset.STATE_ERROR; } @@ -141,6 +164,9 @@ public class BluetoothHeadset { try { return mService.getHeadsetAddress(); } catch (RemoteException e) {Log.e(TAG, e.toString());} + } else { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); } return null; } @@ -150,20 +176,29 @@ public class BluetoothHeadset { * This call does not block. Fails if a headset is already connecting * or connected. * Will connect to the last connected headset if address is null. + * onConnectHeadsetResult() of your ConnectHeadsetCallback will be called + * on completition. * @param address The Bluetooth Address to connect to, or null to connect * to the last connected headset. - * @param callback A callback with onCreateBondingResult() defined, or - * null. + * @param callback Callback on result. Not called if false is returned. Can + * be null. + * to the last connected headset. * @return False if there was a problem initiating the connection * procedure, and your callback will not be used. True if * the connection procedure was initiated, in which case * your callback is guarenteed to be called. */ - public boolean connectHeadset(String address, IBluetoothHeadsetCallback callback) { + public boolean connectHeadset(String address, ConnectHeadsetCallback callback) { if (mService != null) { try { - return mService.connectHeadset(address, callback); + if (mService.connectHeadset(address, mHeadsetCallback)) { + mConnectHeadsetCallback = callback; + return true; + } } catch (RemoteException e) {Log.e(TAG, e.toString());} + } else { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); } return false; } @@ -178,6 +213,9 @@ public class BluetoothHeadset { try { return mService.isConnected(address); } catch (RemoteException e) {Log.e(TAG, e.toString());} + } else { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); } return false; } @@ -191,8 +229,72 @@ public class BluetoothHeadset { if (mService != null) { try { mService.disconnectHeadset(); + return true; + } catch (RemoteException e) {Log.e(TAG, e.toString());} + } else { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); + } + return false; + } + + /** + * Start BT Voice Recognition mode, and set up Bluetooth audio path. + * Returns false if there is no headset connected, or if the + * connected headset does not support voice recognition, or on + * error. + */ + public boolean startVoiceRecognition() { + if (mService != null) { + try { + return mService.startVoiceRecognition(); + } catch (RemoteException e) {Log.e(TAG, e.toString());} + } else { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); + } + return false; + } + + /** + * Stop BT Voice Recognition mode, and shut down Bluetooth audio path. + * Returns false if there is no headset connected, or the connected + * headset is not in voice recognition mode, or on error. + */ + public boolean stopVoiceRecognition() { + if (mService != null) { + try { + return mService.stopVoiceRecognition(); } catch (RemoteException e) {Log.e(TAG, e.toString());} + } else { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); } return false; } + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + if (DBG) Log.d(TAG, "Proxy object connected"); + mService = IBluetoothHeadset.Stub.asInterface(service); + if (mServiceListener != null) { + mServiceListener.onServiceConnected(); + } + } + public void onServiceDisconnected(ComponentName className) { + if (DBG) Log.d(TAG, "Proxy object disconnected"); + mService = null; + if (mServiceListener != null) { + mServiceListener.onServiceDisconnected(); + } + } + }; + + private IBluetoothHeadsetCallback mHeadsetCallback = new IBluetoothHeadsetCallback.Stub() { + public void onConnectHeadsetResult(String address, int resultCode) { + if (mConnectHeadsetCallback != null) { + mConnectHeadsetCallback.onConnectHeadsetResult(address, resultCode); + } + } + }; } diff --git a/core/java/android/bluetooth/DeviceClass.java b/core/java/android/bluetooth/DeviceClass.java deleted file mode 100644 index 36035ca..0000000 --- a/core/java/android/bluetooth/DeviceClass.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.bluetooth; - -/** - * The Android Bluetooth API is not finalized, and *will* change. Use at your - * own risk. - * - * Static helper methods and constants to decode the device class bit vector - * returned by the Bluetooth API. - * - * The Android Bluetooth API returns a 32-bit integer to represent the device - * class. This is actually a bit vector, the format defined at - * http://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm - * (login required). This class provides static helper methods and constants to - * determine what Service Class(es), Major Class, and Minor Class are encoded - * in a 32-bit device class. - * - * Each of the helper methods takes the 32-bit integer device class as an - * argument. - * - * @hide - */ -public class DeviceClass { - - // Baseband class information - // See http://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm - - public static final int SERVICE_CLASS_BITMASK = 0xFFE000; - public static final int SERVICE_CLASS_LIMITED_DISCOVERABILITY = 0x002000; - public static final int SERVICE_CLASS_POSITIONING = 0x010000; - public static final int SERVICE_CLASS_NETWORKING = 0x020000; - public static final int SERVICE_CLASS_RENDER = 0x040000; - public static final int SERVICE_CLASS_CAPTURE = 0x080000; - public static final int SERVICE_CLASS_OBJECT_TRANSFER = 0x100000; - public static final int SERVICE_CLASS_AUDIO = 0x200000; - public static final int SERVICE_CLASS_TELEPHONY = 0x400000; - public static final int SERVICE_CLASS_INFORMATION = 0x800000; - - public static final int MAJOR_CLASS_BITMASK = 0x001F00; - public static final int MAJOR_CLASS_MISC = 0x000000; - public static final int MAJOR_CLASS_COMPUTER = 0x000100; - public static final int MAJOR_CLASS_PHONE = 0x000200; - public static final int MAJOR_CLASS_NETWORKING = 0x000300; - public static final int MAJOR_CLASS_AUDIO_VIDEO = 0x000400; - public static final int MAJOR_CLASS_PERIPHERAL = 0x000500; - public static final int MAJOR_CLASS_IMAGING = 0x000600; - public static final int MAJOR_CLASS_WEARABLE = 0x000700; - public static final int MAJOR_CLASS_TOY = 0x000800; - public static final int MAJOR_CLASS_MEDICAL = 0x000900; - public static final int MAJOR_CLASS_UNCATEGORIZED = 0x001F00; - - // Minor classes for the AUDIO_VIDEO major class - public static final int MINOR_CLASS_AUDIO_VIDEO_BITMASK = 0x0000FC; - public static final int MINOR_CLASS_AUDIO_VIDEO_UNCATEGORIZED = 0x000000; - public static final int MINOR_CLASS_AUDIO_VIDEO_HEADSET = 0x000004; - public static final int MINOR_CLASS_AUDIO_VIDEO_HANDSFREE = 0x000008; - public static final int MINOR_CLASS_AUDIO_VIDEO_MICROPHONE = 0x000010; - public static final int MINOR_CLASS_AUDIO_VIDEO_LOUDSPEAKER = 0x000014; - public static final int MINOR_CLASS_AUDIO_VIDEO_HEADPHONES = 0x000018; - public static final int MINOR_CLASS_AUDIO_VIDEO_PORTABLE_AUDIO = 0x00001C; - public static final int MINOR_CLASS_AUDIO_VIDEO_CAR_AUDIO = 0x000020; - public static final int MINOR_CLASS_AUDIO_VIDEO_SET_TOP_BOX = 0x000024; - public static final int MINOR_CLASS_AUDIO_VIDEO_HIFI_AUDIO = 0x000028; - public static final int MINOR_CLASS_AUDIO_VIDEO_VCR = 0x00002C; - public static final int MINOR_CLASS_AUDIO_VIDEO_VIDEO_CAMERA = 0x000030; - public static final int MINOR_CLASS_AUDIO_VIDEO_CAMCORDER = 0x000034; - public static final int MINOR_CLASS_AUDIO_VIDEO_VIDEO_MONITOR = 0x000038; - public static final int MINOR_CLASS_AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER = 0x00003C; - public static final int MINOR_CLASS_AUDIO_VIDEO_VIDEO_CONFERENCING = 0x000040; - public static final int MINOR_CLASS_AUDIO_VIDEO_VIDEO_GAMING_TOY = 0x000048; - - // Indicates the Bluetooth API could not retrieve the class - public static final int CLASS_UNKNOWN = 0xFF000000; - - /** Returns true if the given device class supports the given Service Class. - * A bluetooth device can claim to support zero or more service classes. - * @param deviceClass The bluetooth device class. - * @param serviceClassType The service class constant to test for. For - * example, DeviceClass.SERVICE_CLASS_AUDIO. This - * must be one of the SERVICE_CLASS_xxx constants, - * results of this function are undefined - * otherwise. - * @return If the deviceClass claims to support the serviceClassType. - */ - public static boolean hasServiceClass(int deviceClass, int serviceClassType) { - if (deviceClass == CLASS_UNKNOWN) { - return false; - } - return ((deviceClass & SERVICE_CLASS_BITMASK & serviceClassType) != 0); - } - - /** Returns the Major Class of a bluetooth device class. - * Values returned from this function can be compared with the constants - * MAJOR_CLASS_xxx. A bluetooth device can only be associated - * with one major class. - */ - public static int getMajorClass(int deviceClass) { - if (deviceClass == CLASS_UNKNOWN) { - return CLASS_UNKNOWN; - } - return (deviceClass & MAJOR_CLASS_BITMASK); - } - - /** Returns the Minor Class of a bluetooth device class. - * Values returned from this function can be compared with the constants - * MINOR_CLASS_xxx_yyy, where xxx is the Major Class. A bluetooth - * device can only be associated with one minor class within its major - * class. - */ - public static int getMinorClass(int deviceClass) { - if (deviceClass == CLASS_UNKNOWN) { - return CLASS_UNKNOWN; - } - return (deviceClass & MINOR_CLASS_AUDIO_VIDEO_BITMASK); - } -} diff --git a/core/java/android/bluetooth/IBluetoothA2dp.aidl b/core/java/android/bluetooth/IBluetoothA2dp.aidl new file mode 100644 index 0000000..7e0226d --- /dev/null +++ b/core/java/android/bluetooth/IBluetoothA2dp.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +/** + * System private API for Bluetooth A2DP service + * + * {@hide} + */ +interface IBluetoothA2dp { + int connectSink(in String address); + int disconnectSink(in String address); + List<String> listConnectedSinks(); + int getSinkState(in String address); +} diff --git a/core/java/android/bluetooth/IBluetoothHeadset.aidl b/core/java/android/bluetooth/IBluetoothHeadset.aidl index 7b6030b..564861f 100644 --- a/core/java/android/bluetooth/IBluetoothHeadset.aidl +++ b/core/java/android/bluetooth/IBluetoothHeadset.aidl @@ -36,7 +36,11 @@ interface IBluetoothHeadset { // returns true boolean connectHeadset(in String address, in IBluetoothHeadsetCallback callback); + void disconnectHeadset(); + boolean isConnected(in String address); - void disconnectHeadset(); + boolean startVoiceRecognition(); + + boolean stopVoiceRecognition(); } diff --git a/core/java/android/bluetooth/package.html b/core/java/android/bluetooth/package.html index ccd8fec..79abf0c 100644 --- a/core/java/android/bluetooth/package.html +++ b/core/java/android/bluetooth/package.html @@ -9,6 +9,5 @@ query the SDP database of other Bluetooth devices, establish RFCOMM channels/sockets on Android, and connect to specified sockets on other devices. </p> <p>Remember, not all Android devices are guaranteed to have Bluetooth functionality.</p> -{@hide} </BODY> </HTML> diff --git a/core/java/android/content/AbstractTableMerger.java b/core/java/android/content/AbstractTableMerger.java index 56e5d4a..e1a484e 100644 --- a/core/java/android/content/AbstractTableMerger.java +++ b/core/java/android/content/AbstractTableMerger.java @@ -548,7 +548,8 @@ public abstract class AbstractTableMerger long numDeletedEntries = 0; if (mDeletedTable != null) { Cursor deletedCursor = mDb.query(mDeletedTable, - syncIdAndVersionProjection, _SYNC_ACCOUNT + "=?", accountSelectionArgs, + syncIdAndVersionProjection, + _SYNC_ACCOUNT + "=? AND " + _SYNC_ID + " IS NOT NULL", accountSelectionArgs, null, null, mDeletedTable + "." + _SYNC_ID); numDeletedEntries = deletedCursor.getCount(); diff --git a/core/java/android/content/AsyncQueryHandler.java b/core/java/android/content/AsyncQueryHandler.java index 48f1bc7..2d651a7 100644 --- a/core/java/android/content/AsyncQueryHandler.java +++ b/core/java/android/content/AsyncQueryHandler.java @@ -24,6 +24,8 @@ import android.os.Looper; import android.os.Message; import android.util.Log; +import java.lang.ref.WeakReference; + /** * A helper class to help make handling asynchronous {@link ContentResolver} * queries easier. @@ -37,7 +39,7 @@ public abstract class AsyncQueryHandler extends Handler { private static final int EVENT_ARG_UPDATE = 3; private static final int EVENT_ARG_DELETE = 4; - /* package */ ContentResolver mResolver; + /* package */ final WeakReference<ContentResolver> mResolver; private static Looper sLooper = null; @@ -62,18 +64,26 @@ public abstract class AsyncQueryHandler extends Handler { @Override public void handleMessage(Message msg) { + final ContentResolver resolver = mResolver.get(); + if (resolver == null) return; + WorkerArgs args = (WorkerArgs) msg.obj; int token = msg.what; int event = msg.arg1; - + switch (event) { case EVENT_ARG_QUERY: Cursor cursor; try { - cursor = mResolver.query(args.uri, args.projection, + cursor = resolver.query(args.uri, args.projection, args.selection, args.selectionArgs, args.orderBy); + // Calling getCount() causes the cursor window to be filled, + // which will make the first access on the main thread a lot faster. + if (cursor != null) { + cursor.getCount(); + } } catch (Exception e) { cursor = null; } @@ -82,18 +92,16 @@ public abstract class AsyncQueryHandler extends Handler { break; case EVENT_ARG_INSERT: - args.result = mResolver.insert(args.uri, args.values); + args.result = resolver.insert(args.uri, args.values); break; case EVENT_ARG_UPDATE: - int r = mResolver.update(args.uri, args.values, args.selection, + args.result = resolver.update(args.uri, args.values, args.selection, args.selectionArgs); - args.result = new Integer(r); break; case EVENT_ARG_DELETE: - int r2 = mResolver.delete(args.uri, args.selection, args.selectionArgs); - args.result = new Integer(r2); + args.result = resolver.delete(args.uri, args.selection, args.selectionArgs); break; } @@ -115,7 +123,7 @@ public abstract class AsyncQueryHandler extends Handler { public AsyncQueryHandler(ContentResolver cr) { super(); - mResolver = cr; + mResolver = new WeakReference<ContentResolver>(cr); synchronized (AsyncQueryHandler.class) { if (sLooper == null) { HandlerThread thread = new HandlerThread("AsyncQueryWorker"); diff --git a/core/java/android/content/BroadcastReceiver.java b/core/java/android/content/BroadcastReceiver.java index 6a6f4f9..cd92002 100644 --- a/core/java/android/content/BroadcastReceiver.java +++ b/core/java/android/content/BroadcastReceiver.java @@ -327,7 +327,7 @@ public abstract class BroadcastReceiver { * current broadcast; only works with broadcasts sent through * {@link Context#sendOrderedBroadcast(Intent, String) * Context.sendOrderedBroadcast}. This will prevent - * any other intent receivers from receiving the broadcast. It will still + * any other broadcast receivers from receiving the broadcast. It will still * call {@link #onReceive} of the BroadcastReceiver that the caller of * {@link Context#sendOrderedBroadcast(Intent, String) * Context.sendOrderedBroadcast} passed in. diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 00a6d31..6da00df 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -982,10 +982,10 @@ public abstract class Context { String profileFile, Bundle arguments); /** - * Return the handle to a system-level service by name. The class of the - * returned object varies by the requested name. Currently available names + * Return the handle to a system-level service by name. The class of the + * returned object varies by the requested name. Currently available names * are: - * + * * <dl> * <dt> {@link #WINDOW_SERVICE} ("window") * <dd> The top-level window manager in which you can place custom @@ -1021,6 +1021,9 @@ public abstract class Context { * <dt> {@link #WIFI_SERVICE} ("wifi") * <dd> A {@link android.net.wifi.WifiManager WifiManager} for management of * Wi-Fi connectivity. + * <dt> {@link #INPUT_METHOD_SERVICE} ("input_method") + * <dd> An {@link android.view.inputmethod.InputMethodManager InputMethodManager} + * for management of input methods. * </dl> * * <p>Note: System services obtained via this API may be closely associated with @@ -1029,9 +1032,9 @@ public abstract class Context { * Services, Providers, etc.) * * @param name The name of the desired service. - * + * * @return The service or null if the name does not exist. - * + * * @see #WINDOW_SERVICE * @see android.view.WindowManager * @see #LAYOUT_INFLATER_SERVICE @@ -1062,6 +1065,8 @@ public abstract class Context { * @see android.media.AudioManager * @see #TELEPHONY_SERVICE * @see android.internal.TelephonyManager + * @see #INPUT_METHOD_SERVICE + * @see android.view.inputmethod.InputMethodManager */ public abstract Object getSystemService(String name); @@ -1235,6 +1240,15 @@ public abstract class Context { public static final String CLIPBOARD_SERVICE = "clipboard"; /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.view.inputmethod.InputMethodManager} for accessing input + * methods. + * + * @see #getSystemService + */ + public static final String INPUT_METHOD_SERVICE = "input_method"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * diff --git a/core/java/android/content/DefaultDataHandler.java b/core/java/android/content/DefaultDataHandler.java index 7dc71b8..863c9f6 100644 --- a/core/java/android/content/DefaultDataHandler.java +++ b/core/java/android/content/DefaultDataHandler.java @@ -28,42 +28,47 @@ import java.io.InputStream; import java.util.Stack; /** - * insert default data from InputStream, should be in XML format: - * if the provider syncs data to the server, the imported data will be synced to the server - * Samples: - * insert one row - * <row uri="content://contacts/people"> - * <Col column = "name" value = "foo feebe "/> - * <Col column = "addr" value = "Tx"/> - * </row> - * - * delete, it must be in order of uri, select and arg - * <del uri="content://contacts/people" select="name=? and addr=?" - * arg1 = "foo feebe" arg2 ="Tx"/> + * Inserts default data from InputStream, should be in XML format. + * If the provider syncs data to the server, the imported data will be synced to the server. + * <p>Samples:</p> + * <br/> + * Insert one row: + * <pre> + * <row uri="content://contacts/people"> + * <Col column = "name" value = "foo feebe "/> + * <Col column = "addr" value = "Tx"/> + * </row></pre> + * <br/> + * Delete, it must be in order of uri, select, and arg: + * <pre> + * <del uri="content://contacts/people" select="name=? and addr=?" + * arg1 = "foo feebe" arg2 ="Tx"/></pre> + * <br/> + * Use first row's uri to insert into another table, + * content://contacts/people/1/phones: + * <pre> + * <row uri="content://contacts/people"> + * <col column = "name" value = "foo feebe"/> + * <col column = "addr" value = "Tx"/> + * <row postfix="phones"> + * <col column="number" value="512-514-6535"/> + * </row> + * <row postfix="phones"> + * <col column="cell" value="512-514-6535"/> + * </row> + * </row></pre> + * <br/> + * Insert multiple rows in to same table and same attributes: + * <pre> + * <row uri="content://contacts/people" > + * <row> + * <col column= "name" value = "foo feebe"/> + * <col column= "addr" value = "Tx"/> + * </row> + * <row> + * </row> + * </row></pre> * - * use first row's uri to insert into another table - * content://contacts/people/1/phones - * <row uri="content://contacts/people"> - * <col column = "name" value = "foo feebe"/> - * <col column = "addr" value = "Tx"/> - * <row postfix="phones"> - * <col column="number" value="512-514-6535"/> - * </row> - * <row postfix="phones"> - * <col column="cell" value="512-514-6535"/> - * </row> - * </row> - * - * insert multiple rows in to same table and same attributes: - * <row uri="content://contacts/people" > - * <row> - * <col column= "name" value = "foo feebe"/> - * <col column= "addr" value = "Tx"/> - * </row> - * <row> - * </row> - * </row> - * * @hide */ public class DefaultDataHandler implements ContentInsertHandler { diff --git a/core/java/android/content/DialogInterface.java b/core/java/android/content/DialogInterface.java index fc94aa6..4afa294 100644 --- a/core/java/android/content/DialogInterface.java +++ b/core/java/android/content/DialogInterface.java @@ -22,10 +22,39 @@ import android.view.KeyEvent; * */ public interface DialogInterface { - public static final int BUTTON1 = -1; - public static final int BUTTON2 = -2; - public static final int BUTTON3 = -3; + /** + * The identifier for the positive button. + */ + public static final int BUTTON_POSITIVE = -1; + + /** + * The identifier for the negative button. + */ + public static final int BUTTON_NEGATIVE = -2; + /** + * The identifier for the neutral button. + */ + public static final int BUTTON_NEUTRAL = -3; + + /** + * @deprecated Use {@link #BUTTON_POSITIVE} + */ + @Deprecated + public static final int BUTTON1 = BUTTON_POSITIVE; + + /** + * @deprecated Use {@link #BUTTON_NEGATIVE} + */ + @Deprecated + public static final int BUTTON2 = BUTTON_NEGATIVE; + + /** + * @deprecated Use {@link #BUTTON_NEUTRAL} + */ + @Deprecated + public static final int BUTTON3 = BUTTON_NEUTRAL; + public void cancel(); public void dismiss(); @@ -71,9 +100,11 @@ public interface DialogInterface { * This method will be invoked when a button in the dialog is clicked. * * @param dialog The dialog that received the click. - * @param which The button that was clicked, i.e. BUTTON1 or BUTTON2 or - * the position of the item clicked. + * @param which The button that was clicked (e.g. + * {@link DialogInterface#BUTTON1}) or the position + * of the item clicked. */ + /* TODO: Change to use BUTTON_POSITIVE after API council */ public void onClick(DialogInterface dialog, int which); } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index c76158c..4a92b4c 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -33,7 +33,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; -import android.util.TypedValue; import com.android.internal.util.XmlUtils; import java.io.IOException; @@ -542,23 +541,8 @@ import java.util.Set; * <h3>Flags</h3> * * <p>These are the possible flags that can be used in the Intent via - * {@link #setFlags} and {@link #addFlags}. - * - * <ul> - * <li> {@link #FLAG_GRANT_READ_URI_PERMISSION} - * <li> {@link #FLAG_GRANT_WRITE_URI_PERMISSION} - * <li> {@link #FLAG_FROM_BACKGROUND} - * <li> {@link #FLAG_DEBUG_LOG_RESOLUTION} - * <li> {@link #FLAG_ACTIVITY_NO_HISTORY} - * <li> {@link #FLAG_ACTIVITY_SINGLE_TOP} - * <li> {@link #FLAG_ACTIVITY_NEW_TASK} - * <li> {@link #FLAG_ACTIVITY_MULTIPLE_TASK} - * <li> {@link #FLAG_ACTIVITY_FORWARD_RESULT} - * <li> {@link #FLAG_ACTIVITY_PREVIOUS_IS_TOP} - * <li> {@link #FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS} - * <li> {@link #FLAG_ACTIVITY_BROUGHT_TO_FRONT} - * <li> {@link #FLAG_RECEIVER_REGISTERED_ONLY} - * </ul> + * {@link #setFlags} and {@link #addFlags}. See {@link #setFlags} for a list + * of all possible flags. */ public class Intent implements Parcelable { // --------------------------------------------------------------------- @@ -573,6 +557,7 @@ public class Intent implements Parcelable { */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_MAIN = "android.intent.action.MAIN"; + /** * Activity Action: Display the data to the user. This is the most common * action performed on data -- it is the generic action you can use on @@ -586,11 +571,13 @@ public class Intent implements Parcelable { */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_VIEW = "android.intent.action.VIEW"; + /** * A synonym for {@link #ACTION_VIEW}, the "standard" action that is * performed on a piece of data. */ public static final String ACTION_DEFAULT = ACTION_VIEW; + /** * Used to indicate that some piece of data should be attached to some other * place. For example, image data could be attached to a contact. It is up @@ -601,6 +588,7 @@ public class Intent implements Parcelable { */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_ATTACH_DATA = "android.intent.action.ATTACH_DATA"; + /** * Activity Action: Provide explicit editable access to the given data. * <p>Input: {@link #getData} is URI of data to be edited. @@ -608,6 +596,7 @@ public class Intent implements Parcelable { */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_EDIT = "android.intent.action.EDIT"; + /** * Activity Action: Pick an existing item, or insert a new item, and then edit it. * <p>Input: {@link #getType} is the desired MIME type of the item to create or edit. @@ -618,6 +607,7 @@ public class Intent implements Parcelable { */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_INSERT_OR_EDIT = "android.intent.action.INSERT_OR_EDIT"; + /** * Activity Action: Pick an item from the data, returning what was selected. * <p>Input: {@link #getData} is URI containing a directory of data @@ -626,13 +616,15 @@ public class Intent implements Parcelable { */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_PICK = "android.intent.action.PICK"; + /** * Activity Action: Creates a shortcut. - * <p>Input: Nothing. + * <p>Input: Nothing.</p> * <p>Output: An Intent representing the shortcut. The intent must contain three * extras: SHORTCUT_INTENT (value: Intent), SHORTCUT_NAME (value: String), * and SHORTCUT_ICON (value: Bitmap) or SHORTCUT_ICON_RESOURCE - * (value: ShortcutIconResource). + * (value: ShortcutIconResource).</p> + * * @see #EXTRA_SHORTCUT_INTENT * @see #EXTRA_SHORTCUT_NAME * @see #EXTRA_SHORTCUT_ICON @@ -670,10 +662,12 @@ public class Intent implements Parcelable { "android.intent.extra.shortcut.ICON_RESOURCE"; /** - * Represents a shortcut icon resource. + * Represents a shortcut/live folder icon resource. * * @see Intent#ACTION_CREATE_SHORTCUT * @see Intent#EXTRA_SHORTCUT_ICON_RESOURCE + * @see android.provider.LiveFolders#ACTION_CREATE_LIVE_FOLDER + * @see android.provider.LiveFolders#EXTRA_LIVE_FOLDER_ICON */ public static class ShortcutIconResource implements Parcelable { /** @@ -972,10 +966,13 @@ public class Intent implements Parcelable { public static final String ACTION_SEARCH = "android.intent.action.SEARCH"; /** * Activity Action: Perform a web search. - * <p>Input: {@link #getData} is URI of data. If it is a url - * starts with http or https, the site will be opened. If it is plain text, - * Google search will be applied. - * <p>Output: nothing. + * <p> + * Input: {@link android.app.SearchManager#QUERY + * getStringExtra(SearchManager.QUERY)} is the text to search for. If it is + * a url starts with http or https, the site will be opened. If it is plain + * text, Google search will be applied. + * <p> + * Output: nothing. */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_WEB_SEARCH = "android.intent.action.WEB_SEARCH"; @@ -1027,7 +1024,7 @@ public class Intent implements Parcelable { */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_VOICE_COMMAND = "android.intent.action.VOICE_COMMAND"; - + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Standard intent broadcast actions (see action variable). @@ -1318,6 +1315,14 @@ public class Intent implements Parcelable { @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_GTALK_SERVICE_DISCONNECTED = "android.intent.action.GTALK_DISCONNECTED"; + + /** + * Broadcast Action: An input method has been changed. + * {@hide pending API Council approval} + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_INPUT_METHOD_CHANGED = + "android.intent.action.INPUT_METHOD_CHANGED"; /** * <p>Broadcast Action: The user has switched the phone into or out of Airplane Mode. One or @@ -1644,6 +1649,15 @@ public class Intent implements Parcelable { */ public static final String EXTRA_ALARM_COUNT = "android.intent.extra.ALARM_COUNT"; + /** + * Used as an int extra field in {@link android.content.Intent#ACTION_VOICE_COMMAND} + * intents to request which audio route the voice command should prefer. + * The value should be a route from {@link android.media.AudioManager}, for + * example ROUTE_BLUETOOTH_SCO. Providing this value is optional. + * {@hide pending API Council approval} + */ + public static final String EXTRA_AUDIO_ROUTE = "android.intent.extra.AUDIO_ROUTE"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Intent flags (see mFlags variable). @@ -1671,7 +1685,10 @@ public class Intent implements Parcelable { public static final int FLAG_DEBUG_LOG_RESOLUTION = 0x00000008; /** - * If set, the new activity is not kept in the history stack. + * If set, the new activity is not kept in the history stack. As soon as + * the user navigates away from it, the activity is finished. This may also + * be set with the {@link android.R.styleable#AndroidManifestActivity_noHistory + * noHistory} attribute. */ public static final int FLAG_ACTIVITY_NO_HISTORY = 0x40000000; /** @@ -1794,9 +1811,33 @@ public class Intent implements Parcelable { */ public static final int FLAG_ACTIVITY_RESET_TASK_IF_NEEDED = 0x00200000; /** - * If set, this activity is being launched from history (longpress home key). + * This flag is not normally set by application code, but set for you by + * the system if this activity is being launched from history + * (longpress home key). */ public static final int FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY = 0x00100000; + /** + * If set, this marks a point in the task's activity stack that should + * be cleared when the task is reset. That is, the next time the task + * is broad to the foreground with + * {@link #FLAG_ACTIVITY_RESET_TASK_IF_NEEDED} (typically as a result of + * the user re-launching it from home), this activity and all on top of + * it will be finished so that the user does not return to them, but + * instead returns to whatever activity preceeded it. + * + * <p>This is useful for cases where you have a logical break in your + * application. For example, an e-mail application may have a command + * to view an attachment, which launches an image view activity to + * display it. This activity should be part of the e-mail application's + * task, since it is a part of the task the user is involved in. However, + * if the user leaves that task, and later selects the e-mail app from + * home, we may like them to return to the conversation they were + * viewing, not the picture attachment, since that is confusing. By + * setting this flag when launching the image viewer, that viewer and + * any activities it starts will be removed the next time the user returns + * to mail. + */ + public static final int FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET = 0x00080000; /** * If set, when sending a broadcast only registered receivers will be @@ -3725,6 +3766,30 @@ public class Intent implements Parcelable { } /** + * Completely replace the extras in the Intent with the extras in the + * given Intent. + * + * @param src The exact extras contained in this Intent are copied + * into the target intent, replacing any that were previously there. + */ + public Intent replaceExtras(Intent src) { + mExtras = src.mExtras != null ? new Bundle(src.mExtras) : null; + return this; + } + + /** + * Completely replace the extras in the Intent with the given Bundle of + * extras. + * + * @param extras The new set of extras in the Intent, or null to erase + * all extras. + */ + public Intent replaceExtras(Bundle extras) { + mExtras = extras != null ? new Bundle(extras) : null; + return this; + } + + /** * Remove extended data from the intent. * * @see #putExtra @@ -3762,14 +3827,17 @@ public class Intent implements Parcelable { * @see #FLAG_GRANT_WRITE_URI_PERMISSION * @see #FLAG_DEBUG_LOG_RESOLUTION * @see #FLAG_FROM_BACKGROUND - * @see #FLAG_ACTIVITY_RESET_TASK_IF_NEEDED * @see #FLAG_ACTIVITY_BROUGHT_TO_FRONT + * @see #FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET * @see #FLAG_ACTIVITY_CLEAR_TOP * @see #FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS * @see #FLAG_ACTIVITY_FORWARD_RESULT + * @see #FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY * @see #FLAG_ACTIVITY_MULTIPLE_TASK * @see #FLAG_ACTIVITY_NEW_TASK * @see #FLAG_ACTIVITY_NO_HISTORY + * @see #FLAG_ACTIVITY_PREVIOUS_IS_TOP + * @see #FLAG_ACTIVITY_RESET_TASK_IF_NEEDED * @see #FLAG_ACTIVITY_SINGLE_TOP * @see #FLAG_RECEIVER_REGISTERED_ONLY */ @@ -4334,7 +4402,11 @@ public class Intent implements Parcelable { XmlUtils.skipCurrentTag(parser); } else if (nodeName.equals("extra")) { - parseExtra(resources, intent, parser, attrs); + if (intent.mExtras == null) { + intent.mExtras = new Bundle(); + } + resources.parseBundleExtra("extra", attrs, intent.mExtras); + XmlUtils.skipCurrentTag(parser); } else { XmlUtils.skipCurrentTag(parser); @@ -4343,49 +4415,4 @@ public class Intent implements Parcelable { return intent; } - - private static void parseExtra(Resources resources, Intent intent, XmlPullParser parser, - AttributeSet attrs) throws XmlPullParserException, IOException { - TypedArray sa = resources.obtainAttributes(attrs, - com.android.internal.R.styleable.IntentExtra); - - String name = sa.getString( - com.android.internal.R.styleable.IntentExtra_name); - if (name == null) { - sa.recycle(); - throw new RuntimeException( - "<extra> requires an android:name attribute at " - + parser.getPositionDescription()); - } - - TypedValue v = sa.peekValue( - com.android.internal.R.styleable.IntentExtra_value); - if (v != null) { - if (v.type == TypedValue.TYPE_STRING) { - CharSequence cs = v.coerceToString(); - intent.putExtra(name, cs != null ? cs.toString() : null); - } else if (v.type == TypedValue.TYPE_INT_BOOLEAN) { - intent.putExtra(name, v.data != 0); - } else if (v.type >= TypedValue.TYPE_FIRST_INT - && v.type <= TypedValue.TYPE_LAST_INT) { - intent.putExtra(name, v.data); - } else if (v.type == TypedValue.TYPE_FLOAT) { - intent.putExtra(name, v.getFloat()); - } else { - sa.recycle(); - throw new RuntimeException( - "<extra> only supports string, integer, float, color, and boolean at " - + parser.getPositionDescription()); - } - } else { - sa.recycle(); - throw new RuntimeException( - "<extra> requires an android:value or android:resource attribute at " - + parser.getPositionDescription()); - } - - sa.recycle(); - - XmlUtils.skipCurrentTag(parser); - } } diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java index 2bf84e7..6bc3774 100644 --- a/core/java/android/content/SyncManager.java +++ b/core/java/android/content/SyncManager.java @@ -50,14 +50,14 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.SystemProperties; -import android.pim.DateUtils; -import android.pim.Time; import android.preference.Preference; import android.preference.PreferenceGroup; import android.provider.Sync; import android.provider.Settings; import android.provider.Sync.History; import android.text.TextUtils; +import android.text.format.DateUtils; +import android.text.format.Time; import android.util.Config; import android.util.EventLog; import android.util.Log; @@ -1484,7 +1484,8 @@ class SyncManager { // skip the sync if it isn't a force and the settings are off for this provider final boolean force = syncOperation.extras.getBoolean( ContentResolver.SYNC_EXTRAS_FORCE, false); - if (!force && (!syncSettings.getListenForNetworkTickles() + if (!force && (!syncSettings.getBackgroundData() + || !syncSettings.getListenForNetworkTickles() || !syncSettings.getSyncProviderAutomatically( syncOperation.authority))) { if (isLoggable) { diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index f577d2d..85d877a 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -120,12 +120,19 @@ public class ActivityInfo extends ComponentInfo */ public static final int FLAG_ALLOW_TASK_REPARENTING = 0x0040; /** + * Bit in {@link #flags} indicating that, when the user navigates away + * from an activity, it should be finished. + * Set from the + * {@link android.R.attr#noHistory} attribute. + */ + public static final int FLAG_NO_HISTORY = 0x0080; + /** * Options that have been set in the activity declaration in the * manifest: {@link #FLAG_MULTIPROCESS}, * {@link #FLAG_FINISH_ON_TASK_LAUNCH}, {@link #FLAG_CLEAR_TASK_ON_LAUNCH}, * {@link #FLAG_ALWAYS_RETAIN_TASK_STATE}, * {@link #FLAG_STATE_NOT_NEEDED}, {@link #FLAG_EXCLUDE_FROM_RECENTS}, - * {@link #FLAG_ALLOW_TASK_REPARENTING}. + * {@link #FLAG_ALLOW_TASK_REPARENTING}, {@link #FLAG_NO_HISTORY}. */ public int flags; @@ -247,6 +254,16 @@ public class ActivityInfo extends ComponentInfo */ public int configChanges; + /** + * The desired soft input mode for this activity's main window. + * Set from the {@link android.R.attr#windowSoftInputMode} attribute + * in the activity's manifest. May be any of the same values allowed + * for {@link android.view.WindowManager.LayoutParams#softInputMode + * WindowManager.LayoutParams.softInputMode}. If 0 (unspecified), + * the mode from the theme will be used. + */ + public int softInputMode; + public ActivityInfo() { } @@ -260,6 +277,7 @@ public class ActivityInfo extends ComponentInfo flags = orig.flags; screenOrientation = orig.screenOrientation; configChanges = orig.configChanges; + softInputMode = orig.softInputMode; } /** @@ -280,9 +298,10 @@ public class ActivityInfo extends ComponentInfo + " targetActivity=" + targetActivity); pw.println(prefix + "launchMode=" + launchMode + " flags=0x" + Integer.toHexString(flags) - + " theme=0x" + Integer.toHexString(theme) - + " orien=" + screenOrientation - + " configChanges=0x" + Integer.toHexString(configChanges)); + + " theme=0x" + Integer.toHexString(theme)); + pw.println(prefix + "screenOrientation=" + screenOrientation + + " configChanges=0x" + Integer.toHexString(configChanges) + + " softInputMode=0x" + Integer.toHexString(softInputMode)); super.dumpBack(pw, prefix); } @@ -306,6 +325,7 @@ public class ActivityInfo extends ComponentInfo dest.writeInt(flags); dest.writeInt(screenOrientation); dest.writeInt(configChanges); + dest.writeInt(softInputMode); } public static final Parcelable.Creator<ActivityInfo> CREATOR @@ -328,5 +348,6 @@ public class ActivityInfo extends ComponentInfo flags = source.readInt(); screenOrientation = source.readInt(); configChanges = source.readInt(); + softInputMode = source.readInt(); } } diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 22d01dc..8d727ed 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -112,6 +112,13 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * android:allowClearUserData} of the <application> tag. */ public static final int FLAG_ALLOW_CLEAR_USER_DATA = 1<<6; + + + /** + * Value for {@link #flags}: default value for the corresponding ActivityInfo flag. + * {@hide} + */ + public static final int FLAG_UPDATED_SYSTEM_APP = 1<<7; /** * Flags associated with the application. Any combination of @@ -195,7 +202,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { sb = ab.packageName; } - return sCollator.compare(sa, sb); + return sCollator.compare(sa.toString(), sb.toString()); } private final Collator sCollator = Collator.getInstance(); diff --git a/core/java/android/content/pm/ConfigurationInfo.java b/core/java/android/content/pm/ConfigurationInfo.java new file mode 100755 index 0000000..9115225 --- /dev/null +++ b/core/java/android/content/pm/ConfigurationInfo.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import android.content.res.Configuration; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Information you can retrieve about hardware configuration preferences + * declared by an application. This corresponds to information collected from the + * AndroidManifest.xml's <uses-configuration> tags. + */ +public class ConfigurationInfo implements Parcelable { + /** + * The kind of touch screen attached to the device. + * One of: {@link android.content.res.Configuration#TOUCHSCREEN_NOTOUCH}, + * {@link android.content.res.Configuration#TOUCHSCREEN_STYLUS}, + * {@link android.content.res.Configuration#TOUCHSCREEN_FINGER}. + */ + public int reqTouchScreen; + + /** + * Application's input method preference. + * One of: {@link android.content.res.Configuration#KEYBOARD_UNDEFINED}, + * {@link android.content.res.Configuration#KEYBOARD_NOKEYS}, + * {@link android.content.res.Configuration#KEYBOARD_QWERTY}, + * {@link android.content.res.Configuration#KEYBOARD_12KEY} + */ + public int reqKeyboardType; + + /** + * A flag indicating whether any keyboard is available. + * one of: {@link android.content.res.Configuration#NAVIGATION_UNDEFINED}, + * {@link android.content.res.Configuration#NAVIGATION_DPAD}, + * {@link android.content.res.Configuration#NAVIGATION_TRACKBALL}, + * {@link android.content.res.Configuration#NAVIGATION_WHEEL} + */ + public int reqNavigation; + + /** + * Value for {@link #reqInputFeatures}: if set, indicates that the application + * requires a hard keyboard + */ + public static final int INPUT_FEATURE_HARD_KEYBOARD = 0x00000001; + + /** + * Value for {@link #reqInputFeatures}: if set, indicates that the application + * requires a hard keyboard + */ + public static final int INPUT_FEATURE_FIVE_WAY_NAV = 0x00000002; + + /** + * Flags associated with the application. Any combination of + * {@link #INPUT_FEATURE_HARD_KEYBOARD}, + * {@link #INPUT_FEATURE_FIVE_WAY_NAV} + */ + public int reqInputFeatures = 0; + + public ConfigurationInfo() { + } + + public ConfigurationInfo(ConfigurationInfo orig) { + reqTouchScreen = orig.reqTouchScreen; + reqKeyboardType = orig.reqKeyboardType; + reqNavigation = orig.reqNavigation; + reqInputFeatures = orig.reqInputFeatures; + } + + public String toString() { + return "ApplicationHardwarePreferences{" + + Integer.toHexString(System.identityHashCode(this)) + + ", touchscreen = " + reqTouchScreen + "}" + + ", inputMethod = " + reqKeyboardType + "}" + + ", navigation = " + reqNavigation + "}" + + ", reqInputFeatures = " + reqInputFeatures + "}"; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + dest.writeInt(reqTouchScreen); + dest.writeInt(reqKeyboardType); + dest.writeInt(reqNavigation); + dest.writeInt(reqInputFeatures); + } + + public static final Creator<ConfigurationInfo> CREATOR = + new Creator<ConfigurationInfo>() { + public ConfigurationInfo createFromParcel(Parcel source) { + return new ConfigurationInfo(source); + } + public ConfigurationInfo[] newArray(int size) { + return new ConfigurationInfo[size]; + } + }; + + private ConfigurationInfo(Parcel source) { + reqTouchScreen = source.readInt(); + reqKeyboardType = source.readInt(); + reqNavigation = source.readInt(); + reqInputFeatures = source.readInt(); + } +} diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index c79655d..fdb2a2f 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -76,6 +76,8 @@ interface IPackageManager { String getNameForUid(int uid); + int getUidForSharedUser(String sharedUserName); + ResolveInfo resolveIntent(in Intent intent, String resolvedType, int flags); List<ResolveInfo> queryIntentActivities(in Intent intent, diff --git a/core/java/android/content/pm/InstrumentationInfo.java b/core/java/android/content/pm/InstrumentationInfo.java index e6745da..30ca002 100644 --- a/core/java/android/content/pm/InstrumentationInfo.java +++ b/core/java/android/content/pm/InstrumentationInfo.java @@ -73,6 +73,7 @@ public class InstrumentationInfo extends PackageItemInfo implements Parcelable { dest.writeString(publicSourceDir); dest.writeString(dataDir); dest.writeInt((handleProfiling == false) ? 0 : 1); + dest.writeInt((functionalTest == false) ? 0 : 1); } public static final Parcelable.Creator<InstrumentationInfo> CREATOR @@ -92,5 +93,6 @@ public class InstrumentationInfo extends PackageItemInfo implements Parcelable { publicSourceDir = source.readString(); dataDir = source.readString(); handleProfiling = source.readInt() != 0; + functionalTest = source.readInt() != 0; } } diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java index 7d694c7..994afc8 100644 --- a/core/java/android/content/pm/PackageInfo.java +++ b/core/java/android/content/pm/PackageInfo.java @@ -103,6 +103,15 @@ public class PackageInfo implements Parcelable { * in if the flag {@link PackageManager#GET_SIGNATURES} was set. */ public Signature[] signatures; + + /** + * Application specified preferred configuration + * {@link android.R.styleable#AndroidManifestUsesConfiguration + * <uses-configuration>} tags included under <manifest>, + * or null if there were none. This is only filled in if the flag + * {@link PackageManager#GET_CONFIGURATIONS} was set. + */ + public ConfigurationInfo[] configPreferences; public PackageInfo() { } @@ -136,6 +145,7 @@ public class PackageInfo implements Parcelable { dest.writeTypedArray(permissions, parcelableFlags); dest.writeStringArray(requestedPermissions); dest.writeTypedArray(signatures, parcelableFlags); + dest.writeTypedArray(configPreferences, parcelableFlags); } public static final Parcelable.Creator<PackageInfo> CREATOR @@ -166,5 +176,6 @@ public class PackageInfo implements Parcelable { permissions = source.createTypedArray(PermissionInfo.CREATOR); requestedPermissions = source.createStringArray(); signatures = source.createTypedArray(Signature.CREATOR); + configPreferences = source.createTypedArray(ConfigurationInfo.CREATOR); } } diff --git a/core/java/android/content/pm/PackageItemInfo.java b/core/java/android/content/pm/PackageItemInfo.java index 406a3eb..46e7ca4 100644 --- a/core/java/android/content/pm/PackageItemInfo.java +++ b/core/java/android/content/pm/PackageItemInfo.java @@ -90,7 +90,10 @@ public class PackageItemInfo { return label; } } - return name; + if(name != null) { + return name; + } + return packageName; } /** @@ -179,7 +182,7 @@ public class PackageItemInfo { if (sa == null) sa = aa.name; CharSequence sb = ab.loadLabel(mPM); if (sb == null) sb = ab.name; - return sCollator.compare(sa, sb); + return sCollator.compare(sa.toString(), sb.toString()); } private final Collator sCollator = Collator.getInstance(); diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index db00a9a..a544550 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -148,6 +148,21 @@ public abstract class PackageManager { * {@link PackageInfo#permissions}. */ public static final int GET_PERMISSIONS = 0x00001000; + + /** + * Flag parameter to retrieve all applications(even uninstalled ones) with data directories. + * This state could have resulted if applications have been deleted with flag + * DONT_DELETE_DATA + * with a possibility of being replaced or reinstalled in future + */ + public static final int GET_UNINSTALLED_PACKAGES = 0x00002000; + + /** + * {@link PackageInfo} flag: return information about + * hardware preferences + * {@link PackageInfo#configPreferences} + */ + public static final int GET_CONFIGURATIONS = 0x00004000; /** * Permission check result: this is returned by {@link #checkPermission} @@ -427,16 +442,38 @@ public abstract class PackageManager { * * @param packageName The full name (i.e. com.google.apps.contacts) of the * desired package. - * @param flags Optional flags to control what information is returned. If - * 0, none of the optional information is returned. - * - * @return Returns a PackageInfo containing information about the package. - * + + * @param flags Additional option flags. Use any combination of + * {@link #GET_ACTIVITIES}, + * {@link #GET_GIDS}, + * {@link #GET_CONFIGURATIONS}, + * {@link #GET_INSTRUMENTATION}, + * {@link #GET_PERMISSIONS}, + * {@link #GET_PROVIDERS}, + * {@link #GET_RECEIVERS}, + * {@link #GET_SERVICES}, + * {@link #GET_SIGNATURES}, + * {@link #GET_UNINSTALLED_PACKAGES} to modify the data returned. + * + * @return Returns a PackageInfo object containing information about the package. + * If flag GET_UNINSTALLED_PACKAGES is set and if the package is not + * found in the list of installed applications, the package information is + * retrieved from the list of uninstalled applications(which includes + * installed applications as well as applications + * with data directory ie applications which had been + * deleted with DONT_DELTE_DATA flag set). + * * @see #GET_ACTIVITIES + * @see #GET_GIDS + * @see #GET_CONFIGURATIONS + * @see #GET_INSTRUMENTATION + * @see #GET_PERMISSIONS + * @see #GET_PROVIDERS * @see #GET_RECEIVERS * @see #GET_SERVICES - * @see #GET_INSTRUMENTATION * @see #GET_SIGNATURES + * @see #GET_UNINSTALLED_PACKAGES + * */ public abstract PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException; @@ -530,10 +567,23 @@ public abstract class PackageManager { * * @param packageName The full name (i.e. com.google.apps.contacts) of an * application. - * @param flags Additional option flags. Currently should always be 0. - * - * @return {@link ApplicationInfo} containing information about the - * application. + * @param flags Additional option flags. Use any combination of + * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, + * {@link #GET_UNINSTALLED_PACKAGES} to modify the data returned. + * + * @return {@link ApplicationInfo} Returns ApplicationInfo object containing + * information about the package. + * If flag GET_UNINSTALLED_PACKAGES is set and if the package is not + * found in the list of installed applications, + * the application information is retrieved from the + * list of uninstalled applications(which includes + * installed applications as well as applications + * with data directory ie applications which had been + * deleted with DONT_DELTE_DATA flag set). + * + * @see #GET_META_DATA + * @see #GET_SHARED_LIBRARY_FILES + * @see #GET_UNINSTALLED_PACKAGES */ public abstract ApplicationInfo getApplicationInfo(String packageName, int flags) throws NameNotFoundException; @@ -548,11 +598,15 @@ public abstract class PackageManager { * @param className The full name (i.e. * com.google.apps.contacts.ContactsList) of an Activity * class. - * @param flags Additional option flags. Usually 0. + * @param flags Additional option flags. Use any combination of + * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, + * to modify the data (in ApplicationInfo) returned. * * @return {@link ActivityInfo} containing information about the activity. * * @see #GET_INTENT_FILTERS + * @see #GET_META_DATA + * @see #GET_SHARED_LIBRARY_FILES */ public abstract ActivityInfo getActivityInfo(ComponentName className, int flags) throws NameNotFoundException; @@ -567,11 +621,15 @@ public abstract class PackageManager { * @param className The full name (i.e. * com.google.apps.contacts.CalendarAlarm) of a Receiver * class. - * @param flags Additional option flags. Usually 0. + * @param flags Additional option flags. Use any combination of + * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, + * to modify the data returned. * * @return {@link ActivityInfo} containing information about the receiver. * * @see #GET_INTENT_FILTERS + * @see #GET_META_DATA + * @see #GET_SHARED_LIBRARY_FILES */ public abstract ActivityInfo getReceiverInfo(ComponentName className, int flags) throws NameNotFoundException; @@ -586,9 +644,14 @@ public abstract class PackageManager { * @param className The full name (i.e. * com.google.apps.media.BackgroundPlayback) of a Service * class. - * @param flags Additional option flags. Currently should always be 0. + * @param flags Additional option flags. Use any combination of + * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, + * to modify the data returned. * * @return ServiceInfo containing information about the service. + * + * @see #GET_META_DATA + * @see #GET_SHARED_LIBRARY_FILES */ public abstract ServiceInfo getServiceInfo(ComponentName className, int flags) throws NameNotFoundException; @@ -597,18 +660,36 @@ public abstract class PackageManager { * Return a List of all packages that are installed * on the device. * - * @param flags Optional flags to control what information is returned. If - * 0, none of the optional information is returned. + * @param flags Additional option flags. Use any combination of + * {@link #GET_ACTIVITIES}, + * {@link #GET_GIDS}, + * {@link #GET_CONFIGURATIONS}, + * {@link #GET_INSTRUMENTATION}, + * {@link #GET_PERMISSIONS}, + * {@link #GET_PROVIDERS}, + * {@link #GET_RECEIVERS}, + * {@link #GET_SERVICES}, + * {@link #GET_SIGNATURES}, + * {@link #GET_UNINSTALLED_PACKAGES} to modify the data returned. * * @return A List of PackageInfo objects, one for each package that is * installed on the device. In the unlikely case of there being no - * installed packages, an empty list is returned. + * installed packages, an empty list is returned. + * If flag GET_UNINSTALLED_PACKAGES is set, a list of all + * applications including those deleted with DONT_DELETE_DATA + * (partially installed apps with data directory) will be returned. * * @see #GET_ACTIVITIES + * @see #GET_GIDS + * @see #GET_CONFIGURATIONS + * @see #GET_INSTRUMENTATION + * @see #GET_PERMISSIONS + * @see #GET_PROVIDERS * @see #GET_RECEIVERS * @see #GET_SERVICES - * @see #GET_INSTRUMENTATION * @see #GET_SIGNATURES + * @see #GET_UNINSTALLED_PACKAGES + * */ public abstract List<PackageInfo> getInstalledPackages(int flags); @@ -731,16 +812,42 @@ public abstract class PackageManager { * user id is not currently assigned. */ public abstract String getNameForUid(int uid); + + /** + * Return the user id associated with a shared user name. Multiple + * applications can specify a shared user name in their manifest and thus + * end up using a common uid. This might be used for new applications + * that use an existing shared user name and need to know the uid of the + * shared user. + * + * @param sharedUserName The shared user name whose uid is to be retrieved. + * @return Returns the uid associated with the shared user, or NameNotFoundException + * if the shared user name is not being used by any installed packages + * @hide + */ + public abstract int getUidForSharedUser(String sharedUserName) + throws NameNotFoundException; /** * Return a List of all application packages that are installed on the - * device. - * - * @param flags Additional option flags. Currently should always be 0. + * device. If flag GET_UNINSTALLED_PACKAGES has been set, a list of all + * applications including those deleted with DONT_DELETE_DATA(partially + * installed apps with data directory) will be returned. + * + * @param flags Additional option flags. Use any combination of + * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, + * {link #GET_UNINSTALLED_PACKAGES} to modify the data returned. * * @return A List of ApplicationInfo objects, one for each application that * is installed on the device. In the unlikely case of there being - * no installed applications, an empty list is returned. + * no installed applications, an empty list is returned. + * If flag GET_UNINSTALLED_PACKAGES is set, a list of all + * applications including those deleted with DONT_DELETE_DATA + * (partially installed apps with data directory) will be returned. + * + * @see #GET_META_DATA + * @see #GET_SHARED_LIBRARY_FILES + * @see #GET_UNINSTALLED_PACKAGES */ public abstract List<ApplicationInfo> getInstalledApplications(int flags); @@ -1139,17 +1246,30 @@ public abstract class PackageManager { * in a package archive file * * @param archiveFilePath The path to the archive file - * @param flags Optional flags to control what information is returned. If - * 0, none of the optional information is returned. + * @param flags Additional option flags. Use any combination of + * {@link #GET_ACTIVITIES}, + * {@link #GET_GIDS}, + * {@link #GET_CONFIGURATIONS}, + * {@link #GET_INSTRUMENTATION}, + * {@link #GET_PERMISSIONS}, + * {@link #GET_PROVIDERS}, + * {@link #GET_RECEIVERS}, + * {@link #GET_SERVICES}, + * {@link #GET_SIGNATURES}, to modify the data returned. * * @return Returns the information about the package. Returns * null if the package could not be successfully parsed. * * @see #GET_ACTIVITIES + * @see #GET_GIDS + * @see #GET_CONFIGURATIONS + * @see #GET_INSTRUMENTATION + * @see #GET_PERMISSIONS + * @see #GET_PROVIDERS * @see #GET_RECEIVERS * @see #GET_SERVICES - * @see #GET_INSTRUMENTATION * @see #GET_SIGNATURES + * */ public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags) { PackageParser packageParser = new PackageParser(archiveFilePath); @@ -1314,16 +1434,28 @@ public abstract class PackageManager { * first package on the list is the most preferred, the last is the * least preferred. * - * @param flags Optional flags to control what information is returned. If - * 0, none of the optional information is returned. + * @param flags Additional option flags. Use any combination of + * {@link #GET_ACTIVITIES}, + * {@link #GET_GIDS}, + * {@link #GET_CONFIGURATIONS}, + * {@link #GET_INSTRUMENTATION}, + * {@link #GET_PERMISSIONS}, + * {@link #GET_PROVIDERS}, + * {@link #GET_RECEIVERS}, + * {@link #GET_SERVICES}, + * {@link #GET_SIGNATURES}, to modify the data returned. * * @return Returns a list of PackageInfo objects describing each * preferred application, in order of preference. * * @see #GET_ACTIVITIES + * @see #GET_GIDS + * @see #GET_CONFIGURATIONS + * @see #GET_INSTRUMENTATION + * @see #GET_PERMISSIONS + * @see #GET_PROVIDERS * @see #GET_RECEIVERS * @see #GET_SERVICES - * @see #GET_INSTRUMENTATION * @see #GET_SIGNATURES */ public abstract List<PackageInfo> getPreferredPackages(int flags); diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 5a90261..e08f1d1 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -23,6 +23,7 @@ import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; import android.content.res.AssetManager; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; @@ -104,6 +105,15 @@ public class PackageParser { if ((flags&PackageManager.GET_GIDS) != 0) { pi.gids = gids; } + if ((flags&PackageManager.GET_CONFIGURATIONS) != 0) { + int N = p.configPreferences.size(); + if (N > 0) { + pi.configPreferences = new ConfigurationInfo[N]; + for (int i=0; i<N; i++) { + pi.configPreferences[i] = p.configPreferences.get(i); + } + } + } if ((flags&PackageManager.GET_ACTIVITIES) != 0) { int N = p.activities.size(); if (N > 0) { @@ -245,18 +255,24 @@ public class PackageParser { XmlResourceParser parser = null; AssetManager assmgr = null; + boolean assetError = true; try { assmgr = new AssetManager(); - assmgr.addAssetPath(mArchiveSourcePath); - parser = assmgr.openXmlResourceParser("AndroidManifest.xml"); + if(assmgr.addAssetPath(mArchiveSourcePath) != 0) { + parser = assmgr.openXmlResourceParser("AndroidManifest.xml"); + assetError = false; + } else { + Log.w(TAG, "Failed adding asset path:"+mArchiveSourcePath); + } } catch (Exception e) { - if (assmgr != null) assmgr.close(); Log.w(TAG, "Unable to read AndroidManifest.xml of " + mArchiveSourcePath, e); + } + if(assetError) { + if (assmgr != null) assmgr.close(); mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST; return null; } - String[] errorText = new String[1]; Package pkg = null; Exception errorException = null; @@ -626,7 +642,35 @@ public class PackageParser { XmlUtils.skipCurrentTag(parser); - } else if (tagName.equals("uses-sdk")) { + } else if (tagName.equals("uses-configuration")) { + ConfigurationInfo cPref = new ConfigurationInfo(); + sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestUsesConfiguration); + cPref.reqTouchScreen = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqTouchScreen, + Configuration.TOUCHSCREEN_UNDEFINED); + cPref.reqKeyboardType = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqKeyboardType, + Configuration.KEYBOARD_UNDEFINED); + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqHardKeyboard, + false)) { + cPref.reqInputFeatures |= ConfigurationInfo.INPUT_FEATURE_HARD_KEYBOARD; + } + cPref.reqNavigation = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqNavigation, + Configuration.NAVIGATION_UNDEFINED); + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqFiveWayNav, + false)) { + cPref.reqInputFeatures |= ConfigurationInfo.INPUT_FEATURE_FIVE_WAY_NAV; + } + sa.recycle(); + pkg.configPreferences.add(cPref); + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals("uses-sdk")) { if (mSdkVersion > 0) { sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.AndroidManifestUsesSdk); @@ -650,6 +694,10 @@ public class PackageParser { if (parseInstrumentation(pkg, res, parser, attrs, outError) == null) { return null; } + } else if (tagName.equals("eat-comment")) { + // Just skip this tag + XmlUtils.skipCurrentTag(parser); + continue; } else if (RIGID_PARSER) { outError[0] = "Bad element under <manifest>: " + parser.getName(); @@ -1038,11 +1086,6 @@ public class PackageParser { if (outError[0] != null) { mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; return false; - } else if (ai.processName != null && !ai.processName.equals(ai.packageName) - && ai.className != null) { - Log.w(TAG, "In package " + ai.packageName - + " <application> specifies both a name and a process; ignoring the process"); - ai.processName = null; } final int innerDepth = parser.getDepth(); @@ -1258,6 +1301,12 @@ public class PackageParser { } if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_noHistory, + false)) { + a.info.flags |= ActivityInfo.FLAG_NO_HISTORY; + } + + if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestActivity_alwaysRetainTaskState, false)) { a.info.flags |= ActivityInfo.FLAG_ALWAYS_RETAIN_TASK_STATE; @@ -1291,6 +1340,9 @@ public class PackageParser { a.info.configChanges = sa.getInt( com.android.internal.R.styleable.AndroidManifestActivity_configChanges, 0); + a.info.softInputMode = sa.getInt( + com.android.internal.R.styleable.AndroidManifestActivity_windowSoftInputMode, + 0); } else { a.info.launchMode = ActivityInfo.LAUNCH_MULTIPLE; a.info.configChanges = 0; @@ -2002,6 +2054,12 @@ public class PackageParser { // Additional data supplied by callers. public Object mExtras; + + /* + * Applications hardware preferences + */ + public final ArrayList<ConfigurationInfo> configPreferences = + new ArrayList<ConfigurationInfo>(); public Package(String _name) { packageName = _name; @@ -2031,7 +2089,7 @@ public class PackageParser { metaData = clone.metaData; } } - + public final static class Permission extends Component<IntentInfo> { public final PermissionInfo info; public boolean tree; diff --git a/core/java/android/content/res/ColorStateList.java b/core/java/android/content/res/ColorStateList.java index ee88c89..4b1e678 100644 --- a/core/java/android/content/res/ColorStateList.java +++ b/core/java/android/content/res/ColorStateList.java @@ -16,6 +16,10 @@ package android.content.res; +import com.google.android.collect.Lists; + +import com.android.internal.util.ArrayUtils; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -30,8 +34,6 @@ import java.io.IOException; import java.lang.ref.WeakReference; import java.util.Arrays; -import com.android.internal.util.ArrayUtils; - /** * * Lets you map {@link android.view.View} state sets to colors. diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index 78a90de..7e4b7ac 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -34,6 +34,12 @@ public final class Configuration implements Parcelable, Comparable<Configuration */ public Locale locale; + /** + * Locale should persist on setting + * @hide pending API council approval + */ + public boolean userSetLocale; + public static final int TOUCHSCREEN_UNDEFINED = 0; public static final int TOUCHSCREEN_NOTOUCH = 1; public static final int TOUCHSCREEN_STYLUS = 2; @@ -60,14 +66,30 @@ public final class Configuration implements Parcelable, Comparable<Configuration public static final int KEYBOARDHIDDEN_UNDEFINED = 0; public static final int KEYBOARDHIDDEN_NO = 1; public static final int KEYBOARDHIDDEN_YES = 2; + /** Constant matching actual resource implementation. {@hide} */ + public static final int KEYBOARDHIDDEN_SOFT = 3; /** - * A flag indicating whether the keyboard has been hidden. This will - * be set on a device with a mechanism to hide the keyboard from the - * user, when that mechanism is closed. + * A flag indicating whether any keyboard is available. Unlike + * {@link #hardKeyboardHidden}, this also takes into account a soft + * keyboard, so if the hard keyboard is hidden but there is soft + * keyboard available, it will be set to NO. Value is one of: + * {@link #KEYBOARDHIDDEN_NO}, {@link #KEYBOARDHIDDEN_YES}. */ public int keyboardHidden; + public static final int HARDKEYBOARDHIDDEN_UNDEFINED = 0; + public static final int HARDKEYBOARDHIDDEN_NO = 1; + public static final int HARDKEYBOARDHIDDEN_YES = 2; + + /** + * A flag indicating whether the hard keyboard has been hidden. This will + * be set on a device with a mechanism to hide the keyboard from the + * user, when that mechanism is closed. One of: + * {@link #HARDKEYBOARDHIDDEN_NO}, {@link #HARDKEYBOARDHIDDEN_YES}. + */ + public int hardKeyboardHidden; + public static final int NAVIGATION_UNDEFINED = 0; public static final int NAVIGATION_NONAV = 1; public static final int NAVIGATION_DPAD = 2; @@ -111,9 +133,11 @@ public final class Configuration implements Parcelable, Comparable<Configuration if (o.locale != null) { locale = (Locale) o.locale.clone(); } + userSetLocale = o.userSetLocale; touchscreen = o.touchscreen; keyboard = o.keyboard; keyboardHidden = o.keyboardHidden; + hardKeyboardHidden = o.hardKeyboardHidden; navigation = o.navigation; orientation = o.orientation; } @@ -122,7 +146,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration return "{ scale=" + fontScale + " imsi=" + mcc + "/" + mnc + " locale=" + locale + " touch=" + touchscreen + " key=" + keyboard + "/" - + keyboardHidden + + keyboardHidden + "/" + hardKeyboardHidden + " nav=" + navigation + " orien=" + orientation + " }"; } @@ -133,9 +157,11 @@ public final class Configuration implements Parcelable, Comparable<Configuration fontScale = 1; mcc = mnc = 0; locale = Locale.getDefault(); + userSetLocale = false; touchscreen = TOUCHSCREEN_UNDEFINED; keyboard = KEYBOARD_UNDEFINED; keyboardHidden = KEYBOARDHIDDEN_UNDEFINED; + hardKeyboardHidden = HARDKEYBOARDHIDDEN_UNDEFINED; navigation = NAVIGATION_UNDEFINED; orientation = ORIENTATION_UNDEFINED; } @@ -173,6 +199,11 @@ public final class Configuration implements Parcelable, Comparable<Configuration locale = delta.locale != null ? (Locale) delta.locale.clone() : null; } + if (delta.userSetLocale && (!userSetLocale || ((changed & ActivityInfo.CONFIG_LOCALE) != 0))) + { + userSetLocale = true; + changed |= ActivityInfo.CONFIG_LOCALE; + } if (delta.touchscreen != TOUCHSCREEN_UNDEFINED && touchscreen != delta.touchscreen) { changed |= ActivityInfo.CONFIG_TOUCHSCREEN; @@ -188,6 +219,11 @@ public final class Configuration implements Parcelable, Comparable<Configuration changed |= ActivityInfo.CONFIG_KEYBOARD_HIDDEN; keyboardHidden = delta.keyboardHidden; } + if (delta.hardKeyboardHidden != HARDKEYBOARDHIDDEN_UNDEFINED + && hardKeyboardHidden != delta.hardKeyboardHidden) { + changed |= ActivityInfo.CONFIG_KEYBOARD_HIDDEN; + hardKeyboardHidden = delta.hardKeyboardHidden; + } if (delta.navigation != NAVIGATION_UNDEFINED && navigation != delta.navigation) { changed |= ActivityInfo.CONFIG_NAVIGATION; @@ -252,6 +288,10 @@ public final class Configuration implements Parcelable, Comparable<Configuration && keyboardHidden != delta.keyboardHidden) { changed |= ActivityInfo.CONFIG_KEYBOARD_HIDDEN; } + if (delta.hardKeyboardHidden != HARDKEYBOARDHIDDEN_UNDEFINED + && hardKeyboardHidden != delta.hardKeyboardHidden) { + changed |= ActivityInfo.CONFIG_KEYBOARD_HIDDEN; + } if (delta.navigation != NAVIGATION_UNDEFINED && navigation != delta.navigation) { changed |= ActivityInfo.CONFIG_NAVIGATION; @@ -298,9 +338,15 @@ public final class Configuration implements Parcelable, Comparable<Configuration dest.writeString(locale.getCountry()); dest.writeString(locale.getVariant()); } + if(userSetLocale) { + dest.writeInt(1); + } else { + dest.writeInt(0); + } dest.writeInt(touchscreen); dest.writeInt(keyboard); dest.writeInt(keyboardHidden); + dest.writeInt(hardKeyboardHidden); dest.writeInt(navigation); dest.writeInt(orientation); } @@ -327,9 +373,11 @@ public final class Configuration implements Parcelable, Comparable<Configuration locale = new Locale(source.readString(), source.readString(), source.readString()); } + userSetLocale = (source.readInt()==1); touchscreen = source.readInt(); keyboard = source.readInt(); keyboardHidden = source.readInt(); + hardKeyboardHidden = source.readInt(); navigation = source.readInt(); orientation = source.readInt(); } @@ -356,6 +404,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration if (n != 0) return n; n = this.keyboardHidden - that.keyboardHidden; if (n != 0) return n; + n = this.hardKeyboardHidden - that.hardKeyboardHidden; + if (n != 0) return n; n = this.navigation - that.navigation; if (n != 0) return n; n = this.orientation - that.orientation; @@ -380,7 +430,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration public int hashCode() { return ((int)this.fontScale) + this.mcc + this.mnc + this.locale.hashCode() + this.touchscreen - + this.keyboard + this.keyboardHidden + this.navigation - + this.orientation; + + this.keyboard + this.keyboardHidden + this.hardKeyboardHidden + + this.navigation + this.orientation; } -}
\ No newline at end of file +} diff --git a/core/java/android/content/res/PluralRules.java b/core/java/android/content/res/PluralRules.java index 2d09ef0..2dce3c1 100644 --- a/core/java/android/content/res/PluralRules.java +++ b/core/java/android/content/res/PluralRules.java @@ -23,7 +23,7 @@ import java.util.Locale; * object has been integrated to android, we should switch to that. For now, yuck-o. */ -abstract class PluralRule { +abstract class PluralRules { static final int QUANTITY_OTHER = 0x0000; static final int QUANTITY_ZERO = 0x0001; @@ -37,7 +37,7 @@ abstract class PluralRule { abstract int quantityForNumber(int n); final int attrForNumber(int n) { - return PluralRule.attrForQuantity(quantityForNumber(n)); + return PluralRules.attrForQuantity(quantityForNumber(n)); } static final int attrForQuantity(int quantity) { @@ -69,7 +69,7 @@ abstract class PluralRule { } } - static final PluralRule ruleForLocale(Locale locale) { + static final PluralRules ruleForLocale(Locale locale) { String lang = locale.getLanguage(); if ("cs".equals(lang)) { if (cs == null) cs = new cs(); @@ -81,8 +81,8 @@ abstract class PluralRule { } } - private static PluralRule cs; - private static class cs extends PluralRule { + private static PluralRules cs; + private static class cs extends PluralRules { int quantityForNumber(int n) { if (n == 1) { return QUANTITY_ONE; @@ -96,8 +96,8 @@ abstract class PluralRule { } } - private static PluralRule en; - private static class en extends PluralRule { + private static PluralRules en; + private static class en extends PluralRules { int quantityForNumber(int n) { if (n == 1) { return QUANTITY_ONE; diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 1014eee..10eced6 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -17,9 +17,16 @@ package android.content.res; +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.Intent; import android.graphics.Movie; import android.graphics.drawable.Drawable; import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; import android.os.SystemProperties; import android.util.AttributeSet; import android.util.DisplayMetrics; @@ -27,6 +34,7 @@ import android.util.Log; import android.util.SparseArray; import android.util.TypedValue; +import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; @@ -70,7 +78,7 @@ public class Resources { /*package*/ final AssetManager mAssets; private final Configuration mConfiguration = new Configuration(); /*package*/ final DisplayMetrics mMetrics = new DisplayMetrics(); - PluralRule mPluralRule; + PluralRules mPluralRule; /** * This exception is thrown by the resource APIs when a requested resource @@ -157,24 +165,24 @@ public class Resources { * possibly styled text information. */ public CharSequence getQuantityText(int id, int quantity) throws NotFoundException { - PluralRule rule = getPluralRule(); + PluralRules rule = getPluralRule(); CharSequence res = mAssets.getResourceBagText(id, rule.attrForNumber(quantity)); if (res != null) { return res; } - res = mAssets.getResourceBagText(id, PluralRule.ID_OTHER); + res = mAssets.getResourceBagText(id, PluralRules.ID_OTHER); if (res != null) { return res; } throw new NotFoundException("Plural resource ID #0x" + Integer.toHexString(id) + " quantity=" + quantity - + " item=" + PluralRule.stringForQuantity(rule.quantityForNumber(quantity))); + + " item=" + PluralRules.stringForQuantity(rule.quantityForNumber(quantity))); } - private PluralRule getPluralRule() { + private PluralRules getPluralRule() { synchronized (mSync) { if (mPluralRule == null) { - mPluralRule = PluralRule.ruleForLocale(mConfiguration.locale); + mPluralRule = PluralRules.ruleForLocale(mConfiguration.locale); } return mPluralRule; } @@ -573,6 +581,33 @@ public class Resources { } /** + * Return a boolean associated with a particular resource ID. This can be + * used with any integral resource value, and will return true if it is + * non-zero. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return Returns the boolean value contained in the resource. + */ + public boolean getBoolean(int id) throws NotFoundException { + synchronized (mTmpValue) { + TypedValue value = mTmpValue; + getValue(id, value, true); + if (value.type >= TypedValue.TYPE_FIRST_INT + && value.type <= TypedValue.TYPE_LAST_INT) { + return value.data != 0; + } + throw new NotFoundException( + "Resource ID #0x" + Integer.toHexString(id) + " type #0x" + + Integer.toHexString(value.type) + " is not valid"); + } + } + + /** * Return an integer associated with a particular resource ID. * * @param id The desired resource identifier, as generated by the aapt @@ -1157,12 +1192,18 @@ public class Resources { width = mMetrics.heightPixels; height = mMetrics.widthPixels; } + int keyboardHidden = mConfiguration.keyboardHidden; + if (keyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO + && mConfiguration.hardKeyboardHidden + == Configuration.HARDKEYBOARDHIDDEN_YES) { + keyboardHidden = Configuration.KEYBOARDHIDDEN_SOFT; + } mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc, locale, mConfiguration.orientation, mConfiguration.touchscreen, (int)(mMetrics.density*160), mConfiguration.keyboard, - mConfiguration.keyboardHidden, - mConfiguration.navigation, width, height, sSdkVersion); + keyboardHidden, mConfiguration.navigation, width, height, + sSdkVersion); int N = mDrawableCache.size(); if (DEBUG_CONFIG) { Log.d(TAG, "Cleaning up drawables config changes: 0x" @@ -1198,12 +1239,26 @@ public class Resources { } synchronized (mSync) { if (mPluralRule != null) { - mPluralRule = PluralRule.ruleForLocale(config.locale); + mPluralRule = PluralRules.ruleForLocale(config.locale); } } } /** + * Update the system resources configuration if they have previously + * been initialized. + * + * @hide + */ + public static void updateSystemConfiguration(Configuration config, DisplayMetrics metrics) { + if (mSystem != null) { + mSystem.updateConfiguration(config, metrics); + //Log.i(TAG, "Updated system resources " + mSystem + // + ": " + mSystem.getConfiguration()); + } + } + + /** * Return the current display metrics that are in effect for this resource * object. The returned object should be treated as read-only. * @@ -1330,6 +1385,102 @@ public class Resources { } /** + * Parse a series of {@link android.R.styleable#Extra <extra>} tags from + * an XML file. You call this when you are at the parent tag of the + * extra tags, and it return once all of the child tags have been parsed. + * This will call {@link #parseBundleExtra} for each extra tag encountered. + * + * @param parser The parser from which to retrieve the extras. + * @param outBundle A Bundle in which to place all parsed extras. + * @throws XmlPullParserException + * @throws IOException + */ + public void parseBundleExtras(XmlResourceParser parser, Bundle outBundle) + throws XmlPullParserException, IOException { + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String nodeName = parser.getName(); + if (nodeName.equals("extra")) { + parseBundleExtra("extra", parser, outBundle); + XmlUtils.skipCurrentTag(parser); + + } else { + XmlUtils.skipCurrentTag(parser); + } + } + } + + /** + * Parse a name/value pair out of an XML tag holding that data. The + * AttributeSet must be holding the data defined by + * {@link android.R.styleable#Extra}. The following value types are supported: + * <ul> + * <li> {@link TypedValue#TYPE_STRING}: + * {@link Bundle#putCharSequence Bundle.putCharSequence()} + * <li> {@link TypedValue#TYPE_INT_BOOLEAN}: + * {@link Bundle#putCharSequence Bundle.putBoolean()} + * <li> {@link TypedValue#TYPE_FIRST_INT}-{@link TypedValue#TYPE_LAST_INT}: + * {@link Bundle#putCharSequence Bundle.putBoolean()} + * <li> {@link TypedValue#TYPE_FLOAT}: + * {@link Bundle#putCharSequence Bundle.putFloat()} + * </ul> + * + * @param tagName The name of the tag these attributes come from; this is + * only used for reporting error messages. + * @param attrs The attributes from which to retrieve the name/value pair. + * @param outBundle The Bundle in which to place the parsed value. + * @throws XmlPullParserException If the attributes are not valid. + */ + public void parseBundleExtra(String tagName, AttributeSet attrs, + Bundle outBundle) throws XmlPullParserException { + TypedArray sa = obtainAttributes(attrs, + com.android.internal.R.styleable.Extra); + + String name = sa.getString( + com.android.internal.R.styleable.Extra_name); + if (name == null) { + sa.recycle(); + throw new XmlPullParserException("<" + tagName + + "> requires an android:name attribute at " + + attrs.getPositionDescription()); + } + + TypedValue v = sa.peekValue( + com.android.internal.R.styleable.Extra_value); + if (v != null) { + if (v.type == TypedValue.TYPE_STRING) { + CharSequence cs = v.coerceToString(); + outBundle.putCharSequence(name, cs); + } else if (v.type == TypedValue.TYPE_INT_BOOLEAN) { + outBundle.putBoolean(name, v.data != 0); + } else if (v.type >= TypedValue.TYPE_FIRST_INT + && v.type <= TypedValue.TYPE_LAST_INT) { + outBundle.putInt(name, v.data); + } else if (v.type == TypedValue.TYPE_FLOAT) { + outBundle.putFloat(name, v.getFloat()); + } else { + sa.recycle(); + throw new XmlPullParserException("<" + tagName + + "> only supports string, integer, float, color, and boolean at " + + attrs.getPositionDescription()); + } + } else { + sa.recycle(); + throw new XmlPullParserException("<" + tagName + + "> requires an android:value or android:resource attribute at " + + attrs.getPositionDescription()); + } + + sa.recycle(); + } + + /** * Retrieve underlying AssetManager storage for these resources. */ public final AssetManager getAssets() { diff --git a/core/java/android/content/res/StringBlock.java b/core/java/android/content/res/StringBlock.java index da32cc8..3df7708 100644 --- a/core/java/android/content/res/StringBlock.java +++ b/core/java/android/content/res/StringBlock.java @@ -96,6 +96,7 @@ final class StringBlock { mStyleIDs.subId = nativeIndexOfString(mNative, "sub"); mStyleIDs.strikeId = nativeIndexOfString(mNative, "strike"); mStyleIDs.listItemId = nativeIndexOfString(mNative, "li"); + mStyleIDs.marqueeId = nativeIndexOfString(mNative, "marquee"); if (localLOGV) Log.v(TAG, "BoldId=" + mStyleIDs.boldId + ", ItalicId=" + mStyleIDs.italicId @@ -127,6 +128,7 @@ final class StringBlock { private int supId; private int strikeId; private int listItemId; + private int marqueeId; } private CharSequence applyStyles(String str, int[] style, StyleIDs ids) { @@ -179,6 +181,10 @@ final class StringBlock { buffer.setSpan(new BulletSpan(10), style[i+1], style[i+2]+1, Spannable.SPAN_PARAGRAPH); + } else if (type == ids.marqueeId) { + buffer.setSpan(TextUtils.TruncateAt.MARQUEE, + style[i+1], style[i+2]+1, + Spannable.SPAN_INCLUSIVE_INCLUSIVE); } else { String tag = nativeGetString(mNative, type); @@ -216,6 +222,15 @@ final class StringBlock { style[i+1], style[i+2]+1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } + } else if (tag.startsWith("a;")) { + String sub; + + sub = subtag(tag, ";href="); + if (sub != null) { + buffer.setSpan(new URLSpan(sub), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } } } diff --git a/core/java/android/database/AbstractCursor.java b/core/java/android/database/AbstractCursor.java index e81f7f8..76f0860 100644 --- a/core/java/android/database/AbstractCursor.java +++ b/core/java/android/database/AbstractCursor.java @@ -21,6 +21,10 @@ import android.net.Uri; 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; @@ -457,9 +461,24 @@ public abstract class AbstractCursor implements CrossProcessCursor { mContentObservable.unregisterObserver(observer); } } - + + /** + * @hide pending API council approval + */ + protected void notifyDataSetChange() { + mDataSetObservable.notifyChanged(); + } + + /** + * @hide pending API council approval + */ + protected DataSetObservable getDataSetObservable() { + return mDataSetObservable; + + } public void registerDataSetObserver(DataSetObserver observer) { mDataSetObservable.registerObserver(observer); + } public void unregisterDataSetObserver(DataSetObserver observer) { diff --git a/core/java/android/database/AbstractWindowedCursor.java b/core/java/android/database/AbstractWindowedCursor.java index 1ec4312..4ac0aef 100644 --- a/core/java/android/database/AbstractWindowedCursor.java +++ b/core/java/android/database/AbstractWindowedCursor.java @@ -172,7 +172,7 @@ public abstract class AbstractWindowedCursor extends AbstractCursor super.checkPosition(); if (mWindow == null) { - throw new StaleDataException("This cursor has changed, you must call requery()"); + throw new StaleDataException("Access closed cursor"); } } diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java index 72dc3a9..8e26730 100644 --- a/core/java/android/database/CursorWindow.java +++ b/core/java/android/database/CursorWindow.java @@ -409,8 +409,13 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { * change across a call to clear(). */ public void clear() { - mStartPos = 0; - native_clear(); + acquireReference(); + try { + mStartPos = 0; + native_clear(); + } finally { + releaseReference(); + } } /** Clears out the native side of things */ diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java index ab0dc3f..2ff7294 100644 --- a/core/java/android/database/DatabaseUtils.java +++ b/core/java/android/database/DatabaseUtils.java @@ -241,6 +241,21 @@ public class DatabaseUtils { } /** + * Concatenates two SQL WHERE clauses, handling empty or null values. + * @hide + */ + public static String concatenateWhere(String a, String b) { + if (TextUtils.isEmpty(a)) { + return b; + } + if (TextUtils.isEmpty(b)) { + return a; + } + + return "(" + a + ") AND (" + b + ")"; + } + + /** * return the collation key * @param name * @return the collation key diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index ae2fc95..70b9b83 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -18,7 +18,12 @@ package android.database.sqlite; import android.database.AbstractWindowedCursor; import android.database.CursorWindow; +import android.database.DataSetObserver; import android.database.SQLException; + +import android.os.Handler; +import android.os.Message; +import android.os.Process; import android.text.TextUtils; import android.util.Config; import android.util.Log; @@ -26,6 +31,7 @@ import android.util.Log; import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; /** * A Cursor implementation that exposes results from a query on a @@ -59,7 +65,131 @@ public class SQLiteCursor extends AbstractWindowedCursor { /** Used to find out where a cursor was allocated in case it never got * released. */ private StackTraceElement[] mStackTraceElements; - + + /** + * mMaxRead is the max items that each cursor window reads + * default to a very high value + */ + private int mMaxRead = Integer.MAX_VALUE; + private int mInitialRead = Integer.MAX_VALUE; + private int mCursorState = 0; + private ReentrantLock mLock = null; + private boolean mPendingData = false; + + /** + * support for a cursor variant that doesn't always read all results + * initialRead is the initial number of items that cursor window reads + * if query contains more than this number of items, a thread will be + * created and handle the left over items so that caller can show + * results as soon as possible + * @param initialRead initial number of items that cursor read + * @param maxRead leftover items read at maxRead items per time + * @hide + */ + public void setLoadStyle(int initialRead, int maxRead) { + mMaxRead = maxRead; + mInitialRead = initialRead; + mLock = new ReentrantLock(true); + } + + private void queryThreadLock() { + if (mLock != null) { + mLock.lock(); + } + } + + private void queryThreadUnlock() { + if (mLock != null) { + mLock.unlock(); + } + } + + + /** + * @hide + */ + final private class QueryThread implements Runnable { + private final int mThreadState; + QueryThread(int version) { + mThreadState = version; + } + private void sendMessage() { + if (mNotificationHandler != null) { + mNotificationHandler.sendEmptyMessage(1); + mPendingData = false; + } else { + mPendingData = true; + } + + } + public void run() { + // use cached mWindow, to avoid get null mWindow + CursorWindow cw = mWindow; + Process.setThreadPriority(Process.myTid(), Process.THREAD_PRIORITY_BACKGROUND); + // the cursor's state doesn't change + while (true) { + mLock.lock(); + if (mCursorState != mThreadState) { + mLock.unlock(); + break; + } + try { + int count = mQuery.fillWindow(cw, mMaxRead, mCount); + // return -1 means not finished + if (count != 0) { + if (count == NO_COUNT){ + mCount += mMaxRead; + sendMessage(); + } else { + mCount = count; + sendMessage(); + break; + } + } else { + break; + } + } catch (Exception e) { + // end the tread when the cursor is close + break; + } finally { + mLock.unlock(); + } + } + } + } + + /** + * @hide + */ + protected class MainThreadNotificationHandler extends Handler { + public void handleMessage(Message msg) { + notifyDataSetChange(); + } + } + + /** + * @hide + */ + protected MainThreadNotificationHandler mNotificationHandler; + + public void registerDataSetObserver(DataSetObserver observer) { + super.registerDataSetObserver(observer); + if ((Integer.MAX_VALUE != mMaxRead || Integer.MAX_VALUE != mInitialRead) && + mNotificationHandler == null) { + queryThreadLock(); + try { + mNotificationHandler = new MainThreadNotificationHandler(); + if (mPendingData) { + notifyDataSetChange(); + mPendingData = false; + } + } finally { + queryThreadUnlock(); + } + } + + } + /** * Execute a query and provide access to its result set through a Cursor * interface. For a query such as: {@code SELECT name, birth, phone FROM @@ -146,11 +276,22 @@ public class SQLiteCursor extends AbstractWindowedCursor { // If there isn't a window set already it will only be accessed locally mWindow = new CursorWindow(true /* the window is local only */); } else { - mWindow.clear(); + mCursorState++; + queryThreadLock(); + try { + mWindow.clear(); + } finally { + queryThreadUnlock(); + } } - - // mWindow must be cleared - mCount = mQuery.fillWindow(mWindow, startPos); + mWindow.setStartPosition(startPos); + mCount = mQuery.fillWindow(mWindow, mInitialRead, 0); + // return -1 means not finished + if (mCount == NO_COUNT){ + mCount = startPos + mInitialRead; + Thread t = new Thread(new QueryThread(mCursorState), "query thread"); + t.start(); + } } @Override @@ -344,6 +485,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { private void deactivateCommon() { if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this); + mCursorState = 0; if (mWindow != null) { mWindow.close(); mWindow = null; @@ -368,6 +510,9 @@ public class SQLiteCursor extends AbstractWindowedCursor { @Override public boolean requery() { + if (isClosed()) { + return false; + } long timeStart = 0; if (Config.LOGV) { timeStart = System.currentTimeMillis(); @@ -385,8 +530,13 @@ public class SQLiteCursor extends AbstractWindowedCursor { // This one will recreate the temp table, and get its count mDriver.cursorRequeried(this); mCount = NO_COUNT; - // Requery the program that runs over the temp table - mQuery.requery(); + mCursorState++; + queryThreadLock(); + try { + mQuery.requery(); + } finally { + queryThreadUnlock(); + } } finally { mDatabase.unlock(); } @@ -405,9 +555,15 @@ public class SQLiteCursor extends AbstractWindowedCursor { } @Override - public void setWindow(CursorWindow window) { + public void setWindow(CursorWindow window) { if (mWindow != null) { - mWindow.close(); + mCursorState++; + queryThreadLock(); + try { + mWindow.close(); + } finally { + queryThreadUnlock(); + } mCount = NO_COUNT; } mWindow = window; diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index e497190..fa062c8 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -54,6 +54,69 @@ public class SQLiteDatabase extends SQLiteClosable { private final static String TAG = "Database"; /** + * Algorithms used in ON CONFLICT clause + * http://www.sqlite.org/lang_conflict.html + * @hide + */ + public enum ConflictAlgorithm { + /** + * When a constraint violation occurs, an immediate ROLLBACK occurs, + * thus ending the current transaction, and the command aborts with a + * return code of SQLITE_CONSTRAINT. If no transaction is active + * (other than the implied transaction that is created on every command) + * then this algorithm works the same as ABORT. + */ + ROLLBACK("ROLLBACK"), + + /** + * When a constraint violation occurs,no ROLLBACK is executed + * so changes from prior commands within the same transaction + * are preserved. This is the default behavior. + */ + ABORT("ABORT"), + + /** + * When a constraint violation occurs, the command aborts with a return + * code SQLITE_CONSTRAINT. But any changes to the database that + * the command made prior to encountering the constraint violation + * are preserved and are not backed out. + */ + FAIL("FAIL"), + + /** + * When a constraint violation occurs, the one row that contains + * the constraint violation is not inserted or changed. + * But the command continues executing normally. Other rows before and + * after the row that contained the constraint violation continue to be + * inserted or updated normally. No error is returned. + */ + IGNORE("IGNORE"), + + /** + * When a UNIQUE constraint violation occurs, the pre-existing rows that + * are causing the constraint violation are removed prior to inserting + * or updating the current row. Thus the insert or update always occurs. + * The command continues executing normally. No error is returned. + * If a NOT NULL constraint violation occurs, the NULL value is replaced + * by the default value for that column. If the column has no default + * value, then the ABORT algorithm is used. If a CHECK constraint + * violation occurs then the IGNORE algorithm is used. When this conflict + * resolution strategy deletes rows in order to satisfy a constraint, + * it does not invoke delete triggers on those rows. + * This behavior might change in a future release. + */ + REPLACE("REPLACE"); + + private final String mValue; + ConflictAlgorithm(String value) { + mValue = value; + } + public String value() { + return mValue; + } + } + + /** * Maximum Length Of A LIKE Or GLOB Pattern * The pattern matching algorithm used in the default LIKE and GLOB implementation * of SQLite can exhibit O(N^2) performance (where N is the number of characters in @@ -437,8 +500,26 @@ public class SQLiteDatabase extends SQLiteClosable { * successful so far. Do not call setTransactionSuccessful before calling this. When this * returns a new transaction will have been created but not marked as successful. * @return true if the transaction was yielded + * @deprecated if the db is locked more than once (becuase of nested transactions) then the lock + * will not be yielded. Use yieldIfContendedSafely instead. */ public boolean yieldIfContended() { + return yieldIfContendedHelper(false /* do not check yielding */); + } + + /** + * Temporarily end the transaction to let other threads run. The transaction is assumed to be + * successful so far. Do not call setTransactionSuccessful before calling this. When this + * returns a new transaction will have been created but not marked as successful. This assumes + * that there are no nested transactions (beginTransaction has only been called once) and will + * through an exception if that is not the case. + * @return true if the transaction was yielded + */ + public boolean yieldIfContendedSafely() { + return yieldIfContendedHelper(true /* check yielding */); + } + + private boolean yieldIfContendedHelper(boolean checkFullyYielded) { if (mLock.getQueueLength() == 0) { // Reset the lock acquire time since we know that the thread was willing to yield // the lock at this time. @@ -448,6 +529,12 @@ public class SQLiteDatabase extends SQLiteClosable { } setTransactionSuccessful(); endTransaction(); + if (checkFullyYielded) { + if (this.isDbLockedByCurrentThread()) { + throw new IllegalStateException( + "Db locked more than once. yielfIfContended cannot yield"); + } + } beginTransaction(); return true; } @@ -1031,6 +1118,28 @@ public class SQLiteDatabase extends SQLiteClosable { } /** + * Runs the provided SQL and returns a cursor over the result set. + * The cursor will read an initial set of rows and the return to the caller. + * It will continue to read in batches and send data changed notifications + * when the later batches are ready. + * @param sql the SQL query. The SQL string must not be ; terminated + * @param selectionArgs You may include ?s in where clause in the query, + * which will be replaced by the values from selectionArgs. The + * values will be bound as Strings. + * @param initialRead set the initial count of items to read from the cursor + * @param maxRead set the count of items to read on each iteration after the first + * @return A {@link Cursor} object, which is positioned before the first entry + * @hide pending API council approval + */ + public Cursor rawQuery(String sql, String[] selectionArgs, + int initialRead, int maxRead) { + SQLiteCursor c = (SQLiteCursor)rawQueryWithFactory( + null, sql, selectionArgs, null); + c.setLoadStyle(initialRead, maxRead); + return c; + } + + /** * Convenience method for inserting a row into the database. * * @param table the table to insert the row into @@ -1044,7 +1153,7 @@ public class SQLiteDatabase extends SQLiteClosable { */ public long insert(String table, String nullColumnHack, ContentValues values) { try { - return insertOrReplace(table, nullColumnHack, values, false); + return insertWithOnConflict(table, nullColumnHack, values, null); } catch (SQLException e) { Log.e(TAG, "Error inserting " + values, e); return -1; @@ -1066,7 +1175,7 @@ public class SQLiteDatabase extends SQLiteClosable { */ public long insertOrThrow(String table, String nullColumnHack, ContentValues values) throws SQLException { - return insertOrReplace(table, nullColumnHack, values, false) ; + return insertWithOnConflict(table, nullColumnHack, values, null); } /** @@ -1082,7 +1191,8 @@ public class SQLiteDatabase extends SQLiteClosable { */ public long replace(String table, String nullColumnHack, ContentValues initialValues) { try { - return insertOrReplace(table, nullColumnHack, initialValues, true); + return insertWithOnConflict(table, nullColumnHack, initialValues, + ConflictAlgorithm.REPLACE); } catch (SQLException e) { Log.e(TAG, "Error inserting " + initialValues, e); return -1; @@ -1103,22 +1213,38 @@ public class SQLiteDatabase extends SQLiteClosable { */ public long replaceOrThrow(String table, String nullColumnHack, ContentValues initialValues) throws SQLException { - return insertOrReplace(table, nullColumnHack, initialValues, true); + return insertWithOnConflict(table, nullColumnHack, initialValues, + ConflictAlgorithm.REPLACE); } - private long insertOrReplace(String table, String nullColumnHack, - ContentValues initialValues, boolean allowReplace) { + /** + * General method for inserting a row into the database. + * + * @param table the table to insert the row into + * @param nullColumnHack SQL doesn't allow inserting a completely empty row, + * so if initialValues is empty this column will explicitly be + * assigned a NULL value + * @param initialValues this map contains the initial column values for the + * row. The keys should be the column names and the values the + * column values + * @param algorithm {@link ConflictAlgorithm} for insert conflict resolver + * @return the row ID of the newly inserted row, or -1 if an error occurred + * @hide + */ + public long insertWithOnConflict(String table, String nullColumnHack, + ContentValues initialValues, ConflictAlgorithm algorithm) { if (!isOpen()) { throw new IllegalStateException("database not open"); } // Measurements show most sql lengths <= 152 StringBuilder sql = new StringBuilder(152); - sql.append("INSERT "); - if (allowReplace) { - sql.append("OR REPLACE "); + sql.append("INSERT"); + if (algorithm != null) { + sql.append(" OR "); + sql.append(algorithm.value()); } - sql.append("INTO "); + sql.append(" INTO "); sql.append(table); // Measurements show most values lengths < 40 StringBuilder values = new StringBuilder(40); @@ -1241,6 +1367,23 @@ public class SQLiteDatabase extends SQLiteClosable { * @return the number of rows affected */ public int update(String table, ContentValues values, String whereClause, String[] whereArgs) { + return updateWithOnConflict(table, values, whereClause, whereArgs, null); + } + + /** + * Convenience method for updating rows in the database. + * + * @param table the table to update in + * @param values a map from column names to new column values. null is a + * valid value that will be translated to NULL. + * @param whereClause the optional WHERE clause to apply when updating. + * Passing null will update all rows. + * @param algorithm {@link ConflictAlgorithm} for update conflict resolver + * @return the number of rows affected + * @hide + */ + public int updateWithOnConflict(String table, ContentValues values, + String whereClause, String[] whereArgs, ConflictAlgorithm algorithm) { if (!isOpen()) { throw new IllegalStateException("database not open"); } @@ -1251,6 +1394,11 @@ public class SQLiteDatabase extends SQLiteClosable { StringBuilder sql = new StringBuilder(120); sql.append("UPDATE "); + if (algorithm != null) { + sql.append(" OR "); + sql.append(algorithm.value()); + } + sql.append(table); sql.append(" SET "); diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java index f6872ac..35bf645 100644 --- a/core/java/android/database/sqlite/SQLiteOpenHelper.java +++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java @@ -26,8 +26,6 @@ import android.util.Log; * optionally {@link #onOpen}, and this class takes care of opening the database * if it exists, creating it if it does not, and upgrading it as necessary. * Transactions are used to make sure the database is always in a sensible state. - * - * @see com.google.provider.NotePad.NotePadProvider */ public abstract class SQLiteOpenHelper { private static final String TAG = SQLiteOpenHelper.class.getSimpleName(); diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java index e0341a2..f89c87d 100644 --- a/core/java/android/database/sqlite/SQLiteProgram.java +++ b/core/java/android/database/sqlite/SQLiteProgram.java @@ -239,7 +239,9 @@ public abstract class SQLiteProgram extends SQLiteClosable { Log.d(TAG, " " + ste); } } - onAllReferencesReleased(); + // when in finalize() it is already removed from weakhashmap + // so it is safe to not removed itself from db + onAllReferencesReleasedFromContainer(); } } diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java index 40855b6..22c53ab 100644 --- a/core/java/android/database/sqlite/SQLiteQuery.java +++ b/core/java/android/database/sqlite/SQLiteQuery.java @@ -40,9 +40,8 @@ public class SQLiteQuery extends SQLiteProgram { * Create a persistent query object. * * @param db The database that this query object is associated with - * @param query The SQL string for this query. It must include "INDEX -1 - * OFFSET ?" at the end - * @param offsetIndex The 1-based index to the OFFSET parameter + * @param query The SQL string for this query. + * @param offsetIndex The 1-based index to the OFFSET parameter, */ /* package */ SQLiteQuery(SQLiteDatabase db, String query, int offsetIndex, String[] bindArgs) { super(db, query); @@ -59,24 +58,28 @@ public class SQLiteQuery extends SQLiteProgram { * @param startPos The position to start reading rows from * @return number of total rows in the query */ - /* package */ int fillWindow(CursorWindow window, int startPos) { - if (startPos < 0) { - throw new IllegalArgumentException("startPos should > 0"); - } - window.setStartPosition(startPos); + /* package */ int fillWindow(CursorWindow window, + int maxRead, int lastPos) { mDatabase.lock(); try { acquireReference(); - window.acquireReference(); - return native_fill_window(window, startPos, mOffsetIndex); - } catch (IllegalStateException e){ - // simply ignore it - return 0; - } catch (SQLiteDatabaseCorruptException e) { - mDatabase.onCorruption(); - throw e; + try { + window.acquireReference(); + // if the start pos is not equal to 0, then most likely window is + // too small for the data set, loading by another thread + // is not safe in this situation. the native code will ignore maxRead + return native_fill_window(window, window.getStartPosition(), mOffsetIndex, + maxRead, lastPos); + } catch (IllegalStateException e){ + // simply ignore it + return 0; + } catch (SQLiteDatabaseCorruptException e) { + mDatabase.onCorruption(); + throw e; + } finally { + window.releaseReference(); + } } finally { - window.releaseReference(); releaseReference(); mDatabase.unlock(); } @@ -113,7 +116,13 @@ public class SQLiteQuery extends SQLiteProgram { releaseReference(); } } - + + /** {@hide pending API Council approval} */ + @Override + public String toString() { + return "SQLiteQuery: " + mQuery; + } + @Override public void close() { super.close(); @@ -124,11 +133,6 @@ public class SQLiteQuery extends SQLiteProgram { * Called by SQLiteCursor when it is requeried. */ /* package */ void requery() { - boolean oldMClosed = mClosed; - if (mClosed) { - mClosed = false; - compile(mQuery, false); - } if (mBindArgs != null) { int len = mBindArgs.length; try { @@ -136,8 +140,7 @@ public class SQLiteQuery extends SQLiteProgram { super.bindString(i + 1, mBindArgs[i]); } } catch (SQLiteMisuseException e) { - StringBuilder errMsg = new StringBuilder - ("old mClosed " + oldMClosed + " mQuery " + mQuery); + StringBuilder errMsg = new StringBuilder("mQuery " + mQuery); for (int i = 0; i < len; i++) { errMsg.append(" "); errMsg.append(mBindArgs[i]); @@ -174,7 +177,8 @@ public class SQLiteQuery extends SQLiteProgram { if (!mClosed) super.bindString(index, value); } - private final native int native_fill_window(CursorWindow window, int startPos, int offsetParam); + private final native int native_fill_window(CursorWindow window, + int startPos, int offsetParam, int maxRead, int lastPos); private final native int native_column_count(); diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 8330750..dc75748 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -18,6 +18,7 @@ package android.hardware; import java.lang.ref.WeakReference; import java.util.HashMap; +import java.io.IOException; import android.util.Log; import android.view.Surface; @@ -98,13 +99,28 @@ public class Camera { } /** + * Reconnect to the camera after passing it to MediaRecorder. To save + * setup/teardown time, a client of Camara can pass an initialized Camera + * object to a MediaRecorder to use for video recording. Once the + * MediaRecorder is done with the Camera, this method can be used to + * re-establish a connection with the camera hardware. + * + * @throws IOException if the method fails. + * + * FIXME: Unhide after approval + * @hide + */ + public native final void reconnect() throws IOException; + + /** * Sets the SurfaceHolder to be used for a picture preview. If the surface * changed since the last call, the screen will blank. Nothing happens * if the same surface is re-set. * * @param holder the SurfaceHolder upon which to place the picture preview + * @throws IOException if the method fails. */ - public final void setPreviewDisplay(SurfaceHolder holder) { + public final void setPreviewDisplay(SurfaceHolder holder) throws IOException { setPreviewDisplay(holder.getSurface()); } @@ -263,10 +279,19 @@ public class Camera { }; /** - * Registers a callback to be invoked when a picture is taken. + * Triggers an asynchronous image capture. The camera service + * will initiate a series of callbacks to the application as the + * image capture progresses. The shutter callback occurs after + * the image is captured. This can be used to trigger a sound + * to let the user know that image has been captured. The raw + * callback occurs when the raw image data is available. The jpeg + * callback occurs when the compressed image is available. If the + * application does not need a particular callback, a null can be + * passed instead of a callback method. * - * @param raw the callback to run for raw images, may be null - * @param jpeg the callback to run for jpeg images, may be null + * @param shutter callback after the image is captured, may be null + * @param raw callback with raw image data, may be null + * @param jpeg callback with jpeg image data, may be null */ public final void takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg) { diff --git a/core/java/android/hardware/ISensorService.aidl b/core/java/android/hardware/ISensorService.aidl index b6ac3ab..8aad9b4 100644 --- a/core/java/android/hardware/ISensorService.aidl +++ b/core/java/android/hardware/ISensorService.aidl @@ -26,5 +26,4 @@ interface ISensorService { ParcelFileDescriptor getDataChanel(); boolean enableSensor(IBinder listener, int sensor, int enable); - oneway void reportAccuracy(int sensor, int value); } diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java new file mode 100644 index 0000000..0ce2f7b --- /dev/null +++ b/core/java/android/hardware/Sensor.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.hardware; + +/** + * Class representing a sensor. Use {@link SensorManager#getSensorList} + * to get the list of available Sensors. + */ +public class Sensor { + + /** + * A constant describing an accelerometer sensor type. + * See {@link android.hardware.SensorEvent SensorEvent} + * for more details. + */ + public static final int TYPE_ACCELEROMETER = 1; + + /** + * A constant describing a magnetic field sensor type. + * See {@link android.hardware.SensorEvent SensorEvent} + * for more details. + */ + public static final int TYPE_MAGNETIC_FIELD = 2; + + /** + * A constant describing an orientation sensor type. + * See {@link android.hardware.SensorEvent SensorEvent} + * for more details. + */ + public static final int TYPE_ORIENTATION = 3; + + /** A constant describing a gyroscope sensor type */ + public static final int TYPE_GYROSCOPE = 4; + /** A constant describing a light sensor type */ + public static final int TYPE_LIGHT = 5; + /** A constant describing a pressure sensor type */ + public static final int TYPE_PRESSURE = 6; + /** A constant describing a temperature sensor type */ + public static final int TYPE_TEMPERATURE = 7; + /** A constant describing a proximity sensor type */ + public static final int TYPE_PROXIMITY = 8; + + + /** + * A constant describing all sensor types. + */ + public static final int TYPE_ALL = -1; + + /* Some of these fields are set only by the native bindings in + * SensorManager. + */ + private String mName; + private String mVendor; + private int mVersion; + private int mHandle; + private int mType; + private float mMaxRange; + private float mResolution; + private float mPower; + private int mLegacyType; + + + Sensor() { + } + + /** + * @return name string of the sensor. + */ + public String getName() { + return mName; + } + + /** + * @return vendor string of this sensor. + */ + public String getVendor() { + return mVendor; + } + + /** + * @return generic type of this sensor. + */ + public int getType() { + return mType; + } + + /** + * @return version of the sensor's module. + */ + public int getVersion() { + return mVersion; + } + + /** + * @return maximum range of the sensor in the sensor's unit. + */ + public float getMaximumRange() { + return mMaxRange; + } + + /** + * @return resolution of the sensor in the sensor's unit. + */ + public float getResolution() { + return mResolution; + } + + /** + * @return the power in mA used by this sensor while in use + */ + public float getPower() { + return mPower; + } + + int getHandle() { + return mHandle; + } + + void setRange(float max, float res) { + mMaxRange = max; + mResolution = res; + } + + void setLegacyType(int legacyType) { + mLegacyType = legacyType; + } + + int getLegacyType() { + return mLegacyType; + } +} diff --git a/core/java/android/hardware/SensorEvent.java b/core/java/android/hardware/SensorEvent.java new file mode 100644 index 0000000..cf939c5 --- /dev/null +++ b/core/java/android/hardware/SensorEvent.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware; + +/** + * This class represents a sensor event and holds informations such as the + * sensor type (eg: accelerometer, orientation, etc...), the time-stamp, + * accuracy and of course the sensor's {@link SensorEvent#values data}. + * + * <p><u>Definition of the coordinate system used by the SensorEvent API.</u><p> + * + * <pre> + * The coordinate space is defined relative to the screen of the phone + * in its default orientation. The axes are not swapped when the device's + * screen orientation changes. + * + * The OpenGL ES coordinate system is used. The origin is in the + * lower-left corner with respect to the screen, with the X axis horizontal + * and pointing right, the Y axis vertical and pointing up and the Z axis + * pointing outside the front face of the screen. In this system, coordinates + * behind the screen have negative Z values. + * + * <b>Note:</b> This coordinate system is different from the one used in the + * Android 2D APIs where the origin is in the top-left corner. + * + * x<0 x>0 + * ^ + * | + * +-----------+--> y>0 + * | | + * | | + * | | + * | | / z<0 + * | | / + * | | / + * O-----------+/ + * |[] [ ] []/ + * +----------/+ y<0 + * / + * / + * |/ z>0 (toward the sky) + * + * O: Origin (x=0,y=0,z=0) + * </pre> + */ + +public class SensorEvent { + /** + * The length and contents of the values array vary depending on which + * sensor type is being monitored (see also {@link SensorEvent} for a + * definition of the coordinate system used): + * + * <p>{@link android.hardware.Sensor#TYPE_ORIENTATION Sensor.TYPE_ORIENTATION}:<p> + * All values are angles in degrees. + * + * <p>values[0]: Azimuth, angle between the magnetic north direction and + * the Y axis, around the Z axis (0 to 359). + * 0=North, 90=East, 180=South, 270=West + * + * <p>values[1]: Pitch, rotation around X axis (-180 to 180), + * with positive values when the z-axis moves <b>toward</b> the y-axis. + * + * <p>values[2]: Roll, rotation around Y axis (-90 to 90), with + * positive values when the x-axis moves <b>away</b> from the z-axis. + * + * <p><b>Note:</b> This definition is different from <b>yaw, pitch and + * roll</b> used in aviation where the X axis is along the long side of + * the plane (tail to nose). + * + * <p><b>Note:</b> It is preferable to use + * {@link android.hardware.SensorManager#getRotationMatrix + * getRotationMatrix()} in conjunction with + * {@link android.hardware.SensorManager#remapCoordinateSystem + * remapCoordinateSystem()} and + * {@link android.hardware.SensorManager#getOrientation getOrientation()} + * to compute these values; while it may be more expensive, it is usually + * more accurate. + * + * <p>{@link android.hardware.Sensor#TYPE_ACCELEROMETER Sensor.TYPE_ACCELEROMETER}:<p> + * All values are in SI units (m/s^2) and measure the acceleration applied + * to the phone minus the force of gravity. + * + * <p>values[0]: Acceleration minus Gx on the x-axis + * <p>values[1]: Acceleration minus Gy on the y-axis + * <p>values[2]: Acceleration minus Gz on the z-axis + * + * <p><u>Examples</u>: + * <li>When the device lies flat on a table and is pushed on its left + * side toward the right, the x acceleration value is positive.</li> + * + * <li>When the device lies flat on a table, the acceleration value is + * +9.81, which correspond to the acceleration of the device (0 m/s^2) + * minus the force of gravity (-9.81 m/s^2).</li> + * + * <li>When the device lies flat on a table and is pushed toward the sky + * with an acceleration of A m/s^2, the acceleration value is equal to + * A+9.81 which correspond to the acceleration of the + * device (+A m/s^2) minus the force of gravity (-9.81 m/s^2).</li> + * + * + * <p>{@link android.hardware.Sensor#TYPE_MAGNETIC_FIELD Sensor.TYPE_MAGNETIC_FIELD}:<p> + * All values are in micro-Tesla (uT) and measure the ambient magnetic + * field in the X, Y and Z axis. + * + */ + public final float[] values; + + /** + * The sensor that generated this event. + * See {@link android.hardware.SensorManager SensorManager} + * for details. + */ + public Sensor sensor; + + /** + * The accuracy of this event. + * See {@link android.hardware.SensorManager SensorManager} + * for details. + */ + public int accuracy; + + + /** + * The time in nanosecond at which the event happened + */ + public long timestamp; + + + SensorEvent(int size) { + values = new float[size]; + } +} diff --git a/core/java/android/hardware/SensorEventListener.java b/core/java/android/hardware/SensorEventListener.java new file mode 100644 index 0000000..716d0d4 --- /dev/null +++ b/core/java/android/hardware/SensorEventListener.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware; + +/** + * Used for receiving notifications from the SensorManager when + * sensor values have changed. + */ +public interface SensorEventListener { + + /** + * Called when sensor values have changed. + * <p>See {@link android.hardware.SensorManager SensorManager} + * for details on possible sensor types. + * <p>See also {@link android.hardware.SensorEvent SensorEvent}. + * + * <p><b>NOTE:</b> The application doesn't own the + * {@link android.hardware.SensorEvent event} + * object passed as a parameter and therefore cannot hold on o it. + * The object may be part of an internal pool and may be reused by + * the framework. + * + * @param event the {@link android.hardware.SensorEvent SensorEvent}. + */ + public void onSensorChanged(SensorEvent event); + + /** + * Called when the accuracy of a sensor has changed. + * <p>See {@link android.hardware.SensorManager SensorManager} + * for details. + * + * @param accuracy The new accuracy of this sensor + */ + public void onAccuracyChanged(Sensor sensor, int accuracy); +} diff --git a/core/java/android/hardware/SensorListener.java b/core/java/android/hardware/SensorListener.java index d676a5e..cfa184b 100644 --- a/core/java/android/hardware/SensorListener.java +++ b/core/java/android/hardware/SensorListener.java @@ -19,18 +19,74 @@ package android.hardware; /** * Used for receiving notifications from the SensorManager when * sensor values have changed. + * + * This interface is deprecated, use + * {@link android.hardware.SensorEventListener SensorEventListener} instead. + * */ +@Deprecated public interface SensorListener { /** - * Called when sensor values have changed. + * <p>Called when sensor values have changed. * The length and contents of the values array vary * depending on which sensor is being monitored. * See {@link android.hardware.SensorManager SensorManager} - * for details on possible sensor types and values. + * for details on possible sensor types. * + * <p><u>Definition of the coordinate system used below.</u><p> + * <p>The X axis refers to the screen's horizontal axis + * (the small edge in portrait mode, the long edge in landscape mode) and + * points to the right. + * <p>The Y axis refers to the screen's vertical axis and points towards + * the top of the screen (the origin is in the lower-left corner). + * <p>The Z axis points toward the sky when the device is lying on its back + * on a table. + * <p> <b>IMPORTANT NOTE:</b> The axis <b><u>are swapped</u></b> when the + * device's screen orientation changes. To access the unswapped values, + * use indices 3, 4 and 5 in values[]. + * + * <p>{@link android.hardware.SensorManager#SENSOR_ORIENTATION SENSOR_ORIENTATION}, + * {@link android.hardware.SensorManager#SENSOR_ORIENTATION_RAW SENSOR_ORIENTATION_RAW}:<p> + * All values are angles in degrees. + * + * <p>values[0]: Azimuth, rotation around the Z axis (0<=azimuth<360). + * 0 = North, 90 = East, 180 = South, 270 = West + * + * <p>values[1]: Pitch, rotation around X axis (-180<=pitch<=180), with positive + * values when the z-axis moves toward the y-axis. + * + * <p>values[2]: Roll, rotation around Y axis (-90<=roll<=90), with positive values + * when the z-axis moves toward the x-axis. + * + * <p>Note that this definition of yaw, pitch and roll is different from the + * traditional definition used in aviation where the X axis is along the long + * side of the plane (tail to nose). + * + * <p>{@link android.hardware.SensorManager#SENSOR_ACCELEROMETER SENSOR_ACCELEROMETER}:<p> + * All values are in SI units (m/s^2) and measure contact forces. + * + * <p>values[0]: force applied by the device on the x-axis + * <p>values[1]: force applied by the device on the y-axis + * <p>values[2]: force applied by the device on the z-axis + * + * <p><u>Examples</u>: + * <li>When the device is pushed on its left side toward the right, the + * x acceleration value is negative (the device applies a reaction force + * to the push toward the left)</li> + * + * <li>When the device lies flat on a table, the acceleration value is + * {@link android.hardware.SensorManager#STANDARD_GRAVITY -STANDARD_GRAVITY}, + * which correspond to the force the device applies on the table in reaction + * to gravity.</li> + * + * <p>{@link android.hardware.SensorManager#SENSOR_MAGNETIC_FIELD SENSOR_MAGNETIC_FIELD}:<p> + * All values are in micro-Tesla (uT) and measure the ambient magnetic + * field in the X, Y and -Z axis. + * <p><b><u>Note:</u></b> the magnetic field's Z axis is inverted. + * * @param sensor The ID of the sensor being monitored - * @param values The new values for the sensor + * @param values The new values for the sensor. */ public void onSensorChanged(int sensor, float[] values); @@ -40,7 +96,7 @@ public interface SensorListener { * for details. * * @param sensor The ID of the sensor being monitored - * @param accuracy The new accuracy of this sensor + * @param accuracy The new accuracy of this sensor. */ public void onAccuracyChanged(int sensor, int accuracy); } diff --git a/core/java/android/hardware/SensorManager.java b/core/java/android/hardware/SensorManager.java index 9b88fff..f02094e 100644 --- a/core/java/android/hardware/SensorManager.java +++ b/core/java/android/hardware/SensorManager.java @@ -26,6 +26,7 @@ import android.os.Handler; import android.os.Message; import android.os.ServiceManager; import android.util.Log; +import android.util.SparseArray; import android.view.IRotationWatcher; import android.view.IWindowManager; import android.view.Surface; @@ -33,7 +34,9 @@ import android.view.Surface; import java.io.FileDescriptor; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; /** * Class that lets you access the device's sensors. Get an instance of this @@ -43,126 +46,119 @@ import java.util.Arrays; public class SensorManager extends IRotationWatcher.Stub { private static final String TAG = "SensorManager"; + private static final float[] mTempMatrix = new float[16]; - /** NOTE: sensor IDs must be a power of 2 */ + /* NOTE: sensor IDs must be a power of 2 */ - /** A constant describing an orientation sensor. - * Sensor values are yaw, pitch and roll - * - * Yaw is the compass heading in degrees, range [0, 360[ - * 0 = North, 90 = East, 180 = South, 270 = West - * - * Pitch indicates the tilt of the top of the device, - * with range -90 to 90. - * Positive values indicate that the bottom of the device is tilted up - * and negative values indicate the top of the device is tilted up. - * - * Roll indicates the side to side tilt of the device, - * with range -90 to 90. - * Positive values indicate that the left side of the device is tilted up - * and negative values indicate the right side of the device is tilted up. + /** + * A constant describing an orientation sensor. + * See {@link android.hardware.SensorListener SensorListener} for more details. + * @deprecated use {@link android.hardware.Sensor Sensor} instead. */ + @Deprecated public static final int SENSOR_ORIENTATION = 1 << 0; - /** A constant describing an accelerometer. - * Sensor values are acceleration in the X, Y and Z axis, - * where the X axis has positive direction toward the right side of the device, - * the Y axis has positive direction toward the top of the device - * and the Z axis has positive direction toward the front of the device. - * - * The direction of the force of gravity is indicated by acceleration values in the - * X, Y and Z axes. The typical case where the device is flat relative to the surface - * of the Earth appears as -STANDARD_GRAVITY in the Z axis - * and X and Z values close to zero. - * - * Acceleration values are given in SI units (m/s^2) - * + /** + * A constant describing an accelerometer. + * See {@link android.hardware.SensorListener SensorListener} for more details. + * @deprecated use {@link android.hardware.Sensor Sensor} instead. */ + @Deprecated public static final int SENSOR_ACCELEROMETER = 1 << 1; - /** A constant describing a temperature sensor - * Only the first value is defined for this sensor and it - * contains the ambient temperature in degree C. + /** + * A constant describing a temperature sensor + * See {@link android.hardware.SensorListener SensorListener} for more details. + * @deprecated use {@link android.hardware.Sensor Sensor} instead. */ + @Deprecated public static final int SENSOR_TEMPERATURE = 1 << 2; - /** A constant describing a magnetic sensor - * Sensor values are the magnetic vector in the X, Y and Z axis, - * where the X axis has positive direction toward the right side of the device, - * the Y axis has positive direction toward the top of the device - * and the Z axis has positive direction toward the front of the device. - * - * Magnetic values are given in micro-Tesla (uT) - * + /** + * A constant describing a magnetic sensor + * See {@link android.hardware.SensorListener SensorListener} for more details. + * @deprecated use {@link android.hardware.Sensor Sensor} instead. */ + @Deprecated public static final int SENSOR_MAGNETIC_FIELD = 1 << 3; - /** A constant describing an ambient light sensor - * Only the first value is defined for this sensor and it contains - * the ambient light measure in lux. - * + /** + * A constant describing an ambient light sensor + * See {@link android.hardware.SensorListener SensorListener} for more details. + * @deprecated use {@link android.hardware.Sensor Sensor} instead. */ + @Deprecated public static final int SENSOR_LIGHT = 1 << 4; - /** A constant describing a proximity sensor - * Only the first value is defined for this sensor and it contains - * the distance between the sensor and the object in meters (m) + /** + * A constant describing a proximity sensor + * See {@link android.hardware.SensorListener SensorListener} for more details. + * @deprecated use {@link android.hardware.Sensor Sensor} instead. */ + @Deprecated public static final int SENSOR_PROXIMITY = 1 << 5; - /** A constant describing a Tricorder - * When this sensor is available and enabled, the device can be - * used as a fully functional Tricorder. All values are returned in - * SI units. + /** + * A constant describing a Tricorder + * See {@link android.hardware.SensorListener SensorListener} for more details. + * @deprecated use {@link android.hardware.Sensor Sensor} instead. */ + @Deprecated public static final int SENSOR_TRICORDER = 1 << 6; - /** A constant describing an orientation sensor. - * Sensor values are yaw, pitch and roll - * - * Yaw is the compass heading in degrees, 0 <= range < 360 - * 0 = North, 90 = East, 180 = South, 270 = West - * - * This is similar to SENSOR_ORIENTATION except the data is not - * smoothed or filtered in any way. + /** + * A constant describing an orientation sensor. + * See {@link android.hardware.SensorListener SensorListener} for more details. + * @deprecated use {@link android.hardware.Sensor Sensor} instead. */ + @Deprecated public static final int SENSOR_ORIENTATION_RAW = 1 << 7; /** A constant that includes all sensors */ + @Deprecated public static final int SENSOR_ALL = 0x7F; /** Smallest sensor ID */ + @Deprecated public static final int SENSOR_MIN = SENSOR_ORIENTATION; /** Largest sensor ID */ + @Deprecated public static final int SENSOR_MAX = ((SENSOR_ALL + 1)>>1); - /** Index of the X value in the array returned by + /** Index of the X value in the array returned by * {@link android.hardware.SensorListener#onSensorChanged} */ + @Deprecated public static final int DATA_X = 0; - /** Index of the Y value in the array returned by + /** Index of the Y value in the array returned by * {@link android.hardware.SensorListener#onSensorChanged} */ + @Deprecated public static final int DATA_Y = 1; - /** Index of the Z value in the array returned by + /** Index of the Z value in the array returned by * {@link android.hardware.SensorListener#onSensorChanged} */ + @Deprecated public static final int DATA_Z = 2; - - /** Offset to the raw values in the array returned by + + /** Offset to the untransformed values in the array returned by * {@link android.hardware.SensorListener#onSensorChanged} */ + @Deprecated public static final int RAW_DATA_INDEX = 3; - /** Index of the raw X value in the array returned by + /** Index of the untransformed X value in the array returned by * {@link android.hardware.SensorListener#onSensorChanged} */ + @Deprecated public static final int RAW_DATA_X = 3; - /** Index of the raw X value in the array returned by + /** Index of the untransformed Y value in the array returned by * {@link android.hardware.SensorListener#onSensorChanged} */ + @Deprecated public static final int RAW_DATA_Y = 4; - /** Index of the raw X value in the array returned by + /** Index of the untransformed Z value in the array returned by * {@link android.hardware.SensorListener#onSensorChanged} */ + @Deprecated public static final int RAW_DATA_Z = 5; - - + + /** Standard gravity (g) on Earth. This value is equivalent to 1G */ public static final float STANDARD_GRAVITY = 9.80665f; @@ -177,7 +173,7 @@ public class SensorManager extends IRotationWatcher.Stub public static final float GRAVITY_JUPITER = 23.12f; public static final float GRAVITY_SATURN = 8.96f; public static final float GRAVITY_URANUS = 8.69f; - public static final float GRAVITY_NEPTUN = 11.0f; + public static final float GRAVITY_NEPTUNE = 11.0f; public static final float GRAVITY_PLUTO = 0.6f; public static final float GRAVITY_DEATH_STAR_I = 0.000000353036145f; public static final float GRAVITY_THE_ISLAND = 4.815162342f; @@ -208,52 +204,84 @@ public class SensorManager extends IRotationWatcher.Stub /** rate suitable for the user interface */ public static final int SENSOR_DELAY_UI = 2; /** rate (default) suitable for screen orientation changes */ - public static final int SENSOR_DELAY_NORMAL = 3; + public static final int SENSOR_DELAY_NORMAL = 3; + - /** The values returned by this sensor cannot be trusted, calibration * is needed or the environment doesn't allow readings */ public static final int SENSOR_STATUS_UNRELIABLE = 0; - + /** This sensor is reporting data with low accuracy, calibration with the * environment is needed */ public static final int SENSOR_STATUS_ACCURACY_LOW = 1; - /** This sensor is reporting data with an average level of accuracy, + /** This sensor is reporting data with an average level of accuracy, * calibration with the environment may improve the readings */ public static final int SENSOR_STATUS_ACCURACY_MEDIUM = 2; - + /** This sensor is reporting data with maximum accuracy */ public static final int SENSOR_STATUS_ACCURACY_HIGH = 3; - + /** see {@link #remapCoordinateSystem} */ + public static final int AXIS_X = 1; + /** see {@link #remapCoordinateSystem} */ + public static final int AXIS_Y = 2; + /** see {@link #remapCoordinateSystem} */ + public static final int AXIS_Z = 3; + /** see {@link #remapCoordinateSystem} */ + public static final int AXIS_MINUS_X = AXIS_X | 0x80; + /** see {@link #remapCoordinateSystem} */ + public static final int AXIS_MINUS_Y = AXIS_Y | 0x80; + /** see {@link #remapCoordinateSystem} */ + public static final int AXIS_MINUS_Z = AXIS_Z | 0x80; + + /*-----------------------------------------------------------------------*/ - private static final int SENSOR_DISABLE = -1; - private static final int SENSOR_ORDER_MASK = 0x1F; - private static final int SENSOR_STATUS_SHIFT = 28; private ISensorService mSensorService; - private Looper mLooper; + Looper mMainLooper; + @SuppressWarnings("deprecation") + private HashMap<SensorListener, LegacyListener> mLegacyListenersMap = + new HashMap<SensorListener, LegacyListener>(); - private static IWindowManager sWindowManager; - private static int sRotation = 0; + /*-----------------------------------------------------------------------*/ - /* The thread and the sensor list are global to the process + private static final int SENSOR_DISABLE = -1; + private static boolean sSensorModuleInitialized = false; + private static ArrayList<Sensor> sFullSensorsList = new ArrayList<Sensor>(); + private static SparseArray<List<Sensor>> sSensorListByType = new SparseArray<List<Sensor>>(); + private static IWindowManager sWindowManager; + private static int sRotation = Surface.ROTATION_0; + /* The thread and the sensor list are global to the process * but the actual thread is spawned on demand */ - static final private SensorThread sSensorThread = new SensorThread(); - static final private ArrayList<ListenerDelegate> sListeners = + private static SensorThread sSensorThread; + + // Used within this module from outside SensorManager, don't make private + static SparseArray<Sensor> sHandleToSensor = new SparseArray<Sensor>(); + static final ArrayList<ListenerDelegate> sListeners = new ArrayList<ListenerDelegate>(); + /*-----------------------------------------------------------------------*/ static private class SensorThread { - private Thread mThread; + Thread mThread; + + SensorThread() { + // this gets to the sensor module. We can have only one per process. + sensors_data_init(); + } + + @Override + protected void finalize() { + sensors_data_uninit(); + } // must be called with sListeners lock void startLocked(ISensorService service) { try { if (mThread == null) { ParcelFileDescriptor fd = service.getDataChanel(); - mThread = new Thread(new SensorThreadRunnable(fd, service), + mThread = new Thread(new SensorThreadRunnable(fd), SensorThread.class.getName()); mThread.start(); } @@ -263,162 +291,165 @@ public class SensorManager extends IRotationWatcher.Stub } private class SensorThreadRunnable implements Runnable { - private ISensorService mSensorService; private ParcelFileDescriptor mSensorDataFd; - private final byte mAccuracies[] = new byte[32]; - SensorThreadRunnable(ParcelFileDescriptor fd, ISensorService service) { + SensorThreadRunnable(ParcelFileDescriptor fd) { mSensorDataFd = fd; - mSensorService = service; - Arrays.fill(mAccuracies, (byte)-1); } public void run() { - int sensors_of_interest; - float[] values = new float[6]; + //Log.d(TAG, "entering main sensor thread"); + final float[] values = new float[3]; + final int[] status = new int[1]; + final long timestamp[] = new long[1]; Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY); - synchronized (sListeners) { - _sensors_data_open(mSensorDataFd.getFileDescriptor()); - try { - mSensorDataFd.close(); - } catch (IOException e) { - // *shrug* - Log.e(TAG, "IOException: ", e); - } - mSensorDataFd = null; - //mSensorDataFd. - // the first time, compute the sensors we need. this is not - // a big deal if it changes by the time we call - // _sensors_data_poll, it'll get recomputed for the next - // round. - sensors_of_interest = 0; - final int size = sListeners.size(); - for (int i=0 ; i<size ; i++) { - sensors_of_interest |= sListeners.get(i).mSensors; - if ((sensors_of_interest & SENSOR_ALL) == SENSOR_ALL) - break; - } + if (mSensorDataFd == null) { + Log.e(TAG, "mSensorDataFd == NULL, exiting"); + return; } + // this thread is guaranteed to be unique + sensors_data_open(mSensorDataFd.getFileDescriptor()); + try { + mSensorDataFd.close(); + } catch (IOException e) { + // *shrug* + Log.e(TAG, "IOException: ", e); + } + mSensorDataFd = null; + while (true) { // wait for an event - final int sensor_result = _sensors_data_poll(values, sensors_of_interest); - final int sensor_order = sensor_result & SENSOR_ORDER_MASK; - final int sensor = 1 << sensor_result; - int accuracy = sensor_result>>>SENSOR_STATUS_SHIFT; - - if ((sensors_of_interest & sensor)!=0) { - // show the notification only if someone is listening for - // this sensor - if (accuracy != mAccuracies[sensor_order]) { - try { - mSensorService.reportAccuracy(sensor, accuracy); - mAccuracies[sensor_order] = (byte)accuracy; - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in reportAccuracy: ", e); - } - } else { - accuracy = -1; - } + final int sensor = sensors_data_poll(values, status, timestamp); + + if (sensor == -1) { + // we lost the connection to the event stream. this happens + // when the last listener is removed. + Log.d(TAG, "_sensors_data_poll() failed, we bail out."); + break; } - + + int accuracy = status[0]; synchronized (sListeners) { if (sListeners.isEmpty()) { // we have no more listeners, terminate the thread - _sensors_data_close(); + sensors_data_close(); mThread = null; break; } - // convert for the current screen orientation - mapSensorDataToWindow(sensor, values, SensorManager.getRotation()); - // report the sensor event to all listeners that - // care about it. - sensors_of_interest = 0; - final int size = sListeners.size(); - for (int i=0 ; i<size ; i++) { - ListenerDelegate listener = sListeners.get(i); - sensors_of_interest |= listener.mSensors; - if (listener.hasSensor(sensor)) { - // this is asynchronous (okay to call - // with sListeners lock held. - listener.onSensorChanged(sensor, values, accuracy); + final Sensor sensorObject = sHandleToSensor.get(sensor); + if (sensorObject != null) { + // report the sensor event to all listeners that + // care about it. + final int size = sListeners.size(); + for (int i=0 ; i<size ; i++) { + ListenerDelegate listener = sListeners.get(i); + if (listener.hasSensor(sensorObject)) { + // this is asynchronous (okay to call + // with sListeners lock held). + listener.onSensorChangedLocked(sensorObject, + values, timestamp, accuracy); + } } } } } + //Log.d(TAG, "exiting main sensor thread"); } } } - private class ListenerDelegate extends Binder { - - private SensorListener mListener; - private int mSensors; - private float[] mValuesPool; + /*-----------------------------------------------------------------------*/ - ListenerDelegate(SensorListener listener, int sensors) { - mListener = listener; - mSensors = sensors; - mValuesPool = new float[6]; + private class ListenerDelegate extends Binder { + final SensorEventListener mSensorEventListener; + private final ArrayList<Sensor> mSensorList = new ArrayList<Sensor>(); + private final Handler mHandler; + private SensorEvent mValuesPool; + public int mSensors; + + ListenerDelegate(SensorEventListener listener, Sensor sensor, Handler handler) { + mSensorEventListener = listener; + Looper looper = (handler != null) ? handler.getLooper() : mMainLooper; + // currently we create one Handler instance per listener, but we could + // have one per looper (we'd need to pass the ListenerDelegate + // instance to handleMessage and keep track of them separately). + mHandler = new Handler(looper) { + @Override + public void handleMessage(Message msg) { + SensorEvent t = (SensorEvent)msg.obj; + if (t.accuracy >= 0) { + mSensorEventListener.onAccuracyChanged(t.sensor, t.accuracy); + } + mSensorEventListener.onSensorChanged(t); + returnToPool(t); + } + }; + addSensor(sensor); } - int addSensors(int sensors) { - mSensors |= sensors; - return mSensors; - } - int removeSensors(int sensors) { - mSensors &= ~sensors; - return mSensors; - } - boolean hasSensor(int sensor) { - return ((mSensors & sensor) != 0); + protected SensorEvent createSensorEvent() { + // maximal size for all legacy events is 3 + return new SensorEvent(3); } - void onSensorChanged(int sensor, float[] values, int accuracy) { - float[] v; + protected SensorEvent getFromPool() { + SensorEvent t = null; synchronized (this) { // remove the array from the pool - v = mValuesPool; + t = mValuesPool; mValuesPool = null; } + if (t == null) { + // the pool was empty, we need a new one + t = createSensorEvent(); + } + return t; + } - if (v != null) { - v[0] = values[0]; - v[1] = values[1]; - v[2] = values[2]; - v[3] = values[3]; - v[4] = values[4]; - v[5] = values[5]; - } else { - // the pool was empty, we need to dup the array - v = values.clone(); + protected void returnToPool(SensorEvent t) { + synchronized (this) { + // put back the array into the pool + if (mValuesPool == null) { + mValuesPool = t; + } } + } + + Object getListener() { + return mSensorEventListener; + } + + int addSensor(Sensor sensor) { + mSensors |= 1<<sensor.getHandle(); + mSensorList.add(sensor); + return mSensors; + } + int removeSensor(Sensor sensor) { + mSensors &= ~(1<<sensor.getHandle()); + mSensorList.remove(sensor); + return mSensors; + } + boolean hasSensor(Sensor sensor) { + return ((mSensors & (1<<sensor.getHandle())) != 0); + } + List<Sensor> getSensors() { + return mSensorList; + } + void onSensorChangedLocked(Sensor sensor, float[] values, long[] timestamp, int accuracy) { + SensorEvent t = getFromPool(); + final float[] v = t.values; + v[0] = values[0]; + v[1] = values[1]; + v[2] = values[2]; + t.timestamp = timestamp[0]; + t.accuracy = accuracy; + t.sensor = sensor; Message msg = Message.obtain(); - msg.what = sensor; - msg.obj = v; - msg.arg1 = accuracy; + msg.what = 0; + msg.obj = t; mHandler.sendMessage(msg); } - - private final Handler mHandler = new Handler(mLooper) { - @Override public void handleMessage(Message msg) { - if (msg.arg1 >= 0) { - try { - mListener.onAccuracyChanged(msg.what, msg.arg1); - } catch (AbstractMethodError e) { - // old app that doesn't implement this method - // just ignore it. - } - } - mListener.onSensorChanged(msg.what, (float[])msg.obj); - synchronized (this) { - // put back the array into the pool - if (mValuesPool == null) { - mValuesPool = (float[])msg.obj; - } - } - } - }; } /** @@ -427,41 +458,157 @@ public class SensorManager extends IRotationWatcher.Stub public SensorManager(Looper mainLooper) { mSensorService = ISensorService.Stub.asInterface( ServiceManager.getService(Context.SENSOR_SERVICE)); - - sWindowManager = IWindowManager.Stub.asInterface( - ServiceManager.getService("window")); + mMainLooper = mainLooper; - if (sWindowManager != null) { - // if it's null we're running in the system process - // which won't get the rotated values - try { - sWindowManager.watchRotation(this); - } catch (RemoteException e) { + + synchronized(sListeners) { + if (!sSensorModuleInitialized) { + sSensorModuleInitialized = true; + + nativeClassInit(); + + sWindowManager = IWindowManager.Stub.asInterface( + ServiceManager.getService("window")); + if (sWindowManager != null) { + // if it's null we're running in the system process + // which won't get the rotated values + try { + sRotation = sWindowManager.watchRotation(this); + } catch (RemoteException e) { + } + } + + // initialize the sensor list + sensors_module_init(); + final ArrayList<Sensor> fullList = sFullSensorsList; + int i = 0; + do { + Sensor sensor = new Sensor(); + i = sensors_module_get_next_sensor(sensor, i); + + if (i>=0) { + Log.d(TAG, "found sensor: " + sensor.getName() + + ", handle=" + sensor.getHandle()); + sensor.setLegacyType(getLegacySensorType(sensor.getType())); + fullList.add(sensor); + sHandleToSensor.append(sensor.getHandle(), sensor); + } + } while (i>0); + + sSensorThread = new SensorThread(); } } + } - mLooper = mainLooper; + private int getLegacySensorType(int type) { + switch (type) { + case Sensor.TYPE_ACCELEROMETER: + return SENSOR_ACCELEROMETER; + case Sensor.TYPE_MAGNETIC_FIELD: + return SENSOR_MAGNETIC_FIELD; + case Sensor.TYPE_ORIENTATION: + return SENSOR_ORIENTATION_RAW; + case Sensor.TYPE_TEMPERATURE: + return SENSOR_TEMPERATURE; + } + return 0; } - /** @return available sensors */ + /** @return available sensors. + * @deprecated This method is deprecated, use + * {@link SensorManager#getSensorList(int)} instead + */ + @Deprecated public int getSensors() { - return _sensors_data_get_sensors(); + int result = 0; + final ArrayList<Sensor> fullList = sFullSensorsList; + for (Sensor i : fullList) { + switch (i.getType()) { + case Sensor.TYPE_ACCELEROMETER: + result |= SensorManager.SENSOR_ACCELEROMETER; + break; + case Sensor.TYPE_MAGNETIC_FIELD: + result |= SensorManager.SENSOR_MAGNETIC_FIELD; + break; + case Sensor.TYPE_ORIENTATION: + result |= SensorManager.SENSOR_ORIENTATION | + SensorManager.SENSOR_ORIENTATION_RAW; + break; + } + } + return result; } /** + * Use this method to get the list of available sensors of a certain + * type. Make multiple calls to get sensors of different types or use + * {@link android.hardware.Sensor#TYPE_ALL Sensor.TYPE_ALL} to get all + * the sensors. + * + * @param type of sensors requested + * @return a list of sensors matching the asked type. + */ + public List<Sensor> getSensorList(int type) { + // cache the returned lists the first time + List<Sensor> list; + final ArrayList<Sensor> fullList = sFullSensorsList; + synchronized(fullList) { + list = sSensorListByType.get(type); + if (list == null) { + if (type == Sensor.TYPE_ALL) { + list = fullList; + } else { + list = new ArrayList<Sensor>(); + for (Sensor i : fullList) { + if (i.getType() == type) + list.add(i); + } + } + list = Collections.unmodifiableList(list); + sSensorListByType.append(type, list); + } + } + return list; + } + + /** + * Use this method to get the default sensor for a given type. Note that + * the returned sensor could be a composite sensor, and its data could be + * averaged or filtered. If you need to access the raw sensors use + * {@link SensorManager#getSensorList(int) getSensorList}. + * + * + * @param type of sensors requested + * @return the default sensors matching the asked type. + */ + public Sensor getDefaultSensor(int type) { + // TODO: need to be smarter, for now, just return the 1st sensor + List<Sensor> l = getSensorList(type); + return l.isEmpty() ? null : l.get(0); + } + + + /** * Registers a listener for given sensors. + * @deprecated This method is deprecated, use + * {@link SensorManager#registerListener(SensorEventListener, Sensor, int)} + * instead. * * @param listener sensor listener object * @param sensors a bit masks of the sensors to register to * * @return true if the sensor is supported and successfully enabled */ + @Deprecated public boolean registerListener(SensorListener listener, int sensors) { return registerListener(listener, sensors, SENSOR_DELAY_NORMAL); } /** - * Registers a listener for given sensors. + * Registers a SensorListener for given sensors. + * @deprecated This method is deprecated, use + * {@link SensorManager#registerListener(SensorEventListener, Sensor, int)} + * instead. * * @param listener sensor listener object * @param sensors a bit masks of the sensors to register to @@ -471,9 +618,189 @@ public class SensorManager extends IRotationWatcher.Stub * * @return true if the sensor is supported and successfully enabled */ + @Deprecated public boolean registerListener(SensorListener listener, int sensors, int rate) { - boolean result; + boolean result = false; + result = registerLegacyListener(SENSOR_ACCELEROMETER, Sensor.TYPE_ACCELEROMETER, + listener, sensors, rate) || result; + result = registerLegacyListener(SENSOR_MAGNETIC_FIELD, Sensor.TYPE_MAGNETIC_FIELD, + listener, sensors, rate) || result; + result = registerLegacyListener(SENSOR_ORIENTATION_RAW, Sensor.TYPE_ORIENTATION, + listener, sensors, rate) || result; + result = registerLegacyListener(SENSOR_ORIENTATION, Sensor.TYPE_ORIENTATION, + listener, sensors, rate) || result; + result = registerLegacyListener(SENSOR_TEMPERATURE, Sensor.TYPE_TEMPERATURE, + listener, sensors, rate) || result; + return result; + } + @SuppressWarnings("deprecation") + private boolean registerLegacyListener(int legacyType, int type, + SensorListener listener, int sensors, int rate) + { + boolean result = false; + // Are we activating this legacy sensor? + if ((sensors & legacyType) != 0) { + // if so, find a suitable Sensor + Sensor sensor = getDefaultSensor(type); + if (sensor != null) { + // If we don't already have one, create a LegacyListener + // to wrap this listener and process the events as + // they are expected by legacy apps. + LegacyListener legacyListener = null; + synchronized (mLegacyListenersMap) { + legacyListener = mLegacyListenersMap.get(listener); + if (legacyListener == null) { + // we didn't find a LegacyListener for this client, + // create one, and put it in our list. + legacyListener = new LegacyListener(listener); + mLegacyListenersMap.put(listener, legacyListener); + } + } + // register this legacy sensor with this legacy listener + legacyListener.registerSensor(legacyType); + // and finally, register the legacy listener with the new apis + result = registerListener(legacyListener, sensor, rate); + } + } + return result; + } + + /** + * Unregisters a listener for the sensors with which it is registered. + * @deprecated This method is deprecated, use + * {@link SensorManager#unregisterListener(SensorEventListener, Sensor)} + * instead. + * + * @param listener a SensorListener object + * @param sensors a bit masks of the sensors to unregister from + */ + @Deprecated + public void unregisterListener(SensorListener listener, int sensors) { + unregisterLegacyListener(SENSOR_ACCELEROMETER, Sensor.TYPE_ACCELEROMETER, + listener, sensors); + unregisterLegacyListener(SENSOR_MAGNETIC_FIELD, Sensor.TYPE_MAGNETIC_FIELD, + listener, sensors); + unregisterLegacyListener(SENSOR_ORIENTATION_RAW, Sensor.TYPE_ORIENTATION, + listener, sensors); + unregisterLegacyListener(SENSOR_ORIENTATION, Sensor.TYPE_ORIENTATION, + listener, sensors); + unregisterLegacyListener(SENSOR_TEMPERATURE, Sensor.TYPE_TEMPERATURE, + listener, sensors); + } + + @SuppressWarnings("deprecation") + private void unregisterLegacyListener(int legacyType, int type, + SensorListener listener, int sensors) + { + // do we know about this listener? + LegacyListener legacyListener = null; + synchronized (mLegacyListenersMap) { + legacyListener = mLegacyListenersMap.get(listener); + } + if (legacyListener != null) { + // Are we deactivating this legacy sensor? + if ((sensors & legacyType) != 0) { + // if so, find the corresponding Sensor + Sensor sensor = getDefaultSensor(type); + if (sensor != null) { + // unregister this legacy sensor and if we don't + // need the corresponding Sensor, unregister it too + if (legacyListener.unregisterSensor(legacyType)) { + // corresponding sensor not needed, unregister + unregisterListener(legacyListener, sensor); + // finally check if we still need the legacyListener + // in our mapping, if not, get rid of it too. + synchronized(sListeners) { + boolean found = false; + for (ListenerDelegate i : sListeners) { + if (i.getListener() == legacyListener) { + found = true; + break; + } + } + if (!found) { + synchronized (mLegacyListenersMap) { + mLegacyListenersMap.remove(listener); + } + } + } + } + } + } + } + } + + /** + * Unregisters a listener for all sensors. + * @deprecated This method is deprecated, use + * {@link SensorManager#unregisterListener(SensorEventListener)} + * instead. + * + * @param listener a SensorListener object + */ + @Deprecated + public void unregisterListener(SensorListener listener) { + unregisterListener(listener, SENSOR_ALL); + } + + /** + * Unregisters a listener for the sensors with which it is registered. + * + * @param listener a SensorEventListener object + * @param sensor the sensor to unregister from + * + */ + public void unregisterListener(SensorEventListener listener, Sensor sensor) { + unregisterListener((Object)listener, sensor); + } + + /** + * Unregisters a listener for all sensors. + * + * @param listener a SensorListener object + * + */ + public void unregisterListener(SensorEventListener listener) { + unregisterListener((Object)listener); + } + + + /** + * Registers a {@link android.hardware.SensorEventListener SensorEventListener} + * for the given sensor. + * + * @param listener A {@link android.hardware.SensorEventListener SensorEventListener} object. + * @param sensor The {@link android.hardware.Sensor Sensor} to register to. + * @param rate The rate {@link android.hardware.SensorEvent sensor events} are delivered at. + * This is only a hint to the system. Events may be received faster or + * slower than the specified rate. Usually events are received faster. + * + * @return true if the sensor is supported and successfully enabled. + * + */ + public boolean registerListener(SensorEventListener listener, Sensor sensor, int rate) { + return registerListener(listener, sensor, rate, null); + } + + /** + * Registers a {@link android.hardware.SensorEventListener SensorEventListener} + * for the given sensor. + * + * @param listener A {@link android.hardware.SensorEventListener SensorEventListener} object. + * @param sensor The {@link android.hardware.Sensor Sensor} to register to. + * @param rate The rate {@link android.hardware.SensorEvent sensor events} are delivered at. + * This is only a hint to the system. Events may be received faster or + * slower than the specified rate. Usually events are received faster. + * @param handler The {@link android.os.Handler Handler} the + * {@link android.hardware.SensorEvent sensor events} will be delivered to. + * + * @return true if the sensor is supported and successfully enabled. + * + */ + public boolean registerListener(SensorEventListener listener, Sensor sensor, int rate, + Handler handler) { + boolean result; int delay = -1; switch (rate) { case SENSOR_DELAY_FASTEST: @@ -496,15 +823,15 @@ public class SensorManager extends IRotationWatcher.Stub synchronized (sListeners) { ListenerDelegate l = null; for (ListenerDelegate i : sListeners) { - if (i.mListener == listener) { + if (i.getListener() == listener) { l = i; break; } } if (l == null) { - l = new ListenerDelegate(listener, sensors); - result = mSensorService.enableSensor(l, sensors, delay); + l = new ListenerDelegate(listener, sensor, handler); + result = mSensorService.enableSensor(l, sensor.getHandle(), delay); if (result) { sListeners.add(l); sListeners.notify(); @@ -513,9 +840,9 @@ public class SensorManager extends IRotationWatcher.Stub sSensorThread.startLocked(mSensorService); } } else { - result = mSensorService.enableSensor(l, sensors, delay); + result = mSensorService.enableSensor(l, sensor.getHandle(), delay); if (result) { - l.addSensors(sensors); + l.addSensor(sensor); } } } @@ -526,25 +853,42 @@ public class SensorManager extends IRotationWatcher.Stub return result; } - /** - * Unregisters a listener for the sensors with which it is registered. - * - * @param listener a SensorListener object - * @param sensors a bit masks of the sensors to unregister from - */ - public void unregisterListener(SensorListener listener, int sensors) { + private void unregisterListener(Object listener, Sensor sensor) { try { synchronized (sListeners) { final int size = sListeners.size(); for (int i=0 ; i<size ; i++) { ListenerDelegate l = sListeners.get(i); - if (l.mListener == listener) { + if (l.getListener() == listener) { // disable these sensors - mSensorService.enableSensor(l, sensors, SENSOR_DISABLE); + int handle = sensor.getHandle(); + mSensorService.enableSensor(l, handle, SENSOR_DISABLE); // if we have no more sensors enabled on this listener, // take it off the list. - if (l.removeSensors(sensors) == 0) + if (l.removeSensor(sensor) == 0) { sListeners.remove(i); + } + break; + } + } + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in unregisterListener: ", e); + } + } + + private void unregisterListener(Object listener) { + try { + synchronized (sListeners) { + final int size = sListeners.size(); + for (int i=0 ; i<size ; i++) { + ListenerDelegate l = sListeners.get(i); + if (l.getListener() == listener) { + // disable all sensors for this listener + for (Sensor sensor : l.getSensors()) { + mSensorService.enableSensor(l, sensor.getHandle(), SENSOR_DISABLE); + } + sListeners.remove(i); break; } } @@ -555,65 +899,541 @@ public class SensorManager extends IRotationWatcher.Stub } /** - * Unregisters a listener for all sensors. + * Computes the inclination matrix <b>I</b> as well as the rotation + * matrix <b>R</b> transforming a vector from the + * device coordinate system to the world's coordinate system which is + * defined as a direct orthonormal basis, where: + * + * <li>X is defined as the vector product <b>Y.Z</b> (It is tangential to + * the ground at the device's current location and roughly points East).</li> + * <li>Y is tangential to the ground at the device's current location and + * points towards the magnetic North Pole.</li> + * <li>Z points towards the sky and is perpendicular to the ground.</li> + * <p> + * <hr> + * <p>By definition: + * <p>[0 0 g] = <b>R</b> * <b>gravity</b> (g = magnitude of gravity) + * <p>[0 m 0] = <b>I</b> * <b>R</b> * <b>geomagnetic</b> + * (m = magnitude of geomagnetic field) + * <p><b>R</b> is the identity matrix when the device is aligned with the + * world's coordinate system, that is, when the device's X axis points + * toward East, the Y axis points to the North Pole and the device is facing + * the sky. * - * @param listener a SensorListener object + * <p><b>I</b> is a rotation matrix transforming the geomagnetic + * vector into the same coordinate space as gravity (the world's coordinate + * space). <b>I</b> is a simple rotation around the X axis. + * The inclination angle in radians can be computed with + * {@link #getInclination}. + * <hr> + * + * <p> Each matrix is returned either as a 3x3 or 4x4 row-major matrix + * depending on the length of the passed array: + * <p><u>If the array length is 16:</u> + * <pre> + * / M[ 0] M[ 1] M[ 2] M[ 3] \ + * | M[ 4] M[ 5] M[ 6] M[ 7] | + * | M[ 8] M[ 9] M[10] M[11] | + * \ M[12] M[13] M[14] M[15] / + *</pre> + * This matrix is ready to be used by OpenGL ES's + * {@link javax.microedition.khronos.opengles.GL10#glLoadMatrixf(float[], int) + * glLoadMatrixf(float[], int)}. + * <p>Note that because OpenGL matrices are column-major matrices you must + * transpose the matrix before using it. However, since the matrix is a + * rotation matrix, its transpose is also its inverse, conveniently, it is + * often the inverse of the rotation that is needed for rendering; it can + * therefore be used with OpenGL ES directly. + * <p> + * Also note that the returned matrices always have this form: + * <pre> + * / M[ 0] M[ 1] M[ 2] 0 \ + * | M[ 4] M[ 5] M[ 6] 0 | + * | M[ 8] M[ 9] M[10] 0 | + * \ 0 0 0 1 / + *</pre> + * <p><u>If the array length is 9:</u> + * <pre> + * / M[ 0] M[ 1] M[ 2] \ + * | M[ 3] M[ 4] M[ 5] | + * \ M[ 6] M[ 7] M[ 8] / + *</pre> + * + * <hr> + * <p>The inverse of each matrix can be computed easily by taking its + * transpose. + * + * <p>The matrices returned by this function are meaningful only when the + * device is not free-falling and it is not close to the magnetic north. + * If the device is accelerating, or placed into a strong magnetic field, + * the returned matrices may be inaccurate. + * + * @param R is an array of 9 floats holding the rotation matrix <b>R</b> + * when this function returns. R can be null.<p> + * @param I is an array of 9 floats holding the rotation matrix <b>I</b> + * when this function returns. I can be null.<p> + * @param gravity is an array of 3 floats containing the gravity vector + * expressed in the device's coordinate. You can simply use the + * {@link android.hardware.SensorEvent#values values} + * returned by a {@link android.hardware.SensorEvent SensorEvent} of a + * {@link android.hardware.Sensor Sensor} of type + * {@link android.hardware.Sensor#TYPE_ACCELEROMETER TYPE_ACCELEROMETER}.<p> + * @param geomagnetic is an array of 3 floats containing the geomagnetic + * vector expressed in the device's coordinate. You can simply use the + * {@link android.hardware.SensorEvent#values values} + * returned by a {@link android.hardware.SensorEvent SensorEvent} of a + * {@link android.hardware.Sensor Sensor} of type + * {@link android.hardware.Sensor#TYPE_MAGNETIC_FIELD TYPE_MAGNETIC_FIELD}. + * @return + * true on success<p> + * false on failure (for instance, if the device is in free fall). + * On failure the output matrices are not modified. */ - public void unregisterListener(SensorListener listener) { - unregisterListener(listener, SENSOR_ALL); + + public static boolean getRotationMatrix(float[] R, float[] I, + float[] gravity, float[] geomagnetic) { + // TODO: move this to native code for efficiency + float Ax = gravity[0]; + float Ay = gravity[1]; + float Az = gravity[2]; + final float Ex = geomagnetic[0]; + final float Ey = geomagnetic[1]; + final float Ez = geomagnetic[2]; + float Hx = Ey*Az - Ez*Ay; + float Hy = Ez*Ax - Ex*Az; + float Hz = Ex*Ay - Ey*Ax; + final float normH = (float)Math.sqrt(Hx*Hx + Hy*Hy + Hz*Hz); + if (normH < 0.1f) { + // device is close to free fall (or in space?), or close to + // magnetic north pole. Typical values are > 100. + return false; + } + final float invH = 1.0f / normH; + Hx *= invH; + Hy *= invH; + Hz *= invH; + final float invA = 1.0f / (float)Math.sqrt(Ax*Ax + Ay*Ay + Az*Az); + Ax *= invA; + Ay *= invA; + Az *= invA; + final float Mx = Ay*Hz - Az*Hy; + final float My = Az*Hx - Ax*Hz; + final float Mz = Ax*Hy - Ay*Hx; + if (R != null) { + if (R.length == 9) { + R[0] = Hx; R[1] = Hy; R[2] = Hz; + R[3] = Mx; R[4] = My; R[5] = Mz; + R[6] = Ax; R[7] = Ay; R[8] = Az; + } else if (R.length == 16) { + R[0] = Hx; R[1] = Hy; R[2] = Hz; R[3] = 0; + R[4] = Mx; R[5] = My; R[6] = Mz; R[7] = 0; + R[8] = Ax; R[9] = Ay; R[10] = Az; R[11] = 0; + R[12] = 0; R[13] = 0; R[14] = 0; R[15] = 1; + } + } + if (I != null) { + // compute the inclination matrix by projecting the geomagnetic + // vector onto the Z (gravity) and X (horizontal component + // of geomagnetic vector) axes. + final float invE = 1.0f / (float)Math.sqrt(Ex*Ex + Ey*Ey + Ez*Ez); + final float c = (Ex*Mx + Ey*My + Ez*Mz) * invE; + final float s = (Ex*Ax + Ey*Ay + Ez*Az) * invE; + if (I.length == 9) { + I[0] = 1; I[1] = 0; I[2] = 0; + I[3] = 0; I[4] = c; I[5] = s; + I[6] = 0; I[7] =-s; I[8] = c; + } else if (I.length == 16) { + I[0] = 1; I[1] = 0; I[2] = 0; + I[4] = 0; I[5] = c; I[6] = s; + I[8] = 0; I[9] =-s; I[10]= c; + I[3] = I[7] = I[11] = I[12] = I[13] = I[14] = 0; + I[15] = 1; + } + } + return true; } + /** + * Computes the geomagnetic inclination angle in radians from the + * inclination matrix <b>I</b> returned by {@link #getRotationMatrix}. + * @param I inclination matrix see {@link #getRotationMatrix}. + * @return The geomagnetic inclination angle in radians. + */ + public static float getInclination(float[] I) { + if (I.length == 9) { + return (float)Math.atan2(I[5], I[4]); + } else { + return (float)Math.atan2(I[6], I[5]); + } + } /** - * Helper function to convert the specified sensor's data to the windows's - * coordinate space from the device's coordinate space. - */ - - private static void mapSensorDataToWindow(int sensor, float[] values, int orientation) { - final float x = values[DATA_X]; - final float y = values[DATA_Y]; - final float z = values[DATA_Z]; - // copy the raw raw values... - values[RAW_DATA_X] = x; - values[RAW_DATA_Y] = y; - values[RAW_DATA_Z] = z; - // TODO: add support for 180 and 270 orientations - if (orientation == Surface.ROTATION_90) { - switch (sensor) { - case SENSOR_ACCELEROMETER: - case SENSOR_MAGNETIC_FIELD: - values[DATA_X] =-y; - values[DATA_Y] = x; - values[DATA_Z] = z; - break; - case SENSOR_ORIENTATION: - case SENSOR_ORIENTATION_RAW: - values[DATA_X] = x + ((x < 270) ? 90 : -270); - values[DATA_Y] = z; - values[DATA_Z] = y; - break; + * Rotates the supplied rotation matrix so it is expressed in a + * different coordinate system. This is typically used when an application + * needs to compute the three orientation angles of the device (see + * {@link #getOrientation}) in a different coordinate system. + * + * <p>When the rotation matrix is used for drawing (for instance with + * OpenGL ES), it usually <b>doesn't need</b> to be transformed by this + * function, unless the screen is physically rotated, such as when used + * in landscape mode. + * + * <p><u>Examples:</u><p> + * + * <li>Using the camera (Y axis along the camera's axis) for an augmented + * reality application where the rotation angles are needed :</li><p> + * + * <code>remapCoordinateSystem(inR, AXIS_X, AXIS_Z, outR);</code><p> + * + * <li>Using the device as a mechanical compass in landscape mode:</li><p> + * + * <code>remapCoordinateSystem(inR, AXIS_Y, AXIS_MINUS_X, outR);</code><p> + * + * Beware of the above example. This call is needed only if the device is + * physically used in landscape mode to calculate the rotation angles (see + * {@link #getOrientation}). + * If the rotation matrix is also used for rendering, it may not need to + * be transformed, for instance if your {@link android.app.Activity + * Activity} is running in landscape mode. + * + * <p>Since the resulting coordinate system is orthonormal, only two axes + * need to be specified. + * + * @param inR the rotation matrix to be transformed. Usually it is the + * matrix returned by {@link #getRotationMatrix}. + * @param X defines on which world axis and direction the X axis of the + * device is mapped. + * @param Y defines on which world axis and direction the Y axis of the + * device is mapped. + * @param outR the transformed rotation matrix. inR and outR can be the same + * array, but it is not recommended for performance reason. + * @return true on success. false if the input parameters are incorrect, for + * instance if X and Y define the same axis. Or if inR and outR don't have + * the same length. + */ + + public static boolean remapCoordinateSystem(float[] inR, int X, int Y, + float[] outR) + { + if (inR == outR) { + final float[] temp = mTempMatrix; + synchronized(temp) { + // we don't expect to have a lot of contention + if (remapCoordinateSystemImpl(inR, X, Y, temp)) { + final int size = outR.length; + for (int i=0 ; i<size ; i++) + outR[i] = temp[i]; + return true; + } } } + return remapCoordinateSystemImpl(inR, X, Y, outR); } + private static boolean remapCoordinateSystemImpl(float[] inR, int X, int Y, + float[] outR) + { + /* + * X and Y define a rotation matrix 'r': + * + * (X==1)?((X&0x80)?-1:1):0 (X==2)?((X&0x80)?-1:1):0 (X==3)?((X&0x80)?-1:1):0 + * (Y==1)?((Y&0x80)?-1:1):0 (Y==2)?((Y&0x80)?-1:1):0 (Y==3)?((X&0x80)?-1:1):0 + * r[0] ^ r[1] + * + * where the 3rd line is the vector product of the first 2 lines + * + */ + + final int length = outR.length; + if (inR.length != length) + return false; // invalid parameter + if ((X & 0x7C)!=0 || (Y & 0x7C)!=0) + return false; // invalid parameter + if (((X & 0x3)==0) || ((Y & 0x3)==0)) + return false; // no axis specified + if ((X & 0x3) == (Y & 0x3)) + return false; // same axis specified + + // Z is "the other" axis, its sign is either +/- sign(X)*sign(Y) + // this can be calculated by exclusive-or'ing X and Y; except for + // the sign inversion (+/-) which is calculated below. + int Z = X ^ Y; + + // extract the axis (remove the sign), offset in the range 0 to 2. + final int x = (X & 0x3)-1; + final int y = (Y & 0x3)-1; + final int z = (Z & 0x3)-1; + + // compute the sign of Z (whether it needs to be inverted) + final int axis_y = (z+1)%3; + final int axis_z = (z+2)%3; + if (((x^axis_y)|(y^axis_z)) != 0) + Z ^= 0x80; + + final boolean sx = (X>=0x80); + final boolean sy = (Y>=0x80); + final boolean sz = (Z>=0x80); + + // Perform R * r, in avoiding actual muls and adds. + final int rowLength = ((length==16)?4:3); + for (int j=0 ; j<3 ; j++) { + final int offset = j*rowLength; + for (int i=0 ; i<3 ; i++) { + if (x==i) outR[offset+i] = sx ? -inR[offset+0] : inR[offset+0]; + if (y==i) outR[offset+i] = sy ? -inR[offset+1] : inR[offset+1]; + if (z==i) outR[offset+i] = sz ? -inR[offset+2] : inR[offset+2]; + } + } + if (length == 16) { + outR[3] = outR[7] = outR[11] = outR[12] = outR[13] = outR[14] = 0; + outR[15] = 1; + } + return true; + } - private static native int _sensors_data_open(FileDescriptor fd); - private static native int _sensors_data_close(); - // returns the sensor's status in the top 4 bits of "res". - private static native int _sensors_data_poll(float[] values, int sensors); - private static native int _sensors_data_get_sensors(); + /** + * Computes the device's orientation based on the rotation matrix. + * <p> When it returns, the array values is filled with the result: + * <li>values[0]: <i>azimuth</i>, rotation around the Z axis.</li> + * <li>values[1]: <i>pitch</i>, rotation around the X axis.</li> + * <li>values[2]: <i>roll</i>, rotation around the Y axis.</li> + * <p> + * + * @param R rotation matrix see {@link #getRotationMatrix}. + * @param values an array of 3 floats to hold the result. + * @return The array values passed as argument. + */ + public static float[] getOrientation(float[] R, float values[]) { + /* + * 4x4 (length=16) case: + * / R[ 0] R[ 1] R[ 2] 0 \ + * | R[ 4] R[ 5] R[ 6] 0 | + * | R[ 8] R[ 9] R[10] 0 | + * \ 0 0 0 1 / + * + * 3x3 (length=9) case: + * / R[ 0] R[ 1] R[ 2] \ + * | R[ 3] R[ 4] R[ 5] | + * \ R[ 6] R[ 7] R[ 8] / + * + */ + if (R.length == 9) { + values[0] = (float)Math.atan2(R[1], R[4]); + values[1] = (float)Math.asin(-R[7]); + values[2] = (float)Math.atan2(-R[6], R[8]); + } else { + values[0] = (float)Math.atan2(R[1], R[5]); + values[1] = (float)Math.asin(-R[9]); + values[2] = (float)Math.atan2(-R[8], R[10]); + } + return values; + } - /** {@hide} */ + + /** + * {@hide} + */ public void onRotationChanged(int rotation) { synchronized(sListeners) { sRotation = rotation; } } - - private static int getRotation() { + + static int getRotation() { synchronized(sListeners) { return sRotation; } } -} + private class LegacyListener implements SensorEventListener { + private float mValues[] = new float[6]; + @SuppressWarnings("deprecation") + private SensorListener mTarget; + private int mSensors; + private final LmsFilter mYawfilter = new LmsFilter(); + + @SuppressWarnings("deprecation") + LegacyListener(SensorListener target) { + mTarget = target; + mSensors = 0; + } + + void registerSensor(int legacyType) { + mSensors |= legacyType; + } + + boolean unregisterSensor(int legacyType) { + mSensors &= ~legacyType; + int mask = SENSOR_ORIENTATION|SENSOR_ORIENTATION_RAW; + if (((legacyType&mask)!=0) && ((mSensors&mask)!=0)) { + return false; + } + return true; + } + + @SuppressWarnings("deprecation") + public void onAccuracyChanged(Sensor sensor, int accuracy) { + try { + mTarget.onAccuracyChanged(sensor.getLegacyType(), accuracy); + } catch (AbstractMethodError e) { + // old app that doesn't implement this method + // just ignore it. + } + } + + @SuppressWarnings("deprecation") + public void onSensorChanged(SensorEvent event) { + final float v[] = mValues; + v[0] = event.values[0]; + v[1] = event.values[1]; + v[2] = event.values[2]; + int legacyType = event.sensor.getLegacyType(); + mapSensorDataToWindow(legacyType, v, SensorManager.getRotation()); + if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) { + if ((mSensors & SENSOR_ORIENTATION_RAW)!=0) { + mTarget.onSensorChanged(SENSOR_ORIENTATION_RAW, v); + } + if ((mSensors & SENSOR_ORIENTATION)!=0) { + v[0] = mYawfilter.filter(event.timestamp, v[0]); + mTarget.onSensorChanged(SENSOR_ORIENTATION, v); + } + } else { + mTarget.onSensorChanged(legacyType, v); + } + } + + /* + * Helper function to convert the specified sensor's data to the windows's + * coordinate space from the device's coordinate space. + * + * output: 3,4,5: values in the old API format + * 0,1,2: transformed values in the old API format + * + */ + private void mapSensorDataToWindow(int sensor, + float[] values, int orientation) { + float x = values[0]; + float y = values[1]; + float z = values[2]; + + switch (sensor) { + case SensorManager.SENSOR_ORIENTATION: + case SensorManager.SENSOR_ORIENTATION_RAW: + z = -z; + break; + case SensorManager.SENSOR_ACCELEROMETER: + x = -x; + y = -y; + z = -z; + break; + case SensorManager.SENSOR_MAGNETIC_FIELD: + x = -x; + y = -y; + break; + } + values[0] = x; + values[1] = y; + values[2] = z; + values[3] = x; + values[4] = y; + values[5] = z; + // TODO: add support for 180 and 270 orientations + if (orientation == Surface.ROTATION_90) { + switch (sensor) { + case SENSOR_ACCELEROMETER: + case SENSOR_MAGNETIC_FIELD: + values[0] =-y; + values[1] = x; + values[2] = z; + break; + case SENSOR_ORIENTATION: + case SENSOR_ORIENTATION_RAW: + values[0] = x + ((x < 270) ? 90 : -270); + values[1] = z; + values[2] = y; + break; + } + } + } + } + + class LmsFilter { + private static final int SENSORS_RATE_MS = 20; + private static final int COUNT = 12; + private static final float PREDICTION_RATIO = 1.0f/3.0f; + private static final float PREDICTION_TIME = (SENSORS_RATE_MS*COUNT/1000.0f)*PREDICTION_RATIO; + private float mV[] = new float[COUNT*2]; + private float mT[] = new float[COUNT*2]; + private int mIndex; + + public LmsFilter() { + mIndex = COUNT; + } + + public float filter(long time, float in) { + float v = in; + final float ns = 1.0f / 1000000000.0f; + final float t = time*ns; + float v1 = mV[mIndex]; + if ((v-v1) > 180) { + v -= 360; + } else if ((v1-v) > 180) { + v += 360; + } + /* Manage the circular buffer, we write the data twice spaced + * by COUNT values, so that we don't have to copy the array + * when it's full + */ + mIndex++; + if (mIndex >= COUNT*2) + mIndex = COUNT; + mV[mIndex] = v; + mT[mIndex] = t; + mV[mIndex-COUNT] = v; + mT[mIndex-COUNT] = t; + + float A, B, C, D, E; + float a, b; + int i; + + A = B = C = D = E = 0; + for (i=0 ; i<COUNT-1 ; i++) { + final int j = mIndex - 1 - i; + final float Z = mV[j]; + final float T = 0.5f*(mT[j] + mT[j+1]) - t; + float dT = mT[j] - mT[j+1]; + dT *= dT; + A += Z*dT; + B += T*(T*dT); + C += (T*dT); + D += Z*(T*dT); + E += dT; + } + b = (A*B + C*D) / (E*B + C*C); + a = (E*b - A) / C; + float f = b + PREDICTION_TIME*a; + + // Normalize + f *= (1.0f / 360.0f); + if (((f>=0)?f:-f) >= 0.5f) + f = f - (float)Math.ceil(f + 0.5f) + 1.0f; + if (f < 0) + f += 1.0f; + f *= 360.0f; + return f; + } + } + + + private static native void nativeClassInit(); + + private static native int sensors_module_init(); + private static native int sensors_module_get_next_sensor(Sensor sensor, int next); + + // Used within this module from outside SensorManager, don't make private + static native int sensors_data_init(); + static native int sensors_data_uninit(); + static native int sensors_data_open(FileDescriptor fd); + static native int sensors_data_close(); + static native int sensors_data_poll(float[] values, int[] status, long[] timestamp); +} diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java new file mode 100644 index 0000000..7d02f65 --- /dev/null +++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.inputmethodservice; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.InputMethod; +import android.view.inputmethod.InputMethodSession; + +/** + * AbstractInputMethodService provides a abstract base class for input methods. + * Normal input method implementations will not derive from this directly, + * instead building on top of {@link InputMethodService} or another more + * complete base class. Be sure to read {@link InputMethod} for more + * information on the basics of writing input methods. + * + * <p>This class combines a Service (representing the input method component + * to the system with the InputMethod interface that input methods must + * implement. This base class takes care of reporting your InputMethod from + * the service when clients bind to it, but provides no standard implementation + * of the InputMethod interface itself. Derived classes must implement that + * interface. + */ +public abstract class AbstractInputMethodService extends Service + implements KeyEvent.Callback { + private InputMethod mInputMethod; + + /** + * Base class for derived classes to implement their {@link InputMethod} + * interface. This takes care of basic maintenance of the input method, + * but most behavior must be implemented in a derived class. + */ + public abstract class AbstractInputMethodImpl implements InputMethod { + /** + * Instantiate a new client session for the input method, by calling + * back to {@link AbstractInputMethodService#onCreateInputMethodSessionInterface() + * AbstractInputMethodService.onCreateInputMethodSessionInterface()}. + */ + public void createSession(SessionCallback callback) { + callback.sessionCreated(onCreateInputMethodSessionInterface()); + } + + /** + * Take care of enabling or disabling an existing session by calling its + * {@link AbstractInputMethodSessionImpl#revokeSelf() + * AbstractInputMethodSessionImpl.setEnabled()} method. + */ + public void setSessionEnabled(InputMethodSession session, boolean enabled) { + ((AbstractInputMethodSessionImpl)session).setEnabled(enabled); + } + + /** + * Take care of killing an existing session by calling its + * {@link AbstractInputMethodSessionImpl#revokeSelf() + * AbstractInputMethodSessionImpl.revokeSelf()} method. + */ + public void revokeSession(InputMethodSession session) { + ((AbstractInputMethodSessionImpl)session).revokeSelf(); + } + } + + /** + * Base class for derived classes to implement their {@link InputMethodSession} + * interface. This takes care of basic maintenance of the session, + * but most behavior must be implemented in a derived class. + */ + public abstract class AbstractInputMethodSessionImpl implements InputMethodSession { + boolean mEnabled = true; + boolean mRevoked; + + /** + * Check whether this session has been enabled by the system. If not + * enabled, you should not execute any calls on to it. + */ + public boolean isEnabled() { + return mEnabled; + } + + /** + * Check whether this session has been revoked by the system. Revoked + * session is also always disabled, so there is generally no need to + * explicitly check for this. + */ + public boolean isRevoked() { + return mRevoked; + } + + /** + * Change the enabled state of the session. This only works if the + * session has not been revoked. + */ + public void setEnabled(boolean enabled) { + if (!mRevoked) { + mEnabled = enabled; + } + } + + /** + * Revoke the session from the client. This disabled the session, and + * prevents it from ever being enabled again. + */ + public void revokeSelf() { + mRevoked = true; + mEnabled = false; + } + + /** + * Take care of dispatching incoming key events to the appropriate + * callbacks on the service, and tell the client when this is done. + */ + public void dispatchKeyEvent(int seq, KeyEvent event, EventCallback callback) { + boolean handled = event.dispatch(AbstractInputMethodService.this); + if (callback != null) { + callback.finishedEvent(seq, handled); + } + } + + /** + * Take care of dispatching incoming trackball events to the appropriate + * callbacks on the service, and tell the client when this is done. + */ + public void dispatchTrackballEvent(int seq, MotionEvent event, EventCallback callback) { + boolean handled = onTrackballEvent(event); + if (callback != null) { + callback.finishedEvent(seq, handled); + } + } + } + + /** + * Called by the framework during initialization, when the InputMethod + * interface for this service needs to be created. + */ + public abstract AbstractInputMethodImpl onCreateInputMethodInterface(); + + /** + * Called by the framework when a new InputMethodSession interface is + * needed for a new client of the input method. + */ + public abstract AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface(); + + @Override + final public IBinder onBind(Intent intent) { + if (mInputMethod == null) { + mInputMethod = onCreateInputMethodInterface(); + } + return new IInputMethodWrapper(this, mInputMethod); + } + + public boolean onTrackballEvent(MotionEvent event) { + return false; + } +} diff --git a/core/java/android/inputmethodservice/ExtractEditText.java b/core/java/android/inputmethodservice/ExtractEditText.java new file mode 100644 index 0000000..e59f38b --- /dev/null +++ b/core/java/android/inputmethodservice/ExtractEditText.java @@ -0,0 +1,23 @@ +package android.inputmethodservice; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.EditText; + +/*** + * Specialization of {@link EditText} for showing and interacting with the + * extracted text in a full-screen input method. + */ +public class ExtractEditText extends EditText { + public ExtractEditText(Context context) { + super(context, null); + } + + public ExtractEditText(Context context, AttributeSet attrs) { + super(context, attrs, com.android.internal.R.attr.editTextStyle); + } + + public ExtractEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } +} diff --git a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java new file mode 100644 index 0000000..40c03cd --- /dev/null +++ b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java @@ -0,0 +1,151 @@ +package android.inputmethodservice; + +import com.android.internal.os.HandlerCaller; +import com.android.internal.view.IInputMethodCallback; +import com.android.internal.view.IInputMethodSession; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.InputMethodSession; +import android.view.inputmethod.EditorInfo; + +class IInputMethodSessionWrapper extends IInputMethodSession.Stub + implements HandlerCaller.Callback { + private static final String TAG = "InputMethodWrapper"; + private static final boolean DEBUG = false; + + private static final int DO_FINISH_INPUT = 60; + private static final int DO_DISPLAY_COMPLETIONS = 65; + private static final int DO_UPDATE_EXTRACTED_TEXT = 67; + private static final int DO_DISPATCH_KEY_EVENT = 70; + private static final int DO_DISPATCH_TRACKBALL_EVENT = 80; + private static final int DO_UPDATE_SELECTION = 90; + private static final int DO_UPDATE_CURSOR = 95; + private static final int DO_APP_PRIVATE_COMMAND = 100; + + final HandlerCaller mCaller; + final InputMethodSession mInputMethodSession; + + // NOTE: we should have a cache of these. + static class InputMethodEventCallbackWrapper implements InputMethodSession.EventCallback { + final IInputMethodCallback mCb; + InputMethodEventCallbackWrapper(IInputMethodCallback cb) { + mCb = cb; + } + public void finishedEvent(int seq, boolean handled) { + try { + mCb.finishedEvent(seq, handled); + } catch (RemoteException e) { + } + } + } + + public IInputMethodSessionWrapper(Context context, + InputMethodSession inputMethodSession) { + mCaller = new HandlerCaller(context, this); + mInputMethodSession = inputMethodSession; + } + + public InputMethodSession getInternalInputMethodSession() { + return mInputMethodSession; + } + + public void executeMessage(Message msg) { + switch (msg.what) { + case DO_FINISH_INPUT: + mInputMethodSession.finishInput(); + return; + case DO_DISPLAY_COMPLETIONS: + mInputMethodSession.displayCompletions((CompletionInfo[])msg.obj); + return; + case DO_UPDATE_EXTRACTED_TEXT: + mInputMethodSession.updateExtractedText(msg.arg1, + (ExtractedText)msg.obj); + return; + case DO_DISPATCH_KEY_EVENT: { + HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj; + mInputMethodSession.dispatchKeyEvent(msg.arg1, + (KeyEvent)args.arg1, + new InputMethodEventCallbackWrapper( + (IInputMethodCallback)args.arg2)); + mCaller.recycleArgs(args); + return; + } + case DO_DISPATCH_TRACKBALL_EVENT: { + HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj; + mInputMethodSession.dispatchTrackballEvent(msg.arg1, + (MotionEvent)args.arg1, + new InputMethodEventCallbackWrapper( + (IInputMethodCallback)args.arg2)); + mCaller.recycleArgs(args); + return; + } + case DO_UPDATE_SELECTION: { + HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj; + mInputMethodSession.updateSelection(args.argi1, args.argi2, + args.argi3, args.argi4); + mCaller.recycleArgs(args); + return; + } + case DO_UPDATE_CURSOR: { + mInputMethodSession.updateCursor((Rect)msg.obj); + return; + } + case DO_APP_PRIVATE_COMMAND: { + HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj; + mInputMethodSession.appPrivateCommand((String)args.arg1, + (Bundle)args.arg2); + mCaller.recycleArgs(args); + return; + } + } + Log.w(TAG, "Unhandled message code: " + msg.what); + } + + public void finishInput() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_FINISH_INPUT)); + } + + public void displayCompletions(CompletionInfo[] completions) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO( + DO_DISPLAY_COMPLETIONS, completions)); + } + + public void updateExtractedText(int token, ExtractedText text) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIO( + DO_UPDATE_EXTRACTED_TEXT, token, text)); + } + + public void dispatchKeyEvent(int seq, KeyEvent event, IInputMethodCallback callback) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_DISPATCH_KEY_EVENT, seq, + event, callback)); + } + + public void dispatchTrackballEvent(int seq, MotionEvent event, IInputMethodCallback callback) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_DISPATCH_TRACKBALL_EVENT, seq, + event, callback)); + } + + public void updateSelection(int oldSelStart, int oldSelEnd, + int newSelStart, int newSelEnd) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIIII(DO_UPDATE_SELECTION, + oldSelStart, oldSelEnd, newSelStart, newSelEnd)); + } + + public void updateCursor(Rect newCursor) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_UPDATE_CURSOR, + newCursor)); + } + + public void appPrivateCommand(String action, Bundle data) { + mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_APP_PRIVATE_COMMAND, action, data)); + } +} diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java new file mode 100644 index 0000000..4108bdd --- /dev/null +++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java @@ -0,0 +1,172 @@ +package android.inputmethodservice; + +import com.android.internal.os.HandlerCaller; +import com.android.internal.view.IInputContext; +import com.android.internal.view.IInputMethod; +import com.android.internal.view.IInputMethodCallback; +import com.android.internal.view.IInputMethodSession; +import com.android.internal.view.InputConnectionWrapper; + +import android.content.Context; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputBinding; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethod; +import android.view.inputmethod.InputMethodSession; + +/** + * Implements the internal IInputMethod interface to convert incoming calls + * on to it back to calls on the public InputMethod interface, scheduling + * them on the main thread of the process. + */ +class IInputMethodWrapper extends IInputMethod.Stub + implements HandlerCaller.Callback { + private static final String TAG = "InputMethodWrapper"; + private static final boolean DEBUG = false; + + private static final int DO_ATTACH_TOKEN = 10; + private static final int DO_SET_INPUT_CONTEXT = 20; + private static final int DO_UNSET_INPUT_CONTEXT = 30; + private static final int DO_START_INPUT = 32; + private static final int DO_RESTART_INPUT = 34; + private static final int DO_CREATE_SESSION = 40; + private static final int DO_SET_SESSION_ENABLED = 45; + private static final int DO_REVOKE_SESSION = 50; + private static final int DO_SHOW_SOFT_INPUT = 60; + private static final int DO_HIDE_SOFT_INPUT = 70; + + final HandlerCaller mCaller; + final InputMethod mInputMethod; + + // NOTE: we should have a cache of these. + static class InputMethodSessionCallbackWrapper implements InputMethod.SessionCallback { + final Context mContext; + final IInputMethodCallback mCb; + InputMethodSessionCallbackWrapper(Context context, IInputMethodCallback cb) { + mContext = context; + mCb = cb; + } + public void sessionCreated(InputMethodSession session) { + try { + if (session != null) { + IInputMethodSessionWrapper wrap = + new IInputMethodSessionWrapper(mContext, session); + mCb.sessionCreated(wrap); + } else { + mCb.sessionCreated(null); + } + } catch (RemoteException e) { + } + } + } + + public IInputMethodWrapper(Context context, InputMethod inputMethod) { + mCaller = new HandlerCaller(context, this); + mInputMethod = inputMethod; + } + + public InputMethod getInternalInputMethod() { + return mInputMethod; + } + + public void executeMessage(Message msg) { + switch (msg.what) { + case DO_ATTACH_TOKEN: { + mInputMethod.attachToken((IBinder)msg.obj); + return; + } + case DO_SET_INPUT_CONTEXT: { + mInputMethod.bindInput((InputBinding)msg.obj); + return; + } + case DO_UNSET_INPUT_CONTEXT: + mInputMethod.unbindInput(); + return; + case DO_START_INPUT: + mInputMethod.startInput((EditorInfo)msg.obj); + return; + case DO_RESTART_INPUT: + mInputMethod.restartInput((EditorInfo)msg.obj); + return; + case DO_CREATE_SESSION: { + mInputMethod.createSession(new InputMethodSessionCallbackWrapper( + mCaller.mContext, (IInputMethodCallback)msg.obj)); + return; + } + case DO_SET_SESSION_ENABLED: + mInputMethod.setSessionEnabled((InputMethodSession)msg.obj, + msg.arg1 != 0); + return; + case DO_REVOKE_SESSION: + mInputMethod.revokeSession((InputMethodSession)msg.obj); + return; + case DO_SHOW_SOFT_INPUT: + mInputMethod.showSoftInput(); + return; + case DO_HIDE_SOFT_INPUT: + mInputMethod.hideSoftInput(); + return; + } + Log.w(TAG, "Unhandled message code: " + msg.what); + } + + public void attachToken(IBinder token) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_ATTACH_TOKEN, token)); + } + + public void bindInput(InputBinding binding) { + InputConnection ic = new InputConnectionWrapper( + IInputContext.Stub.asInterface(binding.getConnectionToken())); + InputBinding nu = new InputBinding(ic, binding); + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_INPUT_CONTEXT, nu)); + } + + public void unbindInput() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_UNSET_INPUT_CONTEXT)); + } + + public void startInput(EditorInfo attribute) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_START_INPUT, attribute)); + } + + public void restartInput(EditorInfo attribute) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_RESTART_INPUT, attribute)); + } + + public void createSession(IInputMethodCallback callback) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_CREATE_SESSION, callback)); + } + + public void setSessionEnabled(IInputMethodSession session, boolean enabled) { + try { + InputMethodSession ls = ((IInputMethodSessionWrapper) + session).getInternalInputMethodSession(); + mCaller.executeOrSendMessage(mCaller.obtainMessageIO( + DO_SET_SESSION_ENABLED, enabled ? 1 : 0, ls)); + } catch (ClassCastException e) { + Log.w(TAG, "Incoming session not of correct type: " + session, e); + } + } + + public void revokeSession(IInputMethodSession session) { + try { + InputMethodSession ls = ((IInputMethodSessionWrapper) + session).getInternalInputMethodSession(); + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_REVOKE_SESSION, ls)); + } catch (ClassCastException e) { + Log.w(TAG, "Incoming session not of correct type: " + session, e); + } + } + + public void showSoftInput() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_SHOW_SOFT_INPUT)); + } + + public void hideSoftInput() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_HIDE_SOFT_INPUT)); + } +} diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java new file mode 100644 index 0000000..9ebf127 --- /dev/null +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -0,0 +1,864 @@ +/* + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.inputmethodservice; + +import static android.view.ViewGroup.LayoutParams.FILL_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.app.Dialog; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputBinding; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethod; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.EditorInfo; +import android.widget.FrameLayout; + +/** + * InputMethodService provides a standard implementation of an InputMethod, + * which final implementations can derive from and customize. See the + * base class {@link AbstractInputMethodService} and the {@link InputMethod} + * interface for more information on the basics of writing input methods. + */ +public class InputMethodService extends AbstractInputMethodService { + static final String TAG = "InputMethodService"; + static final boolean DEBUG = false; + + LayoutInflater mInflater; + View mRootView; + SoftInputWindow mWindow; + boolean mWindowCreated; + boolean mWindowAdded; + boolean mWindowVisible; + FrameLayout mExtractFrame; + FrameLayout mCandidatesFrame; + FrameLayout mInputFrame; + + IBinder mToken; + + InputBinding mInputBinding; + InputConnection mInputConnection; + boolean mInputStarted; + EditorInfo mInputInfo; + + boolean mShowInputRequested; + boolean mShowCandidatesRequested; + + boolean mFullscreenApplied; + boolean mIsFullscreen; + View mExtractView; + ExtractEditText mExtractEditText; + ExtractedText mExtractedText; + int mExtractedToken; + + View mInputView; + boolean mIsInputViewShown; + + int mStatusIcon; + + final Insets mTmpInsets = new Insets(); + final int[] mTmpLocation = new int[2]; + + final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = + new ViewTreeObserver.OnComputeInternalInsetsListener() { + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) { + if (isFullscreenMode()) { + // In fullscreen mode, we just say the window isn't covering + // any content so we don't impact whatever is behind. + View decor = getWindow().getWindow().getDecorView(); + info.contentInsets.top = info.visibleInsets.top + = decor.getHeight(); + info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); + } else { + onComputeInsets(mTmpInsets); + info.contentInsets.top = mTmpInsets.contentTopInsets; + info.visibleInsets.top = mTmpInsets.visibleTopInsets; + info.setTouchableInsets(mTmpInsets.touchableInsets); + } + } + }; + + /** + * Concrete implementation of + * {@link AbstractInputMethodService.AbstractInputMethodImpl} that provides + * all of the standard behavior for an input method. + */ + public class InputMethodImpl extends AbstractInputMethodImpl { + /** + * Take care of attaching the given window token provided by the system. + */ + public void attachToken(IBinder token) { + if (mToken == null) { + mToken = token; + mWindow.setToken(token); + } + } + + /** + * Handle a new input binding, calling + * {@link InputMethodService#onBindInput InputMethodService.onBindInput()} + * when done. + */ + public void bindInput(InputBinding binding) { + mInputBinding = binding; + mInputConnection = binding.getConnection(); + onBindInput(); + } + + /** + * Clear the current input binding. + */ + public void unbindInput() { + mInputStarted = false; + mInputBinding = null; + mInputConnection = null; + } + + public void startInput(EditorInfo attribute) { + doStartInput(attribute, false); + } + + public void restartInput(EditorInfo attribute) { + doStartInput(attribute, false); + } + + /** + * Handle a request by the system to hide the soft input area. + */ + public void hideSoftInput() { + if (DEBUG) Log.v(TAG, "hideSoftInput()"); + mShowInputRequested = false; + hideWindow(); + } + + /** + * Handle a request by the system to show the soft input area. + */ + public void showSoftInput() { + if (DEBUG) Log.v(TAG, "showSoftInput()"); + showWindow(true); + } + } + + /** + * Concrete implementation of + * {@link AbstractInputMethodService.AbstractInputMethodSessionImpl} that provides + * all of the standard behavior for an input method session. + */ + public class InputMethodSessionImpl extends AbstractInputMethodSessionImpl { + public void finishInput() { + if (!isEnabled()) { + return; + } + onFinishInput(); + mInputStarted = false; + } + + /** + * Call {@link InputMethodService#onDisplayCompletions + * InputMethodService.onDisplayCompletions()}. + */ + public void displayCompletions(CompletionInfo[] completions) { + if (!isEnabled()) { + return; + } + onDisplayCompletions(completions); + } + + /** + * Call {@link InputMethodService#onUpdateExtractedText + * InputMethodService.onUpdateExtractedText()}. + */ + public void updateExtractedText(int token, ExtractedText text) { + if (!isEnabled()) { + return; + } + onUpdateExtractedText(token, text); + } + + /** + * Call {@link InputMethodService#onUpdateSelection + * InputMethodService.onUpdateSelection()}. + */ + public void updateSelection(int oldSelStart, int oldSelEnd, + int newSelStart, int newSelEnd) { + if (!isEnabled()) { + return; + } + InputMethodService.this.onUpdateSelection(oldSelStart, oldSelEnd, + newSelStart, newSelEnd); + } + + /** + * Call {@link InputMethodService#onUpdateCursor + * InputMethodService.onUpdateCursor()}. + */ + public void updateCursor(Rect newCursor) { + if (!isEnabled()) { + return; + } + InputMethodService.this.onUpdateCursor(newCursor); + } + + /** + * Call {@link InputMethodService#onAppPrivateCommand + * InputMethodService.onAppPrivateCommand()}. + */ + public void appPrivateCommand(String action, Bundle data) { + if (!isEnabled()) { + return; + } + InputMethodService.this.onAppPrivateCommand(action, data); + } + } + + /** + * Information about where interesting parts of the input method UI appear. + */ + public static final class Insets { + /** + * This is the top part of the UI that is the main content. It is + * used to determine the basic space needed, to resize/pan the + * application behind. It is assumed that this inset does not + * change very much, since any change will cause a full resize/pan + * of the application behind. This value is relative to the top edge + * of the input method window. + */ + int contentTopInsets; + + /** + * This is the top part of the UI that is visibly covering the + * application behind it. This provides finer-grained control over + * visibility, allowing you to change it relatively frequently (such + * as hiding or showing candidates) without disrupting the underlying + * UI too much. For example, this will never resize the application + * UI, will only pan if needed to make the current focus visible, and + * will not aggressively move the pan position when this changes unless + * needed to make the focus visible. This value is relative to the top edge + * of the input method window. + */ + int visibleTopInsets; + + /** + * Option for {@link #touchableInsets}: the entire window frame + * can be touched. + */ + public static final int TOUCHABLE_INSETS_FRAME + = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME; + + /** + * Option for {@link #touchableInsets}: the area inside of + * the content insets can be touched. + */ + public static final int TOUCHABLE_INSETS_CONTENT + = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT; + + /** + * Option for {@link #touchableInsets}: the area inside of + * the visible insets can be touched. + */ + public static final int TOUCHABLE_INSETS_VISIBLE + = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_VISIBLE; + + /** + * Determine which area of the window is touchable by the user. May + * be one of: {@link #TOUCHABLE_INSETS_FRAME}, + * {@link #TOUCHABLE_INSETS_CONTENT}, or {@link #TOUCHABLE_INSETS_VISIBLE}. + */ + public int touchableInsets; + } + + @Override public void onCreate() { + super.onCreate(); + mInflater = (LayoutInflater)getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + mWindow = new SoftInputWindow(this); + initViews(); + } + + void initViews() { + mWindowVisible = false; + mWindowCreated = false; + mShowInputRequested = false; + mShowCandidatesRequested = false; + + mRootView = mInflater.inflate( + com.android.internal.R.layout.input_method, null); + mWindow.setContentView(mRootView); + mRootView.getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsComputer); + + mExtractFrame = (FrameLayout)mRootView.findViewById(android.R.id.extractArea); + mExtractView = null; + mExtractEditText = null; + mFullscreenApplied = false; + + mCandidatesFrame = (FrameLayout)mRootView.findViewById(android.R.id.candidatesArea); + mInputFrame = (FrameLayout)mRootView.findViewById(android.R.id.inputArea); + mInputView = null; + mIsInputViewShown = false; + + mExtractFrame.setVisibility(View.GONE); + mCandidatesFrame.setVisibility(View.GONE); + mInputFrame.setVisibility(View.GONE); + } + + @Override public void onDestroy() { + super.onDestroy(); + mRootView.getViewTreeObserver().removeOnComputeInternalInsetsListener( + mInsetsComputer); + if (mWindowAdded) { + mWindow.dismiss(); + } + } + + /** + * Implement to return our standard {@link InputMethodImpl}. Subclasses + * can override to provide their own customized version. + */ + public AbstractInputMethodImpl onCreateInputMethodInterface() { + return new InputMethodImpl(); + } + + /** + * Implement to return our standard {@link InputMethodSessionImpl}. Subclasses + * can override to provide their own customized version. + */ + public AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface() { + return new InputMethodSessionImpl(); + } + + public LayoutInflater getLayoutInflater() { + return mInflater; + } + + public Dialog getWindow() { + return mWindow; + } + + /** + * Return the currently active InputBinding for the input method, or + * null if there is none. + */ + public InputBinding getCurrentInputBinding() { + return mInputBinding; + } + + /** + * Retrieve the currently active InputConnection that is bound to + * the input method, or null if there is none. + */ + public InputConnection getCurrentInputConnection() { + return mInputConnection; + } + + public boolean getCurrentInputStarted() { + return mInputStarted; + } + + public EditorInfo getCurrentInputInfo() { + return mInputInfo; + } + + /** + * Re-evaluate whether the input method should be running in fullscreen + * mode, and update its UI if this has changed since the last time it + * was evaluated. This will call {@link #onEvaluateFullscreenMode()} to + * determine whether it should currently run in fullscreen mode. You + * can use {@link #isFullscreenMode()} to determine if the input method + * is currently running in fullscreen mode. + */ + public void updateFullscreenMode() { + boolean isFullscreen = onEvaluateFullscreenMode(); + if (mIsFullscreen != isFullscreen || !mFullscreenApplied) { + mIsFullscreen = isFullscreen; + mFullscreenApplied = true; + mWindow.getWindow().setBackgroundDrawable( + onCreateBackgroundDrawable()); + mExtractFrame.setVisibility(isFullscreen ? View.VISIBLE : View.GONE); + if (isFullscreen) { + if (mExtractView == null) { + View v = onCreateExtractTextView(); + if (v != null) { + setExtractView(v); + } + } + startExtractingText(); + mWindow.getWindow().setLayout(FILL_PARENT, FILL_PARENT); + } else { + mWindow.getWindow().setLayout(WRAP_CONTENT, WRAP_CONTENT); + } + } + } + + /** + * Return whether the input method is <em>currently</em> running in + * fullscreen mode. This is the mode that was last determined and + * applied by {@link #updateFullscreenMode()}. + */ + public boolean isFullscreenMode() { + return mIsFullscreen; + } + + /** + * Override this to control when the input method should run in + * fullscreen mode. The default implementation runs in fullsceen only + * when the screen is in landscape mode and the input view is being + * shown ({@link #onEvaluateInputViewShown} returns true). If you change what + * this returns, you will need to call {@link #updateFullscreenMode()} + * yourself whenever the returned value may have changed to have it + * re-evaluated and applied. + */ + public boolean onEvaluateFullscreenMode() { + Configuration config = getResources().getConfiguration(); + return config.orientation == Configuration.ORIENTATION_LANDSCAPE + && onEvaluateInputViewShown(); + } + + /** + * Compute the interesting insets into your UI. The default implementation + * uses the top of the candidates frame for the visible insets, and the + * top of the input frame for the content insets. The default touchable + * insets are {@link Insets#TOUCHABLE_INSETS_VISIBLE}. + * + * <p>Note that this method is not called when in fullscreen mode, since + * in that case the application is left as-is behind the input method and + * not impacted by anything in its UI. + * + * @param outInsets Fill in with the current UI insets. + */ + public void onComputeInsets(Insets outInsets) { + int[] loc = mTmpLocation; + if (mInputFrame.getVisibility() == View.VISIBLE) { + mInputFrame.getLocationInWindow(loc); + outInsets.contentTopInsets = loc[1]; + } + if (mCandidatesFrame.getVisibility() == View.VISIBLE) { + mCandidatesFrame.getLocationInWindow(loc); + outInsets.visibleTopInsets = loc[1]; + } else { + outInsets.visibleTopInsets = loc[1]; + } + outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_VISIBLE; + } + + /** + * Re-evaluate whether the soft input area should currently be shown, and + * update its UI if this has changed since the last time it + * was evaluated. This will call {@link #onEvaluateInputViewShown()} to + * determine whether the input view should currently be shown. You + * can use {@link #isInputViewShown()} to determine if the input view + * is currently shown. + */ + public void updateInputViewShown() { + boolean isShown = onEvaluateInputViewShown(); + if (mIsInputViewShown != isShown && mWindowVisible) { + mIsInputViewShown = isShown; + mInputFrame.setVisibility(isShown ? View.VISIBLE : View.GONE); + if (mInputView == null) { + View v = onCreateInputView(); + if (v != null) { + setInputView(v); + } + } + } + } + + /** + * Return whether the soft input view is <em>currently</em> shown to the + * user. This is the state that was last determined and + * applied by {@link #updateInputViewShown()}. + */ + public boolean isInputViewShown() { + return mIsInputViewShown; + } + + /** + * Override this to control when the soft input area should be shown to + * the user. The default implementation only shows the input view when + * there is no hard keyboard or the keyboard is hidden. If you change what + * this returns, you will need to call {@link #updateInputViewShown()} + * yourself whenever the returned value may have changed to have it + * re-evalauted and applied. + */ + public boolean onEvaluateInputViewShown() { + Configuration config = getResources().getConfiguration(); + return config.keyboard == Configuration.KEYBOARD_NOKEYS + || config.hardKeyboardHidden == Configuration.KEYBOARDHIDDEN_YES; + } + + /** + * Controls the visibility of the candidates display area. By default + * it is hidden. + */ + public void setCandidatesViewShown(boolean shown) { + if (mShowCandidatesRequested != shown) { + mCandidatesFrame.setVisibility(shown ? View.VISIBLE : View.INVISIBLE); + if (!mShowInputRequested) { + // If we are being asked to show the candidates view while the app + // has not asked for the input view to be shown, then we need + // to update whether the window is shown. + if (shown) { + showWindow(false); + } else { + hideWindow(); + } + } + mShowCandidatesRequested = shown; + } + } + + public void setStatusIcon(int iconResId) { + mStatusIcon = iconResId; + if (mInputConnection != null && mWindowVisible) { + mInputConnection.showStatusIcon(getPackageName(), iconResId); + } + } + + /** + * Force switch to a new input method, as identified by <var>id</var>. This + * input method will be destroyed, and the requested one started on the + * current input field. + * + * @param id Unique identifier of the new input method ot start. + */ + public void switchInputMethod(String id) { + ((InputMethodManager)getSystemService(INPUT_METHOD_SERVICE)) + .setInputMethod(mToken, id); + } + + public void setExtractView(View view) { + mExtractFrame.removeAllViews(); + mExtractFrame.addView(view, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + mExtractView = view; + if (view != null) { + mExtractEditText = (ExtractEditText)view.findViewById( + com.android.internal.R.id.inputExtractEditText); + startExtractingText(); + } else { + mExtractEditText = null; + } + } + + /** + * Replaces the current candidates view with a new one. You only need to + * call this when dynamically changing the view; normally, you should + * implement {@link #onCreateCandidatesView()} and create your view when + * first needed by the input method. + */ + public void setCandidatesView(View view) { + mCandidatesFrame.removeAllViews(); + mCandidatesFrame.addView(view, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + } + + /** + * Replaces the current input view with a new one. You only need to + * call this when dynamically changing the view; normally, you should + * implement {@link #onCreateInputView()} and create your view when + * first needed by the input method. + */ + public void setInputView(View view) { + mInputFrame.removeAllViews(); + mInputFrame.addView(view, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + mInputView = view; + } + + /** + * Called by the framework to create a Drawable for the background of + * the input method window. May return null for no background. The default + * implementation returns a non-null standard background only when in + * fullscreen mode. + */ + public Drawable onCreateBackgroundDrawable() { + if (isFullscreenMode()) { + return getResources().getDrawable( + com.android.internal.R.drawable.input_method_fullscreen_background); + } + return null; + } + + /** + * Called by the framework to create the layout for showing extacted text. + * Only called when in fullscreen mode. The returned view hierarchy must + * have an {@link ExtractEditText} whose ID is + * {@link android.R.id#inputExtractEditText}. + */ + public View onCreateExtractTextView() { + return mInflater.inflate( + com.android.internal.R.layout.input_method_extract_view, null); + } + + /** + * Create and return the view hierarchy used to show candidates. This will + * be called once, when the candidates are first displayed. You can return + * null to have no candidates view; the default implementation returns null. + * + * <p>To control when the candidates view is displayed, use + * {@link #setCandidatesViewShown(boolean)}. + * To change the candidates view after the first one is created by this + * function, use {@link #setCandidatesView(View)}. + */ + public View onCreateCandidatesView() { + return null; + } + + /** + * Create and return the view hierarchy used for the input area (such as + * a soft keyboard). This will be called once, when the input area is + * first displayed. You can return null to have no input area; the default + * implementation returns null. + * + * <p>To control when the input view is displayed, implement + * {@link #onEvaluateInputViewShown()}. + * To change the input view after the first one is created by this + * function, use {@link #setInputView(View)}. + */ + public View onCreateInputView() { + return null; + } + + /** + * Called when an input session is starting or restarting. + * + * @param info Description of the type of text being edited. + * @param restarting Set to true if we are restarting input on the + * same text field as before. + */ + public void onStartInputView(EditorInfo info, boolean restarting) { + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + boolean visible = mWindowVisible; + boolean showingInput = mShowInputRequested; + boolean showingCandidates = mShowCandidatesRequested; + initViews(); + if (visible) { + if (showingCandidates) { + setCandidatesViewShown(true); + } + showWindow(showingInput); + } + } + + public void showWindow(boolean showInput) { + if (DEBUG) Log.v(TAG, "Showing window: showInput=" + showInput + + " mShowInputRequested=" + mShowInputRequested + + " mWindowAdded=" + mWindowAdded + + " mWindowCreated=" + mWindowCreated + + " mWindowVisible=" + mWindowVisible + + " mInputStarted=" + mInputStarted); + boolean doShowInput = false; + boolean wasVisible = mWindowVisible; + mWindowVisible = true; + if (!mShowInputRequested) { + doShowInput = true; + mShowInputRequested = true; + } else { + showInput = true; + } + + if (doShowInput) { + if (DEBUG) Log.v(TAG, "showWindow: updating UI"); + updateFullscreenMode(); + updateInputViewShown(); + } + + if (!mWindowAdded || !mWindowCreated) { + mWindowAdded = true; + mWindowCreated = true; + View v = onCreateCandidatesView(); + if (DEBUG) Log.v(TAG, "showWindow: candidates=" + v); + if (v != null) { + setCandidatesView(v); + } + } + if (doShowInput) { + if (mInputStarted) { + if (DEBUG) Log.v(TAG, "showWindow: starting input view"); + onStartInputView(mInputInfo, false); + } + startExtractingText(); + } + + if (!wasVisible) { + if (DEBUG) Log.v(TAG, "showWindow: showing!"); + mWindow.show(); + if (mInputConnection != null) { + mInputConnection.showStatusIcon(getPackageName(), mStatusIcon); + } + } + } + + public void hideWindow() { + if (mWindowVisible) { + mWindow.hide(); + mWindowVisible = false; + if (mInputConnection != null) { + mInputConnection.hideStatusIcon(); + } + } + } + + public void onBindInput() { + } + + public void onStartInput(EditorInfo attribute, boolean restarting) { + } + + void doStartInput(EditorInfo attribute, boolean restarting) { + mInputStarted = true; + mInputInfo = attribute; + onStartInput(attribute, restarting); + if (mWindowVisible) { + if (mWindowCreated) { + onStartInputView(mInputInfo, restarting); + } + startExtractingText(); + } + } + + public void onFinishInput() { + } + + /** + * Called when the application has reported auto-completion candidates that + * it would like to have the input method displayed. Typically these are + * only used when an input method is running in full-screen mode, since + * otherwise the user can see and interact with the pop-up window of + * completions shown by the application. + * + * <p>The default implementation here does nothing. + */ + public void onDisplayCompletions(CompletionInfo[] completions) { + } + + /** + * Called when the application has reported new extracted text to be shown + * due to changes in its current text state. The default implementation + * here places the new text in the extract edit text, when the input + * method is running in fullscreen mode. + */ + public void onUpdateExtractedText(int token, ExtractedText text) { + if (mExtractedToken != token) { + return; + } + if (mExtractEditText != null && text != null) { + mExtractedText = text; + mExtractEditText.setExtractedText(text); + } + } + + /** + * Called when the application has reported a new selection region of + * the text. This is called whether or not the input method has requested + * extracted text updates, although if so it will not receive this call + * if the extracted text has changed as well. + * + * <p>The default implementation takes care of updating the cursor in + * the extract text, if it is being shown. + */ + public void onUpdateSelection(int oldSelStart, int oldSelEnd, + int newSelStart, int newSelEnd) { + if (mExtractEditText != null && mExtractedText != null) { + final int off = mExtractedText.startOffset; + mExtractEditText.setSelection(newSelStart-off, newSelEnd-off); + } + } + + /** + * Called when the application has reported a new location of its text + * cursor. This is only called if explicitly requested by the input method. + * The default implementation does nothing. + */ + public void onUpdateCursor(Rect newCursor) { + } + + /** + * Close this input method's soft input area, removing it from the display. + * The input method will continue running, but the user can no longer use + * it to generate input by touching the screen. + */ + public void dismissSoftInput() { + ((InputMethodManager)getSystemService(INPUT_METHOD_SERVICE)) + .hideSoftInputFromInputMethod(mToken); + } + + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (mWindowVisible && event.getKeyCode() == KeyEvent.KEYCODE_BACK + && event.getRepeatCount() == 0) { + dismissSoftInput(); + return true; + } + return false; + } + + public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { + return false; + } + + public boolean onKeyUp(int keyCode, KeyEvent event) { + return false; + } + + public boolean onTrackballEvent(MotionEvent event) { + return false; + } + + public void onAppPrivateCommand(String action, Bundle data) { + } + + void startExtractingText() { + if (mExtractEditText != null && getCurrentInputStarted() + && isFullscreenMode()) { + mExtractedToken++; + ExtractedTextRequest req = new ExtractedTextRequest(); + req.token = mExtractedToken; + req.hintMaxLines = 10; + req.hintMaxChars = 10000; + mExtractedText = mInputConnection.getExtractedText(req, + InputConnection.EXTRACTED_TEXT_MONITOR); + if (mExtractedText != null) { + mExtractEditText.setExtractedText(mExtractedText); + } + mExtractEditText.setInputType(getCurrentInputInfo().inputType); + mExtractEditText.setHint(mInputInfo.hintText); + } + } +} diff --git a/core/java/android/inputmethodservice/Keyboard.java b/core/java/android/inputmethodservice/Keyboard.java new file mode 100755 index 0000000..75a2911 --- /dev/null +++ b/core/java/android/inputmethodservice/Keyboard.java @@ -0,0 +1,756 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.inputmethodservice; + +import org.xmlpull.v1.XmlPullParserException; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.Log; +import android.util.TypedValue; +import android.util.Xml; +import android.view.Display; +import android.view.WindowManager; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + + +/** + * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard + * consists of rows of keys. + * <p>The layout file for a keyboard contains XML that looks like the following snippet:</p> + * <pre> + * <Keyboard + * android:keyWidth="%10p" + * android:keyHeight="50px" + * android:horizontalGap="2px" + * android:verticalGap="2px" > + * <Row android:keyWidth="32px" > + * <Key android:keyLabel="A" /> + * ... + * </Row> + * ... + * </Keyboard> + * </pre> + * @attr ref android.R.styleable#Keyboard_keyWidth + * @attr ref android.R.styleable#Keyboard_keyHeight + * @attr ref android.R.styleable#Keyboard_horizontalGap + * @attr ref android.R.styleable#Keyboard_verticalGap + */ +public class Keyboard { + + static final String TAG = "Keyboard"; + + // Keyboard XML Tags + private static final String TAG_KEYBOARD = "Keyboard"; + private static final String TAG_ROW = "Row"; + private static final String TAG_KEY = "Key"; + + public static final int EDGE_LEFT = 0x01; + public static final int EDGE_RIGHT = 0x02; + public static final int EDGE_TOP = 0x04; + public static final int EDGE_BOTTOM = 0x08; + + public static final int KEYCODE_SHIFT = -1; + public static final int KEYCODE_MODE_CHANGE = -2; + public static final int KEYCODE_CANCEL = -3; + public static final int KEYCODE_DONE = -4; + public static final int KEYCODE_DELETE = -5; + public static final int KEYCODE_ALT = -6; + + /** Keyboard label **/ + private CharSequence mLabel; + + /** Horizontal gap default for all rows */ + private int mDefaultHorizontalGap; + + /** Default key width */ + private int mDefaultWidth; + + /** Default key height */ + private int mDefaultHeight; + + /** Default gap between rows */ + private int mDefaultVerticalGap; + + /** Is the keyboard in the shifted state */ + private boolean mShifted; + + /** Key instance for the shift key, if present */ + private Key mShiftKey; + + /** Key index for the shift key, if present */ + private int mShiftKeyIndex = -1; + + /** Current key width, while loading the keyboard */ + private int mKeyWidth; + + /** Current key height, while loading the keyboard */ + private int mKeyHeight; + + /** Total height of the keyboard, including the padding and keys */ + private int mTotalHeight; + + /** + * Total width of the keyboard, including left side gaps and keys, but not any gaps on the + * right side. + */ + private int mTotalWidth; + + /** List of keys in this keyboard */ + private List<Key> mKeys; + + /** List of modifier keys such as Shift & Alt, if any */ + private List<Key> mModifierKeys; + + /** Width of the screen available to fit the keyboard */ + private int mDisplayWidth; + + /** Height of the screen */ + private int mDisplayHeight; + + /** Keyboard mode, or zero, if none. */ + private int mKeyboardMode; + + /** + * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate. + * Some of the key size defaults can be overridden per row from what the {@link Keyboard} + * defines. + * @attr ref android.R.styleable#Keyboard_keyWidth + * @attr ref android.R.styleable#Keyboard_keyHeight + * @attr ref android.R.styleable#Keyboard_horizontalGap + * @attr ref android.R.styleable#Keyboard_verticalGap + * @attr ref android.R.styleable#Keyboard_Row_rowEdgeFlags + * @attr ref android.R.styleable#Keyboard_Row_keyboardMode + */ + public static class Row { + /** Default width of a key in this row. */ + public int defaultWidth; + /** Default height of a key in this row. */ + public int defaultHeight; + /** Default horizontal gap between keys in this row. */ + public int defaultHorizontalGap; + /** Vertical gap following this row. */ + public int verticalGap; + /** + * Edge flags for this row of keys. Possible values that can be assigned are + * {@link Keyboard#EDGE_TOP EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM EDGE_BOTTOM} + */ + public int rowEdgeFlags; + + /** The keyboard mode for this row */ + public int mode; + + private Keyboard parent; + + public Row(Keyboard parent) { + this.parent = parent; + } + + public Row(Resources res, Keyboard parent, XmlResourceParser parser) { + this.parent = parent; + TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), + com.android.internal.R.styleable.Keyboard); + defaultWidth = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyWidth, + parent.mDisplayWidth, parent.mDefaultWidth); + defaultHeight = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyHeight, + parent.mDisplayWidth, parent.mDefaultHeight); + defaultHorizontalGap = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_horizontalGap, + parent.mDisplayWidth, parent.mDefaultHorizontalGap); + verticalGap = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_verticalGap, + parent.mDisplayWidth, parent.mDefaultVerticalGap); + a.recycle(); + a = res.obtainAttributes(Xml.asAttributeSet(parser), + com.android.internal.R.styleable.Keyboard_Row); + rowEdgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Row_rowEdgeFlags, 0); + mode = a.getResourceId(com.android.internal.R.styleable.Keyboard_Row_keyboardMode, + 0); + } + } + + /** + * Class for describing the position and characteristics of a single key in the keyboard. + * + * @attr ref android.R.styleable#Keyboard_keyWidth + * @attr ref android.R.styleable#Keyboard_keyHeight + * @attr ref android.R.styleable#Keyboard_horizontalGap + * @attr ref android.R.styleable#Keyboard_Key_codes + * @attr ref android.R.styleable#Keyboard_Key_keyIcon + * @attr ref android.R.styleable#Keyboard_Key_keyLabel + * @attr ref android.R.styleable#Keyboard_Key_iconPreview + * @attr ref android.R.styleable#Keyboard_Key_isSticky + * @attr ref android.R.styleable#Keyboard_Key_isRepeatable + * @attr ref android.R.styleable#Keyboard_Key_isModifier + * @attr ref android.R.styleable#Keyboard_Key_popupKeyboard + * @attr ref android.R.styleable#Keyboard_Key_popupCharacters + * @attr ref android.R.styleable#Keyboard_Key_keyOutputText + * @attr ref android.R.styleable#Keyboard_Key_keyEdgeFlags + */ + public static class Key { + /** + * All the key codes (unicode or custom code) that this key could generate, zero'th + * being the most important. + */ + public int[] codes; + + /** Label to display */ + public CharSequence label; + + /** Icon to display instead of a label. Icon takes precedence over a label */ + public Drawable icon; + /** Preview version of the icon, for the preview popup */ + public Drawable iconPreview; + /** Width of the key, not including the gap */ + public int width; + /** Height of the key, not including the gap */ + public int height; + /** The horizontal gap before this key */ + public int gap; + /** Whether this key is sticky, i.e., a toggle key */ + public boolean sticky; + /** X coordinate of the key in the keyboard layout */ + public int x; + /** Y coordinate of the key in the keyboard layout */ + public int y; + /** The current pressed state of this key */ + public boolean pressed; + /** If this is a sticky key, is it on? */ + public boolean on; + /** Text to output when pressed. This can be multiple characters, like ".com" */ + public CharSequence text; + /** Popup characters */ + public CharSequence popupCharacters; + + /** + * Flags that specify the anchoring to edges of the keyboard for detecting touch events + * that are just out of the boundary of the key. This is a bit mask of + * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT}, {@link Keyboard#EDGE_TOP} and + * {@link Keyboard#EDGE_BOTTOM}. + */ + public int edgeFlags; + /** Whether this is a modifier key, such as Shift or Alt */ + public boolean modifier; + /** The keyboard that this key belongs to */ + private Keyboard keyboard; + /** + * If this key pops up a mini keyboard, this is the resource id for the XML layout for that + * keyboard. + */ + public int popupResId; + /** Whether this key repeats itself when held down */ + public boolean repeatable; + + + private final static int[] KEY_STATE_NORMAL_ON = { + android.R.attr.state_checkable, + android.R.attr.state_checked + }; + + private final static int[] KEY_STATE_PRESSED_ON = { + android.R.attr.state_pressed, + android.R.attr.state_checkable, + android.R.attr.state_checked + }; + + private final static int[] KEY_STATE_NORMAL_OFF = { + android.R.attr.state_checkable + }; + + private final static int[] KEY_STATE_PRESSED_OFF = { + android.R.attr.state_pressed, + android.R.attr.state_checkable + }; + + private final static int[] KEY_STATE_NORMAL = { + }; + + private final static int[] KEY_STATE_PRESSED = { + android.R.attr.state_pressed + }; + + /** Create an empty key with no attributes. */ + public Key(Row parent) { + keyboard = parent.parent; + } + + /** Create a key with the given top-left coordinate and extract its attributes from + * the XML parser. + * @param res resources associated with the caller's context + * @param parent the row that this key belongs to. The row must already be attached to + * a {@link Keyboard}. + * @param x the x coordinate of the top-left + * @param y the y coordinate of the top-left + * @param parser the XML parser containing the attributes for this key + */ + public Key(Resources res, Row parent, int x, int y, XmlResourceParser parser) { + this(parent); + + this.x = x; + this.y = y; + + TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), + com.android.internal.R.styleable.Keyboard); + + width = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyWidth, + keyboard.mDisplayWidth, parent.defaultWidth); + height = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyHeight, + keyboard.mDisplayHeight, parent.defaultHeight); + gap = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_horizontalGap, + keyboard.mDisplayWidth, parent.defaultHorizontalGap); + a.recycle(); + a = res.obtainAttributes(Xml.asAttributeSet(parser), + com.android.internal.R.styleable.Keyboard_Key); + this.x += gap; + TypedValue codesValue = new TypedValue(); + a.getValue(com.android.internal.R.styleable.Keyboard_Key_codes, + codesValue); + if (codesValue.type == TypedValue.TYPE_INT_DEC + || codesValue.type == TypedValue.TYPE_INT_HEX) { + codes = new int[] { codesValue.data }; + } else if (codesValue.type == TypedValue.TYPE_STRING) { + codes = parseCSV(codesValue.string.toString()); + } + + iconPreview = a.getDrawable(com.android.internal.R.styleable.Keyboard_Key_iconPreview); + if (iconPreview != null) { + iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(), + iconPreview.getIntrinsicHeight()); + } + popupCharacters = a.getText( + com.android.internal.R.styleable.Keyboard_Key_popupCharacters); + popupResId = a.getResourceId( + com.android.internal.R.styleable.Keyboard_Key_popupKeyboard, 0); + repeatable = a.getBoolean( + com.android.internal.R.styleable.Keyboard_Key_isRepeatable, false); + modifier = a.getBoolean( + com.android.internal.R.styleable.Keyboard_Key_isModifier, false); + sticky = a.getBoolean( + com.android.internal.R.styleable.Keyboard_Key_isSticky, false); + edgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Key_keyEdgeFlags, 0); + edgeFlags |= parent.rowEdgeFlags; + + icon = a.getDrawable( + com.android.internal.R.styleable.Keyboard_Key_keyIcon); + if (icon != null) { + icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); + } + label = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyLabel); + text = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyOutputText); + + if (codes == null && !TextUtils.isEmpty(label)) { + codes = new int[] { label.charAt(0) }; + } + a.recycle(); + } + + /** + * Informs the key that it has been pressed, in case it needs to change its appearance or + * state. + * @see #onReleased(boolean) + */ + public void onPressed() { + pressed = !pressed; + } + + /** + * Changes the pressed state of the key. If it is a sticky key, it will also change the + * toggled state of the key if the finger was release inside. + * @param inside whether the finger was released inside the key + * @see #onPressed() + */ + public void onReleased(boolean inside) { + pressed = !pressed; + if (sticky) { + on = !on; + } + } + + int[] parseCSV(String value) { + int count = 0; + int lastIndex = 0; + if (value.length() > 0) { + count++; + while ((lastIndex = value.indexOf(",", lastIndex + 1)) > 0) { + count++; + } + } + int[] values = new int[count]; + count = 0; + StringTokenizer st = new StringTokenizer(value, ","); + while (st.hasMoreTokens()) { + try { + values[count++] = Integer.parseInt(st.nextToken()); + } catch (NumberFormatException nfe) { + Log.e(TAG, "Error parsing keycodes " + value); + } + } + return values; + } + + /** + * Detects if a point falls inside this key. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return whether or not the point falls inside the key. If the key is attached to an edge, + * it will assume that all points between the key and the edge are considered to be inside + * the key. + */ + public boolean isInside(int x, int y) { + boolean leftEdge = (edgeFlags & EDGE_LEFT) > 0; + boolean rightEdge = (edgeFlags & EDGE_RIGHT) > 0; + boolean topEdge = (edgeFlags & EDGE_TOP) > 0; + boolean bottomEdge = (edgeFlags & EDGE_BOTTOM) > 0; + if ((x >= this.x || (leftEdge && x <= this.x + this.width)) + && (x < this.x + this.width || (rightEdge && x >= this.x)) + && (y >= this.y || (topEdge && y <= this.y + this.height)) + && (y < this.y + this.height || (bottomEdge && y >= this.y))) { + return true; + } else { + return false; + } + } + + + /** + * Returns the square of the distance between the center of the key and the given point. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return the square of the distance of the point from the center of the key + */ + public int squaredDistanceFrom(int x, int y) { + float xDist = Math.abs((this.x + this.x + width) / 2f - x); + float yDist = Math.abs((this.y + this.y + height) / 2f - y); + return (int) (xDist * xDist + yDist * yDist); + } + + /** + * Returns the drawable state for the key, based on the current state and type of the key. + * @return the drawable state of the key. + * @see android.graphics.drawable.StateListDrawable#setState(int[]) + */ + public int[] getCurrentDrawableState() { + int[] states = KEY_STATE_NORMAL; + + if (on) { + if (pressed) { + states = KEY_STATE_PRESSED_ON; + } else { + states = KEY_STATE_NORMAL_ON; + } + } else { + if (sticky) { + if (pressed) { + states = KEY_STATE_PRESSED_OFF; + } else { + states = KEY_STATE_NORMAL_OFF; + } + } else { + if (pressed) { + states = KEY_STATE_PRESSED; + } + } + } + return states; + } + } + + /** + * Creates a keyboard from the given xml key layout file. + * @param context the application or service context + * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. + */ + public Keyboard(Context context, int xmlLayoutResId) { + this(context, xmlLayoutResId, 0); + } + + /** + * Creates a keyboard from the given xml key layout file. Weeds out rows + * that have a keyboard mode defined but don't match the specified mode. + * @param context the application or service context + * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. + * @param modeId keyboard mode identifier + */ + public Keyboard(Context context, int xmlLayoutResId, int modeId) { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + final Display display = wm.getDefaultDisplay(); + mDisplayWidth = display.getWidth(); + mDisplayHeight = display.getHeight(); + mDefaultHorizontalGap = 0; + mDefaultWidth = mDisplayWidth / 10; + mDefaultVerticalGap = 0; + mDefaultHeight = mDefaultWidth; + mKeys = new ArrayList<Key>(); + mModifierKeys = new ArrayList<Key>(); + mKeyboardMode = modeId; + loadKeyboard(context, context.getResources().getXml(xmlLayoutResId)); + } + + /** + * <p>Creates a blank keyboard from the given resource file and populates it with the specified + * characters in left-to-right, top-to-bottom fashion, using the specified number of columns. + * </p> + * <p>If the specified number of columns is -1, then the keyboard will fit as many keys as + * possible in each row.</p> + * @param context the application or service context + * @param layoutTemplateResId the layout template file, containing no keys. + * @param characters the list of characters to display on the keyboard. One key will be created + * for each character. + * @param columns the number of columns of keys to display. If this number is greater than the + * number of keys that can fit in a row, it will be ignored. If this number is -1, the + * keyboard will fit as many keys as possible in each row. + */ + public Keyboard(Context context, int layoutTemplateResId, + CharSequence characters, int columns, int horizontalPadding) { + this(context, layoutTemplateResId); + int x = 0; + int y = 0; + int column = 0; + mTotalWidth = 0; + + Row row = new Row(this); + row.defaultHeight = mDefaultHeight; + row.defaultWidth = mDefaultWidth; + row.defaultHorizontalGap = mDefaultHorizontalGap; + row.verticalGap = mDefaultVerticalGap; + row.rowEdgeFlags = EDGE_TOP | EDGE_BOTTOM; + + final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns; + for (int i = 0; i < characters.length(); i++) { + char c = characters.charAt(i); + if (column >= maxColumns + || x + mDefaultWidth + horizontalPadding > mDisplayWidth) { + x = 0; + y += mDefaultVerticalGap + mDefaultHeight; + column = 0; + } + final Key key = new Key(row); + key.x = x; + key.y = y; + key.width = mDefaultWidth; + key.height = mDefaultHeight; + key.gap = mDefaultHorizontalGap; + key.label = String.valueOf(c); + key.codes = new int[] { c }; + column++; + x += key.width + key.gap; + mKeys.add(key); + if (x > mTotalWidth) { + mTotalWidth = x; + } + } + mTotalHeight = y + mDefaultHeight; + } + + public List<Key> getKeys() { + return mKeys; + } + + public List<Key> getModifierKeys() { + return mModifierKeys; + } + + protected int getHorizontalGap() { + return mDefaultHorizontalGap; + } + + protected void setHorizontalGap(int gap) { + mDefaultHorizontalGap = gap; + } + + protected int getVerticalGap() { + return mDefaultVerticalGap; + } + + protected void setVerticalGap(int gap) { + mDefaultVerticalGap = gap; + } + + protected int getKeyHeight() { + return mDefaultHeight; + } + + protected void setKeyHeight(int height) { + mDefaultHeight = height; + } + + protected int getKeyWidth() { + return mDefaultWidth; + } + + protected void setKeyWidth(int width) { + mDefaultWidth = width; + } + + /** + * Returns the total height of the keyboard + * @return the total height of the keyboard + */ + public int getHeight() { + return mTotalHeight; + } + + public int getMinWidth() { + return mTotalWidth; + } + + public boolean setShifted(boolean shiftState) { + if (mShiftKey != null) { + mShiftKey.on = shiftState; + } + if (mShifted != shiftState) { + mShifted = shiftState; + return true; + } + return false; + } + + public boolean isShifted() { + return mShifted; + } + + public int getShiftKeyIndex() { + return mShiftKeyIndex; + } + + protected Row createRowFromXml(Resources res, XmlResourceParser parser) { + return new Row(res, this, parser); + } + + protected Key createKeyFromXml(Resources res, Row parent, int x, int y, + XmlResourceParser parser) { + return new Key(res, parent, x, y, parser); + } + + private void loadKeyboard(Context context, XmlResourceParser parser) { + boolean inKey = false; + boolean inRow = false; + boolean leftMostKey = false; + int row = 0; + int x = 0; + int y = 0; + Key key = null; + Row currentRow = null; + Resources res = context.getResources(); + boolean skipRow = false; + + try { + int event; + while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) { + if (event == XmlResourceParser.START_TAG) { + String tag = parser.getName(); + if (TAG_ROW.equals(tag)) { + inRow = true; + x = 0; + currentRow = createRowFromXml(res, parser); + skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode; + if (skipRow) { + skipToEndOfRow(parser); + inRow = false; + } + } else if (TAG_KEY.equals(tag)) { + inKey = true; + key = createKeyFromXml(res, currentRow, x, y, parser); + mKeys.add(key); + if (key.codes[0] == KEYCODE_SHIFT) { + mShiftKey = key; + mShiftKeyIndex = mKeys.size()-1; + mModifierKeys.add(key); + } else if (key.codes[0] == KEYCODE_ALT) { + mModifierKeys.add(key); + } + } else if (TAG_KEYBOARD.equals(tag)) { + parseKeyboardAttributes(res, parser); + } + } else if (event == XmlResourceParser.END_TAG) { + if (inKey) { + inKey = false; + x += key.gap + key.width; + if (x > mTotalWidth) { + mTotalWidth = x; + } + } else if (inRow) { + inRow = false; + y += currentRow.verticalGap; + y += currentRow.defaultHeight; + row++; + } else { + // TODO: error or extend? + } + } + } + } catch (Exception e) { + Log.e(TAG, "Parse error:" + e); + e.printStackTrace(); + } + mTotalHeight = y - mDefaultVerticalGap; + } + + private void skipToEndOfRow(XmlResourceParser parser) + throws XmlPullParserException, IOException { + int event; + while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) { + if (event == XmlResourceParser.END_TAG + && parser.getName().equals(TAG_ROW)) { + break; + } + } + } + + private void parseKeyboardAttributes(Resources res, XmlResourceParser parser) { + TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), + com.android.internal.R.styleable.Keyboard); + + mDefaultWidth = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyWidth, + mDisplayWidth, mDisplayWidth / 10); + mDefaultHeight = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyHeight, + mDisplayHeight, 50); + mDefaultHorizontalGap = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_horizontalGap, + mDisplayWidth, 0); + mDefaultVerticalGap = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_verticalGap, + mDisplayHeight, 0); + a.recycle(); + } + + static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) { + TypedValue value = a.peekValue(index); + if (value == null) return defValue; + if (value.type == TypedValue.TYPE_DIMENSION) { + return a.getDimensionPixelOffset(index, defValue); + } else if (value.type == TypedValue.TYPE_FRACTION) { + return (int) a.getFraction(index, base, base, defValue); + } + return defValue; + } +} diff --git a/core/java/android/inputmethodservice/KeyboardView.java b/core/java/android/inputmethodservice/KeyboardView.java new file mode 100755 index 0000000..56473da --- /dev/null +++ b/core/java/android/inputmethodservice/KeyboardView.java @@ -0,0 +1,1049 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.inputmethodservice; + +import com.android.internal.R; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Paint.Align; +import android.graphics.drawable.Drawable; +import android.inputmethodservice.Keyboard.Key; +import android.os.Handler; +import android.os.Message; +import android.os.Vibrator; +import android.preference.PreferenceManager; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.widget.Button; +import android.widget.PopupWindow; +import android.widget.TextView; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and + * detecting key presses and touch movements. + * + * @attr ref android.R.styleable#KeyboardView_keyBackground + * @attr ref android.R.styleable#KeyboardView_keyPreviewLayout + * @attr ref android.R.styleable#KeyboardView_keyPreviewOffset + * @attr ref android.R.styleable#KeyboardView_labelTextSize + * @attr ref android.R.styleable#KeyboardView_keyTextSize + * @attr ref android.R.styleable#KeyboardView_keyTextColor + * @attr ref android.R.styleable#KeyboardView_verticalCorrection + * @attr ref android.R.styleable#KeyboardView_popupLayout + */ +public class KeyboardView extends View implements View.OnClickListener { + + /** + * Listener for virtual keyboard events. + */ + public interface OnKeyboardActionListener { + /** + * Send a key press to the listener. + * @param primaryCode this is the key that was pressed + * @param keyCodes the codes for all the possible alternative keys + * with the primary code being the first. If the primary key code is + * a single character such as an alphabet or number or symbol, the alternatives + * will include other characters that may be on the same key or adjacent keys. + * These codes are useful to correct for accidental presses of a key adjacent to + * the intended key. + */ + void onKey(int primaryCode, int[] keyCodes); + + /** + * Called when the user quickly moves the finger from right to left. + */ + void swipeLeft(); + + /** + * Called when the user quickly moves the finger from left to right. + */ + void swipeRight(); + + /** + * Called when the user quickly moves the finger from up to down. + */ + void swipeDown(); + + /** + * Called when the user quickly moves the finger from down to up. + */ + void swipeUp(); + } + + private static final boolean DEBUG = false; + private static final int NOT_A_KEY = -1; + private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE }; + private static final int[] LONG_PRESSABLE_STATE_SET = { R.attr.state_long_pressable }; + + private Keyboard mKeyboard; + private int mCurrentKeyIndex = NOT_A_KEY; + private int mLabelTextSize; + private int mKeyTextSize; + private int mKeyTextColor; + + private TextView mPreviewText; + private PopupWindow mPreviewPopup; + private int mPreviewTextSizeLarge; + private int mPreviewOffset; + private int mPreviewHeight; + private int[] mOffsetInWindow; + + private PopupWindow mPopupKeyboard; + private View mMiniKeyboardContainer; + private KeyboardView mMiniKeyboard; + private boolean mMiniKeyboardOnScreen; + private View mPopupParent; + private int mMiniKeyboardOffsetX; + private int mMiniKeyboardOffsetY; + private Map<Key,View> mMiniKeyboardCache; + private int[] mWindowOffset; + + /** Listener for {@link OnKeyboardActionListener}. */ + private OnKeyboardActionListener mKeyboardActionListener; + + private static final int MSG_REMOVE_PREVIEW = 1; + private static final int MSG_REPEAT = 2; + + private int mVerticalCorrection; + private int mProximityThreshold; + + private boolean mPreviewCentered = false; + private boolean mShowPreview = true; + private boolean mShowTouchPoints = false; + private int mPopupPreviewX; + private int mPopupPreviewY; + + private int mLastX; + private int mLastY; + private int mStartX; + private int mStartY; + + private boolean mVibrateOn; + private boolean mSoundOn; + private boolean mProximityCorrectOn; + + private Paint mPaint; + private Rect mPadding; + + private long mDownTime; + private long mLastMoveTime; + private int mLastKey; + private int mLastCodeX; + private int mLastCodeY; + private int mCurrentKey = NOT_A_KEY; + private long mLastKeyTime; + private long mCurrentKeyTime; + private int[] mKeyIndices = new int[12]; + private GestureDetector mGestureDetector; + private int mPopupX; + private int mPopupY; + private int mRepeatKeyIndex = NOT_A_KEY; + private int mPopupLayout; + private boolean mAbortKey; + + private Drawable mKeyBackground; + + private static final String PREF_VIBRATE_ON = "vibrate_on"; + private static final String PREF_SOUND_ON = "sound_on"; + private static final String PREF_PROXIMITY_CORRECTION = "hit_correction"; + + private static final int REPEAT_INTERVAL = 50; // ~20 keys per second + private static final int REPEAT_START_DELAY = 400; + + private Vibrator mVibrator; + private long[] mVibratePattern = new long[] {1, 20}; + + private static int MAX_NEARBY_KEYS = 12; + private int[] mDistances = new int[MAX_NEARBY_KEYS]; + + // For multi-tap + private int mLastSentIndex; + private int mTapCount; + private long mLastTapTime; + private boolean mInMultiTap; + private static final int MULTITAP_INTERVAL = 800; // milliseconds + private StringBuilder mPreviewLabel = new StringBuilder(1); + + Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_REMOVE_PREVIEW: + mPreviewText.setVisibility(INVISIBLE); + break; + case MSG_REPEAT: + if (repeatKey()) { + Message repeat = Message.obtain(this, MSG_REPEAT); + sendMessageDelayed(repeat, REPEAT_INTERVAL); + } + break; + } + + } + }; + + public KeyboardView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.keyboardViewStyle); + } + + public KeyboardView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = + context.obtainStyledAttributes( + attrs, android.R.styleable.KeyboardView, defStyle, 0); + + LayoutInflater inflate = + (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + int previewLayout = 0; + int keyTextSize = 0; + + int n = a.getIndexCount(); + + for (int i = 0; i < n; i++) { + int attr = a.getIndex(i); + + switch (attr) { + case com.android.internal.R.styleable.KeyboardView_keyBackground: + mKeyBackground = a.getDrawable(attr); + break; + case com.android.internal.R.styleable.KeyboardView_verticalCorrection: + mVerticalCorrection = a.getDimensionPixelOffset(attr, 0); + break; + case com.android.internal.R.styleable.KeyboardView_keyPreviewLayout: + previewLayout = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.KeyboardView_keyPreviewOffset: + mPreviewOffset = a.getDimensionPixelOffset(attr, 0); + break; + case com.android.internal.R.styleable.KeyboardView_keyPreviewHeight: + mPreviewHeight = a.getDimensionPixelSize(attr, 80); + break; + case com.android.internal.R.styleable.KeyboardView_keyTextSize: + mKeyTextSize = a.getDimensionPixelSize(attr, 18); + break; + case com.android.internal.R.styleable.KeyboardView_keyTextColor: + mKeyTextColor = a.getColor(attr, 0xFF000000); + break; + case com.android.internal.R.styleable.KeyboardView_labelTextSize: + mLabelTextSize = a.getDimensionPixelSize(attr, 14); + break; + case com.android.internal.R.styleable.KeyboardView_popupLayout: + mPopupLayout = a.getResourceId(attr, 0); + break; + } + } + + // Get the settings preferences + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); + mVibrateOn = sp.getBoolean(PREF_VIBRATE_ON, mVibrateOn); + mSoundOn = sp.getBoolean(PREF_SOUND_ON, mSoundOn); + mProximityCorrectOn = sp.getBoolean(PREF_PROXIMITY_CORRECTION, true); + + mPreviewPopup = new PopupWindow(context); + if (previewLayout != 0) { + mPreviewText = (TextView) inflate.inflate(previewLayout, null); + mPreviewTextSizeLarge = (int) mPreviewText.getTextSize(); + mPreviewPopup.setContentView(mPreviewText); + mPreviewPopup.setBackgroundDrawable(null); + } else { + mShowPreview = false; + } + + mPreviewPopup.setTouchable(false); + + mPopupKeyboard = new PopupWindow(context); + mPopupKeyboard.setBackgroundDrawable(null); + //mPopupKeyboard.setClippingEnabled(false); + + mPopupParent = this; + //mPredicting = true; + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setTextSize(keyTextSize); + mPaint.setTextAlign(Align.CENTER); + + mPadding = new Rect(0, 0, 0, 0); + mMiniKeyboardCache = new HashMap<Key,View>(); + mKeyBackground.getPadding(mPadding); + + resetMultiTap(); + initGestureDetector(); + } + + private void initGestureDetector() { + mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onFling(MotionEvent me1, MotionEvent me2, + float velocityX, float velocityY) { + if (velocityX > 400 && Math.abs(velocityY) < 400) { + swipeRight(); + return true; + } else if (velocityX < -400 && Math.abs(velocityY) < 400) { + swipeLeft(); + return true; + } else if (velocityY < -400 && Math.abs(velocityX) < 400) { + swipeUp(); + return true; + } else if (velocityY > 400 && Math.abs(velocityX) < 400) { + swipeDown(); + return true; + } + return false; + } + + @Override + public void onLongPress(MotionEvent me) { + openPopupIfRequired(me); + } + }); + } + + public void setOnKeyboardActionListener(OnKeyboardActionListener listener) { + mKeyboardActionListener = listener; + } + + /** + * Returns the {@link OnKeyboardActionListener} object. + * @return the listener attached to this keyboard + */ + protected OnKeyboardActionListener getOnKeyboardActionListener() { + return mKeyboardActionListener; + } + + /** + * Attaches a keyboard to this view. The keyboard can be switched at any time and the + * view will re-layout itself to accommodate the keyboard. + * @see Keyboard + * @see #getKeyboard() + * @param keyboard the keyboard to display in this view + */ + public void setKeyboard(Keyboard keyboard) { + mKeyboard = keyboard; + requestLayout(); + invalidate(); + computeProximityThreshold(keyboard); + } + + /** + * Returns the current keyboard being displayed by this view. + * @return the currently attached keyboard + * @see #setKeyboard(Keyboard) + */ + public Keyboard getKeyboard() { + return mKeyboard; + } + + /** + * Sets the state of the shift key of the keyboard, if any. + * @param shifted whether or not to enable the state of the shift key + * @return true if the shift key state changed, false if there was no change + * @see KeyboardView#isShifted() + */ + public boolean setShifted(boolean shifted) { + if (mKeyboard != null) { + if (mKeyboard.setShifted(shifted)) { + // The whole keyboard probably needs to be redrawn + invalidate(); + return true; + } + } + return false; + } + + /** + * Returns the state of the shift key of the keyboard, if any. + * @return true if the shift is in a pressed state, false otherwise. If there is + * no shift key on the keyboard or there is no keyboard attached, it returns false. + * @see KeyboardView#setShifted(boolean) + */ + public boolean isShifted() { + if (mKeyboard != null) { + return mKeyboard.isShifted(); + } + return false; + } + + /** + * Enables or disables the key feedback popup. This is a popup that shows a magnified + * version of the depressed key. By default the preview is enabled. + * @param previewEnabled whether or not to enable the key feedback popup + * @see #isPreviewEnabled() + */ + public void setPreviewEnabled(boolean previewEnabled) { + mShowPreview = previewEnabled; + } + + /** + * Returns the enabled state of the key feedback popup. + * @return whether or not the key feedback popup is enabled + * @see #setPreviewEnabled(boolean) + */ + public boolean isPreviewEnabled() { + return mShowPreview; + } + + public void setVerticalCorrection(int verticalOffset) { + + } + public void setPopupParent(View v) { + mPopupParent = v; + } + + public void setPopupOffset(int x, int y) { + mMiniKeyboardOffsetX = x; + mMiniKeyboardOffsetY = y; + if (mPreviewPopup.isShowing()) { + mPreviewPopup.dismiss(); + } + } + + /** + * Popup keyboard close button clicked. + * @hide + */ + public void onClick(View v) { + dismissPopupKeyboard(); + } + + private CharSequence adjustCase(CharSequence label) { + if (mKeyboard.isShifted() && label != null && label.length() == 1 + && Character.isLowerCase(label.charAt(0))) { + label = label.toString().toUpperCase(); + } + return label; + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Round up a little + if (mKeyboard == null) { + setMeasuredDimension(mPaddingLeft + mPaddingRight, mPaddingTop + mPaddingBottom); + } else { + int width = mKeyboard.getMinWidth() + mPaddingLeft + mPaddingRight; + if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { + width = MeasureSpec.getSize(widthMeasureSpec); + } + setMeasuredDimension(width, mKeyboard.getHeight() + mPaddingTop + mPaddingBottom); + } + } + + /** + * Compute the average distance between adjacent keys (horizontally and vertically) + * and square it to get the proximity threshold. We use a square here and in computing + * the touch distance from a key's center to avoid taking a square root. + * @param keyboard + */ + private void computeProximityThreshold(Keyboard keyboard) { + if (keyboard == null) return; + List<Key> keys = keyboard.getKeys(); + if (keys == null) return; + int length = keys.size(); + int dimensionSum = 0; + for (int i = 0; i < length; i++) { + Key key = keys.get(i); + dimensionSum += key.width + key.gap + key.height; + } + if (dimensionSum < 0 || length == 0) return; + mProximityThreshold = dimensionSum / (length * 2); + mProximityThreshold *= mProximityThreshold; // Square it + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mKeyboard == null) return; + + final Paint paint = mPaint; + //final int descent = (int) paint.descent(); + final Drawable keyBackground = mKeyBackground; + final Rect padding = mPadding; + final int kbdPaddingLeft = mPaddingLeft; + final int kbdPaddingTop = mPaddingTop; + List<Key> keys = mKeyboard.getKeys(); + //canvas.translate(0, mKeyboardPaddingTop); + paint.setAlpha(255); + paint.setColor(mKeyTextColor); + + final int keyCount = keys.size(); + for (int i = 0; i < keyCount; i++) { + final Key key = keys.get(i); + int[] drawableState = key.getCurrentDrawableState(); + keyBackground.setState(drawableState); + + // Switch the character to uppercase if shift is pressed + String label = key.label == null? null : adjustCase(key.label).toString(); + + final Rect bounds = keyBackground.getBounds(); + if (key.width != bounds.right || + key.height != bounds.bottom) { + keyBackground.setBounds(0, 0, key.width, key.height); + } + canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop); + keyBackground.draw(canvas); + + if (label != null) { + // For characters, use large font. For labels like "Done", use small font. + if (label.length() > 1 && key.codes.length < 2) { + paint.setTextSize(mLabelTextSize); + paint.setFakeBoldText(true); + } else { + paint.setTextSize(mKeyTextSize); + paint.setFakeBoldText(false); + } + // Draw a drop shadow for the text + paint.setShadowLayer(3f, 0, 0, 0xCC000000); + // Draw the text + canvas.drawText(label, + (key.width - padding.left - padding.right) / 2 + + padding.left, + (key.height - padding.top - padding.bottom) / 2 + + (paint.getTextSize() - paint.descent()) / 2 + padding.top, + paint); + // Turn off drop shadow + paint.setShadowLayer(0, 0, 0, 0); + } else if (key.icon != null) { + final int drawableX = (key.width - padding.left - padding.right + - key.icon.getIntrinsicWidth()) / 2 + padding.left; + final int drawableY = (key.height - padding.top - padding.bottom + - key.icon.getIntrinsicHeight()) / 2 + padding.top; + canvas.translate(drawableX, drawableY); + key.icon.setBounds(0, 0, + key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight()); + key.icon.draw(canvas); + canvas.translate(-drawableX, -drawableY); + } + canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop); + } + + // Overlay a dark rectangle to dim the keyboard + if (mMiniKeyboardOnScreen) { + paint.setColor(0xA0000000); + canvas.drawRect(0, 0, getWidth(), getHeight(), paint); + } + + if (DEBUG && mShowTouchPoints) { + paint.setAlpha(128); + paint.setColor(0xFFFF0000); + canvas.drawCircle(mStartX, mStartY, 3, paint); + canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint); + paint.setColor(0xFF0000FF); + canvas.drawCircle(mLastX, mLastY, 3, paint); + paint.setColor(0xFF00FF00); + canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint); + } + } + + private void playKeyClick() { + if (mSoundOn) { + playSoundEffect(0); + } + } + + private void vibrate() { + if (!mVibrateOn) { + return; + } + if (mVibrator == null) { + mVibrator = new Vibrator(); + } + mVibrator.vibrate(mVibratePattern, -1); + } + + private int getKeyIndices(int x, int y, int[] allKeys) { + final List<Key> keys = mKeyboard.getKeys(); + final boolean shifted = mKeyboard.isShifted(); + int primaryIndex = NOT_A_KEY; + int closestKey = NOT_A_KEY; + int closestKeyDist = mProximityThreshold + 1; + java.util.Arrays.fill(mDistances, Integer.MAX_VALUE); + final int keyCount = keys.size(); + for (int i = 0; i < keyCount; i++) { + final Key key = keys.get(i); + int dist = 0; + boolean isInside = key.isInside(x,y); + if (((mProximityCorrectOn + && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold) + || isInside) + && key.codes[0] > 32) { + // Find insertion point + final int nCodes = key.codes.length; + if (dist < closestKeyDist) { + closestKeyDist = dist; + closestKey = i; + } + + if (allKeys == null) continue; + + for (int j = 0; j < mDistances.length; j++) { + if (mDistances[j] > dist) { + // Make space for nCodes codes + System.arraycopy(mDistances, j, mDistances, j + nCodes, + mDistances.length - j - nCodes); + System.arraycopy(allKeys, j, allKeys, j + nCodes, + allKeys.length - j - nCodes); + for (int c = 0; c < nCodes; c++) { + allKeys[j + c] = key.codes[c]; + if (shifted) { + //allKeys[j + c] = Character.toUpperCase(key.codes[c]); + } + mDistances[j + c] = dist; + } + break; + } + } + } + + if (isInside) { + primaryIndex = i; + } + } + if (primaryIndex == NOT_A_KEY) { + primaryIndex = closestKey; + } + return primaryIndex; + } + + private void detectAndSendKey(int x, int y, long eventTime) { + int index = mCurrentKey; + if (index != NOT_A_KEY) { + vibrate(); + final Key key = mKeyboard.getKeys().get(index); + if (key.text != null) { + for (int i = 0; i < key.text.length(); i++) { + mKeyboardActionListener.onKey(key.text.charAt(i), key.codes); + } + } else { + int code = key.codes[0]; + //TextEntryState.keyPressedAt(key, x, y); + int[] codes = new int[MAX_NEARBY_KEYS]; + Arrays.fill(codes, NOT_A_KEY); + getKeyIndices(x, y, codes); + // Multi-tap + if (mInMultiTap) { + if (mTapCount != -1) { + mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE); + } else { + mTapCount = 0; + } + code = key.codes[mTapCount]; + } + mKeyboardActionListener.onKey(code, codes); + } + mLastSentIndex = index; + mLastTapTime = eventTime; + } + } + + /** + * Handle multi-tap keys by producing the key label for the current multi-tap state. + */ + private CharSequence getPreviewText(Key key) { + if (mInMultiTap) { + // Multi-tap + mPreviewLabel.setLength(0); + mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]); + return adjustCase(mPreviewLabel); + } else { + return adjustCase(key.label); + } + } + + private void showPreview(int keyIndex) { + int oldKeyIndex = mCurrentKeyIndex; + final PopupWindow previewPopup = mPreviewPopup; + + mCurrentKeyIndex = keyIndex; + // Release the old key and press the new key + final List<Key> keys = mKeyboard.getKeys(); + if (oldKeyIndex != mCurrentKeyIndex) { + if (oldKeyIndex != NOT_A_KEY && keys.size() > oldKeyIndex) { + keys.get(oldKeyIndex).onReleased(mCurrentKeyIndex == NOT_A_KEY); + invalidateKey(oldKeyIndex); + } + if (mCurrentKeyIndex != NOT_A_KEY && keys.size() > mCurrentKeyIndex) { + keys.get(mCurrentKeyIndex).onPressed(); + invalidateKey(mCurrentKeyIndex); + } + } + // If key changed and preview is on ... + if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) { + if (previewPopup.isShowing()) { + if (keyIndex == NOT_A_KEY) { + mHandler.sendMessageDelayed(mHandler + .obtainMessage(MSG_REMOVE_PREVIEW), 60); + } + } + if (keyIndex != NOT_A_KEY) { + Key key = keys.get(keyIndex); + if (key.icon != null) { + mPreviewText.setCompoundDrawables(null, null, null, + key.iconPreview != null ? key.iconPreview : key.icon); + mPreviewText.setText(null); + } else { + mPreviewText.setCompoundDrawables(null, null, null, null); + mPreviewText.setText(getPreviewText(key)); + if (key.label.length() > 1 && key.codes.length < 2) { + mPreviewText.setTextSize(mLabelTextSize); + } else { + mPreviewText.setTextSize(mPreviewTextSizeLarge); + } + } + mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width + + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); + final int popupHeight = mPreviewHeight; + LayoutParams lp = mPreviewText.getLayoutParams(); + if (lp != null) { + lp.width = popupWidth; + lp.height = popupHeight; + } + previewPopup.setWidth(popupWidth); + previewPopup.setHeight(popupHeight); + if (!mPreviewCentered) { + mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft; + mPopupPreviewY = key.y - popupHeight + mPreviewOffset; + } else { + // TODO: Fix this if centering is brought back + mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2; + mPopupPreviewY = - mPreviewText.getMeasuredHeight(); + } + mHandler.removeMessages(MSG_REMOVE_PREVIEW); + if (mOffsetInWindow == null) { + mOffsetInWindow = new int[2]; + getLocationInWindow(mOffsetInWindow); + mOffsetInWindow[0] += mMiniKeyboardOffsetX; // Offset may be zero + mOffsetInWindow[1] += mMiniKeyboardOffsetY; // Offset may be zero + } + // Set the preview background state + mPreviewText.getBackground().setState( + key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); + if (previewPopup.isShowing()) { + previewPopup.update(mPopupPreviewX + mOffsetInWindow[0], + mPopupPreviewY + mOffsetInWindow[1], + popupWidth, popupHeight); + } else { + previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY, + mPopupPreviewX + mOffsetInWindow[0], + mPopupPreviewY + mOffsetInWindow[1]); + } + mPreviewText.setVisibility(VISIBLE); + } + } + } + + private void invalidateKey(int keyIndex) { + if (keyIndex < 0 || keyIndex >= mKeyboard.getKeys().size()) { + return; + } + final Key key = mKeyboard.getKeys().get(keyIndex); + invalidate(key.x + mPaddingLeft, key.y + mPaddingTop, + key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop); + } + + private boolean openPopupIfRequired(MotionEvent me) { + // Check if we have a popup layout specified first. + if (mPopupLayout == 0) { + return false; + } + if (mCurrentKey < 0 || mCurrentKey >= mKeyboard.getKeys().size()) { + return false; + } + + Key popupKey = mKeyboard.getKeys().get(mCurrentKey); + boolean result = onLongPress(popupKey); + if (result) { + mAbortKey = true; + showPreview(NOT_A_KEY); + } + return result; + } + + /** + * Called when a key is long pressed. By default this will open any popup keyboard associated + * with this key through the attributes popupLayout and popupCharacters. + * @param popupKey the key that was long pressed + * @return true if the long press is handled, false otherwise. Subclasses should call the + * method on the base class if the subclass doesn't wish to handle the call. + */ + protected boolean onLongPress(Key popupKey) { + int popupKeyboardId = popupKey.popupResId; + + if (popupKeyboardId != 0) { + mMiniKeyboardContainer = mMiniKeyboardCache.get(popupKey); + if (mMiniKeyboardContainer == null) { + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null); + mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( + com.android.internal.R.id.keyboardView); + View closeButton = mMiniKeyboardContainer.findViewById( + com.android.internal.R.id.button_close); + if (closeButton != null) closeButton.setOnClickListener(this); + mMiniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() { + public void onKey(int primaryCode, int[] keyCodes) { + mKeyboardActionListener.onKey(primaryCode, keyCodes); + dismissPopupKeyboard(); + } + + public void swipeLeft() { } + public void swipeRight() { } + public void swipeUp() { } + public void swipeDown() { } + }); + //mInputView.setSuggest(mSuggest); + Keyboard keyboard; + if (popupKey.popupCharacters != null) { + keyboard = new Keyboard(getContext(), popupKeyboardId, + popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight()); + } else { + keyboard = new Keyboard(getContext(), popupKeyboardId); + } + mMiniKeyboard.setKeyboard(keyboard); + mMiniKeyboard.setPopupParent(this); + mMiniKeyboardContainer.measure( + MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); + + mMiniKeyboardCache.put(popupKey, mMiniKeyboardContainer); + } else { + mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( + com.android.internal.R.id.keyboardView); + } + if (mWindowOffset == null) { + mWindowOffset = new int[2]; + getLocationInWindow(mWindowOffset); + } + mPopupX = popupKey.x + mPaddingLeft; + mPopupY = popupKey.y + mPaddingTop; + mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth(); + mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight(); + final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mWindowOffset[0]; + final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mWindowOffset[1]; + mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y); + mMiniKeyboard.setShifted(isShifted()); + mPopupKeyboard.setContentView(mMiniKeyboardContainer); + mPopupKeyboard.setWidth(mMiniKeyboardContainer.getMeasuredWidth()); + mPopupKeyboard.setHeight(mMiniKeyboardContainer.getMeasuredHeight()); + mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y); + mMiniKeyboardOnScreen = true; + //mMiniKeyboard.onTouchEvent(getTranslatedEvent(me)); + invalidate(); + return true; + } + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent me) { + int touchX = (int) me.getX() - mPaddingLeft; + int touchY = (int) me.getY() + mVerticalCorrection - mPaddingTop; + int action = me.getAction(); + long eventTime = me.getEventTime(); + int keyIndex = getKeyIndices(touchX, touchY, null); + + if (mGestureDetector.onTouchEvent(me)) { + showPreview(NOT_A_KEY); + mHandler.removeMessages(MSG_REPEAT); + return true; + } + + // Needs to be called after the gesture detector gets a turn, as it may have + // displayed the mini keyboard + if (mMiniKeyboardOnScreen) { + return true; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + mAbortKey = false; + mStartX = touchX; + mStartY = touchY; + mLastCodeX = touchX; + mLastCodeY = touchY; + mLastKeyTime = 0; + mCurrentKeyTime = 0; + mLastKey = NOT_A_KEY; + mCurrentKey = keyIndex; + mDownTime = me.getEventTime(); + mLastMoveTime = mDownTime; + checkMultiTap(eventTime, keyIndex); + if (mCurrentKey >= 0 && mKeyboard.getKeys().get(mCurrentKey).repeatable) { + mRepeatKeyIndex = mCurrentKey; + repeatKey(); + Message msg = mHandler.obtainMessage(MSG_REPEAT); + mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY); + } + showPreview(keyIndex); + playKeyClick(); + vibrate(); + break; + + case MotionEvent.ACTION_MOVE: + if (keyIndex != NOT_A_KEY) { + if (mCurrentKey == NOT_A_KEY) { + mCurrentKey = keyIndex; + mCurrentKeyTime = eventTime - mDownTime; + } else { + if (keyIndex == mCurrentKey) { + mCurrentKeyTime += eventTime - mLastMoveTime; + } else { + resetMultiTap(); + mLastKey = mCurrentKey; + mLastCodeX = mLastX; + mLastCodeY = mLastY; + mLastKeyTime = + mCurrentKeyTime + eventTime - mLastMoveTime; + mCurrentKey = keyIndex; + mCurrentKeyTime = 0; + } + } + if (keyIndex != mRepeatKeyIndex) { + mHandler.removeMessages(MSG_REPEAT); + mRepeatKeyIndex = NOT_A_KEY; + } + } + showPreview(keyIndex); + break; + + case MotionEvent.ACTION_UP: + mHandler.removeMessages(MSG_REPEAT); + if (keyIndex == mCurrentKey) { + mCurrentKeyTime += eventTime - mLastMoveTime; + } else { + resetMultiTap(); + mLastKey = mCurrentKey; + mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime; + mCurrentKey = keyIndex; + mCurrentKeyTime = 0; + } + if (mCurrentKeyTime < mLastKeyTime && mLastKey != NOT_A_KEY) { + mCurrentKey = mLastKey; + touchX = mLastCodeX; + touchY = mLastCodeY; + } + showPreview(NOT_A_KEY); + Arrays.fill(mKeyIndices, NOT_A_KEY); + invalidateKey(keyIndex); + // If we're not on a repeating key (which sends on a DOWN event) + if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) { + detectAndSendKey(touchX, touchY, eventTime); + } + mRepeatKeyIndex = NOT_A_KEY; + break; + } + mLastX = touchX; + mLastY = touchY; + return true; + } + + private boolean repeatKey() { + Key key = mKeyboard.getKeys().get(mRepeatKeyIndex); + detectAndSendKey(key.x, key.y, mLastTapTime); + return true; + } + + protected void swipeRight() { + mKeyboardActionListener.swipeRight(); + } + + protected void swipeLeft() { + mKeyboardActionListener.swipeLeft(); + } + + protected void swipeUp() { + mKeyboardActionListener.swipeUp(); + } + + protected void swipeDown() { + mKeyboardActionListener.swipeDown(); + } + + public void closing() { + if (mPreviewPopup.isShowing()) { + mPreviewPopup.dismiss(); + } + dismissPopupKeyboard(); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + closing(); + } + + private void dismissPopupKeyboard() { + if (mPopupKeyboard.isShowing()) { + mPopupKeyboard.dismiss(); + mMiniKeyboardOnScreen = false; + invalidate(); + } + } + + public boolean handleBack() { + if (mPopupKeyboard.isShowing()) { + dismissPopupKeyboard(); + return true; + } + return false; + } + + private void resetMultiTap() { + mLastSentIndex = NOT_A_KEY; + mTapCount = 0; + mLastTapTime = -1; + mInMultiTap = false; + } + + private void checkMultiTap(long eventTime, int keyIndex) { + if (keyIndex == NOT_A_KEY) return; + Key key = mKeyboard.getKeys().get(keyIndex); + if (key.codes.length > 1) { + mInMultiTap = true; + if (eventTime < mLastTapTime + MULTITAP_INTERVAL + && keyIndex == mLastSentIndex) { + mTapCount = (mTapCount + 1) % key.codes.length; + return; + } else { + mTapCount = -1; + return; + } + } + if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) { + resetMultiTap(); + } + } +} diff --git a/core/java/android/inputmethodservice/SoftInputWindow.java b/core/java/android/inputmethodservice/SoftInputWindow.java new file mode 100644 index 0000000..9ff1665 --- /dev/null +++ b/core/java/android/inputmethodservice/SoftInputWindow.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.inputmethodservice; + +import android.app.Dialog; +import android.content.Context; +import android.os.IBinder; +import android.view.Gravity; +import android.view.WindowManager; + +/** + * A SoftInputWindow is a Dialog that is intended to be used for a top-level input + * method window. It will be displayed along the edge of the screen, moving + * the application user interface away from it so that the focused item is + * always visible. + */ +class SoftInputWindow extends Dialog { + + /** + * Create a DockWindow that uses the default style. + * + * @param context The Context the DockWindow is to run it. In particular, it + * uses the window manager and theme in this context to present its + * UI. + */ + public SoftInputWindow(Context context) { + super(context, com.android.internal.R.style.Theme_InputMethod); + initDockWindow(); + } + + public void setToken(IBinder token) { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + lp.token = token; + getWindow().setAttributes(lp); + } + + /** + * Create a DockWindow that uses a custom style. + * + * @param context The Context in which the DockWindow should run. In + * particular, it uses the window manager and theme from this context + * to present its UI. + * @param theme A style resource describing the theme to use for the window. + * See <a href="{@docRoot}reference/available-resources.html#stylesandthemes">Style + * and Theme Resources</a> for more information about defining and + * using styles. This theme is applied on top of the current theme in + * <var>context</var>. If 0, the default dialog theme will be used. + */ + public SoftInputWindow(Context context, int theme) { + super(context, theme); + initDockWindow(); + } + + /** + * Get the size of the DockWindow. + * + * @return If the DockWindow sticks to the top or bottom of the screen, the + * return value is the height of the DockWindow, and its width is + * equal to the width of the screen; If the DockWindow sticks to the + * left or right of the screen, the return value is the width of the + * DockWindow, and its height is equal to the height of the screen. + */ + public int getSize() { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + + if (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM) { + return lp.height; + } else { + return lp.width; + } + } + + /** + * Set the size of the DockWindow. + * + * @param size If the DockWindow sticks to the top or bottom of the screen, + * <var>size</var> is the height of the DockWindow, and its width is + * equal to the width of the screen; If the DockWindow sticks to the + * left or right of the screen, <var>size</var> is the width of the + * DockWindow, and its height is equal to the height of the screen. + */ + public void setSize(int size) { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + + if (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM) { + lp.width = -1; + lp.height = size; + } else { + lp.width = size; + lp.height = -1; + } + getWindow().setAttributes(lp); + } + + /** + * Set which boundary of the screen the DockWindow sticks to. + * + * @param gravity The boundary of the screen to stick. See {#link + * android.view.Gravity.LEFT}, {#link android.view.Gravity.TOP}, + * {#link android.view.Gravity.BOTTOM}, {#link + * android.view.Gravity.RIGHT}. + */ + public void setGravity(int gravity) { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + + boolean oldIsVertical = (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM); + + lp.gravity = gravity; + + boolean newIsVertical = (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM); + + if (oldIsVertical != newIsVertical) { + int tmp = lp.width; + lp.width = lp.height; + lp.height = tmp; + getWindow().setAttributes(lp); + } + } + + private void initDockWindow() { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + + lp.type = WindowManager.LayoutParams.TYPE_INPUT_METHOD; + lp.setTitle("InputMethod"); + + lp.gravity = Gravity.BOTTOM; + lp.width = -1; + + getWindow().setAttributes(lp); + getWindow().setFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_DIM_BEHIND); + } +} diff --git a/core/java/android/inputmethodservice/package.html b/core/java/android/inputmethodservice/package.html new file mode 100644 index 0000000..164349b --- /dev/null +++ b/core/java/android/inputmethodservice/package.html @@ -0,0 +1,8 @@ +<html> +<body> +Base classes for writing input methods. These APIs are not for use by +normal applications, they are a framework specifically for writing input +method components. Implementations will typically derive from +{@link android.inputmethodservice.InputMethodService}. +</body> +</html> diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java index ae74e6f..1d939e1 100644 --- a/core/java/android/net/MobileDataStateTracker.java +++ b/core/java/android/net/MobileDataStateTracker.java @@ -30,7 +30,6 @@ import com.android.internal.telephony.TelephonyIntents; import android.net.NetworkInfo.DetailedState; import android.telephony.TelephonyManager; import android.util.Log; -import android.util.Config; import android.text.TextUtils; import java.util.List; @@ -71,7 +70,9 @@ public class MobileDataStateTracker extends NetworkStateTracker { * @param target a message handler for getting callbacks about state changes */ public MobileDataStateTracker(Context context, Handler target) { - super(context, target, ConnectivityManager.TYPE_MOBILE); + super(context, target, ConnectivityManager.TYPE_MOBILE, + TelephonyManager.getDefault().getNetworkType(), "MOBILE", + TelephonyManager.getDefault().getNetworkTypeName()); mPhoneService = null; mDnsServers = new ArrayList<String>(); } @@ -80,9 +81,10 @@ public class MobileDataStateTracker extends NetworkStateTracker { * Begin monitoring mobile data connectivity. */ public void startMonitoring() { - - IntentFilter filter = new IntentFilter(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED); + IntentFilter filter = + new IntentFilter(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED); filter.addAction(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED); + filter.addAction(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED); Intent intent = mContext.registerReceiver(new MobileDataStateReceiver(), filter); if (intent != null) @@ -146,6 +148,9 @@ public class MobileDataStateTracker extends NetworkStateTracker { reason == null ? "" : "(" + reason + ")"); setDetailedState(DetailedState.FAILED, reason, apnName); } + TelephonyManager tm = TelephonyManager.getDefault(); + setRoamingStatus(tm.isNetworkRoaming()); + setSubtype(tm.getNetworkType(), tm.getNetworkTypeName()); } } @@ -223,6 +228,15 @@ public class MobileDataStateTracker extends NetworkStateTracker { } /** + * {@inheritDoc} + * The mobile data network subtype indicates what generation network technology is in effect, + * e.g., GPRS, EDGE, UMTS, etc. + */ + public int getNetworkSubtype() { + return TelephonyManager.getDefault().getNetworkType(); + } + + /** * Return the system properties name associated with the tcp buffer sizes * for this network. */ @@ -358,8 +372,8 @@ public class MobileDataStateTracker extends NetworkStateTracker { } /** - * Tells the phone sub-system that the caller is finished is - * finished using the named feature. The only supported feature at + * Tells the phone sub-system that the caller is finished + * using the named feature. The only supported feature at * this time is {@code Phone.FEATURE_ENABLE_MMS}, which allows an application * to specify that it wants to send and/or receive MMS data. * @param feature the name of the feature that is no longer needed diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java index f776abf..8c82212 100644 --- a/core/java/android/net/NetworkInfo.java +++ b/core/java/android/net/NetworkInfo.java @@ -99,24 +99,38 @@ public class NetworkInfo implements Parcelable { } private int mNetworkType; + private int mSubtype; + private String mTypeName; + private String mSubtypeName; private State mState; private DetailedState mDetailedState; private String mReason; private String mExtraInfo; private boolean mIsFailover; + private boolean mIsRoaming; /** * Indicates whether network connectivity is possible: */ private boolean mIsAvailable; - public NetworkInfo(int type) { + /** + * TODO This is going away as soon as API council review happens. + * @param type network type + */ + public NetworkInfo(int type) {} + + NetworkInfo(int type, int subtype, String typeName, String subtypeName) { if (!ConnectivityManager.isNetworkTypeValid(type)) { throw new IllegalArgumentException("Invalid network type: " + type); } - this.mNetworkType = type; + mNetworkType = type; + mSubtype = subtype; + mTypeName = typeName; + mSubtypeName = subtypeName; setDetailedState(DetailedState.IDLE, null, null); mState = State.UNKNOWN; mIsAvailable = true; + mIsRoaming = false; } /** @@ -129,6 +143,41 @@ public class NetworkInfo implements Parcelable { } /** + * Return a network-type-specific integer describing the subtype + * of the network. + * @return the network subtype + * + * @hide pending API council review + */ + public int getSubtype() { + return mSubtype; + } + + void setSubtype(int subtype, String subtypeName) { + mSubtype = subtype; + mSubtypeName = subtypeName; + } + + /** + * Return a human-readable name describe the type of the network, + * for example "WIFI" or "MOBILE". + * @return the name of the network type + */ + public String getTypeName() { + return mTypeName; + } + + /** + * Return a human-readable name describing the subtype of the network. + * @return the name of the network subtype + * + * @hide pending API council review + */ + public String getSubtypeName() { + return mSubtypeName; + } + + /** * Indicates whether network connectivity exists or is in the process * of being established. This is good for applications that need to * do anything related to the network other than read or write data. @@ -170,7 +219,7 @@ public class NetworkInfo implements Parcelable { * Sets if the network is available, ie, if the connectivity is possible. * @param isAvailable the new availability value. * - * {@hide} + * @hide */ public void setIsAvailable(boolean isAvailable) { mIsAvailable = isAvailable; @@ -187,12 +236,33 @@ public class NetworkInfo implements Parcelable { return mIsFailover; } - /** {@hide} */ + /** + * Set the failover boolean. + * @param isFailover {@code true} to mark the current connection attempt + * as a failover. + * @hide + */ public void setFailover(boolean isFailover) { mIsFailover = isFailover; } /** + * Indicates whether the device is currently roaming on this network. + * When {@code true}, it suggests that use of data on this network + * may incur extra costs. + * @return {@code true} if roaming is in effect, {@code false} otherwise. + * + * @hide pending API council + */ + public boolean isRoaming() { + return mIsRoaming; + } + + void setRoaming(boolean isRoaming) { + mIsRoaming = isRoaming; + } + + /** * Reports the current coarse-grained state of the network. * @return the coarse-grained state */ @@ -215,8 +285,6 @@ public class NetworkInfo implements Parcelable { * if one was supplied. May be {@code null}. * @param extraInfo an optional {@code String} providing addditional network state * information passed up from the lower networking layers. - * - * {@hide} */ void setDetailedState(DetailedState detailedState, String reason, String extraInfo) { this.mDetailedState = detailedState; @@ -247,52 +315,59 @@ public class NetworkInfo implements Parcelable { @Override public String toString() { StringBuilder builder = new StringBuilder("NetworkInfo: "); - builder.append("type: ").append(getTypeName()).append(", state: ").append(mState). - append("/").append(mDetailedState). + builder.append("type: ").append(getTypeName()).append("[").append(getSubtypeName()). + append("], state: ").append(mState).append("/").append(mDetailedState). append(", reason: ").append(mReason == null ? "(unspecified)" : mReason). append(", extra: ").append(mExtraInfo == null ? "(none)" : mExtraInfo). + append(", roaming: ").append(mIsRoaming). append(", failover: ").append(mIsFailover). append(", isAvailable: ").append(mIsAvailable); return builder.toString(); } - public String getTypeName() { - switch (mNetworkType) { - case ConnectivityManager.TYPE_WIFI: - return "WIFI"; - case ConnectivityManager.TYPE_MOBILE: - return "MOBILE"; - default: - return "<invalid>"; - } - } - - /** Implement the Parcelable interface {@hide} */ + /** + * Implement the Parcelable interface + * @hide + */ public int describeContents() { return 0; } - /** Implement the Parcelable interface {@hide} */ + /** + * Implement the Parcelable interface. + * @hide + */ public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mNetworkType); + dest.writeInt(mSubtype); + dest.writeString(mTypeName); + dest.writeString(mSubtypeName); dest.writeString(mState.name()); dest.writeString(mDetailedState.name()); dest.writeInt(mIsFailover ? 1 : 0); dest.writeInt(mIsAvailable ? 1 : 0); + dest.writeInt(mIsRoaming ? 1 : 0); dest.writeString(mReason); dest.writeString(mExtraInfo); } - /** Implement the Parcelable interface {@hide} */ + /** + * Implement the Parcelable interface. + * @hide + */ public static final Creator<NetworkInfo> CREATOR = new Creator<NetworkInfo>() { public NetworkInfo createFromParcel(Parcel in) { int netType = in.readInt(); - NetworkInfo netInfo = new NetworkInfo(netType); + int subtype = in.readInt(); + String typeName = in.readString(); + String subtypeName = in.readString(); + NetworkInfo netInfo = new NetworkInfo(netType, subtype, typeName, subtypeName); netInfo.mState = State.valueOf(in.readString()); netInfo.mDetailedState = DetailedState.valueOf(in.readString()); netInfo.mIsFailover = in.readInt() != 0; netInfo.mIsAvailable = in.readInt() != 0; + netInfo.mIsRoaming = in.readInt() != 0; netInfo.mReason = in.readString(); netInfo.mExtraInfo = in.readString(); return netInfo; diff --git a/core/java/android/net/NetworkStateTracker.java b/core/java/android/net/NetworkStateTracker.java index 4e1efa6..37087ac 100644 --- a/core/java/android/net/NetworkStateTracker.java +++ b/core/java/android/net/NetworkStateTracker.java @@ -22,7 +22,6 @@ import java.io.IOException; import android.os.Handler; import android.os.Message; import android.os.SystemProperties; -import android.os.PowerManager; import android.content.Context; import android.text.TextUtils; import android.util.Config; @@ -41,6 +40,7 @@ public abstract class NetworkStateTracker extends Handler { protected NetworkInfo mNetworkInfo; protected Context mContext; protected Handler mTarget; + private boolean mTeardownRequested; private static boolean DBG = Config.LOGV; private static final String TAG = "NetworkStateTracker"; @@ -54,12 +54,20 @@ public abstract class NetworkStateTracker extends Handler { */ public static final int EVENT_NOTIFICATION_CHANGED = 3; public static final int EVENT_CONFIGURATION_CHANGED = 4; - - public NetworkStateTracker(Context context, Handler target, int networkType) { + public static final int EVENT_ROAMING_CHANGED = 5; + public static final int EVENT_NETWORK_SUBTYPE_CHANGED = 6; + + public NetworkStateTracker(Context context, + Handler target, + int networkType, + int subType, + String typeName, + String subtypeName) { super(); mContext = context; mTarget = target; - this.mNetworkInfo = new NetworkInfo(networkType); + mTeardownRequested = false; + this.mNetworkInfo = new NetworkInfo(networkType, subType, typeName, subtypeName); } public NetworkInfo getNetworkInfo() { @@ -222,6 +230,14 @@ public abstract class NetworkStateTracker extends Handler { mNetworkInfo.setDetailedState(state, null, null); } + public void setTeardownRequested(boolean isRequested) { + mTeardownRequested = isRequested; + } + + public boolean isTeardownRequested() { + return mTeardownRequested; + } + /** * Send a notification that the results of a scan for network access * points has completed, and results are available. @@ -231,6 +247,32 @@ public abstract class NetworkStateTracker extends Handler { msg.sendToTarget(); } + /** + * Record the roaming status of the device, and if it is a change from the previous + * status, send a notification to any listeners. + * @param isRoaming {@code true} if the device is now roaming, {@code false} + * if it is no longer roaming. + */ + protected void setRoamingStatus(boolean isRoaming) { + if (isRoaming != mNetworkInfo.isRoaming()) { + mNetworkInfo.setRoaming(isRoaming); + Message msg = mTarget.obtainMessage(EVENT_ROAMING_CHANGED, mNetworkInfo); + msg.sendToTarget(); + } + } + + protected void setSubtype(int subtype, String subtypeName) { + if (mNetworkInfo.isConnected()) { + int oldSubtype = mNetworkInfo.getSubtype(); + if (subtype != oldSubtype) { + mNetworkInfo.setSubtype(subtype, subtypeName); + Message msg = mTarget.obtainMessage( + EVENT_NETWORK_SUBTYPE_CHANGED, oldSubtype, 0, mNetworkInfo); + msg.sendToTarget(); + } + } + } + public abstract void startMonitoring(); /** diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index 129248a..1153648 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -67,6 +67,14 @@ public class NetworkUtils { public native static boolean stopDhcp(String interfaceName); /** + * Release the current DHCP lease. + * @param interfaceName the name of the interface for which the lease should + * be released + * @return {@code true} for success, {@code false} for failure + */ + public native static boolean releaseDhcpLease(String interfaceName); + + /** * Return the last DHCP-related error message that was recorded. * <p/>NOTE: This string is not localized, but currently it is only * used in logging. diff --git a/core/java/android/net/Proxy.java b/core/java/android/net/Proxy.java index 86e1d5b..9f07c0a 100644 --- a/core/java/android/net/Proxy.java +++ b/core/java/android/net/Proxy.java @@ -43,9 +43,9 @@ final public class Proxy { static final public String getHost(Context ctx) { ContentResolver contentResolver = ctx.getContentResolver(); Assert.assertNotNull(contentResolver); - String host = Settings.System.getString( + String host = Settings.Secure.getString( contentResolver, - Settings.System.HTTP_PROXY); + Settings.Secure.HTTP_PROXY); if (host != null) { int i = host.indexOf(':'); if (i == -1) { @@ -67,9 +67,9 @@ final public class Proxy { static final public int getPort(Context ctx) { ContentResolver contentResolver = ctx.getContentResolver(); Assert.assertNotNull(contentResolver); - String host = Settings.System.getString( + String host = Settings.Secure.getString( contentResolver, - Settings.System.HTTP_PROXY); + Settings.Secure.HTTP_PROXY); if (host != null) { int i = host.indexOf(':'); if (i == -1) { diff --git a/core/java/android/net/http/Connection.java b/core/java/android/net/http/Connection.java index 2c82582..563634f 100644 --- a/core/java/android/net/http/Connection.java +++ b/core/java/android/net/http/Connection.java @@ -375,6 +375,11 @@ abstract class Connection { if (HttpLog.LOGV) HttpLog.v("Failed to open connection"); error = EventHandler.ERROR_LOOKUP; exception = e; + } catch (IllegalArgumentException e) { + if (HttpLog.LOGV) HttpLog.v("Illegal argument exception"); + error = EventHandler.ERROR_CONNECT; + req.mFailCount = RETRY_REQUEST_LIMIT; + exception = e; } catch (SSLConnectionClosedByUserException e) { // hack: if we have an SSL connection failure, // we don't want to reconnect diff --git a/core/java/android/net/http/Headers.java b/core/java/android/net/http/Headers.java index 5d85ba4..b0923d1 100644 --- a/core/java/android/net/http/Headers.java +++ b/core/java/android/net/http/Headers.java @@ -30,7 +30,7 @@ import org.apache.http.util.CharArrayBuffer; /** * Manages received headers - * + * * {@hide} */ public final class Headers { @@ -42,16 +42,16 @@ public final class Headers { */ public final static int CONN_CLOSE = 1; /** - * indicate HTTP 1.1 connection keep alive + * indicate HTTP 1.1 connection keep alive */ public final static int CONN_KEEP_ALIVE = 2; - + // initial values. public final static int NO_CONN_TYPE = 0; public final static long NO_TRANSFER_ENCODING = 0; public final static long NO_CONTENT_LENGTH = -1; - // header string + // header strings public final static String TRANSFER_ENCODING = "transfer-encoding"; public final static String CONTENT_LEN = "content-length"; public final static String CONTENT_TYPE = "content-type"; @@ -93,25 +93,61 @@ public final class Headers { private final static int HASH_PRAGMA = -980228804; private final static int HASH_REFRESH = 1085444827; + // keep any headers that require direct access in a presized + // string array + private final static int IDX_TRANSFER_ENCODING = 0; + private final static int IDX_CONTENT_LEN = 1; + private final static int IDX_CONTENT_TYPE = 2; + private final static int IDX_CONTENT_ENCODING = 3; + private final static int IDX_CONN_DIRECTIVE = 4; + private final static int IDX_LOCATION = 5; + private final static int IDX_PROXY_CONNECTION = 6; + private final static int IDX_WWW_AUTHENTICATE = 7; + private final static int IDX_PROXY_AUTHENTICATE = 8; + private final static int IDX_CONTENT_DISPOSITION = 9; + private final static int IDX_ACCEPT_RANGES = 10; + private final static int IDX_EXPIRES = 11; + private final static int IDX_CACHE_CONTROL = 12; + private final static int IDX_LAST_MODIFIED = 13; + private final static int IDX_ETAG = 14; + private final static int IDX_SET_COOKIE = 15; + private final static int IDX_PRAGMA = 16; + private final static int IDX_REFRESH = 17; + + private final static int HEADER_COUNT = 18; + + /* parsed values */ private long transferEncoding; private long contentLength; // Content length of the incoming data private int connectionType; - - private String contentType; - private String contentEncoding; - private String location; - private String wwwAuthenticate; - private String proxyAuthenticate; - private String contentDisposition; - private String acceptRanges; - private String expires; - private String cacheControl; - private String lastModified; - private String etag; - private String pragma; - private String refresh; private ArrayList<String> cookies = new ArrayList<String>(2); + private String[] mHeaders = new String[HEADER_COUNT]; + private final static String[] sHeaderNames = { + TRANSFER_ENCODING, + CONTENT_LEN, + CONTENT_TYPE, + CONTENT_ENCODING, + CONN_DIRECTIVE, + LOCATION, + PROXY_CONNECTION, + WWW_AUTHENTICATE, + PROXY_AUTHENTICATE, + CONTENT_DISPOSITION, + ACCEPT_RANGES, + EXPIRES, + CACHE_CONTROL, + LAST_MODIFIED, + ETAG, + SET_COOKIE, + PRAGMA, + REFRESH + }; + + // Catch-all for headers not explicitly handled + private ArrayList<String> mExtraHeaderNames = new ArrayList<String>(4); + private ArrayList<String> mExtraHeaderValues = new ArrayList<String>(4); + public Headers() { transferEncoding = NO_TRANSFER_ENCODING; contentLength = NO_CONTENT_LENGTH; @@ -129,23 +165,22 @@ public final class Headers { } pos++; + String val = buffer.substringTrimmed(pos, buffer.length()); if (HttpLog.LOGV) { - String val = buffer.substringTrimmed(pos, buffer.length()); HttpLog.v("hdr " + buffer.length() + " " + buffer); } switch (name.hashCode()) { case HASH_TRANSFER_ENCODING: if (name.equals(TRANSFER_ENCODING)) { - // headers.transferEncoding = + mHeaders[IDX_TRANSFER_ENCODING] = val; HeaderElement[] encodings = BasicHeaderValueParser.DEFAULT - .parseElements(buffer, new ParserCursor(pos, + .parseElements(buffer, new ParserCursor(pos, buffer.length())); // The chunked encoding must be the last one applied RFC2616, // 14.41 int len = encodings.length; - if (HTTP.IDENTITY_CODING.equalsIgnoreCase(buffer - .substringTrimmed(pos, buffer.length()))) { + if (HTTP.IDENTITY_CODING.equalsIgnoreCase(val)) { transferEncoding = ContentLengthStrategy.IDENTITY; } else if ((len > 0) && (HTTP.CHUNK_CODING @@ -158,9 +193,9 @@ public final class Headers { break; case HASH_CONTENT_LEN: if (name.equals(CONTENT_LEN)) { + mHeaders[IDX_CONTENT_LEN] = val; try { - contentLength = Long.parseLong(buffer.substringTrimmed(pos, - buffer.length())); + contentLength = Long.parseLong(val); } catch (NumberFormatException e) { if (Config.LOGV) { Log.v(LOGTAG, "Headers.headers(): error parsing" @@ -171,88 +206,90 @@ public final class Headers { break; case HASH_CONTENT_TYPE: if (name.equals(CONTENT_TYPE)) { - contentType = buffer.substringTrimmed(pos, buffer.length()); + mHeaders[IDX_CONTENT_TYPE] = val; } break; case HASH_CONTENT_ENCODING: if (name.equals(CONTENT_ENCODING)) { - contentEncoding = buffer.substringTrimmed(pos, buffer.length()); + mHeaders[IDX_CONTENT_ENCODING] = val; } break; case HASH_CONN_DIRECTIVE: if (name.equals(CONN_DIRECTIVE)) { + mHeaders[IDX_CONN_DIRECTIVE] = val; setConnectionType(buffer, pos); } break; case HASH_LOCATION: if (name.equals(LOCATION)) { - location = buffer.substringTrimmed(pos, buffer.length()); + mHeaders[IDX_LOCATION] = val; } break; case HASH_PROXY_CONNECTION: if (name.equals(PROXY_CONNECTION)) { + mHeaders[IDX_PROXY_CONNECTION] = val; setConnectionType(buffer, pos); } break; case HASH_WWW_AUTHENTICATE: if (name.equals(WWW_AUTHENTICATE)) { - wwwAuthenticate = buffer.substringTrimmed(pos, buffer.length()); + mHeaders[IDX_WWW_AUTHENTICATE] = val; } break; case HASH_PROXY_AUTHENTICATE: if (name.equals(PROXY_AUTHENTICATE)) { - proxyAuthenticate = buffer.substringTrimmed(pos, buffer - .length()); + mHeaders[IDX_PROXY_AUTHENTICATE] = val; } break; case HASH_CONTENT_DISPOSITION: if (name.equals(CONTENT_DISPOSITION)) { - contentDisposition = buffer.substringTrimmed(pos, buffer - .length()); + mHeaders[IDX_CONTENT_DISPOSITION] = val; } break; case HASH_ACCEPT_RANGES: if (name.equals(ACCEPT_RANGES)) { - acceptRanges = buffer.substringTrimmed(pos, buffer.length()); + mHeaders[IDX_ACCEPT_RANGES] = val; } break; case HASH_EXPIRES: if (name.equals(EXPIRES)) { - expires = buffer.substringTrimmed(pos, buffer.length()); + mHeaders[IDX_EXPIRES] = val; } break; case HASH_CACHE_CONTROL: if (name.equals(CACHE_CONTROL)) { - cacheControl = buffer.substringTrimmed(pos, buffer.length()); + mHeaders[IDX_CACHE_CONTROL] = val; } break; case HASH_LAST_MODIFIED: if (name.equals(LAST_MODIFIED)) { - lastModified = buffer.substringTrimmed(pos, buffer.length()); + mHeaders[IDX_LAST_MODIFIED] = val; } break; case HASH_ETAG: if (name.equals(ETAG)) { - etag = buffer.substringTrimmed(pos, buffer.length()); + mHeaders[IDX_ETAG] = val; } break; case HASH_SET_COOKIE: if (name.equals(SET_COOKIE)) { - cookies.add(buffer.substringTrimmed(pos, buffer.length())); + mHeaders[IDX_SET_COOKIE] = val; + cookies.add(val); } break; case HASH_PRAGMA: if (name.equals(PRAGMA)) { - pragma = buffer.substringTrimmed(pos, buffer.length()); + mHeaders[IDX_PRAGMA] = val; } break; case HASH_REFRESH: if (name.equals(REFRESH)) { - refresh = buffer.substringTrimmed(pos, buffer.length()); + mHeaders[IDX_REFRESH] = val; } break; default: - // ignore + mExtraHeaderNames.add(name); + mExtraHeaderValues.add(val); } } @@ -268,70 +305,60 @@ public final class Headers { return connectionType; } - private void setConnectionType(CharArrayBuffer buffer, int pos) { - if (CharArrayBuffers.containsIgnoreCaseTrimmed( - buffer, pos, HTTP.CONN_CLOSE)) { - connectionType = CONN_CLOSE; - } else if (CharArrayBuffers.containsIgnoreCaseTrimmed( - buffer, pos, HTTP.CONN_KEEP_ALIVE)) { - connectionType = CONN_KEEP_ALIVE; - } - } - public String getContentType() { - return this.contentType; + return mHeaders[IDX_CONTENT_TYPE]; } public String getContentEncoding() { - return this.contentEncoding; + return mHeaders[IDX_CONTENT_ENCODING]; } public String getLocation() { - return this.location; + return mHeaders[IDX_LOCATION]; } public String getWwwAuthenticate() { - return this.wwwAuthenticate; + return mHeaders[IDX_WWW_AUTHENTICATE]; } public String getProxyAuthenticate() { - return this.proxyAuthenticate; + return mHeaders[IDX_PROXY_AUTHENTICATE]; } public String getContentDisposition() { - return this.contentDisposition; + return mHeaders[IDX_CONTENT_DISPOSITION]; } public String getAcceptRanges() { - return this.acceptRanges; + return mHeaders[IDX_ACCEPT_RANGES]; } public String getExpires() { - return this.expires; + return mHeaders[IDX_EXPIRES]; } public String getCacheControl() { - return this.cacheControl; + return mHeaders[IDX_CACHE_CONTROL]; } public String getLastModified() { - return this.lastModified; + return mHeaders[IDX_LAST_MODIFIED]; } public String getEtag() { - return this.etag; + return mHeaders[IDX_ETAG]; } public ArrayList<String> getSetCookie() { return this.cookies; } - + public String getPragma() { - return this.pragma; + return mHeaders[IDX_PRAGMA]; } - + public String getRefresh() { - return this.refresh; + return mHeaders[IDX_REFRESH]; } public void setContentLength(long value) { @@ -339,46 +366,82 @@ public final class Headers { } public void setContentType(String value) { - this.contentType = value; + mHeaders[IDX_CONTENT_TYPE] = value; } public void setContentEncoding(String value) { - this.contentEncoding = value; + mHeaders[IDX_CONTENT_ENCODING] = value; } public void setLocation(String value) { - this.location = value; + mHeaders[IDX_LOCATION] = value; } public void setWwwAuthenticate(String value) { - this.wwwAuthenticate = value; + mHeaders[IDX_WWW_AUTHENTICATE] = value; } public void setProxyAuthenticate(String value) { - this.proxyAuthenticate = value; + mHeaders[IDX_PROXY_AUTHENTICATE] = value; } public void setContentDisposition(String value) { - this.contentDisposition = value; + mHeaders[IDX_CONTENT_DISPOSITION] = value; } public void setAcceptRanges(String value) { - this.acceptRanges = value; + mHeaders[IDX_ACCEPT_RANGES] = value; } public void setExpires(String value) { - this.expires = value; + mHeaders[IDX_EXPIRES] = value; } public void setCacheControl(String value) { - this.cacheControl = value; + mHeaders[IDX_CACHE_CONTROL] = value; } public void setLastModified(String value) { - this.lastModified = value; + mHeaders[IDX_LAST_MODIFIED] = value; } public void setEtag(String value) { - this.etag = value; + mHeaders[IDX_ETAG] = value; + } + + public interface HeaderCallback { + public void header(String name, String value); + } + + /** + * Reports all non-null headers to the callback + */ + public void getHeaders(HeaderCallback hcb) { + for (int i = 0; i < HEADER_COUNT; i++) { + String h = mHeaders[i]; + if (h != null) { + hcb.header(sHeaderNames[i], h); + } + } + int extraLen = mExtraHeaderNames.size(); + for (int i = 0; i < extraLen; i++) { + if (Config.LOGV) { + HttpLog.v("Headers.getHeaders() extra: " + i + " " + + mExtraHeaderNames.get(i) + " " + mExtraHeaderValues.get(i)); + } + hcb.header(mExtraHeaderNames.get(i), + mExtraHeaderValues.get(i)); + } + + } + + private void setConnectionType(CharArrayBuffer buffer, int pos) { + if (CharArrayBuffers.containsIgnoreCaseTrimmed( + buffer, pos, HTTP.CONN_CLOSE)) { + connectionType = CONN_CLOSE; + } else if (CharArrayBuffers.containsIgnoreCaseTrimmed( + buffer, pos, HTTP.CONN_KEEP_ALIVE)) { + connectionType = CONN_KEEP_ALIVE; + } } } diff --git a/core/java/android/net/http/Request.java b/core/java/android/net/http/Request.java index bcbecf0..df4fff0 100644 --- a/core/java/android/net/http/Request.java +++ b/core/java/android/net/http/Request.java @@ -16,6 +16,7 @@ package android.net.http; +import java.io.EOFException; import java.io.InputStream; import java.io.IOException; import java.util.Iterator; @@ -279,6 +280,11 @@ class Request { count = 0; } } + } catch (EOFException e) { + /* InflaterInputStream throws an EOFException when the + server truncates gzipped content. Handle this case + as we do truncated non-gzipped content: no error */ + if (HttpLog.LOGV) HttpLog.v( "readResponse() handling " + e); } catch(IOException e) { // don't throw if we have a non-OK status code if (statusCode == HttpStatus.SC_OK) { diff --git a/core/java/android/net/http/RequestHandle.java b/core/java/android/net/http/RequestHandle.java index 5d81250..65e6117 100644 --- a/core/java/android/net/http/RequestHandle.java +++ b/core/java/android/net/http/RequestHandle.java @@ -113,7 +113,7 @@ public class RequestHandle { * @param statusCode HTTP status code returned from original request * @param cacheHeaders Cache header for redirect URL * @return true if setup succeeds, false otherwise (redirect loop - * count exceeded) + * count exceeded, body provider unable to rewind on 307 redirect) */ public boolean setupRedirect(String redirectTo, int statusCode, Map<String, String> cacheHeaders) { @@ -164,8 +164,22 @@ public class RequestHandle { } mMethod = "GET"; } - mHeaders.remove("Content-Type"); - mBodyProvider = null; + /* Only repost content on a 307. If 307, reset the body + provider so we can replay the body */ + if (statusCode == 307) { + try { + if (mBodyProvider != null) mBodyProvider.reset(); + } catch (java.io.IOException ex) { + if (HttpLog.LOGV) { + HttpLog.v("setupAuthResponse() failed to reset body provider"); + } + return false; + } + + } else { + mHeaders.remove("Content-Type"); + mBodyProvider = null; + } // Update the cache headers for this URL mHeaders.putAll(cacheHeaders); diff --git a/core/java/android/net/http/RequestQueue.java b/core/java/android/net/http/RequestQueue.java index d592995..66d5722 100644 --- a/core/java/android/net/http/RequestQueue.java +++ b/core/java/android/net/http/RequestQueue.java @@ -596,7 +596,7 @@ public class RequestQueue implements RequestFeeder { } protected synchronized void queueRequest(Request request, boolean head) { - HttpHost host = request.mHost; + HttpHost host = request.mProxyHost == null ? request.mHost : request.mProxyHost; LinkedList<Request> reqList; if (mPending.containsKey(host)) { reqList = mPending.get(host); diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java new file mode 100644 index 0000000..ee4e897 --- /dev/null +++ b/core/java/android/os/AsyncTask.java @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package 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.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * <p>AsyncTask enables proper and easy use of the UI thread. This class allows to + * perform background operations and publish results on the UI thread without + * having to manipulate threads and/or handlers.</p> + * + * <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> + * + * <h2>Usage</h2> + * <p>AsyncTask must be subclassed to be used. The subclass will override at least + * one method ({@link #doInBackground}), and most often will override a + * second one ({@link #onPostExecute}.)</p> + * + * <p>Here is an example of subclassing:</p> + * <pre class="prettyprint"> + * private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> { + * protected Long doInBackground(URL... urls) { + * int count = urls.length; + * long totalSize = 0; + * for (int i = 0; i < count; i++) { + * totalSize += Downloader.downloadFile(urls[i]); + * publishProgress((int) ((i / (float) count) * 100)); + * } + * return totalSize; + * } + * + * protected void onProgressUpdate(Integer... progress) { + * setProgressPercent(progress[0]); + * } + * + * protected void onPostExecute(Long result) { + * showDialog("Downloaded " + result + " bytes"); + * } + * } + * </pre> + * + * <p>Once created, a task is executed very simply:</p> + * <pre class="prettyprint"> + * new DownloadFilesTask().execute(url1, url2, url3); + * </pre> + * + * <h2>AsyncTask's generic types</h2> + * <p>The three types used by an asynchronous task are the following:</p> + * <ol> + * <li><code>Params</code>, the type of the parameters sent to the task upon + * execution.</li> + * <li><code>Progress</code>, the type of the progress units published during + * the background computation.</li> + * <li><code>Result</code>, the type of the result of the background + * computation.</li> + * </ol> + * <p>Not all types are always used by am asynchronous task. To mark a type as unused, + * simply use the type {@link Void}:</p> + * <pre> + * private class MyTask extends AsyncTask<Void, Void, Void) { ... } + * </pre> + * + * <h2>The 4 steps</h2> + * <p>When an asynchronous task is executed, the task goes through 4 steps:</p> + * <ol> + * <li>{@link #onPreExecute()}, invoked on the UI thread immediately after the task + * is executed. This step is normally used to setup the task, for instance by + * showing a progress bar in the user interface.</li> + * <li>{@link #doInBackground}, invoked on the background thread + * immediately after {@link #onPreExecute()} finishes executing. This step is used + * to perform background computation that can take a long time. The parameters + * of the asynchronous task are passed to this step. The result of the computation must + * be returned by this step and will be passed back to the last step. This step + * can also use {@link #publishProgress} to publish one or more units + * of progress. These values are published on the UI thread, in the + * {@link #onProgressUpdate} step.</li> + * <li>{@link #onProgressUpdate}, invoked on the UI thread after a + * call to {@link #publishProgress}. The timing of the execution is + * undefined. This method is used to display any form of progress in the user + * interface while the background computation is still executing. For instance, + * it can be used to animate a progress bar or show logs in a text field.</li> + * <li>{@link #onPostExecute}, invoked on the UI thread after the background + * computation finishes. The result of the background computation is passed to + * this step as a parameter.</li> + * </ol> + * + * <h2>Threading rules</h2> + * <p>There are a few threading rules that must be followed for this class to + * work properly:</p> + * <ul> + * <li>The task instance must be created on the UI thread.</li> + * <li>{@link #execute} must be invoked on the UI thread.</li> + * <li>Do not call {@link #onPreExecute()}, {@link #onPostExecute}, + * {@link #doInBackground}, {@link #onProgressUpdate} manually.</li> + * <li>The task can be executed only once (an exception will be thrown if + * a second execution is attempted.)</li> + * </ul> + */ +public abstract class AsyncTask<Params, Progress, Result> { + private static final String LOG_TAG = "AsyncTask"; + + private static final int CORE_POOL_SIZE = 1; + private static final int MAXIMUM_POOL_SIZE = 10; + private static final int KEEP_ALIVE = 10; + + private static final BlockingQueue<Runnable> sWorkQueue = + new LinkedBlockingQueue<Runnable>(MAXIMUM_POOL_SIZE); + + private static final ThreadFactory sThreadFactory = new ThreadFactory() { + private final AtomicInteger mCount = new AtomicInteger(1); + + public Thread newThread(Runnable r) { + return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); + } + }; + + private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, + MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory); + + private static final int MESSAGE_POST_RESULT = 0x1; + private static final int MESSAGE_POST_PROGRESS = 0x2; + private static final int MESSAGE_POST_CANCEL = 0x3; + + private static final InternalHandler sHandler = new InternalHandler(); + + private final WorkerRunnable<Params, Result> mWorker; + private final FutureTask<Result> mFuture; + + private volatile Status mStatus = Status.PENDING; + + /** + * Indicates the current status of the task. Each status will be set only once + * during the lifetime of a task. + */ + public enum Status { + /** + * Indicates that the task has not been executed yet. + */ + PENDING, + /** + * Indicates that the task is running. + */ + RUNNING, + /** + * Indicates that {@link AsyncTask#onPostExecute} has finished. + */ + FINISHED, + } + + /** + * Creates a new asynchronous task. This constructor must be invoked on the UI thread. + */ + public AsyncTask() { + mWorker = new WorkerRunnable<Params, Result>() { + public Result call() throws Exception { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + return doInBackground(mParams); + } + }; + + mFuture = new FutureTask<Result>(mWorker) { + @Override + protected void done() { + Message message; + Result result = null; + + try { + result = get(); + } catch (InterruptedException e) { + android.util.Log.w(LOG_TAG, e); + } catch (ExecutionException e) { + throw new RuntimeException("An error occured while executing doInBackground()", + e.getCause()); + } catch (CancellationException e) { + message = sHandler.obtainMessage(MESSAGE_POST_CANCEL, + new AsyncTaskResult<Result>(AsyncTask.this, (Result[]) null)); + message.sendToTarget(); + return; + } catch (Throwable t) { + throw new RuntimeException("An error occured while executing " + + "doInBackground()", t); + } + + message = sHandler.obtainMessage(MESSAGE_POST_RESULT, + new AsyncTaskResult<Result>(AsyncTask.this, result)); + message.sendToTarget(); + } + }; + } + + /** + * Returns the current status of this task. + * + * @return The current status. + */ + public final Status getStatus() { + return mStatus; + } + + /** + * Override this method to perform a computation on a background thread. The + * specified parameters are the parameters passed to {@link #execute} + * by the caller of this task. + * + * This method can call {@link #publishProgress} to publish updates + * on the UI thread. + * + * @param params The parameters of the task. + * + * @return A result, defined by the subclass of this task. + * + * @see #onPreExecute() + * @see #onPostExecute + * @see #publishProgress + */ + protected abstract Result doInBackground(Params... params); + + /** + * Runs on the UI thread before {@link #doInBackground}. + * + * @see #onPostExecute + * @see #doInBackground + */ + protected void onPreExecute() { + } + + /** + * Runs on the UI thread after {@link #doInBackground}. The + * specified result is the value returned by {@link #doInBackground} + * or null if the task was cancelled or an exception occured. + * + * @param result The result of the operation computed by {@link #doInBackground}. + * + * @see #onPreExecute + * @see #doInBackground + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onPostExecute(Result result) { + } + + /** + * Runs on the UI thread after {@link #publishProgress} is invoked. + * The specified values are the values passed to {@link #publishProgress}. + * + * @param values The values indicating progress. + * + * @see #publishProgress + * @see #doInBackground + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onProgressUpdate(Progress... values) { + } + + /** + * Runs on the UI thread after {@link #cancel(boolean)} is invoked. + * + * @see #cancel(boolean) + * @see #isCancelled() + */ + protected void onCancelled() { + } + + /** + * Returns <tt>true</tt> if this task was cancelled before it completed + * normally. + * + * @return <tt>true</tt> if task was cancelled before it completed + * + * @see #cancel(boolean) + */ + public final boolean isCancelled() { + return mFuture.isCancelled(); + } + + /** + * Attempts to cancel execution of this task. This attempt will + * fail if the task has already completed, already been cancelled, + * or could not be cancelled for some other reason. If successful, + * and this task has not started when <tt>cancel</tt> is called, + * this task should never run. If the task has already started, + * then the <tt>mayInterruptIfRunning</tt> parameter determines + * whether the thread executing this task should be interrupted in + * an attempt to stop the task. + * + * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this + * task should be interrupted; otherwise, in-progress tasks are allowed + * to complete. + * + * @return <tt>false</tt> if the task could not be cancelled, + * typically because it has already completed normally; + * <tt>true</tt> otherwise + * + * @see #isCancelled() + * @see #onCancelled() + */ + public final boolean cancel(boolean mayInterruptIfRunning) { + return mFuture.cancel(mayInterruptIfRunning); + } + + /** + * Waits if necessary for the computation to complete, and then + * retrieves its result. + * + * @return The computed result. + * + * @throws CancellationException If the computation was cancelled. + * @throws ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + */ + public final Result get() throws InterruptedException, ExecutionException { + return mFuture.get(); + } + + /** + * Waits if necessary for at most the given time for the computation + * to complete, and then retrieves its result. + * + * @param timeout Time to wait before cancelling the operation. + * @param unit The time unit for the timeout. + * + * @return The computed result. + * + * @throws CancellationException If the computation was cancelled. + * @throws ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + * @throws TimeoutException If the wait timed out. + */ + public final Result get(long timeout, TimeUnit unit) throws InterruptedException, + ExecutionException, TimeoutException { + return mFuture.get(timeout, unit); + } + + /** + * Executes the task with the specified parameters. The task returns + * itself (this) so that the caller can keep a reference to it. + * + * This method must be invoked on the UI thread. + * + * @param params The parameters of the task. + * + * @return This instance of AsyncTask. + * + * @throws IllegalStateException If {@link #getStatus()} returns either + * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. + */ + public final AsyncTask<Params, Progress, Result> execute(Params... params) { + if (mStatus != Status.PENDING) { + switch (mStatus) { + case RUNNING: + throw new IllegalStateException("Cannot execute task:" + + " the task is already running."); + case FINISHED: + throw new IllegalStateException("Cannot execute task:" + + " the task has already been executed " + + "(a task can be executed only once)"); + } + } + + mStatus = Status.RUNNING; + + onPreExecute(); + + mWorker.mParams = params; + sExecutor.execute(mFuture); + + return this; + } + + /** + * This method can be invoked from {@link #doInBackground} to + * publish updates on the UI thread while the background computation is + * still running. Each call to this method will trigger the execution of + * {@link #onProgressUpdate} on the UI thread. + * + * @param values The progress values to update the UI with. + * + * @see #onProgressUpdate + * @see #doInBackground + */ + protected final void publishProgress(Progress... values) { + sHandler.obtainMessage(MESSAGE_POST_PROGRESS, + new AsyncTaskResult<Progress>(this, values)).sendToTarget(); + } + + private void finish(Result result) { + onPostExecute(result); + mStatus = Status.FINISHED; + } + + private static class InternalHandler extends Handler { + @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) + @Override + public void handleMessage(Message msg) { + AsyncTaskResult result = (AsyncTaskResult) msg.obj; + switch (msg.what) { + case MESSAGE_POST_RESULT: + // There is only one result + result.mTask.finish(result.mData[0]); + break; + case MESSAGE_POST_PROGRESS: + result.mTask.onProgressUpdate(result.mData); + break; + case MESSAGE_POST_CANCEL: + result.mTask.onCancelled(); + break; + } + } + } + + private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> { + Params[] mParams; + } + + @SuppressWarnings({"RawUseOfParameterizedType"}) + private static class AsyncTaskResult<Data> { + final AsyncTask mTask; + final Data[] mData; + + AsyncTaskResult(AsyncTask task, Data... data) { + mTask = task; + mData = data; + } + } +} diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java index bf47555..8f1a756 100644 --- a/core/java/android/os/BatteryManager.java +++ b/core/java/android/os/BatteryManager.java @@ -37,8 +37,10 @@ public class BatteryManager { public static final int BATTERY_HEALTH_OVER_VOLTAGE = 5; public static final int BATTERY_HEALTH_UNSPECIFIED_FAILURE = 6; - // values of the "plugged" field in the ACTION_BATTERY_CHANGED intent + // values of the "plugged" field in the ACTION_BATTERY_CHANGED intent. + // These must be powers of 2. + /** Power source is an AC charger. */ public static final int BATTERY_PLUGGED_AC = 1; + /** Power source is a USB port. */ public static final int BATTERY_PLUGGED_USB = 2; - } diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java new file mode 100644 index 0000000..e065063 --- /dev/null +++ b/core/java/android/os/BatteryStats.java @@ -0,0 +1,518 @@ +package android.os; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Formatter; +import java.util.Map; + +import android.util.SparseArray; + +/** + * A class providing access to battery usage statistics, including information on + * wakelocks, processes, packages, and services. All times are represented in microseconds + * except where indicated otherwise. + */ +public abstract class BatteryStats { + + /** + * A constant indicating a partial wake lock. + */ + public static final int WAKE_TYPE_PARTIAL = 0; + + /** + * A constant indicating a full wake lock. + */ + public static final int WAKE_TYPE_FULL = 1; + + /** + * A constant indicating a window wake lock. + */ + public static final int WAKE_TYPE_WINDOW = 2; + + /** + * Include all of the data in the stats, including previously saved data. + */ + public static final int STATS_TOTAL = 0; + + /** + * Include only the last run in the stats. + */ + public static final int STATS_LAST = 1; + + /** + * Include only the current run in the stats. + */ + public static final int STATS_CURRENT = 2; + + /** + * Include only the run since the last time the device was unplugged in the stats. + */ + public static final int STATS_UNPLUGGED = 3; + + private final StringBuilder mFormatBuilder = new StringBuilder(8); + private final Formatter mFormatter = new Formatter(mFormatBuilder); + + /** + * State for keeping track of timing information. + */ + public static abstract class Timer { + + /** + * Returns the count associated with this Timer for the + * selected type of statistics. + * + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT + */ + public abstract int getCount(int which); + + /** + * Returns the total time in microseconds associated with this Timer for the + * selected type of statistics. + * + * @param now system uptime time in microseconds + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT + * @return a time in microseconds + */ + public abstract long getTotalTime(long now, int which); + } + + /** + * The statistics associated with a particular uid. + */ + public static abstract class Uid { + + /** + * Returns a mapping containing wakelock statistics. + * + * @return a Map from Strings to Uid.Wakelock objects. + */ + public abstract Map<String, ? extends Wakelock> getWakelockStats(); + + /** + * The statistics associated with a particular wake lock. + */ + public static abstract class Wakelock { + public abstract Timer getWakeTime(int type); + } + + /** + * Returns a mapping containing sensor statistics. + * + * @return a Map from Integer sensor ids to Uid.Sensor objects. + */ + public abstract Map<Integer, ? extends Sensor> getSensorStats(); + + /** + * Returns a mapping containing process statistics. + * + * @return a Map from Strings to Uid.Proc objects. + */ + public abstract Map<String, ? extends Proc> getProcessStats(); + + /** + * Returns a mapping containing package statistics. + * + * @return a Map from Strings to Uid.Pkg objects. + */ + public abstract Map<String, ? extends Pkg> getPackageStats(); + + public static abstract class Sensor { + public abstract Timer getSensorTime(); + } + + /** + * The statistics associated with a particular process. + */ + public static abstract class Proc { + + /** + * Returns the total time (in 1/100 sec) spent executing in user code. + * + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + */ + public abstract long getUserTime(int which); + + /** + * Returns the total time (in 1/100 sec) spent executing in system code. + * + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + */ + public abstract long getSystemTime(int which); + + /** + * Returns the number of times the process has been started. + * + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + */ + public abstract int getStarts(int which); + } + + /** + * The statistics associated with a particular package. + */ + public static abstract class Pkg { + + /** + * Returns the number of times this package has done something that could wake up the + * device from sleep. + * + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + */ + public abstract int getWakeups(int which); + + /** + * Returns a mapping containing service statistics. + */ + public abstract Map<String, ? extends Serv> getServiceStats(); + + /** + * The statistics associated with a particular service. + */ + public abstract class Serv { + + /** + * Returns the amount of time spent started. + * + * @param now elapsed realtime in microseconds. + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + * @return + */ + public abstract long getStartTime(long now, int which); + + /** + * Returns the total number of times startService() has been called. + * + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + */ + public abstract int getStarts(int which); + + /** + * Returns the total number times the service has been launched. + * + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + */ + public abstract int getLaunches(int which); + } + } + } + + /** + * Returns the number of times the device has been started. + */ + public abstract int getStartCount(); + + /** + * Returns a SparseArray containing the statistics for each uid. + */ + public abstract SparseArray<? extends Uid> getUidStats(); + + /** + * Returns the current battery uptime in microseconds. + * + * @param curTime the amount of elapsed realtime in microseconds. + */ + public abstract long getBatteryUptime(long curTime); + + /** + * Returns the current battery realtime in microseconds. + * + * @param curTime the amount of elapsed realtime in microseconds. + */ + public abstract long getBatteryRealtime(long curTime); + + /** + * Returns the total, last, or current battery uptime in microseconds. + * + * @param curTime the elapsed realtime in microseconds. + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + */ + public abstract long computeBatteryUptime(long curTime, int which); + + /** + * Returns the total, last, or current battery realtime in microseconds. + * + * @param curTime the current elapsed realtime in microseconds. + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + */ + public abstract long computeBatteryRealtime(long curTime, int which); + + /** + * Returns the total, last, or current uptime in micropeconds. + * + * @param curTime the current elapsed realtime in microseconds. + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + */ + public abstract long computeUptime(long curTime, int which); + + /** + * Returns the total, last, or current realtime in microseconds. + * * + * @param curTime the current elapsed realtime in microseconds. + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + */ + public abstract long computeRealtime(long curTime, int which); + + private final static void formatTime(StringBuilder out, long seconds) { + long days = seconds / (60 * 60 * 24); + if (days != 0) { + out.append(days); + out.append("d "); + } + long used = days * 60 * 60 * 24; + + long hours = (seconds - used) / (60 * 60); + if (hours != 0 || used != 0) { + out.append(hours); + out.append("h "); + } + used += hours * 60 * 60; + + long mins = (seconds-used) / 60; + if (mins != 0 || used != 0) { + out.append(mins); + out.append("m "); + } + used += mins * 60; + + if (seconds != 0 || used != 0) { + out.append(seconds-used); + out.append("s "); + } + } + + private final static String formatTime(long time) { + long sec = time / 100; + StringBuilder sb = new StringBuilder(); + formatTime(sb, sec); + sb.append((time - (sec * 100)) * 10); + sb.append("ms "); + return sb.toString(); + } + + private final static String formatTimeMs(long time) { + long sec = time / 1000; + StringBuilder sb = new StringBuilder(); + formatTime(sb, sec); + sb.append(time - (sec * 1000)); + sb.append("ms "); + return sb.toString(); + } + + private final String formatRatioLocked(long num, long den) { + float perc = ((float)num) / ((float)den) * 100; + mFormatBuilder.setLength(0); + mFormatter.format("%.1f%%", perc); + return mFormatBuilder.toString(); + } + + /** + * + * @param sb a StringBuilder object. + * @param timer a Timer object contining the wakelock times. + * @param now the current time in microseconds. + * @param name the name of the wakelock. + * @param which which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + * @param linePrefix a String to be prepended to each line of output. + * @return the line prefix + */ + private final String printWakeLock(StringBuilder sb, Timer timer, long now, + String name, int which, String linePrefix) { + if (timer != null) { + // Convert from microseconds to milliseconds with rounding + long totalTimeMillis = (timer.getTotalTime(now, which) + 500) / 1000; + int count = timer.getCount(which); + if (totalTimeMillis != 0) { + sb.append(linePrefix); + sb.append(formatTimeMs(totalTimeMillis)); + sb.append(name); + sb.append(' '); + sb.append('('); + sb.append(count); + sb.append(" times)"); + return ", "; + } + } + return linePrefix; + } + + @SuppressWarnings("unused") + private final void dumpLocked(FileDescriptor fd, PrintWriter pw, String prefix, int which) { + long uSecTime = SystemClock.elapsedRealtime() * 1000; + final long uSecNow = getBatteryUptime(uSecTime); + + StringBuilder sb = new StringBuilder(128); + if (which == STATS_TOTAL) { + pw.println(prefix + "Current and Historic Battery Usage Statistics:"); + pw.println(prefix + " System starts: " + getStartCount()); + } else if (which == STATS_LAST) { + pw.println(prefix + "Last Battery Usage Statistics:"); + } else { + pw.println(prefix + "Current Battery Usage Statistics:"); + } + long batteryUptime = computeBatteryUptime(uSecNow, which); + long batteryRealtime = computeBatteryRealtime(getBatteryRealtime(uSecTime), which); + long elapsedRealtime = computeRealtime(uSecTime, which); + pw.println(prefix + + " On battery: " + formatTimeMs(batteryUptime) + "(" + + formatRatioLocked(batteryUptime, batteryRealtime) + + ") uptime, " + + formatTimeMs(batteryRealtime) + "(" + + formatRatioLocked(batteryRealtime, elapsedRealtime) + + ") realtime"); + pw.println(prefix + + " Total: " + + formatTimeMs(computeUptime(SystemClock.uptimeMillis() * 1000, which)) + + "uptime, " + + formatTimeMs(elapsedRealtime) + + "realtime"); + + pw.println(" "); + + SparseArray<? extends Uid> uidStats = getUidStats(); + final int NU = uidStats.size(); + for (int iu=0; iu<NU; iu++) { + final int uid = uidStats.keyAt(iu); + Uid u = uidStats.valueAt(iu); + pw.println(prefix + " #" + uid + ":"); + boolean uidActivity = false; + + Map<String, ? extends BatteryStats.Uid.Wakelock> wakelocks = u.getWakelockStats(); + if (wakelocks.size() > 0) { + for (Map.Entry<String, ? extends BatteryStats.Uid.Wakelock> ent + : wakelocks.entrySet()) { + Uid.Wakelock wl = ent.getValue(); + String linePrefix = ": "; + sb.setLength(0); + sb.append(prefix); + sb.append(" Wake lock "); + sb.append(ent.getKey()); + linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_FULL), uSecNow, + "full", which, linePrefix); + linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_PARTIAL), uSecNow, + "partial", which, linePrefix); + linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_WINDOW), uSecNow, + "window", which, linePrefix); + if (linePrefix.equals(": ")) { + sb.append(": (nothing executed)"); + } + pw.println(sb.toString()); + uidActivity = true; + } + } + + Map<Integer, ? extends BatteryStats.Uid.Sensor> sensors = u.getSensorStats(); + if (sensors.size() > 0) { + for (Map.Entry<Integer, ? extends BatteryStats.Uid.Sensor> ent + : sensors.entrySet()) { + Uid.Sensor se = ent.getValue(); + int sensorNumber = ent.getKey(); + sb.setLength(0); + sb.append(prefix); + sb.append(" Sensor "); + sb.append(sensorNumber); + + Timer timer = se.getSensorTime(); + if (timer != null) { + // Convert from microseconds to milliseconds with rounding + long totalTime = (timer.getTotalTime(uSecNow, which) + 500) / 1000; + int count = timer.getCount(which); + if (totalTime != 0) { + sb.append(": "); + sb.append(formatTimeMs(totalTime)); + sb.append(' '); + sb.append('('); + sb.append(count); + sb.append(" times)"); + } + } else { + sb.append(": (none used)"); + } + + pw.println(sb.toString()); + uidActivity = true; + } + } + + Map<String, ? extends BatteryStats.Uid.Proc> processStats = u.getProcessStats(); + if (processStats.size() > 0) { + for (Map.Entry<String, ? extends BatteryStats.Uid.Proc> ent + : processStats.entrySet()) { + Uid.Proc ps = ent.getValue(); + long userTime; + long systemTime; + int starts; + + userTime = ps.getUserTime(which); + systemTime = ps.getSystemTime(which); + starts = ps.getStarts(which); + + if (userTime != 0 || systemTime != 0 || starts != 0) { + pw.println(prefix + " Proc " + ent.getKey() + ":"); + pw.println(prefix + " CPU: " + formatTime(userTime) + "user + " + + formatTime(systemTime) + "kernel"); + pw.println(prefix + " " + starts + " process starts"); + uidActivity = true; + } + } + } + + Map<String, ? extends BatteryStats.Uid.Pkg> packageStats = u.getPackageStats(); + if (packageStats.size() > 0) { + for (Map.Entry<String, ? extends BatteryStats.Uid.Pkg> ent + : packageStats.entrySet()) { + pw.println(prefix + " Apk " + ent.getKey() + ":"); + boolean apkActivity = false; + Uid.Pkg ps = ent.getValue(); + int wakeups = ps.getWakeups(which); + if (wakeups != 0) { + pw.println(prefix + " " + wakeups + " wakeup alarms"); + apkActivity = true; + } + Map<String, ? extends Uid.Pkg.Serv> serviceStats = ps.getServiceStats(); + if (serviceStats.size() > 0) { + for (Map.Entry<String, ? extends BatteryStats.Uid.Pkg.Serv> sent + : serviceStats.entrySet()) { + BatteryStats.Uid.Pkg.Serv ss = sent.getValue(); + long startTime = ss.getStartTime(uSecNow, which); + int starts = ss.getStarts(which); + int launches = ss.getLaunches(which); + if (startTime != 0 || starts != 0 || launches != 0) { + pw.println(prefix + " Service " + sent.getKey() + ":"); + pw.println(prefix + " Time spent started: " + + formatTimeMs(startTime)); + pw.println(prefix + " Starts: " + starts + + ", launches: " + launches); + apkActivity = true; + } + } + } + if (!apkActivity) { + pw.println(prefix + " (nothing executed)"); + } + uidActivity = true; + } + } + if (!uidActivity) { + pw.println(prefix + " (nothing executed)"); + } + } + } + + /** + * Dumps a human-readable summary of the battery statistics to the given PrintWriter. + * + * @param fd a FileDescriptor, currently unused. + * @param pw a PrintWriter to receive the dump output. + * @param args an array of Strings, currently unused. + */ + @SuppressWarnings("unused") + public void dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args) { + synchronized (this) { + dumpLocked(fd, pw, "", STATS_TOTAL); + pw.println(""); + dumpLocked(fd, pw, "", STATS_LAST); + pw.println(""); + dumpLocked(fd, pw, "", STATS_CURRENT); + } + } +} diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index b9b2773..c3bb967 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -117,6 +117,10 @@ public final class Debug * waitForDebugger() call if you want to start tracing immediately. */ public static void waitForDebugger() { + if (!VMDebug.isDebuggingEnabled()) { + //System.out.println("debugging not enabled, not waiting"); + return; + } if (isDebuggerConnected()) return; diff --git a/core/java/android/os/Handler.java b/core/java/android/os/Handler.java index b6f38d9..2a32e54 100644 --- a/core/java/android/os/Handler.java +++ b/core/java/android/os/Handler.java @@ -54,7 +54,7 @@ import java.lang.reflect.Modifier; * <p>When a * process is created for your application, its main thread is dedicated to * running a message queue that takes care of managing the top-level - * application objects (activities, intent receivers, etc) and any windows + * application objects (activities, broadcast receivers, etc) and any windows * they create. You can create your own threads, and communicate back with * the main application thread through a Handler. This is done by calling * the same <em>post</em> or <em>sendMessage</em> methods as before, but from @@ -71,20 +71,31 @@ public class Handler { private static final String TAG = "Handler"; /** + * Callback interface you can use when instantiating a Handler to avoid + * having to implement your own subclass of Handler. + */ + public interface Callback { + public boolean handleMessage(Message msg); + } + + /** * Subclasses must implement this to receive messages. */ - public void handleMessage(Message msg) - { + public void handleMessage(Message msg) { } /** * Handle system messages here. */ - public void dispatchMessage(Message msg) - { + public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg); } else { + if (mCallback != null) { + if (mCallback.handleMessage(msg)) { + return; + } + } handleMessage(msg); } } @@ -95,8 +106,31 @@ public class Handler { * * If there isn't one, this handler won't be able to receive messages. */ - public Handler() - { + public Handler() { + if (FIND_POTENTIAL_LEAKS) { + final Class<? extends Handler> klass = getClass(); + if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) && + (klass.getModifiers() & Modifier.STATIC) == 0) { + Log.w(TAG, "The following Handler class should be static or leaks might occur: " + + klass.getCanonicalName()); + } + } + + mLooper = Looper.myLooper(); + if (mLooper == null) { + throw new RuntimeException( + "Can't create handler inside thread that has not called Looper.prepare()"); + } + mQueue = mLooper.mQueue; + mCallback = null; + } + + /** + * Constructor associates this handler with the queue for the + * current thread and takes a callback interface in which you can handle + * messages. + */ + public Handler(Callback callback) { if (FIND_POTENTIAL_LEAKS) { final Class<? extends Handler> klass = getClass(); if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) && @@ -112,15 +146,26 @@ public class Handler { "Can't create handler inside thread that has not called Looper.prepare()"); } mQueue = mLooper.mQueue; + mCallback = callback; } /** * Use the provided queue instead of the default one. */ - public Handler(Looper looper) - { + public Handler(Looper looper) { + mLooper = looper; + mQueue = looper.mQueue; + mCallback = null; + } + + /** + * Use the provided queue instead of the default one and take a callback + * interface in which to handle messages. + */ + public Handler(Looper looper, Callback callback) { mLooper = looper; mQueue = looper.mQueue; + mCallback = callback; } /** @@ -544,5 +589,6 @@ public class Handler { final MessageQueue mQueue; final Looper mLooper; + final Callback mCallback; IMessenger mMessenger; } diff --git a/core/java/android/os/ICheckinService.aidl b/core/java/android/os/ICheckinService.aidl index aa43852..70ad28e 100644 --- a/core/java/android/os/ICheckinService.aidl +++ b/core/java/android/os/ICheckinService.aidl @@ -39,5 +39,6 @@ interface ICheckinService { * Determine if the device is under parental control. Return null if * we are unable to check the parental control status. */ - void getParentalControlState(IParentalControlCallback p); + void getParentalControlState(IParentalControlCallback p, + String requestingApp); } diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl index 9d05917..abc1e2f 100644 --- a/core/java/android/os/IPowerManager.aidl +++ b/core/java/android/os/IPowerManager.aidl @@ -26,6 +26,6 @@ interface IPowerManager void userActivity(long when, boolean noChangeLights); void userActivityWithForce(long when, boolean noChangeLights, boolean force); void setPokeLock(int pokey, IBinder lock, String tag); - void setStayOnSetting(boolean val); + void setStayOnSetting(int val); long getScreenOnTime(); } diff --git a/core/java/android/os/Looper.java b/core/java/android/os/Looper.java index 80b68e2..9581893 100644 --- a/core/java/android/os/Looper.java +++ b/core/java/android/os/Looper.java @@ -130,7 +130,8 @@ public class Looper { } /** - * Return the Looper object associated with the current thread. + * Return the Looper object associated with the current thread. Returns + * null if the calling thread is not associated with a Looper. */ public static final Looper myLooper() { return (Looper)sThreadLocal.get(); @@ -151,7 +152,8 @@ public class Looper { /** * Return the {@link MessageQueue} object associated with the current - * thread. + * thread. This must be called from a thread running a Looper, or a + * NullPointerException will be thrown. */ public static final MessageQueue myQueue() { return myLooper().mQueue; @@ -171,6 +173,16 @@ public class Looper { mQueue.enqueueMessage(msg, 0); } + /** + * Return the Thread associated with this Looper. + * + * @since CURRENT + * {@hide pending API Council approval} + */ + public Thread getThread() { + return mThread; + } + public void dump(Printer pw, String prefix) { pw.println(prefix + this); pw.println(prefix + "mRun=" + mRun); diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 2be7768..cd86fbe 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -19,6 +19,7 @@ package android.os; import android.net.LocalSocketAddress; import android.net.LocalSocket; import android.util.Log; +import dalvik.system.Zygote; import java.io.BufferedWriter; import java.io.DataInputStream; @@ -221,13 +222,13 @@ public class Process { public static final int start(final String processClass, final String niceName, int uid, int gid, int[] gids, - boolean enableDebugger, + int debugFlags, String[] zygoteArgs) { if (supportsProcesses()) { try { return startViaZygote(processClass, niceName, uid, gid, gids, - enableDebugger, zygoteArgs); + debugFlags, zygoteArgs); } catch (ZygoteStartFailedEx ex) { Log.e(LOG_TAG, "Starting VM process through Zygote failed"); @@ -259,9 +260,9 @@ public class Process { * {@hide} */ public static final int start(String processClass, int uid, int gid, - int[] gids, boolean enableDebugger, String[] zygoteArgs) { + int[] gids, int debugFlags, String[] zygoteArgs) { return start(processClass, "", uid, gid, gids, - enableDebugger, zygoteArgs); + debugFlags, zygoteArgs); } private static void invokeStaticMain(String className) { @@ -452,7 +453,7 @@ public class Process { final String niceName, final int uid, final int gid, final int[] gids, - boolean enableDebugger, + int debugFlags, String[] extraArgs) throws ZygoteStartFailedEx { int pid; @@ -465,9 +466,15 @@ public class Process { argsForZygote.add("--runtime-init"); argsForZygote.add("--setuid=" + uid); argsForZygote.add("--setgid=" + gid); - if (enableDebugger) { + if ((debugFlags & Zygote.DEBUG_ENABLE_DEBUGGER) != 0) { argsForZygote.add("--enable-debugger"); } + if ((debugFlags & Zygote.DEBUG_ENABLE_CHECKJNI) != 0) { + argsForZygote.add("--enable-checkjni"); + } + if ((debugFlags & Zygote.DEBUG_ENABLE_ASSERT) != 0) { + argsForZygote.add("--enable-assert"); + } //TODO optionally enable debuger //argsForZygote.add("--enable-debugger"); @@ -529,7 +536,12 @@ public class Process { public static final native int myTid(); /** - * Returns the UID assigned to a partlicular user name, or -1 if there is + * Returns the identifier of this process's user. + */ + public static final native int myUid(); + + /** + * Returns the UID assigned to a particular user name, or -1 if there is * none. If the given string consists of only numbers, it is converted * directly to a uid. */ diff --git a/core/java/android/pim/EventRecurrence.java b/core/java/android/pim/EventRecurrence.java index ad671f6..edf69ee 100644 --- a/core/java/android/pim/EventRecurrence.java +++ b/core/java/android/pim/EventRecurrence.java @@ -18,6 +18,7 @@ package android.pim; import android.content.res.Resources; import android.text.TextUtils; +import android.text.format.Time; import java.util.Calendar; diff --git a/core/java/android/pim/RecurrenceSet.java b/core/java/android/pim/RecurrenceSet.java index c02ff52..c6615da 100644 --- a/core/java/android/pim/RecurrenceSet.java +++ b/core/java/android/pim/RecurrenceSet.java @@ -21,6 +21,7 @@ import android.database.Cursor; import android.os.Bundle; import android.provider.Calendar; import android.text.TextUtils; +import android.text.format.Time; import android.util.Config; import android.util.Log; @@ -145,7 +146,7 @@ public class RecurrenceSet { long[] dates = new long[n]; for (int i = 0; i<n; ++i) { // The timezone is updated to UTC if the time string specified 'Z'. - time.parse2445(rawDates[i]); + time.parse(rawDates[i]); dates[i] = time.toMillis(false /* use isDst */); time.timezone = tz; } @@ -173,7 +174,7 @@ public class RecurrenceSet { // NOTE: the timezone may be null, if this is a floating time. String tzid = tzidParam == null ? null : tzidParam.value; Time start = new Time(tzidParam == null ? Time.TIMEZONE_UTC : tzid); - boolean inUtc = start.parse2445(dtstart); + boolean inUtc = start.parse(dtstart); boolean allDay = start.allDay; if (inUtc) { @@ -350,7 +351,7 @@ public class RecurrenceSet { ? start.timezone : endTzidParameter.value; Time end = new Time(endTzid); - end.parse2445(dtendProperty.getValue()); + end.parse(dtendProperty.getValue()); long durationMillis = end.toMillis(false /* use isDst */) - start.toMillis(false /* use isDst */); long durationSeconds = (durationMillis / 1000); diff --git a/core/java/android/preference/CheckBoxPreference.java b/core/java/android/preference/CheckBoxPreference.java index 4bebf87..1e9b7ae 100644 --- a/core/java/android/preference/CheckBoxPreference.java +++ b/core/java/android/preference/CheckBoxPreference.java @@ -27,7 +27,7 @@ import android.widget.Checkable; import android.widget.TextView; /** - * The {@link CheckBoxPreference} is a preference that provides checkbox widget + * A {@link Preference} that provides checkbox widget * functionality. * <p> * This preference will store a boolean into the SharedPreferences. diff --git a/core/java/android/preference/DialogPreference.java b/core/java/android/preference/DialogPreference.java index 3eb65e2..666efae 100644 --- a/core/java/android/preference/DialogPreference.java +++ b/core/java/android/preference/DialogPreference.java @@ -34,7 +34,7 @@ import android.view.View; import android.widget.TextView; /** - * The {@link DialogPreference} class is a base class for preferences that are + * A base class for {@link Preference} objects that are * dialog-based. These preferences will, when clicked, open a dialog showing the * actual preference controls. * @@ -356,7 +356,7 @@ public abstract class DialogPreference extends Preference implements getPreferenceManager().unregisterOnActivityDestroyListener(this); mDialog = null; - onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON1); + onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE); } /** @@ -370,6 +370,15 @@ public abstract class DialogPreference extends Preference implements } /** + * Gets the dialog that is shown by this preference. + * + * @return The dialog, or null if a dialog is not being shown. + */ + public Dialog getDialog() { + return mDialog; + } + + /** * {@inheritDoc} */ public void onActivityDestroy() { diff --git a/core/java/android/preference/EditTextPreference.java b/core/java/android/preference/EditTextPreference.java index be56003..a12704f 100644 --- a/core/java/android/preference/EditTextPreference.java +++ b/core/java/android/preference/EditTextPreference.java @@ -31,7 +31,7 @@ import android.widget.EditText; import android.widget.LinearLayout; /** - * The {@link EditTextPreference} class is a preference that allows for string + * A {@link Preference} that allows for string * input. * <p> * It is a subclass of {@link DialogPreference} and shows the {@link EditText} diff --git a/core/java/android/preference/ListPreference.java b/core/java/android/preference/ListPreference.java index 6c98ded..f842d75 100644 --- a/core/java/android/preference/ListPreference.java +++ b/core/java/android/preference/ListPreference.java @@ -26,7 +26,7 @@ import android.os.Parcelable; import android.util.AttributeSet; /** - * The {@link ListPreference} is a preference that displays a list of entries as + * A {@link Preference} that displays a list of entries as * a dialog. * <p> * This preference will store a string into the SharedPreferences. This string will be the value @@ -192,8 +192,22 @@ public class ListPreference extends DialogPreference { new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { mClickedDialogEntryIndex = which; + + /* + * Clicking on an item simulates the positive button + * click, and dismisses the dialog. + */ + ListPreference.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE); + dialog.dismiss(); } }); + + /* + * The typical interaction for list-based dialogs is to have + * click-on-an-item dismiss the dialog instead of the user having to + * press 'Ok'. + */ + builder.setPositiveButton(null, null); } @Override diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java index 1db7525..3820f28 100644 --- a/core/java/android/preference/Preference.java +++ b/core/java/android/preference/Preference.java @@ -37,13 +37,13 @@ import android.widget.ListView; import android.widget.TextView; /** - * The {@link Preference} class represents the basic preference UI building - * block that is displayed by a {@link PreferenceActivity} in the form of a + * Represents the basic Preference UI building + * block displayed by a {@link PreferenceActivity} in the form of a * {@link ListView}. This class provides the {@link View} to be displayed in * the activity and associates with a {@link SharedPreferences} to * store/retrieve the preference data. * <p> - * When specifying a preference hierarchy in XML, each tag name can point to a + * When specifying a preference hierarchy in XML, each element can point to a * subclass of {@link Preference}, similar to the view hierarchy and layouts. * <p> * This class contains a {@code key} that will be used as the key into the @@ -109,46 +109,46 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis private boolean mBaseMethodCalled; /** - * Interface definition for a callback to be invoked when this - * {@link Preference Preference's} value has been changed by the user and is + * Interface definition for a callback to be invoked when the value of this + * {@link Preference} has been changed by the user and is * about to be set and/or persisted. This gives the client a chance * to prevent setting and/or persisting the value. */ public interface OnPreferenceChangeListener { /** - * Called when this preference has been changed by the user. This is - * called before the preference's state is about to be updated and + * Called when a Preference has been changed by the user. This is + * called before the state of the Preference is about to be updated and * before the state is persisted. * - * @param preference This preference. - * @param newValue The new value of the preference. - * @return Whether or not to update this preference's state with the new value. + * @param preference The changed Preference. + * @param newValue The new value of the Preference. + * @return True to update the state of the Preference with the new value. */ boolean onPreferenceChange(Preference preference, Object newValue); } /** - * Interface definition for a callback to be invoked when a preference is + * Interface definition for a callback to be invoked when a {@link Preference} is * clicked. */ public interface OnPreferenceClickListener { /** - * Called when a preference has been clicked. + * Called when a Preference has been clicked. * - * @param preference The preference that was clicked. - * @return Whether the click was handled. + * @param preference The Preference that was clicked. + * @return True if the click was handled. */ boolean onPreferenceClick(Preference preference); } /** * Interface definition for a callback to be invoked when this - * {@link Preference} is changed or if this is a group, there is an + * {@link Preference} is changed or, if this is a group, there is an * addition/removal of {@link Preference}(s). This is used internally. */ interface OnPreferenceChangeInternalListener { /** - * Called when this preference has changed. + * Called when this Preference has changed. * * @param preference This preference. */ @@ -157,18 +157,18 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis /** * Called when this group has added/removed {@link Preference}(s). * - * @param preference This preference. + * @param preference This Preference. */ void onPreferenceHierarchyChange(Preference preference); } /** * Perform inflation from XML and apply a class-specific base style. This - * constructor of {@link Preference} allows subclasses to use their own base - * style when they are inflating. For example, a {@link CheckBoxPreference}'s - * constructor would call this version of the super class constructor and - * supply {@code android.R.attr.checkBoxPreferenceStyle} for <var>defStyle</var>; - * this allows the theme's checkbox preference style to modify all of the base + * constructor of Preference allows subclasses to use their own base + * style when they are inflating. For example, a {@link CheckBoxPreference} + * constructor calls this version of the super class constructor and + * supplies {@code android.R.attr.checkBoxPreferenceStyle} for <var>defStyle</var>. + * This allows the theme's checkbox preference style to modify all of the base * preference attributes as well as the {@link CheckBoxPreference} class's * attributes. * @@ -254,8 +254,8 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Constructor that is called when inflating a preference from XML. This is - * called when a preference is being constructed from an XML file, supplying + * Constructor that is called when inflating a Preference from XML. This is + * called when a Preference is being constructed from an XML file, supplying * attributes that were specified in the XML file. This version uses a * default style of 0, so the only attribute values applied are those in the * Context's Theme and the given AttributeSet. @@ -274,15 +274,15 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis /** * Constructor to create a Preference. * - * @param context The context to store preference values. + * @param context The Context in which to store Preference values. */ public Preference(Context context) { this(context, null); } /** - * Called when {@link Preference} is being inflated and the default value - * attribute needs to be read. Since different preference types have + * Called when a Preference is being inflated and the default value + * attribute needs to be read. Since different Preference types have * different value types, the subclass should get and return the default * value which will be its value type. * <p> @@ -299,16 +299,16 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis /** * Sets an {@link Intent} to be used for - * {@link Context#startActivity(Intent)} when the preference is clicked. + * {@link Context#startActivity(Intent)} when this Preference is clicked. * - * @param intent The intent associated with the preference. + * @param intent The intent associated with this Preference. */ public void setIntent(Intent intent) { mIntent = intent; } /** - * Return the {@link Intent} associated with this preference. + * Return the {@link Intent} associated with this Preference. * * @return The {@link Intent} last set via {@link #setIntent(Intent)} or XML. */ @@ -318,12 +318,12 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis /** * Sets the layout resource that is inflated as the {@link View} to be shown - * for this preference. In most cases, the default layout is sufficient for - * custom preferences and only the widget layout needs to be changed. + * for this Preference. In most cases, the default layout is sufficient for + * custom Preference objects and only the widget layout needs to be changed. * <p> * This layout should contain a {@link ViewGroup} with ID * {@link android.R.id#widget_frame} to be the parent of the specific widget - * for this preference. It should similarly contain + * for this Preference. It should similarly contain * {@link android.R.id#title} and {@link android.R.id#summary}. * * @param layoutResId The layout resource ID to be inflated and returned as @@ -340,7 +340,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Gets the layout resource that will be shown as the {@link View} for this preference. + * Gets the layout resource that will be shown as the {@link View} for this Preference. * * @return The layout resource ID. */ @@ -349,8 +349,8 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Sets The layout for the controllable widget portion of a preference. This - * is inflated into the main layout. For example, a checkbox preference + * Sets The layout for the controllable widget portion of this Preference. This + * is inflated into the main layout. For example, a {@link CheckBoxPreference} * would specify a custom layout (consisting of just the CheckBox) here, * instead of creating its own main layout. * @@ -363,7 +363,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Gets the layout resource for the controllable widget portion of a preference. + * Gets the layout resource for the controllable widget portion of this Preference. * * @return The layout resource ID. */ @@ -374,11 +374,11 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis /** * Gets the View that will be shown in the {@link PreferenceActivity}. * - * @param convertView The old view to reuse, if possible. Note: You should - * check that this view is non-null and of an appropriate type - * before using. If it is not possible to convert this view to - * display the correct data, this method can create a new view. - * @param parent The parent that this view will eventually be attached to. + * @param convertView The old View to reuse, if possible. Note: You should + * check that this View is non-null and of an appropriate type + * before using. If it is not possible to convert this View to + * display the correct data, this method can create a new View. + * @param parent The parent that this View will eventually be attached to. * @return Returns the same Preference object, for chaining multiple calls * into a single statement. * @see #onCreateView(ViewGroup) @@ -393,16 +393,16 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Creates the View to be shown for this preference in the + * Creates the View to be shown for this Preference in the * {@link PreferenceActivity}. The default behavior is to inflate the main - * layout of this preference (see {@link #setLayoutResource(int)}. If + * layout of this Preference (see {@link #setLayoutResource(int)}. If * changing this behavior, please specify a {@link ViewGroup} with ID * {@link android.R.id#widget_frame}. * <p> * Make sure to call through to the superclass's implementation. * - * @param parent The parent that this view will eventually be attached to. - * @return The View that displays this preference. + * @param parent The parent that this View will eventually be attached to. + * @return The View that displays this Preference. * @see #onBindView(View) */ protected View onCreateView(ViewGroup parent) { @@ -420,14 +420,14 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Binds the created View to the data for the preference. + * Binds the created View to the data for this Preference. * <p> * This is a good place to grab references to custom Views in the layout and * set properties on them. * <p> * Make sure to call through to the superclass's implementation. * - * @param view The View that shows this preference. + * @param view The View that shows this Preference. * @see #onCreateView(ViewGroup) */ protected void onBindView(View view) { @@ -453,7 +453,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } if (mShouldDisableView) { - setEnabledStateOnViews(view, mEnabled); + setEnabledStateOnViews(view, isEnabled()); } } @@ -472,13 +472,13 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Sets the order of this {@link Preference} with respect to other - * {@link Preference} on the same level. If this is not specified, the + * Sets the order of this Preference with respect to other + * Preference objects on the same level. If this is not specified, the * default behavior is to sort alphabetically. The * {@link PreferenceGroup#setOrderingAsAdded(boolean)} can be used to order - * preferences based on the order they appear in the XML. + * Preference objects based on the order they appear in the XML. * - * @param order The order for this preference. A lower value will be shown + * @param order The order for this Preference. A lower value will be shown * first. Use {@link #DEFAULT_ORDER} to sort alphabetically or * allow ordering from XML. * @see PreferenceGroup#setOrderingAsAdded(boolean) @@ -494,9 +494,10 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Gets the order of this {@link Preference}. + * Gets the order of this Preference with respect to other Preference objects + * on the same level. * - * @return The order of this {@link Preference}. + * @return The order of this Preference. * @see #setOrder(int) */ public int getOrder() { @@ -504,11 +505,12 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Sets the title for the preference. This title will be placed into the ID + * Sets the title for this Preference with a CharSequence. + * This title will be placed into the ID * {@link android.R.id#title} within the View created by * {@link #onCreateView(ViewGroup)}. * - * @param title The title of the preference. + * @param title The title for this Preference. */ public void setTitle(CharSequence title) { if (title == null && mTitle != null || title != null && !title.equals(mTitle)) { @@ -518,6 +520,8 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** + * Sets the title for this Preference with a resource ID. + * * @see #setTitle(CharSequence) * @param titleResId The title as a resource ID. */ @@ -526,7 +530,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Returns the title of the preference. + * Returns the title of this Preference. * * @return The title. * @see #setTitle(CharSequence) @@ -536,7 +540,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Returns the summary of the preference. + * Returns the summary of this Preference. * * @return The summary. * @see #setSummary(CharSequence) @@ -546,11 +550,9 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Sets the summary for the preference. This summary will be placed into the - * ID {@link android.R.id#summary} within the View created by - * {@link #onCreateView(ViewGroup)}. + * Sets the summary for this Preference with a CharSequence. * - * @param summary The summary of the preference. + * @param summary The summary for the preference. */ public void setSummary(CharSequence summary) { if (summary == null && mSummary != null || summary != null && !summary.equals(mSummary)) { @@ -560,6 +562,8 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** + * Sets the summary for this Preference with a resource ID. + * * @see #setSummary(CharSequence) * @param summaryResId The summary as a resource. */ @@ -568,10 +572,10 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Sets whether this preference is enabled. If disabled, the preference will + * Sets whether this Preference is enabled. If disabled, it will * not handle clicks. * - * @param enabled Whether the preference is enabled. + * @param enabled Set true to enable it. */ public void setEnabled(boolean enabled) { if (mEnabled != enabled) { @@ -585,18 +589,18 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Whether this {@link Preference} should be enabled in the list. + * Checks whether this Preference should be enabled in the list. * - * @return Whether this preference is enabled. + * @return True if this Preference is enabled, false otherwise. */ public boolean isEnabled() { return mEnabled; } /** - * Sets whether this preference is selectable. + * Sets whether this Preference is selectable. * - * @param selectable Whether the preference is selectable. + * @param selectable Set true to make it selectable. */ public void setSelectable(boolean selectable) { if (mSelectable != selectable) { @@ -606,23 +610,23 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Whether this {@link Preference} should be selectable in the list. + * Checks whether this Preference should be selectable in the list. * - * @return Whether this preference is selectable. + * @return True if it is selectable, false otherwise. */ public boolean isSelectable() { return mSelectable; } /** - * Sets whether this {@link Preference} should disable its view when it gets + * Sets whether this Preference should disable its view when it gets * disabled. * <p> * For example, set this and {@link #setEnabled(boolean)} to false for * preferences that are only displaying information and 1) should not be * clickable 2) should not have the view set to the disabled state. * - * @param shouldDisableView Whether this preference should disable its view + * @param shouldDisableView Set true if this preference should disable its view * when the preference is disabled. */ public void setShouldDisableView(boolean shouldDisableView) { @@ -631,18 +635,19 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** + * Checks whether this Preference should disable its view when it's action is disabled. * @see #setShouldDisableView(boolean) - * @return Whether this preference should disable its view when it is disabled. + * @return True if it should disable the view. */ public boolean getShouldDisableView() { return mShouldDisableView; } /** - * Returns a unique ID for this preference. This ID should be unique across all - * preferences in a hierarchy. + * Returns a unique ID for this Preference. This ID should be unique across all + * Preference objects in a hierarchy. * - * @return A unique ID for this preference. + * @return A unique ID for this Preference. */ long getId() { return mId; @@ -658,7 +663,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Sets the key for the preference which is used as a key to the + * Sets the key for this Preference, which is used as a key to the * {@link SharedPreferences}. This should be unique for the package. * * @param key The key for the preference. @@ -673,7 +678,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Gets the key for the preference, which is also the key used for storing + * Gets the key for this Preference, which is also the key used for storing * values into SharedPreferences. * * @return The key. @@ -686,6 +691,8 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis * Checks whether the key is present, and if it isn't throws an * exception. This should be called by subclasses that store preferences in * the {@link SharedPreferences}. + * + * @throws IllegalStateException If there is no key assigned. */ void requireKey() { if (mKey == null) { @@ -696,43 +703,43 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Returns whether this {@link Preference} has a valid key. + * Checks whether this Preference has a valid key. * - * @return Whether the key exists and is not a blank string. + * @return True if the key exists and is not a blank string, false otherwise. */ public boolean hasKey() { return !TextUtils.isEmpty(mKey); } /** - * Returns whether this {@link Preference} is persistent. If it is persistent, it stores its value(s) into + * Checks whether this Preference is persistent. If it is, it stores its value(s) into * the persistent {@link SharedPreferences} storage. * - * @return Whether this is persistent. + * @return True if it is persistent. */ public boolean isPersistent() { return mPersistent; } /** - * Convenience method of whether at the given time this method is called, - * the {@link Preference} should store/restore its value(s) into the - * {@link SharedPreferences}. This, at minimum, checks whether the - * {@link Preference} is persistent and it currently has a key. Before you + * Checks whether, at the given time this method is called, + * this Preference should store/restore its value(s) into the + * {@link SharedPreferences}. This, at minimum, checks whether this + * Preference is persistent and it currently has a key. Before you * save/restore from the {@link SharedPreferences}, check this first. * - * @return Whether to persist the value. + * @return True if it should persist the value. */ protected boolean shouldPersist() { return mPreferenceManager != null && isPersistent() && hasKey(); } /** - * Sets whether this {@link Preference} is persistent. If it is persistent, + * Sets whether this Preference is persistent. When persistent, * it stores its value(s) into the persistent {@link SharedPreferences} * storage. * - * @param persistent Whether it should store its value(s) into the {@link SharedPreferences}. + * @param persistent Set true if it should store its value(s) into the {@link SharedPreferences}. */ public void setPersistent(boolean persistent) { mPersistent = persistent; @@ -742,8 +749,8 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis * Call this method after the user changes the preference, but before the * internal state is set. This allows the client to ignore the user value. * - * @param newValue The new value of the preference. - * @return Whether or not the user value should be set as the preference + * @param newValue The new value of this Preference. + * @return True if the user value should be set as the preference * value (and persisted). */ protected boolean callChangeListener(Object newValue) { @@ -751,7 +758,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Sets the callback to be invoked when this preference is changed by the + * Sets the callback to be invoked when this Preference is changed by the * user (but before the internal state has been updated). * * @param onPreferenceChangeListener The callback to be invoked. @@ -761,7 +768,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Gets the callback to be invoked when this preference is changed by the + * Returns the callback to be invoked when this Preference is changed by the * user (but before the internal state has been updated). * * @return The callback to be invoked. @@ -771,7 +778,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Sets the callback to be invoked when this preference is clicked. + * Sets the callback to be invoked when this Preference is clicked. * * @param onPreferenceClickListener The callback to be invoked. */ @@ -780,7 +787,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Gets the callback to be invoked when this preference is clicked. + * Returns the callback to be invoked when this Preference is clicked. * * @return The callback to be invoked. */ @@ -791,9 +798,9 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis /** * Called when a click should be performed. * - * @param preferenceScreen Optional {@link PreferenceScreen} whose hierarchy click + * @param preferenceScreen A {@link PreferenceScreen} whose hierarchy click * listener should be called in the proper order (between other - * processing). + * processing). May be null. */ void performClick(PreferenceScreen preferenceScreen) { @@ -824,18 +831,19 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Returns the context of this preference. Each preference in a preference hierarchy can be - * from different context (for example, if multiple activities provide preferences into a single - * {@link PreferenceActivity}). This context will be used to save the preference valus. + * Returns the {@link android.content.Context} of this Preference. + * Each Preference in a Preference hierarchy can be + * from different Context (for example, if multiple activities provide preferences into a single + * {@link PreferenceActivity}). This Context will be used to save the Preference values. * - * @return The context of this preference. + * @return The Context of this Preference. */ public Context getContext() { return mContext; } /** - * Returns the {@link SharedPreferences} where this preference can read its + * Returns the {@link SharedPreferences} where this Preference can read its * value(s). Usually, it's easier to use one of the helper read methods: * {@link #getPersistedBoolean(boolean)}, {@link #getPersistedFloat(float)}, * {@link #getPersistedInt(int)}, {@link #getPersistedLong(long)}, @@ -847,8 +855,8 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis * {@link SharedPreferences}, this is intended behavior to improve * performance. * - * @return The {@link SharedPreferences} where this preference reads its - * value(s), or null if it isn't attached to a preference hierarchy. + * @return The {@link SharedPreferences} where this Preference reads its + * value(s), or null if it isn't attached to a Preference hierarchy. * @see #getEditor() */ public SharedPreferences getSharedPreferences() { @@ -860,7 +868,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Returns an {@link SharedPreferences.Editor} where this preference can + * Returns an {@link SharedPreferences.Editor} where this Preference can * save its value(s). Usually it's easier to use one of the helper save * methods: {@link #persistBoolean(boolean)}, {@link #persistFloat(float)}, * {@link #persistInt(int)}, {@link #persistLong(long)}, @@ -869,11 +877,11 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis * true, it is this Preference's responsibility to commit. * <p> * In some cases, writes to this will not be committed right away and hence - * not show up in the shared preferences, this is intended behavior to + * not show up in the SharedPreferences, this is intended behavior to * improve performance. * * @return A {@link SharedPreferences.Editor} where this preference saves - * its value(s), or null if it isn't attached to a preference + * its value(s), or null if it isn't attached to a Preference * hierarchy. * @see #shouldCommit() * @see #getSharedPreferences() @@ -903,9 +911,11 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Compares preferences based on order (if set), otherwise alphabetically on title. - * <p> - * {@inheritDoc} + * Compares Preference objects based on order (if set), otherwise alphabetically on the titles. + * + * @param another The Preference to compare to this one. + * @return 0 if the same; less than 0 if this Preference sorts ahead of <var>another</var>; + * greater than 0 if this Preference sorts after <var>another</var>. */ public int compareTo(Preference another) { if (mOrder != DEFAULT_ORDER @@ -942,7 +952,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Should be called this is a group a {@link Preference} has been + * Should be called when a Preference has been * added/removed from this group, or the ordering should be * re-evaluated. */ @@ -953,7 +963,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Gets the {@link PreferenceManager} that manages this preference's tree. + * Gets the {@link PreferenceManager} that manages this Preference object's tree. * * @return The {@link PreferenceManager}. */ @@ -962,10 +972,10 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Called when this preference has been attached to a preference hierarchy. + * Called when this Preference has been attached to a Preference hierarchy. * Make sure to call the super implementation. * - * @param preferenceManager The preference manager of the hierarchy. + * @param preferenceManager The PreferenceManager of the hierarchy. */ protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { mPreferenceManager = preferenceManager; @@ -976,9 +986,9 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Called when the preference hierarchy has been attached to the + * Called when the Preference hierarchy has been attached to the * {@link PreferenceActivity}. This can also be called when this - * {@link Preference} has been attached to a group that was already attached + * Preference has been attached to a group that was already attached * to the {@link PreferenceActivity}. */ protected void onAttachedToActivity() { @@ -1010,15 +1020,14 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Find a Preference in this hierarchy (the whole thing, + * Finds a Preference in this hierarchy (the whole thing, * even above/below your {@link PreferenceScreen} screen break) with the given * key. * <p> * This only functions after we have been attached to a hierarchy. * - * @param key The key of the {@link Preference} to find. - * @return The {@link Preference} object of a preference - * with the given key. + * @param key The key of the Preference to find. + * @return The Preference that uses the given key. */ protected Preference findPreferenceInHierarchy(String key) { if (TextUtils.isEmpty(key) || mPreferenceManager == null) { @@ -1029,13 +1038,13 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Adds a dependent Preference on this preference so we can notify it. - * Usually, the dependent preference registers itself (it's good for it to + * Adds a dependent Preference on this Preference so we can notify it. + * Usually, the dependent Preference registers itself (it's good for it to * know it depends on something), so please use - * {@link Preference#setDependency(String)} on the dependent preference. + * {@link Preference#setDependency(String)} on the dependent Preference. * * @param dependent The dependent Preference that will be enabled/disabled - * according to the state of this preference. + * according to the state of this Preference. */ private void registerDependent(Preference dependent) { if (mDependents == null) { @@ -1048,10 +1057,10 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Removes a dependent Preference on this preference. + * Removes a dependent Preference on this Preference. * * @param dependent The dependent Preference that will be enabled/disabled - * according to the state of this preference. + * according to the state of this Preference. * @return Returns the same Preference object, for chaining multiple calls * into a single statement. */ @@ -1065,7 +1074,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis * Notifies any listening dependents of a change that affects the * dependency. * - * @param disableDependents Whether this {@link Preference} should disable + * @param disableDependents Whether this Preference should disable * its dependents. */ public void notifyDependencyChange(boolean disableDependents) { @@ -1084,15 +1093,15 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis /** * Called when the dependency changes. * - * @param dependency The preference that this preference depends on. - * @param disableDependent Whether to disable this preference. + * @param dependency The Preference that this Preference depends on. + * @param disableDependent Set true to disable this Preference. */ public void onDependencyChanged(Preference dependency, boolean disableDependent) { setEnabled(!disableDependent); } /** - * Should return whether this preference's dependents should currently be + * Checks whether this preference's dependents should currently be * disabled. * * @return True if the dependents should be disabled, otherwise false. @@ -1117,7 +1126,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Returns the key of the dependency on this preference. + * Returns the key of the dependency on this Preference. * * @return The key of the dependency. * @see #setDependency(String) @@ -1136,7 +1145,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Sets the default value for the preference, which will be set either if + * Sets the default value for this Preference, which will be set either if * persistence is off or persistence is on and the preference is not found * in the persistent storage. * @@ -1159,17 +1168,21 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Implement this to set the initial value of the Preference. If the - * restoreValue flag is true, you should restore the value from the shared - * preferences. If false, you should set (and possibly store to shared - * preferences if {@link #shouldPersist()}) to defaultValue. + * Implement this to set the initial value of the Preference. + * <p> + * If <var>restorePersistedValue</var> is true, you should restore the + * Preference value from the {@link android.content.SharedPreferences}. If + * <var>restorePersistedValue</var> is false, you should set the Preference + * value to defaultValue that is given (and possibly store to SharedPreferences + * if {@link #shouldPersist()} is true). * <p> * This may not always be called. One example is if it should not persist * but there is no default value given. * - * @param restorePersistedValue Whether to restore the persisted value - * (true), or use the given default value (false). - * @param defaultValue The default value. Only use if restoreValue is false. + * @param restorePersistedValue True to restore the persisted value; + * false to use the given <var>defaultValue</var>. + * @param defaultValue The default value for this Preference. Only use this + * if <var>restorePersistedValue</var> is false. */ protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { } @@ -1181,14 +1194,14 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Attempts to persist a String to the SharedPreferences. + * Attempts to persist a String to the {@link android.content.SharedPreferences}. * <p> - * This will check if the Preference is persistent, get an editor from - * the preference manager, put the string, check if we should commit (and + * This will check if this Preference is persistent, get an editor from + * the {@link PreferenceManager}, put in the string, and check if we should commit (and * commit if so). * * @param value The value to persist. - * @return Whether the Preference is persistent. (This is not whether the + * @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(String) @@ -1210,15 +1223,15 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Attempts to get a persisted String from the SharedPreferences. + * Attempts to get a persisted String from the {@link android.content.SharedPreferences}. * <p> - * This will check if the Preference is persistent, get the shared - * preferences from the preference manager, get the value. + * 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 shared preferences or the default return + * @return The value from the SharedPreferences or the default return * value. * @see #persistString(String) */ @@ -1231,10 +1244,10 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Attempts to persist an int to the SharedPreferences. + * Attempts to persist an int to the {@link android.content.SharedPreferences}. * * @param value The value to persist. - * @return Whether the Preference is persistent. (This is not whether the + * @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 #persistString(String) @@ -1256,12 +1269,12 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Attempts to get a persisted int from the SharedPreferences. + * Attempts to get a persisted int from the {@link android.content.SharedPreferences}. * - * @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 shared preferences or the default return + * @param defaultReturnValue The default value to return if either this + * Preference is not persistent or this Preference is not in the + * SharedPreferences. + * @return The value from the SharedPreferences or the default return * value. * @see #getPersistedString(String) * @see #persistInt(int) @@ -1275,10 +1288,10 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Attempts to persist a float to the SharedPreferences. + * Attempts to persist a float to the {@link android.content.SharedPreferences}. * * @param value The value to persist. - * @return Whether the Preference is persistent. (This is not whether the + * @return True if this 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 #persistString(String) @@ -1300,12 +1313,12 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Attempts to get a persisted float from the SharedPreferences. + * Attempts to get a persisted float from the {@link android.content.SharedPreferences}. * - * @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 shared preferences or the default return + * @param defaultReturnValue The default value to return if either this + * Preference is not persistent or this Preference is not in the + * SharedPreferences. + * @return The value from the SharedPreferences or the default return * value. * @see #getPersistedString(String) * @see #persistFloat(float) @@ -1319,10 +1332,10 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Attempts to persist a long to the SharedPreferences. + * Attempts to persist a long to the {@link android.content.SharedPreferences}. * * @param value The value to persist. - * @return Whether the Preference is persistent. (This is not whether the + * @return True if this 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 #persistString(String) @@ -1344,12 +1357,12 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Attempts to get a persisted long from the SharedPreferences. + * Attempts to get a persisted long from the {@link android.content.SharedPreferences}. * - * @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 shared preferences or the default return + * @param defaultReturnValue The default value to return if either this + * Preference is not persistent or this Preference is not in the + * SharedPreferences. + * @return The value from the SharedPreferences or the default return * value. * @see #getPersistedString(String) * @see #persistLong(long) @@ -1363,10 +1376,10 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Attempts to persist a boolean to the SharedPreferences. + * Attempts to persist a boolean to the {@link android.content.SharedPreferences}. * * @param value The value to persist. - * @return Whether the Preference is persistent. (This is not whether the + * @return True if this 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 #persistString(String) @@ -1388,12 +1401,12 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Attempts to get a persisted boolean from the SharedPreferences. + * Attempts to get a persisted boolean from the {@link android.content.SharedPreferences}. * - * @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 shared preferences or the default return + * @param defaultReturnValue The default value to return if either this + * Preference is not persistent or this Preference is not in the + * SharedPreferences. + * @return The value from the SharedPreferences or the default return * value. * @see #getPersistedString(String) * @see #persistBoolean(boolean) @@ -1416,7 +1429,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Returns the text that will be used to filter this preference depending on + * Returns the text that will be used to filter this Preference depending on * user input. * <p> * If overridding and calling through to the superclass, make sure to prepend @@ -1442,9 +1455,9 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Store this preference hierarchy's frozen state into the given container. + * Store this Preference hierarchy's frozen state into the given container. * - * @param container The Bundle in which to save the preference's icicles. + * @param container The Bundle in which to save the instance of this Preference. * * @see #restoreHierarchyState * @see #dispatchSaveInstanceState @@ -1455,11 +1468,11 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Called by {@link #saveHierarchyState} to store the icicles for this preference and its children. - * May be overridden to modify how freezing happens to a preference's children; for example, some - * preferences may want to not store icicles for their children. + * Called by {@link #saveHierarchyState} to store the instance for this Preference and its children. + * May be overridden to modify how the save happens for children. For example, some + * Preference objects may want to not store an instance for their children. * - * @param container The Bundle in which to save the preference's icicles. + * @param container The Bundle in which to save the instance of this Preference. * * @see #dispatchRestoreInstanceState * @see #saveHierarchyState @@ -1480,13 +1493,13 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Hook allowing a preference to generate a representation of its internal + * Hook allowing a Preference to generate a representation of its internal * state that can later be used to create a new instance with that same * state. This state should only contain information that is not persistent * or can be reconstructed later. * - * @return Returns a Parcelable object containing the preference's current - * dynamic state, or null if there is nothing interesting to save. + * @return A Parcelable object containing the current dynamic state of + * this Preference, or null if there is nothing interesting to save. * The default implementation returns null. * @see #onRestoreInstanceState * @see #saveHierarchyState @@ -1498,9 +1511,9 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Restore this preference hierarchy's frozen state from the given container. + * Restore this Preference hierarchy's previously saved state from the given container. * - * @param container The Bundle which holds previously frozen icicles. + * @param container The Bundle that holds the previously saved state. * * @see #saveHierarchyState * @see #dispatchRestoreInstanceState @@ -1511,12 +1524,12 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Called by {@link #restoreHierarchyState} to retrieve the icicles for this - * preference and its children. May be overridden to modify how restoreing - * happens to a preference's children; for example, some preferences may - * want to not store icicles for their children. + * Called by {@link #restoreHierarchyState} to retrieve the saved state for this + * Preference and its children. May be overridden to modify how restoring + * happens to the children of a Preference. For example, some Preference objects may + * not want to save state for their children. * - * @param container The Bundle which holds previously frozen icicles. + * @param container The Bundle that holds the previously saved state. * @see #dispatchSaveInstanceState * @see #restoreHierarchyState * @see #onRestoreInstanceState @@ -1536,11 +1549,11 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } /** - * Hook allowing a preference to re-apply a representation of its internal + * Hook allowing a Preference to re-apply a representation of its internal * state that had previously been generated by {@link #onSaveInstanceState}. - * This function will never be called with a null icicle. + * This function will never be called with a null state. * - * @param state The frozen state that had previously been returned by + * @param state The saved state that had previously been returned by * {@link #onSaveInstanceState}. * @see #onSaveInstanceState * @see #restoreHierarchyState @@ -1553,6 +1566,9 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } } + /** + * A base class for managing the instance state of a {@link Preference}. + */ public static class BaseSavedState extends AbsSavedState { public BaseSavedState(Parcel source) { super(source); diff --git a/core/java/android/preference/PreferenceActivity.java b/core/java/android/preference/PreferenceActivity.java index 98144ca..95970ea 100644 --- a/core/java/android/preference/PreferenceActivity.java +++ b/core/java/android/preference/PreferenceActivity.java @@ -27,7 +27,7 @@ import android.view.View; import android.view.Window; /** - * The {@link PreferenceActivity} activity shows a hierarchy of preferences as + * Shows a hierarchy of {@link Preference} objects as * lists, possibly spanning multiple screens. These preferences will * automatically save to {@link SharedPreferences} as the user interacts with * them. To retrieve an instance of {@link SharedPreferences} that the @@ -108,7 +108,7 @@ public abstract class PreferenceActivity extends ListActivity implements setContentView(com.android.internal.R.layout.preference_list_content); mPreferenceManager = onCreatePreferenceManager(); - getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET); + getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); } @Override diff --git a/core/java/android/preference/PreferenceCategory.java b/core/java/android/preference/PreferenceCategory.java index a1b6f09..237c5ce 100644 --- a/core/java/android/preference/PreferenceCategory.java +++ b/core/java/android/preference/PreferenceCategory.java @@ -22,7 +22,7 @@ import android.content.Context; import android.util.AttributeSet; /** - * The {@link PreferenceCategory} class is used to group {@link Preference}s + * Used to group {@link Preference} objects * and provide a disabled title above the group. */ public class PreferenceCategory extends PreferenceGroup { diff --git a/core/java/android/preference/PreferenceGroup.java b/core/java/android/preference/PreferenceGroup.java index 55b3753..4258b41 100644 --- a/core/java/android/preference/PreferenceGroup.java +++ b/core/java/android/preference/PreferenceGroup.java @@ -28,8 +28,8 @@ import android.os.Parcelable; import android.util.AttributeSet; /** - * The {@link PreferenceGroup} class is a container for multiple - * {@link Preference}s. It is a base class for {@link Preference} that are + * A container for multiple + * {@link Preference} objects. It is a base class for Preference objects that are * parents, such as {@link PreferenceCategory} and {@link PreferenceScreen}. * * @attr ref android.R.styleable#PreferenceGroup_orderingFromXml diff --git a/core/java/android/preference/PreferenceManager.java b/core/java/android/preference/PreferenceManager.java index 9963544..a7a3eef 100644 --- a/core/java/android/preference/PreferenceManager.java +++ b/core/java/android/preference/PreferenceManager.java @@ -35,7 +35,7 @@ import android.os.Bundle; import android.util.Log; /** - * The {@link PreferenceManager} is used to help create preference hierarchies + * Used to help create {@link Preference} hierarchies * from activities or XML. * <p> * In most cases, clients should use @@ -643,17 +643,23 @@ public class PreferenceManager { * event. */ void dispatchActivityDestroy() { - List<OnActivityDestroyListener> list; + List<OnActivityDestroyListener> list = null; synchronized (this) { - if (mActivityDestroyListeners == null) return; - list = new ArrayList<OnActivityDestroyListener>(mActivityDestroyListeners); + if (mActivityDestroyListeners != null) { + list = new ArrayList<OnActivityDestroyListener>(mActivityDestroyListeners); + } } - final int N = list.size(); - for (int i = 0; i < N; i++) { - list.get(i).onActivityDestroy(); + if (list != null) { + final int N = list.size(); + for (int i = 0; i < N; i++) { + list.get(i).onActivityDestroy(); + } } + + // Dismiss any PreferenceScreens still showing + dismissAllScreens(); } /** @@ -697,10 +703,13 @@ public class PreferenceManager { * @param intent The new Intent. */ void dispatchNewIntent(Intent intent) { + dismissAllScreens(); + } + private void dismissAllScreens() { // Remove any of the previously shown preferences screens ArrayList<DialogInterface> screensToDismiss; - + synchronized (this) { if (mPreferencesScreens == null) { @@ -715,7 +724,7 @@ public class PreferenceManager { screensToDismiss.get(i).dismiss(); } } - + /** * Sets the callback to be invoked when a {@link Preference} in the * hierarchy rooted at this {@link PreferenceManager} is clicked. diff --git a/core/java/android/preference/PreferenceScreen.java b/core/java/android/preference/PreferenceScreen.java index e4ecb88..9929b96 100644 --- a/core/java/android/preference/PreferenceScreen.java +++ b/core/java/android/preference/PreferenceScreen.java @@ -30,11 +30,11 @@ import android.widget.ListAdapter; import android.widget.ListView; /** - * The {@link PreferenceScreen} class represents a top-level {@link Preference} that - * is the root of a {@link Preference} hierarchy. A {@link PreferenceActivity} + * Represents a top-level {@link Preference} that + * is the root of a Preference hierarchy. A {@link PreferenceActivity} * points to an instance of this class to show the preferences. To instantiate * this class, use {@link PreferenceManager#createPreferenceScreen(Context)}. - * <p> + * <ul> * This class can appear in two places: * <li> When a {@link PreferenceActivity} points to this, it is used as the root * and is not shown (only the contained preferences are shown). @@ -45,24 +45,25 @@ import android.widget.ListView; * {@link Preference#getIntent()}). The children of this {@link PreferenceScreen} * are NOT shown in the screen that this {@link PreferenceScreen} is shown in. * Instead, a separate screen will be shown when this preference is clicked. + * </ul> + * <p>Here's an example XML layout of a PreferenceScreen:</p> + * <pre> +<PreferenceScreen + xmlns:android="http://schemas.android.com/apk/res/android" + android:key="first_preferencescreen"> + <CheckBoxPreference + android:key="wifi enabled" + android:title="WiFi" /> + <PreferenceScreen + android:key="second_preferencescreen" + android:title="WiFi settings"> + <CheckBoxPreference + android:key="prefer wifi" + android:title="Prefer WiFi" /> + ... other preferences here ... + </PreferenceScreen> +</PreferenceScreen> </pre> * <p> - * <code> - <PreferenceScreen - xmlns:android="http://schemas.android.com/apk/res/android" - android:key="first_preferencescreen"> - <CheckBoxPreference - android:key="wifi enabled" - android:title="WiFi" /> - <PreferenceScreen - android:key="second_preferencescreen" - android:title="WiFi settings"> - <CheckBoxPreference - android:key="prefer wifi" - android:title="Prefer WiFi" /> - ... other preferences here ... - </PreferenceScreen> - </PreferenceScreen> - * </code> * In this example, the "first_preferencescreen" will be used as the root of the * hierarchy and given to a {@link PreferenceActivity}. The first screen will * show preferences "WiFi" (which can be used to quickly enable/disable WiFi) diff --git a/core/java/android/preference/RingtonePreference.java b/core/java/android/preference/RingtonePreference.java index 97674ce..6beb06d 100644 --- a/core/java/android/preference/RingtonePreference.java +++ b/core/java/android/preference/RingtonePreference.java @@ -27,8 +27,8 @@ import android.util.AttributeSet; import android.util.Log; /** - * The {@link RingtonePreference} allows the user to choose one from all of the - * available ringtones. The chosen ringtone's URI will be persisted as a string. + * A {@link Preference} that allows the user to choose a ringtone from those on the device. + * The chosen ringtone's URI will be persisted as a string. * <p> * If the user chooses the "Default" item, the saved string will be one of * {@link System#DEFAULT_RINGTONE_URI} or diff --git a/core/java/android/preference/VolumePreference.java b/core/java/android/preference/VolumePreference.java index 5a0a089..6e215dc 100644 --- a/core/java/android/preference/VolumePreference.java +++ b/core/java/android/preference/VolumePreference.java @@ -16,7 +16,6 @@ package android.preference; -import android.content.ContentResolver; import android.content.Context; import android.content.res.TypedArray; import android.database.ContentObserver; @@ -35,33 +34,15 @@ import android.widget.SeekBar.OnSeekBarChangeListener; /** * @hide */ -public class VolumePreference extends SeekBarPreference implements OnSeekBarChangeListener, - Runnable, PreferenceManager.OnActivityStopListener { +public class VolumePreference extends SeekBarPreference implements + PreferenceManager.OnActivityStopListener { private static final String TAG = "VolumePreference"; - private ContentResolver mContentResolver; - private Handler mHandler = new Handler(); - - private AudioManager mVolume; private int mStreamType; - private int mOriginalStreamVolume; - private Ringtone mRingtone; - - private int mLastProgress; - private SeekBar mSeekBar; - private ContentObserver mVolumeObserver = new ContentObserver(mHandler) { - @Override - public void onChange(boolean selfChange) { - super.onChange(selfChange); - - if (mSeekBar != null) { - mSeekBar.setProgress(System.getInt(mContentResolver, - System.VOLUME_SETTINGS[mStreamType], 0)); - } - } - }; + /** May be null if the dialog isn't visible. */ + private SeekBarVolumizer mSeekBarVolumizer; public VolumePreference(Context context, AttributeSet attrs) { super(context, attrs); @@ -70,39 +51,32 @@ public class VolumePreference extends SeekBarPreference implements OnSeekBarChan com.android.internal.R.styleable.VolumePreference, 0, 0); mStreamType = a.getInt(android.R.styleable.VolumePreference_streamType, 0); a.recycle(); - - mContentResolver = context.getContentResolver(); - - mVolume = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); } + public void setStreamType(int streamType) { + mStreamType = streamType; + } + @Override protected void onBindDialogView(View view) { super.onBindDialogView(view); - final SeekBar seekBar = mSeekBar = (SeekBar) view.findViewById(com.android.internal.R.id.seekbar); - seekBar.setMax(mVolume.getStreamMaxVolume(mStreamType)); - mOriginalStreamVolume = mVolume.getStreamVolume(mStreamType); - seekBar.setProgress(mOriginalStreamVolume); - seekBar.setOnSeekBarChangeListener(this); + final SeekBar seekBar = (SeekBar) view.findViewById(com.android.internal.R.id.seekbar); + mSeekBarVolumizer = new SeekBarVolumizer(getContext(), seekBar, mStreamType); - mContentResolver.registerContentObserver(System.getUriFor(System.VOLUME_SETTINGS[mStreamType]), false, mVolumeObserver); - getPreferenceManager().registerOnActivityStopListener(this); - mRingtone = RingtoneManager.getRingtone(getContext(), Settings.System.DEFAULT_RINGTONE_URI); } @Override protected void onDialogClosed(boolean positiveResult) { super.onDialogClosed(positiveResult); - if (!positiveResult) { - mVolume.setStreamVolume(mStreamType, mOriginalStreamVolume, 0); + if (!positiveResult && mSeekBarVolumizer != null) { + mSeekBarVolumizer.revertVolume(); } cleanup(); } - public void onActivityStop() { cleanup(); @@ -112,50 +86,134 @@ public class VolumePreference extends SeekBarPreference implements OnSeekBarChan * Do clean up. This can be called multiple times! */ private void cleanup() { - stopSample(); - if (mVolumeObserver != null) { - mContentResolver.unregisterContentObserver(mVolumeObserver); + getPreferenceManager().unregisterOnActivityStopListener(this); + + if (mSeekBarVolumizer != null) { + mSeekBarVolumizer.stop(); + mSeekBarVolumizer = null; + } + } + + protected void onSampleStarting(SeekBarVolumizer volumizer) { + if (mSeekBarVolumizer != null && volumizer != mSeekBarVolumizer) { + mSeekBarVolumizer.stopSample(); } - getPreferenceManager().unregisterOnActivityStopListener(this); - mSeekBar = null; } + + /** + * Turns a {@link SeekBar} into a volume control. + */ + public class SeekBarVolumizer implements OnSeekBarChangeListener, Runnable { - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) { - if (!fromTouch) { - return; + private Context mContext; + private Handler mHandler = new Handler(); + + private AudioManager mAudioManager; + private int mStreamType; + private int mOriginalStreamVolume; + private Ringtone mRingtone; + + private int mLastProgress; + private SeekBar mSeekBar; + + private ContentObserver mVolumeObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + + if (mSeekBar != null) { + mSeekBar.setProgress(System.getInt(mContext.getContentResolver(), + System.VOLUME_SETTINGS[mStreamType], 0)); + } + } + }; + + public SeekBarVolumizer(Context context, SeekBar seekBar, int streamType) { + mContext = context; + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + mStreamType = streamType; + mSeekBar = seekBar; + + initSeekBar(seekBar); } - postSetVolume(progress); - } - - private void postSetVolume(int progress) { - // Do the volume changing separately to give responsive UI - mLastProgress = progress; - mHandler.removeCallbacks(this); - mHandler.post(this); - } + private void initSeekBar(SeekBar seekBar) { + seekBar.setMax(mAudioManager.getStreamMaxVolume(mStreamType)); + mOriginalStreamVolume = mAudioManager.getStreamVolume(mStreamType); + seekBar.setProgress(mOriginalStreamVolume); + seekBar.setOnSeekBarChangeListener(this); + + mContext.getContentResolver().registerContentObserver( + System.getUriFor(System.VOLUME_SETTINGS[mStreamType]), + false, mVolumeObserver); - public void onStartTrackingTouch(SeekBar seekBar) { - } + mRingtone = RingtoneManager.getRingtone(mContext, + mStreamType == AudioManager.STREAM_NOTIFICATION + ? Settings.System.DEFAULT_NOTIFICATION_URI + : Settings.System.DEFAULT_RINGTONE_URI); + mRingtone.setStreamType(mStreamType); + } + + public void stop() { + stopSample(); + mContext.getContentResolver().unregisterContentObserver(mVolumeObserver); + mSeekBar.setOnSeekBarChangeListener(null); + } + + public void revertVolume() { + mAudioManager.setStreamVolume(mStreamType, mOriginalStreamVolume, 0); + } + + public void onProgressChanged(SeekBar seekBar, int progress, + boolean fromTouch) { + if (!fromTouch) { + return; + } + + postSetVolume(progress); + } - public void onStopTrackingTouch(SeekBar seekBar) { - if (mRingtone != null && !mRingtone.isPlaying()) { - sample(); + private void postSetVolume(int progress) { + // Do the volume changing separately to give responsive UI + mLastProgress = progress; + mHandler.removeCallbacks(this); + mHandler.post(this); + } + + public void onStartTrackingTouch(SeekBar seekBar) { } - } - public void run() { - mVolume.setStreamVolume(mStreamType, mLastProgress, 0); - } + public void onStopTrackingTouch(SeekBar seekBar) { + if (mRingtone != null && !mRingtone.isPlaying()) { + sample(); + } + } + + public void run() { + mAudioManager.setStreamVolume(mStreamType, mLastProgress, 0); + } + + private void sample() { - private void sample() { - mRingtone.play(); - } + // Only play a preview sample when controlling the ringer stream + if (mStreamType != AudioManager.STREAM_RING + && mStreamType != AudioManager.STREAM_NOTIFICATION) { + return; + } + + onSampleStarting(this); + mRingtone.play(); + } + + public void stopSample() { + if (mRingtone != null) { + mRingtone.stop(); + } + } - private void stopSample() { - if (mRingtone != null) { - mRingtone.stop(); + public SeekBar getSeekBar() { + return mSeekBar; } + } - } diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java index b07f1b8..b137b34 100644 --- a/core/java/android/provider/Calendar.java +++ b/core/java/android/provider/Calendar.java @@ -38,11 +38,11 @@ import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; -import android.pim.DateUtils; import android.pim.ICalendar; import android.pim.RecurrenceSet; -import android.pim.Time; import android.text.TextUtils; +import android.text.format.DateUtils; +import android.text.format.Time; import android.util.Config; import android.util.Log; @@ -429,18 +429,27 @@ public final class Calendar { public static final String EXDATE = "exdate"; /** - * The original event this event is an exception for + * The _sync_id of the original recurring event for which this event is + * an exception. * <P>Type: TEXT</P> */ public static final String ORIGINAL_EVENT = "originalEvent"; /** - * The time of the original instance time this event is an exception for + * The original instance time of the recurring event for which this + * event is an exception. * <P>Type: INTEGER (long; millis since epoch)</P> */ public static final String ORIGINAL_INSTANCE_TIME = "originalInstanceTime"; /** + * The allDay status (true or false) of the original recurring event + * for which this event is an exception. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String ORIGINAL_ALL_DAY = "originalAllDay"; + + /** * The last date this event repeats on, or NULL if it never ends * <P>Type: INTEGER (long; millis since epoch)</P> */ @@ -543,7 +552,7 @@ public final class Calendar { time.clear(tzidParam.value); } try { - time.parse2445(dtstart); + time.parse(dtstart); } catch (Exception e) { if (Config.LOGD) { Log.d(TAG, "Cannot parse dtstart " + dtstart, e); @@ -564,7 +573,7 @@ public final class Calendar { // TODO: make sure the timezones are the same for // start, end. try { - time.parse2445(dtend); + time.parse(dtend); } catch (Exception e) { if (Config.LOGD) { Log.d(TAG, "Cannot parse dtend " + dtend, e); diff --git a/core/java/android/provider/Checkin.java b/core/java/android/provider/Checkin.java index 5b79482..ef5eded 100644 --- a/core/java/android/provider/Checkin.java +++ b/core/java/android/provider/Checkin.java @@ -96,8 +96,6 @@ public final class Checkin { SYSTEM_SERVICE_LOOPING, SYSTEM_TOMBSTONE, TEST, - NETWORK_RX_MOBILE, - NETWORK_TX_MOBILE, } } diff --git a/core/java/android/provider/Contacts.java b/core/java/android/provider/Contacts.java index 91b1853..6d24ba8 100644 --- a/core/java/android/provider/Contacts.java +++ b/core/java/android/provider/Contacts.java @@ -16,6 +16,8 @@ package android.provider; +import com.android.internal.R; + import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; @@ -29,8 +31,6 @@ import android.text.TextUtils; import android.util.Log; import android.widget.ImageView; -import com.android.internal.R; - import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -1229,7 +1229,7 @@ public class Contacts { try { display = labels[type - 1]; } catch (ArrayIndexOutOfBoundsException e) { - display = labels[People.Phones.TYPE_HOME - 1]; + display = labels[Organizations.TYPE_WORK - 1]; } } else { if (!TextUtils.isEmpty(label)) { @@ -1546,6 +1546,32 @@ public class Contacts { public static final String PHONE_ISPRIMARY = "phone_isprimary"; /** + * The extra field for an optional second contact phone number. + * <P>Type: String</P> + */ + public static final String SECONDARY_PHONE = "secondary_phone"; + + /** + * The extra field for an optional second contact phone number type. + * <P>Type: Either an integer value from {@link android.provider.Contacts.PhonesColumns PhonesColumns}, + * or a string specifying a type and label.</P> + */ + public static final String SECONDARY_PHONE_TYPE = "secondary_phone_type"; + + /** + * The extra field for an optional third contact phone number. + * <P>Type: String</P> + */ + public static final String TERTIARY_PHONE = "tertiary_phone"; + + /** + * The extra field for an optional third contact phone number type. + * <P>Type: Either an integer value from {@link android.provider.Contacts.PhonesColumns PhonesColumns}, + * or a string specifying a type and label.</P> + */ + public static final String TERTIARY_PHONE_TYPE = "tertiary_phone_type"; + + /** * The extra field for the contact email address. * <P>Type: String</P> */ @@ -1565,6 +1591,32 @@ public class Contacts { public static final String EMAIL_ISPRIMARY = "email_isprimary"; /** + * The extra field for an optional second contact email address. + * <P>Type: String</P> + */ + public static final String SECONDARY_EMAIL = "secondary_email"; + + /** + * The extra field for an optional second contact email type. + * <P>Type: Either an integer value from {@link android.provider.Contacts.ContactMethodsColumns ContactMethodsColumns} + * or a string specifying a type and label.</P> + */ + public static final String SECONDARY_EMAIL_TYPE = "secondary_email_type"; + + /** + * The extra field for an optional third contact email address. + * <P>Type: String</P> + */ + public static final String TERTIARY_EMAIL = "tertiary_email"; + + /** + * The extra field for an optional third contact email type. + * <P>Type: Either an integer value from {@link android.provider.Contacts.ContactMethodsColumns ContactMethodsColumns} + * or a string specifying a type and label.</P> + */ + public static final String TERTIARY_EMAIL_TYPE = "tertiary_email_type"; + + /** * The extra field for the contact postal address. * <P>Type: String</P> */ diff --git a/core/java/android/provider/Downloads.java b/core/java/android/provider/Downloads.java index 42e9d95..a5a30b9 100644 --- a/core/java/android/provider/Downloads.java +++ b/core/java/android/provider/Downloads.java @@ -29,6 +29,29 @@ import android.net.Uri; // this API is hidden. public final class Downloads implements BaseColumns { private Downloads() {} + + /** + * The permission to access the download manager + */ + public static final String PERMISSION_ACCESS = "android.permission.ACCESS_DOWNLOAD_MANAGER"; + + /** + * The permission to access the download manager's advanced functions + */ + public static final String PERMISSION_ACCESS_ADVANCED = + "android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED"; + + /** + * The permission to directly access the download manager's cache directory + */ + public static final String PERMISSION_CACHE = "android.permission.ACCESS_CACHE_FILESYSTEM"; + + /** + * The permission to send broadcasts on download completion + */ + public static final String PERMISSION_SEND_INTENTS = + "android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS"; + /** * The content:// URI for the data table in the provider */ @@ -64,21 +87,11 @@ public final class Downloads implements BaseColumns { public static final String URI = "uri"; /** - * The name of the column containing the HTTP method to use for this - * download. See the METHOD_* constants for a list of legal values. - * <P>Type: INTEGER</P> - * <P>Owner can Init/Read</P> - */ - public static final String METHOD = "method"; - - /** - * The name of the column containing the entity to be sent with the - * request of this download. Only use for methods that support sending - * entities, i.e. POST. + * The name of the column containing application-specific data. * <P>Type: TEXT</P> - * <P>Owner can Init</P> + * <P>Owner can Init/Read/Write</P> */ - public static final String ENTITY = "entity"; + public static final String APP_DATA = "entity"; /** * The name of the column containing the flags that indicates whether @@ -89,7 +102,7 @@ public final class Downloads implements BaseColumns { * a byte-range request without an ETag, or when it can't determine * whether a download fully completed). * <P>Type: BOOLEAN</P> - * <P>Owner can Init/Read</P> + * <P>Owner can Init</P> */ public static final String NO_INTEGRITY = "no_integrity"; @@ -98,7 +111,7 @@ public final class Downloads implements BaseColumns { * application recommends. When possible, the download manager will attempt * to use this filename, or a variation, as the actual name for the file. * <P>Type: TEXT</P> - * <P>Owner can Init/Read</P> + * <P>Owner can Init</P> */ public static final String FILENAME_HINT = "hint"; @@ -107,15 +120,13 @@ public final class Downloads implements BaseColumns { * was actually stored. * <P>Type: TEXT</P> * <P>Owner can Read</P> - * <P>UI can Read</P> */ - public static final String FILENAME = "_data"; + public static final String _DATA = "_data"; /** * The name of the column containing the MIME type of the downloaded data. * <P>Type: TEXT</P> * <P>Owner can Init/Read</P> - * <P>UI can Read</P> */ public static final String MIMETYPE = "mimetype"; @@ -123,51 +134,25 @@ public final class Downloads implements BaseColumns { * The name of the column containing the flag that controls the destination * of the download. See the DESTINATION_* constants for a list of legal values. * <P>Type: INTEGER</P> - * <P>Owner can Init/Read</P> - * <P>UI can Read</P> + * <P>Owner can Init</P> */ public static final String DESTINATION = "destination"; /** - * The name of the column containing the flags that controls whether - * the download must be saved with the filename used for OTA updates. - * Must be used with INTERNAL, and the initiating application must hold the - * android.permission.DOWNLOAD_OTA_UPDATE permission. - * <P>Type: BOOLEAN</P> - * <P>Owner can Init/Read</P> - * <P>UI can Read</P> - */ - public static final String OTA_UPDATE = "otaupdate"; - - /** - * The name of the columns containing the flag that controls whether - * files with private/inernal/system MIME types can be downloaded. - * <P>Type: BOOLEAN</P> - * <P>Owner can Init/Read</P> - */ - public static final String NO_SYSTEM_FILES = "no_system"; - - /** * The name of the column containing the flags that controls whether the * download is displayed by the UI. See the VISIBILITY_* constants for * a list of legal values. * <P>Type: INTEGER</P> * <P>Owner can Init/Read/Write</P> - * <P>UI can Read/Write (only for entries that are visible)</P> */ public static final String VISIBILITY = "visibility"; /** - * The name of the column containing the command associated with the - * download. After a download is initiated, this is the only column that - * applications can modify. See the CONTROL_* constants for a list of legal - * values. Note: doesn't do anything in 1.0. The API will be hooked up - * in a future version, and is provided here as an indication of things - * to come. + * The name of the column containing the current control state of the download. + * Applications can write to this to control (pause/resume) the download. + * the CONTROL_* constants for a list of legal values. * <P>Type: INTEGER</P> - * <P>Owner can Init/Read/Write</P> - * <P>UI can Init/Read/Write</P> - * @hide + * <P>Owner can Read</P> */ public static final String CONTROL = "control"; @@ -177,7 +162,6 @@ public final class Downloads implements BaseColumns { * the STATUS_* constants for a list of legal values. * <P>Type: INTEGER</P> * <P>Owner can Read</P> - * <P>UI can Read</P> */ public static final String STATUS = "status"; @@ -187,24 +171,15 @@ public final class Downloads implements BaseColumns { * value. * <P>Type: BIGINT</P> * <P>Owner can Read</P> - * <P>UI can Read</P> */ public static final String LAST_MODIFICATION = "lastmod"; /** - * The name of the column containing the number of consecutive connections - * that have failed. - * <P>Type: INTEGER</P> - */ - public static final String FAILED_CONNECTIONS = "numfailed"; - - /** * The name of the column containing the package name of the application * that initiating the download. The download manager will send * notifications to a component in this package when the download completes. * <P>Type: TEXT</P> * <P>Owner can Init/Read</P> - * <P>UI can Read</P> */ public static final String NOTIFICATION_PACKAGE = "notificationpackage"; @@ -215,13 +190,14 @@ public final class Downloads implements BaseColumns { * Intent.setClassName(String,String). * <P>Type: TEXT</P> * <P>Owner can Init/Read</P> - * <P>UI can Read</P> */ public static final String NOTIFICATION_CLASS = "notificationclass"; /** * If extras are specified when requesting a download they will be provided in the intent that * is sent to the specified class and package when a download has finished. + * <P>Type: TEXT</P> + * <P>Owner can Init</P> */ public static final String NOTIFICATION_EXTRAS = "notificationextras"; @@ -255,7 +231,6 @@ public final class Downloads implements BaseColumns { * downloaded. * <P>Type: INTEGER</P> * <P>Owner can Read</P> - * <P>UI can Read</P> */ public static final String TOTAL_BYTES = "total_bytes"; @@ -264,33 +239,18 @@ public final class Downloads implements BaseColumns { * has been downloaded so far. * <P>Type: INTEGER</P> * <P>Owner can Read</P> - * <P>UI can Read</P> */ public static final String CURRENT_BYTES = "current_bytes"; /** - * The name of the column containing the entity tag for the response. - * <P>Type: TEXT</P> - * @hide - */ - public static final String ETAG = "etag"; - - /** - * The name of the column containing the UID of the application that - * initiated the download. - * <P>Type: INTEGER</P> - * @hide - */ - public static final String UID = "uid"; - - /** * The name of the column where the initiating application can provide the * UID of another application that is allowed to access this download. If * multiple applications share the same UID, all those applications will be * allowed to access this download. This column can be updated after the - * download is initiated. + * download is initiated. This requires the permission + * android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED. * <P>Type: INTEGER</P> - * <P>Owner can Init/Read/Write</P> + * <P>Owner can Init</P> */ public static final String OTHER_UID = "otheruid"; @@ -300,7 +260,6 @@ public final class Downloads implements BaseColumns { * list of downloads. * <P>Type: TEXT</P> * <P>Owner can Init/Read/Write</P> - * <P>UI can Read</P> */ public static final String TITLE = "title"; @@ -310,18 +269,9 @@ public final class Downloads implements BaseColumns { * user in the list of downloads. * <P>Type: TEXT</P> * <P>Owner can Init/Read/Write</P> - * <P>UI can Read</P> */ public static final String DESCRIPTION = "description"; - /** - * The name of the column where the download manager indicates whether the - * media scanner was notified about this download. - * <P>Type: BOOLEAN</P> - * @hide - */ - public static final String MEDIA_SCANNED = "scanned"; - /* * Lists the destinations that an application can specify for a download. */ @@ -343,7 +293,8 @@ public final class Downloads implements BaseColumns { * download private files that are used and deleted soon after they * get downloaded. All file types are allowed, and only the initiating * application can access the file (indirectly through a content - * provider). + * provider). This requires the + * android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED permission. */ public static final int DESTINATION_CACHE_PARTITION = 1; @@ -357,40 +308,21 @@ public final class Downloads implements BaseColumns { public static final int DESTINATION_CACHE_PARTITION_PURGEABLE = 2; /** - * This download will be saved to the download manager's cache - * on the shared data partition. Use CACHE_PARTITION_PURGEABLE instead. - */ - public static final int DESTINATION_DATA_CACHE = 3; - - /* (not javadoc) - * This download will be saved to a file specified by the initiating - * applications. - * @hide - */ - //public static final int DESTINATION_PROVIDER = 4; - - /* - * Lists the commands that an application can set to control an ongoing - * download. Note: those aren't working. + * This download will be saved to the download manager's private + * partition, as with DESTINATION_CACHE_PARTITION, but the download + * will not proceed if the user is on a roaming data connection. */ + public static final int DESTINATION_CACHE_PARTITION_NOROAMING = 3; /** - * This download can run - * @hide + * This download is allowed to run. */ public static final int CONTROL_RUN = 0; /** - * This download must pause (might be restarted) - * @hide - */ - public static final int CONTROL_PAUSE = 1; - - /** - * This download must abort (will never be restarted) - * @hide + * This download must pause at the first opportunity. */ - public static final int CONTROL_STOP = 2; + public static final int CONTROL_PAUSED = 1; /* * Lists the states that the download manager can set on a download @@ -490,11 +422,6 @@ public final class Downloads implements BaseColumns { public static final int STATUS_BAD_REQUEST = 400; /** - * The server returned an auth error. - */ - public static final int STATUS_NOT_AUTHORIZED = 401; - - /** * This download can't be performed because the content type cannot be * handled. */ @@ -522,11 +449,6 @@ public final class Downloads implements BaseColumns { * This download was canceled */ public static final int STATUS_CANCELED = 490; - /** - * @hide - * Alternate spelling - */ - public static final int STATUS_CANCELLED = 490; /** * This download has completed with an error. @@ -534,11 +456,6 @@ public final class Downloads implements BaseColumns { * the future. Use isStatusError() to capture the entire category. */ public static final int STATUS_UNKNOWN_ERROR = 491; - /** - * @hide - * Legacy name - use STATUS_UNKNOWN_ERROR - */ - public static final int STATUS_ERROR = 491; /** * This download couldn't be completed because of a storage issue. @@ -548,54 +465,40 @@ public final class Downloads implements BaseColumns { /** * This download couldn't be completed because of an HTTP - * redirect code. + * redirect response that the download manager couldn't + * handle. */ public static final int STATUS_UNHANDLED_REDIRECT = 493; /** - * This download couldn't be completed because of an - * unspecified unhandled HTTP code. + * This download couldn't be completed because there were + * too many redirects. */ - public static final int STATUS_UNHANDLED_HTTP_CODE = 494; + public static final int STATUS_TOO_MANY_REDIRECTS = 494; /** * This download couldn't be completed because of an - * error receiving or processing data at the HTTP level. + * unspecified unhandled HTTP code. */ - public static final int STATUS_HTTP_DATA_ERROR = 495; + public static final int STATUS_UNHANDLED_HTTP_CODE = 495; /** * This download couldn't be completed because of an - * HttpException while setting up the request. - */ - public static final int STATUS_HTTP_EXCEPTION = 496; - - /* - * Lists the HTTP methods that the download manager can use. - */ - - /** - * GET + * error receiving or processing data at the HTTP level. */ - public static final int METHOD_GET = 0; + public static final int STATUS_HTTP_DATA_ERROR = 496; /** - * POST + * This download is visible and shows in the notifications while + * in progress and after completion. */ - public static final int METHOD_POST = 1; + public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 0; /** * This download is visible but only shows in the notifications - * while it's running (a separate download UI would still show it - * after completion). - */ - public static final int VISIBILITY_VISIBLE = 0; - - /** - * This download is visible and shows in the notifications after - * completion. + * while it's in progress. */ - public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1; + public static final int VISIBILITY_VISIBLE = 1; /** * This download doesn't show in the UI or in the notifications. diff --git a/core/java/android/provider/Gmail.java b/core/java/android/provider/Gmail.java index 038ba21..0b6a758 100644 --- a/core/java/android/provider/Gmail.java +++ b/core/java/android/provider/Gmail.java @@ -72,6 +72,7 @@ public final class Gmail { public static final String LABEL_STARRED = "^t"; public static final String LABEL_CHAT = "^b"; // 'b' for 'buzz' public static final String LABEL_VOICEMAIL = "^vm"; + public static final String LABEL_IGNORED = "^g"; public static final String LABEL_ALL = "^all"; // These constants (starting with "^^") are only used locally and are not understood by the // server. @@ -142,6 +143,8 @@ public final class Gmail { public static final String RESPOND_INPUT_COMMAND = "command"; public static final String COMMAND_RETRY = "retry"; public static final String COMMAND_ACTIVATE = "activate"; + public static final String COMMAND_SET_VISIBLE = "setVisible"; + public static final String SET_VISIBLE_PARAM_VISIBLE = "visible"; public static final String RESPOND_OUTPUT_COMMAND_RESPONSE = "commandResponse"; public static final String COMMAND_RESPONSE_OK = "ok"; public static final String COMMAND_RESPONSE_UNKNOWN = "unknownCommand"; @@ -210,7 +213,8 @@ public final class Gmail { Gmail.LABEL_UNREAD, Gmail.LABEL_TRASH, Gmail.LABEL_SPAM, - Gmail.LABEL_STARRED); + Gmail.LABEL_STARRED, + Gmail.LABEL_IGNORED); /** * Returns whether the label is user-settable. For example, labels such as LABEL_DRAFT should @@ -337,6 +341,8 @@ public final class Gmail { public static final String LABEL_IDS = "labelIds"; public static final String JOINED_ATTACHMENT_INFOS = "joinedAttachmentInfos"; public static final String ERROR = "error"; + // TODO: add a method for accessing this + public static final String REF_MESSAGE_ID = "refMessageId"; // Fake columns used only for saving or sending messages. public static final String FAKE_SAVE = "save"; @@ -773,7 +779,8 @@ public final class Gmail { priorityToLength.clear(); int maxFoundPriority = Integer.MIN_VALUE; - String numMessagesFragment = ""; + int numMessages = 0; + int numDrafts = 0; CharSequence draftsFragment = ""; CharSequence sendingFragment = ""; CharSequence sendFailedFragment = ""; @@ -799,10 +806,10 @@ public final class Gmail { } else if (Gmail.SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) { // ignore } else if (Gmail.SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) { - numMessagesFragment = " (" + fragments[i++] + ")"; + numMessages = Integer.valueOf(fragments[i++]); } else if (Gmail.SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) { String numDraftsString = fragments[i++]; - int numDrafts = Integer.parseInt(numDraftsString); + numDrafts = Integer.parseInt(numDraftsString); draftsFragment = numDrafts == 1 ? draftString : draftPluralString + " (" + numDraftsString + ")"; } else if (Gmail.SENDER_LIST_TOKEN_LITERAL.equals(fragment0)) { @@ -821,6 +828,8 @@ public final class Gmail { maxFoundPriority = Math.max(maxFoundPriority, priority); } } + String numMessagesFragment = + (numMessages != 0) ? " (" + Integer.toString(numMessages + numDrafts) + ")" : ""; // Don't allocate fixedFragment unless we need it SpannableStringBuilder fixedFragment = null; @@ -1242,6 +1251,7 @@ public final class Gmail { private long mLabelIdStarred; private long mLabelIdChat; private long mLabelIdVoicemail; + private long mLabelIdIgnored; private long mLabelIdVoicemailInbox; private long mLabelIdCached; private long mLabelIdOutbox; @@ -1313,6 +1323,8 @@ public final class Gmail { mLabelIdStarred = labelId; } else if (LABEL_CHAT.equals(canonicalName)) { mLabelIdChat = labelId; + } else if (LABEL_IGNORED.equals(canonicalName)) { + mLabelIdIgnored = labelId; } else if (LABEL_VOICEMAIL.equals(canonicalName)) { mLabelIdVoicemail = labelId; } else if (LABEL_VOICEMAIL_INBOX.equals(canonicalName)) { @@ -1330,6 +1342,7 @@ public final class Gmail { && mLabelIdSpam != 0 && mLabelIdStarred != 0 && mLabelIdChat != 0 + && mLabelIdIgnored != 0 && mLabelIdVoicemail != 0; } } @@ -1374,6 +1387,11 @@ public final class Gmail { return mLabelIdChat; } + public long getLabelIdIgnored() { + checkLabelsSynced(); + return mLabelIdIgnored; + } + public long getLabelIdVoicemail() { checkLabelsSynced(); return mLabelIdVoicemail; @@ -2237,9 +2255,28 @@ public final class Gmail { } /** - * Gets the conversation id. This must be immutable. (For example, with - * GMail this should be the original conversation id rather than the - * default notion of converation id.) + * Tells the cursor whether its contents are visible to the user. The cursor will + * automatically broadcast intents to remove any matching new-mail notifications when this + * cursor's results become visible and, if they are visible, when the cursor is requeried. + * + * Note that contents shown in an activity that is resumed but not focused + * (onWindowFocusChanged/hasWindowFocus) then results shown in that activity do not count + * as visible. (This happens when the activity is behind the lock screen or a dialog.) + * + * @param visible whether the contents of this cursor are visible to the user. + */ + public void setContentsVisibleToUser(boolean visible) { + Bundle input = new Bundle(); + input.putString(RESPOND_INPUT_COMMAND, COMMAND_SET_VISIBLE); + input.putBoolean(SET_VISIBLE_PARAM_VISIBLE, visible); + Bundle output = mCursor.respond(input); + String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE); + assert COMMAND_RESPONSE_OK.equals(response); + } + + /** + * Gets the conversation id. This is immutable. (The server calls it the original + * conversation id.) * * @return the conversation id */ diff --git a/core/java/android/provider/Im.java b/core/java/android/provider/Im.java index 8ca97e1..68b2acd 100644 --- a/core/java/android/provider/Im.java +++ b/core/java/android/provider/Im.java @@ -43,16 +43,25 @@ public class Im { public interface ProviderColumns { /** * The name of the IM provider + * <P>Type: TEXT</P> */ String NAME = "name"; /** * The full name of the provider + * <P>Type: TEXT</P> */ String FULLNAME = "fullname"; /** + * The category for the provider, used to form intent. + * <P>Type: TEXT</P> + */ + String CATEGORY = "category"; + + /** * The url users should visit to create a new account for this provider + * <P>Type: TEXT</P> */ String SIGNUP_URL = "signup_url"; } @@ -187,6 +196,9 @@ public class Im { public static final String ACTIVE_ACCOUNT_ID = "account_id"; public static final String ACTIVE_ACCOUNT_USERNAME = "account_username"; public static final String ACTIVE_ACCOUNT_PW = "account_pw"; + public static final String ACTIVE_ACCOUNT_LOCKED = "account_locked"; + public static final String ACCOUNT_PRESENCE_STATUS = "account_presenceStatus"; + public static final String ACCOUNT_CONNECTION_STATUS = "account_connStatus"; /** * The content:// style URL for this table @@ -204,6 +216,9 @@ public class Im { public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-providers"; + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/im-providers"; + /** * The default sort order for this table */ @@ -320,6 +335,76 @@ public class Im { } /** + * Connection status + */ + public interface ConnectionStatus { + /** + * The connection is offline, not logged in. + */ + int OFFLINE = 0; + + /** + * The connection is attempting to connect. + */ + int CONNECTING = 1; + + /** + * The connection is suspended due to network not available. + */ + int SUSPENDED = 2; + + /** + * The connection is logged in and online. + */ + int ONLINE = 3; + } + + public interface AccountStatusColumns { + /** + * account id + * <P>Type: INTEGER</P> + */ + String ACCOUNT = "account"; + + /** + * User's presence status, see definitions in {#link CommonPresenceColumn} + * <P>Type: INTEGER</P> + */ + String PRESENCE_STATUS = "presenceStatus"; + + /** + * The connection status of this account, see {#link ConnectionStatus} + * <P>Type: INTEGER</P> + */ + String CONNECTION_STATUS = "connStatus"; + } + + public static final class AccountStatus implements BaseColumns, AccountStatusColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://im/accountStatus"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of account status. + */ + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/im-account-status"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single account status. + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/im-account-status"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "name ASC"; + } + + /** * Columns from the Contacts table. */ public interface ContactsColumns { @@ -451,9 +536,37 @@ public class Im { * <P>Type: INTEGER</P> */ String REJECTED = "rejected"; + + /** + * Off The Record status: 0 for disabled, 1 for enabled + * <P>Type: INTEGER </P> + */ + String OTR = "otr"; } /** + * This defines the different type of values of {@link ContactsColumns#OTR} + */ + public interface OffTheRecordType { + /* + * Off the record not turned on + */ + int DISABLED = 0; + /** + * Off the record turned on, but we don't know who turned it on + */ + int ENABLED = 1; + /** + * Off the record turned on by the user + */ + int ENABLED_BY_USER = 2; + /** + * Off the record turned on by the buddy + */ + int ENABLED_BY_BUDDY = 3; + }; + + /** * This table contains contacts. */ public static final class Contacts implements BaseColumns, @@ -755,15 +868,32 @@ public class Im { * Message type definition */ public interface MessageType { + /* sent message */ int OUTGOING = 0; + /* received message */ int INCOMING = 1; + /* presence became available */ int PRESENCE_AVAILABLE = 2; + /* presence became away */ int PRESENCE_AWAY = 3; + /* presence became DND (busy) */ int PRESENCE_DND = 4; + /* presence became unavailable */ int PRESENCE_UNAVAILABLE = 5; + /* the message is converted to a group chat */ int CONVERT_TO_GROUPCHAT = 6; + /* generic status */ int STATUS = 7; + /* the message cannot be sent now, but will be sent later */ int POSTPONED = 8; + /* off The Record status is turned off */ + int OTR_IS_TURNED_OFF = 9; + /* off the record status is turned on */ + int OTR_IS_TURNED_ON = 10; + /* off the record status turned on by user */ + int OTR_TURNED_ON_BY_USER = 11; + /* off the record status turned on by buddy */ + int OTR_TURNED_ON_BY_BUDDY = 12; } /** @@ -1244,26 +1374,25 @@ public class Im { public interface ChatsColumns { /** * The contact ID this chat belongs to. The value is a long. - * <P>Type: TEXT</P> + * <P>Type: INT</P> */ String CONTACT_ID = "contact_id"; /** * The GTalk JID resource. The value is a string. + * <P>Type: TEXT</P> */ String JID_RESOURCE = "jid_resource"; /** * Whether this is a groupchat or not. + * <P>Type: INT</P> */ - // TODO: remove this column since we already have a tag in contacts - // table to indicate it's a group chat. String GROUP_CHAT = "groupchat"; /** * The last unread message. This both indicates that there is an * unread message, and what the message is. - * * <P>Type: TEXT</P> */ String LAST_UNREAD_MESSAGE = "last_unread_message"; @@ -1278,10 +1407,17 @@ public class Im { * A message that is being composed. This indicates that there was a * message being composed when the chat screen was shutdown, and what the * message is. - * * <P>Type: TEXT</P> */ String UNSENT_COMPOSED_MESSAGE = "unsent_composed_message"; + + /** + * A value from 0-9 indicating which quick-switch chat screen slot this + * chat is occupying. If none (for instance, this is the 12th active chat) + * then the value is -1. + * <P>Type: INT</P> + */ + String SHORTCUT = "shortcut"; } /** diff --git a/core/java/android/provider/LiveFolders.java b/core/java/android/provider/LiveFolders.java new file mode 100644 index 0000000..6e95fb7 --- /dev/null +++ b/core/java/android/provider/LiveFolders.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.provider; + +import android.annotation.SdkConstant; + +/** + * <p>A LiveFolder is a special folder whose content is provided by a + * {@link android.content.ContentProvider}. To create a live folder, two components + * are required:</p> + * <ul> + * <li>An activity that can respond to the intent action {@link #ACTION_CREATE_LIVE_FOLDER}. The + * activity is responsible for creating the live folder.</li> + * <li>A {@link android.content.ContentProvider} to provide the live folder items.</li> + * </ul> + * + * <h3>Lifecycle</h3> + * <p>When a user wants to create a live folder, the system looks for all activities with the + * intent filter action {@link #ACTION_CREATE_LIVE_FOLDER} and presents the list to the user. + * When the user chooses one of the activities, the activity is invoked with the + * {@link #ACTION_CREATE_LIVE_FOLDER} action. The activity then creates the live folder and + * passes it back to the system by setting it as an + * {@link android.app.Activity#setResult(int, android.content.Intent) activity result}. The + * live folder is described by a content provider URI, a name, an icon and a display mode. + * Finally, when the user opens the live folder, the system queries the content provider + * to retrieve the folder's content.</p> + * + * <h3>Setting up the live folder activity</h3> + * <p>The following code sample shows how to write an activity that creates a live fodler:</p> + * <pre> + * public static class MyLiveFolder extends Activity { + * public static final Uri CONTENT_URI = Uri.parse("content://my.app/live"); + * + * @Override + * protected void onCreate(Bundle savedInstanceState) { + * super.onCreate(savedInstanceState); + * + * final Intent intent = getIntent(); + * final String action = intent.getAction(); + * + * if (LiveFolders.ACTION_CREATE_LIVE_FOLDER.equals(action)) { + * setResult(RESULT_OK, createLiveFolder(this, CONTENT_URI, "My LiveFolder", + * R.drawable.ic_launcher_contacts_phones)); + * } else { + * setResult(RESULT_CANCELED); + * } + * + * finish(); + * } + * + * private static Intent createLiveFolder(Context context, Uri uri, String name, + * int icon) { + * + * final Intent intent = new Intent(); + * + * intent.setData(uri); + * intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_NAME, name); + * intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_ICON, + * Intent.ShortcutIconResource.fromContext(context, icon)); + * intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_DISPLAY_MODE, LiveFolders.DISPLAY_MODE_LIST); + * + * return intent; + * } + * } + * </pre> + * <p>The live folder is described by an {@link android.content.Intent} as follows:</p> + * <table border="2" width="85%" align="center" frame="hsides" rules="rows"> + * <thead> + * <tr><th>Component</th> <th>Type</th> <th>Description</th> <th>Required</th></tr> + * </thead> + * + * <tbody> + * <tr><th>URI</th> + * <td>URI</td> + * <td>The ContentProvider URI</td> + * <td align="center">Yes</td> + * </tr> + * <tr><th>{@link #EXTRA_LIVE_FOLDER_NAME}</th> + * <td>Extra String</td> + * <td>The name of the live folder</td> + * <td align="center">Yes</td> + * </tr> + * <tr><th>{@link #EXTRA_LIVE_FOLDER_ICON}</th> + * <td>Extra {@link android.content.Intent.ShortcutIconResource}</td> + * <td>The icon of the live folder</td> + * <td align="center">Yes</td> + * </tr> + * <tr><th>{@link #EXTRA_LIVE_FOLDER_DISPLAY_MODE}</th> + * <td>Extra int</td> + * <td>The display mode of the live folder. The value must be either + * {@link #DISPLAY_MODE_GRID} or {@link #DISPLAY_MODE_LIST}.</td> + * <td align="center">Yes</td> + * </tr> + * <tr><th>{@link #EXTRA_LIVE_FOLDER_BASE_INTENT}</th> + * <td>Extra Intent</td> + * <td>When the user clicks an item inside a live folder, the system will either fire + * the intent associated with that item or, if present, the live folder's base intent + * with the id of the item appended to the base intent's URI.</td> + * <td align="center">No</td> + * </tr> + * </tbody> + * </table> + * + * <h3>Setting up the content provider</h3> + * <p>The live folder's content provider must, upon query, return a {@link android.database.Cursor} + * whose columns match the following names:</p> + * <table border="2" width="85%" align="center" frame="hsides" rules="rows"> + * <thead> + * <tr><th>Column</th> <th>Type</th> <th>Description</th> <th>Required</th></tr> + * </thead> + * + * <tbody> + * <tr><th>{@link #NAME}</th> + * <td>String</td> + * <td>The name of the item</td> + * <td align="center">Yes</td> + * </tr> + * <tr><th>{@link #DESCRIPTION}</th> + * <td>String</td> + * <td>The description of the item. The description is ignored when the live folder's + * display mode is {@link #DISPLAY_MODE_GRID}.</td> + * <td align="center">No</td> + * </tr> + * <tr><th>{@link #INTENT}</th> + * <td>{@link android.content.Intent}</td> + * <td>The intent to fire when the item is clicked. Ignored when the live folder defines + * a base intent.</td> + * <td align="center">No</td> + * </tr> + * <tr><th>{@link #ICON_BITMAP}</th> + * <td>Bitmap</td> + * <td>The icon for the item. When this column value is not null, the values for the + * columns {@link #ICON_PACKAGE} and {@link #ICON_RESOURCE} must be null.</td> + * <td align="center">No</td> + * </tr> + * <tr><th>{@link #ICON_PACKAGE}</th> + * <td>String</td> + * <td>The package of the item's icon. When this value is not null, the value for the + * column {@link #ICON_RESOURCE} must be specified and the value for the column + * {@link #ICON_BITMAP} must be null.</td> + * <td align="center">No</td> + * </tr> + * <tr><th>{@link #ICON_RESOURCE}</th> + * <td>String</td> + * <td>The resource name of the item's icon. When this value is not null, the value for the + * column {@link #ICON_PACKAGE} must be specified and the value for the column + * {@link #ICON_BITMAP} must be null.</td> + * <td align="center">No</td> + * </tr> + * </tbody> + * </table> + */ +public final class LiveFolders implements BaseColumns { + /** + * <p>Content provider column.</p> + * <p>Name of the live folder item.</p> + * <p>Required.</p> + * <p>Type: String.</p> + */ + public static final String NAME = "name"; + + /** + * <p>Content provider column.</p> + * <p>Description of the live folder item. This value is ignored if the + * live folder's display mode is {@link LiveFolders#DISPLAY_MODE_GRID}.</p> + * <p>Optional.</p> + * <p>Type: String.</p> + * + * @see LiveFolders#EXTRA_LIVE_FOLDER_DISPLAY_MODE + */ + public static final String DESCRIPTION = "description"; + + /** + * <p>Content provider column.</p> + * <p>Intent of the live folder item.</p> + * <p>Optional if the live folder has a base intent.</p> + * <p>Type: {@link android.content.Intent}.</p> + * + * @see LiveFolders#EXTRA_LIVE_FOLDER_BASE_INTENT + */ + public static final String INTENT = "intent"; + + /** + * <p>Content provider column.</p> + * <p>Icon of the live folder item, as a custom bitmap.</p> + * <p>Optional.</p> + * <p>Type: {@link android.graphics.Bitmap}.</p> + */ + public static final String ICON_BITMAP = "icon_bitmap"; + + /** + * <p>Content provider column.</p> + * <p>Package where to find the icon of the live folder item. This value can be + * obtained easily using + * {@link android.content.Intent.ShortcutIconResource#fromContext(android.content.Context, int)}.</p> + * <p>Optional.</p> + * <p>Type: String.</p> + * + * @see #ICON_RESOURCE + * @see android.content.Intent.ShortcutIconResource + */ + public static final String ICON_PACKAGE = "icon_package"; + + /** + * <p>Content provider column.</p> + * <p>Resource name of the live folder item. This value can be obtained easily using + * {@link android.content.Intent.ShortcutIconResource#fromContext(android.content.Context, int)}.</p> + * <p>Optional.</p> + * <p>Type: String.</p> + * + * @see #ICON_PACKAGE + * @see android.content.Intent.ShortcutIconResource + */ + public static final String ICON_RESOURCE = "icon_resource"; + + /** + * Displays a live folder's content in a grid. + * + * @see LiveFolders#EXTRA_LIVE_FOLDER_DISPLAY_MODE + */ + public static final int DISPLAY_MODE_GRID = 0x1; + + /** + * Displays a live folder's content in a list. + * + * @see LiveFolders#EXTRA_LIVE_FOLDER_DISPLAY_MODE + */ + public static final int DISPLAY_MODE_LIST = 0x2; + + /** + * The name of the extra used to define the name of a live folder. + * + * @see #ACTION_CREATE_LIVE_FOLDER + */ + public static final String EXTRA_LIVE_FOLDER_NAME = "android.intent.extra.livefolder.NAME"; + + /** + * The name of the extra used to define the icon of a live folder. + * + * @see #ACTION_CREATE_LIVE_FOLDER + */ + public static final String EXTRA_LIVE_FOLDER_ICON = "android.intent.extra.livefolder.ICON"; + + /** + * The name of the extra used to define the display mode of a live folder. + * + * @see #ACTION_CREATE_LIVE_FOLDER + * @see #DISPLAY_MODE_GRID + * @see #DISPLAY_MODE_LIST + */ + public static final String EXTRA_LIVE_FOLDER_DISPLAY_MODE = + "android.intent.extra.livefolder.DISPLAY_MODE"; + + /** + * The name of the extra used to define the base Intent of a live folder. + * + * @see #ACTION_CREATE_LIVE_FOLDER + */ + public static final String EXTRA_LIVE_FOLDER_BASE_INTENT = + "android.intent.extra.livefolder.BASE_INTENT"; + + /** + * Activity Action: Creates a live folder. + * <p>Input: Nothing.</p> + * <p>Output: An Intent representing the live folder. The intent must contain four + * extras: EXTRA_LIVE_FOLDER_NAME (value: String), + * EXTRA_LIVE_FOLDER_ICON (value: ShortcutIconResource), + * EXTRA_LIVE_FOLDER_URI (value: String) and + * EXTRA_LIVE_FOLDER_DISPLAY_MODE (value: int). The Intent can optionnally contain + * EXTRA_LIVE_FOLDER_BASE_INTENT (value: Intent).</p> + * + * @see #EXTRA_LIVE_FOLDER_NAME + * @see #EXTRA_LIVE_FOLDER_ICON + * @see #EXTRA_LIVE_FOLDER_DISPLAY_MODE + * @see #EXTRA_LIVE_FOLDER_BASE_INTENT + * @see android.content.Intent.ShortcutIconResource + */ + @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CREATE_LIVE_FOLDER = + "android.intent.action.CREATE_LIVE_FOLDER"; + + private LiveFolders() { + } +} diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index d99ad36..d4b728b 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -45,11 +45,70 @@ import java.text.Collator; public final class MediaStore { private final static String TAG = "MediaStore"; - + public static final String AUTHORITY = "media"; - + private static final String CONTENT_AUTHORITY_SLASH = "content://" + AUTHORITY + "/"; - + + /** + * Activity Action: Perform a search for media. + * Contains at least the {@link android.app.SearchManager#QUERY} extra. + * May also contain any combination of the following extras: + * EXTRA_MEDIA_ARTIST, EXTRA_MEDIA_ALBUM, EXTRA_MEDIA_TITLE, EXTRA_MEDIA_FOCUS + * + * @see android.provider.MediaStore#EXTRA_MEDIA_ARTIST + * @see android.provider.MediaStore#EXTRA_MEDIA_ALBUM + * @see android.provider.MediaStore#EXTRA_MEDIA_TITLE + * @see android.provider.MediaStore#EXTRA_MEDIA_FOCUS + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String INTENT_ACTION_MEDIA_SEARCH = "android.intent.action.MEDIA_SEARCH"; + + /** + * The name of the Intent-extra used to define the artist + */ + public static final String EXTRA_MEDIA_ARTIST = "android.intent.extra.artist"; + /** + * The name of the Intent-extra used to define the album + */ + public static final String EXTRA_MEDIA_ALBUM = "android.intent.extra.album"; + /** + * The name of the Intent-extra used to define the song title + */ + public static final String EXTRA_MEDIA_TITLE = "android.intent.extra.title"; + /** + * The name of the Intent-extra used to define the search focus. The search focus + * indicates whether the search should be for things related to the artist, album + * or song that is identified by the other extras. + */ + public static final String EXTRA_MEDIA_FOCUS = "android.intent.extra.focus"; + + /** + * The name of the Intent-extra used to control the orientation of a MovieView. + * This is an int property that overrides the MovieView activity's requestedOrientation. + * @see android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + */ + public static final String EXTRA_SCREEN_ORIENTATION = "android.intent.extra.screenOrientation"; + + /** + * The name of the Intent-extra used to control the onCompletion behavior of a MovieView. + * This is a boolean property that specifies whether or not to finish the MovieView activity + * when the movie completes playing. The default value is true, which means to automatically + * exit the movie player activity when the movie completes playing. + */ + public static final String EXTRA_FINISH_ON_COMPLETION = "android.intent.extra.finishOnCompletion"; + + /** + * The name of the Intent action used to launch a camera in still image mode. + */ + public static final String INTENT_ACTION_STILL_IMAGE_CAMERA = "android.media.action.STILL_IMAGE_CAMERA"; + + + /** + * The name of the Intent action used to launch a camera in video mode. + */ + public static final String INTENT_ACTION_VIDEO_CAMERA = "android.media.action.VIDEO_CAMERA"; + /** * Standard Intent action that can be sent to have the media application * capture an image and return it. The image is returned as a Bitmap @@ -57,11 +116,11 @@ public final class MediaStore * @hide */ public final static String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE"; - - /** + + /** * Common fields for most MediaProvider tables */ - + public interface MediaColumns extends BaseColumns { /** * The data stream for the file @@ -108,7 +167,7 @@ public final class MediaStore */ public static final String MIME_TYPE = "mime_type"; } - + /** * Contains meta data for all available images. */ @@ -120,19 +179,19 @@ public final class MediaStore * <P>Type: TEXT</P> */ public static final String DESCRIPTION = "description"; - + /** * The picasa id of the image * <P>Type: TEXT</P> */ public static final String PICASA_ID = "picasa_id"; - + /** * Whether the video should be published as public or private * <P>Type: INTEGER</P> */ public static final String IS_PRIVATE = "isprivate"; - + /** * The latitude where the image was captured. * <P>Type: DOUBLE</P> @@ -144,14 +203,14 @@ public final class MediaStore * <P>Type: DOUBLE</P> */ public static final String LONGITUDE = "longitude"; - + /** * The date & time that the image was taken in units * of milliseconds since jan 1, 1970. * <P>Type: INTEGER</P> */ public static final String DATE_TAKEN = "datetaken"; - + /** * The orientation for the image expressed as degrees. * Only degrees 0, 90, 180, 270 will work. @@ -164,15 +223,17 @@ public final class MediaStore * <P>Type: INTEGER</P> */ public static final String MINI_THUMB_MAGIC = "mini_thumb_magic"; - + /** - * The bucket id of the image + * The bucket id of the image. This is a read-only property that + * is automatically computed from the DATA column. * <P>Type: TEXT</P> */ public static final String BUCKET_ID = "bucket_id"; - + /** - * The bucket display name of the image + * The bucket display name of the image. This is a read-only property that + * is automatically computed from the DATA column. * <P>Type: TEXT</P> */ public static final String BUCKET_DISPLAY_NAME = "bucket_display_name"; @@ -183,14 +244,14 @@ public final class MediaStore { return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER); } - + public static final Cursor query(ContentResolver cr, Uri uri, String[] projection, String where, String orderBy) { return cr.query(uri, projection, where, null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); } - + public static final Cursor query(ContentResolver cr, Uri uri, String[] projection, String selection, String [] selectionArgs, String orderBy) { @@ -200,7 +261,7 @@ public final class MediaStore /** * Retrieves an image for the given url as a {@link Bitmap}. - * + * * @param cr The content resolver to use * @param url The url of the image * @throws FileNotFoundException @@ -214,10 +275,10 @@ public final class MediaStore input.close(); return bitmap; } - + /** * Insert an image and create a thumbnail for it. - * + * * @param cr The content resolver to use * @param imagePath The path to the image to insert * @param name The name of the image @@ -239,38 +300,38 @@ public final class MediaStore } } } - + private static final Bitmap StoreThumbnail( - ContentResolver cr, + ContentResolver cr, Bitmap source, long id, - float width, float height, + float width, float height, int kind) { // create the matrix to scale it Matrix matrix = new Matrix(); - + float scaleX = width / source.getWidth(); float scaleY = height / source.getHeight(); - + matrix.setScale(scaleX, scaleY); - - Bitmap thumb = Bitmap.createBitmap(source, 0, 0, + + Bitmap thumb = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true); - + ContentValues values = new ContentValues(4); values.put(Images.Thumbnails.KIND, kind); values.put(Images.Thumbnails.IMAGE_ID, (int)id); values.put(Images.Thumbnails.HEIGHT, thumb.getHeight()); values.put(Images.Thumbnails.WIDTH, thumb.getWidth()); - + Uri url = cr.insert(Images.Thumbnails.EXTERNAL_CONTENT_URI, values); - + try { OutputStream thumbOut = cr.openOutputStream(url); - - thumb.compress(Bitmap.CompressFormat.JPEG, 100, thumbOut); + + thumb.compress(Bitmap.CompressFormat.JPEG, 100, thumbOut); thumbOut.close(); return thumb; } @@ -281,10 +342,10 @@ public final class MediaStore return null; } } - + /** * Insert an image and create a thumbnail for it. - * + * * @param cr The content resolver to use * @param source The stream to use for the image * @param title The name of the image @@ -306,7 +367,7 @@ public final class MediaStore try { url = cr.insert(EXTERNAL_CONTENT_URI, values); - + if (source != null) { OutputStream imageOut = cr.openOutputStream(url); try { @@ -337,11 +398,11 @@ public final class MediaStore return stringUrl; } - + /** - * Get the content:// style URI for the image media table on the + * Get the content:// style URI for the image media table on the * given volume. - * + * * @param volumeName the name of the volume to get the URI for * @return the URI to the image media table on the given volume */ @@ -355,7 +416,7 @@ public final class MediaStore */ public static final Uri INTERNAL_CONTENT_URI = getContentUri("internal"); - + /** * The content:// style URI for the "primary" external storage * volume. @@ -369,7 +430,7 @@ public final class MediaStore * image MIME type as appropriate -- for example, image/jpeg. */ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/image"; - + /** * The default sort order for this table */ @@ -382,23 +443,23 @@ public final class MediaStore { return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER); } - + public static final Cursor queryMiniThumbnails(ContentResolver cr, Uri uri, int kind, String[] projection) { return cr.query(uri, projection, "kind = " + kind, null, DEFAULT_SORT_ORDER); } - + public static final Cursor queryMiniThumbnail(ContentResolver cr, long origId, int kind, String[] projection) { return cr.query(EXTERNAL_CONTENT_URI, projection, IMAGE_ID + " = " + origId + " AND " + KIND + " = " + kind, null, null); } - + /** - * Get the content:// style URI for the image media table on the + * Get the content:// style URI for the image media table on the * given volume. - * + * * @param volumeName the name of the volume to get the URI for * @return the URI to the image media table on the given volume */ @@ -406,13 +467,13 @@ public final class MediaStore return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + "/images/thumbnails"); } - + /** * The content:// style URI for the internal storage. */ public static final Uri INTERNAL_CONTENT_URI = getContentUri("internal"); - + /** * The content:// style URI for the "primary" external storage * volume. @@ -424,35 +485,35 @@ public final class MediaStore * The default sort order for this table */ public static final String DEFAULT_SORT_ORDER = "image_id ASC"; - + /** * The data stream for the thumbnail * <P>Type: DATA STREAM</P> */ public static final String DATA = "_data"; - + /** * The original image for the thumbnal * <P>Type: INTEGER (ID from Images table)</P> */ public static final String IMAGE_ID = "image_id"; - + /** * The kind of the thumbnail * <P>Type: INTEGER (One of the values below)</P> */ public static final String KIND = "kind"; - + public static final int MINI_KIND = 1; public static final int FULL_SCREEN_KIND = 2; public static final int MICRO_KIND = 3; - + /** * The width of the thumbnal * <P>Type: INTEGER (long)</P> */ public static final String WIDTH = "width"; - + /** * The height of the thumbnail * <P>Type: INTEGER (long)</P> @@ -460,7 +521,7 @@ public final class MediaStore public static final String HEIGHT = "height"; } } - + /** * Container for all audio content. */ @@ -532,7 +593,7 @@ public final class MediaStore * <P>Type: TEXT</P> */ public static final String ALBUM_ART = "album_art"; - + /** * The track number of this song on the album, if any. * This number encodes both the track number and the @@ -632,9 +693,9 @@ public final class MediaStore public static final class Media implements AudioColumns { /** - * Get the content:// style URI for the audio media table on the + * Get the content:// style URI for the audio media table on the * given volume. - * + * * @param volumeName the name of the volume to get the URI for * @return the URI to the audio media table on the given volume */ @@ -642,46 +703,56 @@ public final class MediaStore return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + "/audio/media"); } - + public static Uri getContentUriForPath(String path) { return (path.startsWith(Environment.getExternalStorageDirectory().getPath()) ? EXTERNAL_CONTENT_URI : INTERNAL_CONTENT_URI); } - + /** * The content:// style URI for the internal storage. */ public static final Uri INTERNAL_CONTENT_URI = getContentUri("internal"); - + /** * The content:// style URI for the "primary" external storage * volume. */ public static final Uri EXTERNAL_CONTENT_URI = getContentUri("external"); - + /** * The MIME type for this table. */ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/audio"; - + /** * The default sort order for this table */ public static final String DEFAULT_SORT_ORDER = TITLE; - + /** * Activity Action: Start SoundRecorder application. * <p>Input: nothing. * <p>Output: An uri to the recorded sound stored in the Media Library * if the recording was successful. - * + * May also contain the extra EXTRA_MAX_BYTES. + * @see #EXTRA_MAX_BYTES */ - public static final String RECORD_SOUND_ACTION = + public static final String RECORD_SOUND_ACTION = "android.provider.MediaStore.RECORD_SOUND"; + + /** + * The name of the Intent-extra used to define a maximum file size for + * a recording made by the SoundRecorder application. + * + * @see #RECORD_SOUND_ACTION + */ + public static final String EXTRA_MAX_BYTES = + "android.provider.MediaStore.extra.MAX_BYTES"; } - + /** * Columns representing an audio genre */ @@ -698,9 +769,9 @@ public final class MediaStore */ public static final class Genres implements BaseColumns, GenresColumns { /** - * Get the content:// style URI for the audio genres table on the + * Get the content:// style URI for the audio genres table on the * given volume. - * + * * @param volumeName the name of the volume to get the URI for * @return the URI to the audio genres table on the given volume */ @@ -714,7 +785,7 @@ public final class MediaStore */ public static final Uri INTERNAL_CONTENT_URI = getContentUri("internal"); - + /** * The content:// style URI for the "primary" external storage * volume. @@ -726,7 +797,7 @@ public final class MediaStore * The MIME type for this table. */ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/genre"; - + /** * The MIME type for entries in this table. */ @@ -794,7 +865,7 @@ public final class MediaStore * <P>Type: INTEGER (long)</P> */ public static final String DATE_ADDED = "date_added"; - + /** * The time the file was last modified * Units are seconds since 1970. @@ -803,16 +874,16 @@ public final class MediaStore */ public static final String DATE_MODIFIED = "date_modified"; } - + /** * Contains playlists for audio files */ public static final class Playlists implements BaseColumns, PlaylistsColumns { /** - * Get the content:// style URI for the audio playlists table on the + * Get the content:// style URI for the audio playlists table on the * given volume. - * + * * @param volumeName the name of the volume to get the URI for * @return the URI to the audio playlists table on the given volume */ @@ -826,7 +897,7 @@ public final class MediaStore */ public static final Uri INTERNAL_CONTENT_URI = getContentUri("internal"); - + /** * The content:// style URI for the "primary" external storage * volume. @@ -838,7 +909,7 @@ public final class MediaStore * The MIME type for this table. */ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/playlist"; - + /** * The MIME type for entries in this table. */ @@ -863,7 +934,7 @@ public final class MediaStore * The ID within the playlist. */ public static final String _ID = "_id"; - + /** * A subdirectory of each playlist containing all member audio * files. @@ -881,7 +952,7 @@ public final class MediaStore * <P>Type: INTEGER (long)</P> */ public static final String PLAYLIST_ID = "playlist_id"; - + /** * The order of the songs in the playlist * <P>Type: INTEGER (long)></P> @@ -922,15 +993,15 @@ public final class MediaStore */ public static final String NUMBER_OF_TRACKS = "number_of_tracks"; } - + /** * Contains artists for audio files */ public static final class Artists implements BaseColumns, ArtistColumns { /** - * Get the content:// style URI for the artists table on the + * Get the content:// style URI for the artists table on the * given volume. - * + * * @param volumeName the name of the volume to get the URI for * @return the URI to the audio artists table on the given volume */ @@ -944,7 +1015,7 @@ public final class MediaStore */ public static final Uri INTERNAL_CONTENT_URI = getContentUri("internal"); - + /** * The content:// style URI for the "primary" external storage * volume. @@ -956,7 +1027,7 @@ public final class MediaStore * The MIME type for this table. */ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/artists"; - + /** * The MIME type for entries in this table. */ @@ -979,7 +1050,7 @@ public final class MediaStore } } } - + /** * Columns representing an album */ @@ -1010,6 +1081,16 @@ public final class MediaStore public static final String NUMBER_OF_SONGS = "numsongs"; /** + * This column is available when getting album info via artist, + * and indicates the number of songs on the album by the given + * artist. + * <P>Type: INTEGER</P> + * + * @hide pending API Council approval + */ + public static final String NUMBER_OF_SONGS_FOR_ARTIST = "numsongs_by_artist"; + + /** * The year in which the earliest and latest songs * on this album were released. These will often * be the same, but for compilation albums they @@ -1025,22 +1106,22 @@ public final class MediaStore * <P>Type: TEXT</P> */ public static final String ALBUM_KEY = "album_key"; - + /** * Cached album art. * <P>Type: TEXT</P> */ public static final String ALBUM_ART = "album_art"; } - + /** * Contains artists for audio files */ public static final class Albums implements BaseColumns, AlbumColumns { /** - * Get the content:// style URI for the albums table on the + * Get the content:// style URI for the albums table on the * given volume. - * + * * @param volumeName the name of the volume to get the URI for * @return the URI to the audio albums table on the given volume */ @@ -1054,7 +1135,7 @@ public final class MediaStore */ public static final Uri INTERNAL_CONTENT_URI = getContentUri("internal"); - + /** * The content:// style URI for the "primary" external storage * volume. @@ -1066,7 +1147,7 @@ public final class MediaStore * The MIME type for this table. */ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/albums"; - + /** * The MIME type for entries in this table. */ @@ -1145,7 +1226,7 @@ public final class MediaStore * <P>Type: TEXT</P> */ public static final String LANGUAGE = "language"; - + /** * The latitude where the image was captured. * <P>Type: DOUBLE</P> @@ -1157,7 +1238,7 @@ public final class MediaStore * <P>Type: DOUBLE</P> */ public static final String LONGITUDE = "longitude"; - + /** * The date & time that the image was taken in units * of milliseconds since jan 1, 1970. @@ -1170,13 +1251,27 @@ public final class MediaStore * <P>Type: INTEGER</P> */ public static final String MINI_THUMB_MAGIC = "mini_thumb_magic"; + + /** + * The bucket id of the video. This is a read-only property that + * is automatically computed from the DATA column. + * <P>Type: TEXT</P> + */ + public static final String BUCKET_ID = "bucket_id"; + + /** + * The bucket display name of the video. This is a read-only property that + * is automatically computed from the DATA column. + * <P>Type: TEXT</P> + */ + public static final String BUCKET_DISPLAY_NAME = "bucket_display_name"; } public static final class Media implements VideoColumns { /** - * Get the content:// style URI for the video media table on the + * Get the content:// style URI for the video media table on the * given volume. - * + * * @param volumeName the name of the volume to get the URI for * @return the URI to the video media table on the given volume */ @@ -1190,7 +1285,7 @@ public final class MediaStore */ public static final Uri INTERNAL_CONTENT_URI = getContentUri("internal"); - + /** * The content:// style URI for the "primary" external storage * volume. diff --git a/core/java/android/provider/SearchRecentSuggestions.java b/core/java/android/provider/SearchRecentSuggestions.java index 1439b26..0632d94 100644 --- a/core/java/android/provider/SearchRecentSuggestions.java +++ b/core/java/android/provider/SearchRecentSuggestions.java @@ -190,6 +190,11 @@ public class SearchRecentSuggestions { /** * Completely delete the history. Use this call to implement a "clear history" UI. + * + * Any application that implements search suggestions based on previous actions (such as + * recent queries, page/items viewed, etc.) should provide a way for the user to clear the + * history. This gives the user a measure of privacy, if they do not wish for their recent + * searches to be replayed by other users of the device (via suggestions). */ public void clearHistory() { ContentResolver cr = mContext.getContentResolver(); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 6897bd5..e93bbeb 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -40,6 +40,7 @@ import android.util.Log; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; +import java.util.HashSet; /** @@ -100,6 +101,20 @@ public final class Settings { "android.settings.WIRELESS_SETTINGS"; /** + * Activity Action: Show settings to allow entering/exiting airplane mode. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_AIRPLANE_MODE_SETTINGS = + "android.settings.AIRPLANE_MODE_SETTINGS"; + + /** * Activity Action: Show settings to allow configuration of security and * location privacy. * <p> @@ -116,6 +131,7 @@ public final class Settings { /** * Activity Action: Show settings to allow configuration of Wi-Fi. + * <p> * In some cases, a matching Activity may not exist, so ensure you * safeguard against this. @@ -123,10 +139,26 @@ public final class Settings { * Input: Nothing. * <p> * Output: Nothing. + */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_WIFI_SETTINGS = "android.settings.WIFI_SETTINGS"; + + /** + * Activity Action: Show settings to allow configuration of a static IP + * address for Wi-Fi. + * <p> + * In some cases, a matching Activity may not exist, so ensure you safeguard + * against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_WIFI_IP_SETTINGS = + "android.settings.WIFI_IP_SETTINGS"; /** * Activity Action: Show settings to allow configuration of Bluetooth. @@ -213,7 +245,50 @@ public final class Settings { "android.settings.APPLICATION_SETTINGS"; /** - * Activity Action: Show settings to allow configuration of sync settings. + * Activity Action: Show settings to allow configuration of application + * development-related settings. + * <p> + * In some cases, a matching Activity may not exist, so ensure you safeguard + * against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_APPLICATION_DEVELOPMENT_SETTINGS = + "android.settings.APPLICATION_DEVELOPMENT_SETTINGS"; + + /** + * Activity Action: Show settings to allow configuration of quick launch shortcuts. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_QUICK_LAUNCH_SETTINGS = + "android.settings.QUICK_LAUNCH_SETTINGS"; + + /** + * Activity Action: Show settings to manage installed applications. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_MANAGE_APPLICATIONS_SETTINGS = + "android.settings.MANAGE_APPLICATIONS_SETTINGS"; + + /** + * Activity Action: Show settings for system update functionality. * <p> * In some cases, a matching Activity may not exist, so ensure you * safeguard against this. @@ -225,9 +300,78 @@ public final class Settings { * @hide */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SYSTEM_UPDATE_SETTINGS = + "android.settings.SYSTEM_UPDATE_SETTINGS"; + + /** + * Activity Action: Show settings to allow configuration of sync settings. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_SYNC_SETTINGS = "android.settings.SYNC_SETTINGS"; + /** + * Activity Action: Show settings for selecting the network operator. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_NETWORK_OPERATOR_SETTINGS = + "android.settings.NETWORK_OPERATOR_SETTINGS"; + + /** + * Activity Action: Show settings for selection of 2G/3G. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_DATA_ROAMING_SETTINGS = + "android.settings.DATA_ROAMING_SETTINGS"; + + /** + * Activity Action: Show settings for internal storage. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_INTERNAL_STORAGE_SETTINGS = + "android.settings.INTERNAL_STORAGE_SETTINGS"; + /** + * Activity Action: Show settings for memory card storage. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_MEMORY_CARD_SETTINGS = + "android.settings.MEMORY_CARD_SETTINGS"; + // End of Intent actions for Settings private static final String JID_RESOURCE_PREFIX = "android"; @@ -246,8 +390,6 @@ public final class Settings { /** * Common base for tables of name/value settings. - * - * */ public static class NameValueTable implements BaseColumns { public static final String NAME = "name"; @@ -296,7 +438,7 @@ public final class Settings { try { c = cr.query(mUri, new String[] { Settings.NameValueTable.VALUE }, Settings.NameValueTable.NAME + "=?", new String[]{name}, null); - if (c.moveToNext()) value = c.getString(0); + if (c != null && c.moveToNext()) value = c.getString(0); mValues.put(name, value); } catch (SQLException e) { // SQL error: return null, but don't cache it. @@ -320,6 +462,41 @@ public final class Settings { public static final String SYS_PROP_SETTING_VERSION = "sys.settings_system_version"; private static volatile NameValueCache mNameValueCache = null; + + private static final HashSet<String> MOVED_TO_SECURE; + static { + MOVED_TO_SECURE = new HashSet<String>(30); + MOVED_TO_SECURE.add(Secure.ADB_ENABLED); + MOVED_TO_SECURE.add(Secure.ANDROID_ID); + MOVED_TO_SECURE.add(Secure.BLUETOOTH_ON); + MOVED_TO_SECURE.add(Secure.DATA_ROAMING); + MOVED_TO_SECURE.add(Secure.DEVICE_PROVISIONED); + MOVED_TO_SECURE.add(Secure.HTTP_PROXY); + MOVED_TO_SECURE.add(Secure.INSTALL_NON_MARKET_APPS); + MOVED_TO_SECURE.add(Secure.LOCATION_PROVIDERS_ALLOWED); + MOVED_TO_SECURE.add(Secure.LOGGING_ID); + MOVED_TO_SECURE.add(Secure.PARENTAL_CONTROL_ENABLED); + MOVED_TO_SECURE.add(Secure.PARENTAL_CONTROL_LAST_UPDATE); + MOVED_TO_SECURE.add(Secure.PARENTAL_CONTROL_REDIRECT_URL); + MOVED_TO_SECURE.add(Secure.SETTINGS_CLASSNAME); + MOVED_TO_SECURE.add(Secure.USB_MASS_STORAGE_ENABLED); + MOVED_TO_SECURE.add(Secure.USE_GOOGLE_MAIL); + MOVED_TO_SECURE.add(Secure.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON); + MOVED_TO_SECURE.add(Secure.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY); + MOVED_TO_SECURE.add(Secure.WIFI_NUM_OPEN_NETWORKS_KEPT); + MOVED_TO_SECURE.add(Secure.WIFI_ON); + MOVED_TO_SECURE.add(Secure.WIFI_WATCHDOG_ACCEPTABLE_PACKET_LOSS_PERCENTAGE); + MOVED_TO_SECURE.add(Secure.WIFI_WATCHDOG_AP_COUNT); + MOVED_TO_SECURE.add(Secure.WIFI_WATCHDOG_BACKGROUND_CHECK_DELAY_MS); + MOVED_TO_SECURE.add(Secure.WIFI_WATCHDOG_BACKGROUND_CHECK_ENABLED); + MOVED_TO_SECURE.add(Secure.WIFI_WATCHDOG_BACKGROUND_CHECK_TIMEOUT_MS); + MOVED_TO_SECURE.add(Secure.WIFI_WATCHDOG_INITIAL_IGNORED_PING_COUNT); + MOVED_TO_SECURE.add(Secure.WIFI_WATCHDOG_MAX_AP_CHECKS); + MOVED_TO_SECURE.add(Secure.WIFI_WATCHDOG_ON); + MOVED_TO_SECURE.add(Secure.WIFI_WATCHDOG_PING_COUNT); + MOVED_TO_SECURE.add(Secure.WIFI_WATCHDOG_PING_DELAY_MS); + MOVED_TO_SECURE.add(Secure.WIFI_WATCHDOG_PING_TIMEOUT_MS); + } /** * Look up a name in the database. @@ -328,6 +505,11 @@ public final class Settings { * @return the corresponding value, or null if not present */ public synchronized static String getString(ContentResolver resolver, String name) { + if (MOVED_TO_SECURE.contains(name)) { + Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.System" + + " to android.provider.Settings.Secure, returning read-only value."); + return Secure.getString(resolver, name); + } if (mNameValueCache == null) { mNameValueCache = new NameValueCache(SYS_PROP_SETTING_VERSION, CONTENT_URI); } @@ -341,8 +523,12 @@ public final class Settings { * @param value to associate with the name * @return true if the value was set, false on database errors */ - public static boolean putString(ContentResolver resolver, - String name, String value) { + public static boolean putString(ContentResolver resolver, String name, String value) { + if (MOVED_TO_SECURE.contains(name)) { + Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.System" + + " to android.provider.Settings.Secure, value is unchanged."); + return false; + } return putString(resolver, CONTENT_URI, name, value); } @@ -353,6 +539,11 @@ public final class Settings { * @return the corresponding content URI, or null if not present */ public static Uri getUriFor(String name) { + if (MOVED_TO_SECURE.contains(name)) { + Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.System" + + " to android.provider.Settings.Secure, returning Secure URI."); + return Secure.getUriFor(Secure.CONTENT_URI, name); + } return getUriFor(CONTENT_URI, name); } @@ -426,6 +617,75 @@ public final class Settings { /** * Convenience function for retrieving a single system settings value + * as a {@code long}. Note that internally setting values are always + * stored as strings; this function converts the string to a {@code long} + * for you. The default value will be returned if the setting is + * not defined or not a {@code long}. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to retrieve. + * @param def Value to return if the setting is not defined. + * + * @return The setting's current value, or 'def' if it is not defined + * or not a valid {@code long}. + */ + public static long getLong(ContentResolver cr, String name, long def) { + String valString = getString(cr, name); + long value; + try { + value = valString != null ? Long.parseLong(valString) : def; + } catch (NumberFormatException e) { + value = def; + } + return value; + } + + /** + * Convenience function for retrieving a single system settings value + * as a {@code long}. Note that internally setting values are always + * stored as strings; this function converts the string to a {@code long} + * for you. + * <p> + * This version does not take a default value. If the setting has not + * been set, or the string value is not a number, + * it throws {@link SettingNotFoundException}. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to retrieve. + * + * @return The setting's current value. + * @throws SettingNotFoundException Thrown if a setting by the given + * name can't be found or the setting value is not an integer. + */ + public static long getLong(ContentResolver cr, String name) + throws SettingNotFoundException { + String valString = getString(cr, name); + try { + return Long.parseLong(valString); + } catch (NumberFormatException e) { + throw new SettingNotFoundException(name); + } + } + + /** + * Convenience function for updating a single settings value as a long + * integer. This will either create a new entry in the table if the + * given name does not exist, or modify the value of the existing row + * with that name. Note that internally setting values are always + * stored as strings, so this function converts the given value to a + * string before storing it. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to modify. + * @param value The new value for the setting. + * @return true if the value was set, false on database errors + */ + public static boolean putLong(ContentResolver cr, String name, long value) { + return putString(cr, name, Long.toString(value)); + } + + /** + * Convenience function for retrieving a single system settings value * as a floating point number. Note that internally setting values are * always stored as strings; this function converts the string to an * float for you. The default value will be returned if the setting @@ -536,7 +796,13 @@ public final class Settings { /** * Whether we keep the device on while the device is plugged in. - * 0=no 1=yes + * Supported values are: + * <ul> + * <li>{@code 0} to never stay on while plugged in</li> + * <li>{@link BatteryManager#BATTERY_PLUGGED_AC} to stay on for AC charger</li> + * <li>{@link BatteryManager#BATTERY_PLUGGED_USB} to stay on for USB charger</li> + * </ul> + * These values can be OR-ed together. */ public static final String STAY_ON_WHILE_PLUGGED_IN = "stay_on_while_plugged_in"; @@ -580,104 +846,15 @@ public final class Settings { public static final String AIRPLANE_MODE_RADIOS = "airplane_mode_radios"; /** - * Whether the Wi-Fi should be on. Only the Wi-Fi service should touch this. - */ - public static final String WIFI_ON = "wifi_on"; - - /** - * Whether to notify the user of open networks. - * <p> - * If not connected and the scan results have an open network, we will - * put this notification up. If we attempt to connect to a network or - * the open network(s) disappear, we remove the notification. When we - * show the notification, we will not show it again for - * {@link #WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY} time. - */ - public static final String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON = - "wifi_networks_available_notification_on"; - - /** - * Delay (in seconds) before repeating the Wi-Fi networks available notification. - * Connecting to a network will reset the timer. - */ - public static final String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY = - "wifi_networks_available_repeat_delay"; - - /** - * When the number of open networks exceeds this number, the - * least-recently-used excess networks will be removed. - */ - public static final String WIFI_NUM_OPEN_NETWORKS_KEPT = "wifi_num_open_networks_kept"; - - /** - * Whether the Wi-Fi watchdog is enabled. - */ - public static final String WIFI_WATCHDOG_ON = "wifi_watchdog_on"; - - /** - * The number of access points required for a network in order for the - * watchdog to monitor it. - */ - public static final String WIFI_WATCHDOG_AP_COUNT = "wifi_watchdog_ap_count"; - - /** - * The number of initial pings to perform that *may* be ignored if they - * fail. Again, if these fail, they will *not* be used in packet loss - * calculation. For example, one network always seemed to time out for - * the first couple pings, so this is set to 3 by default. - */ - public static final String WIFI_WATCHDOG_INITIAL_IGNORED_PING_COUNT = "wifi_watchdog_initial_ignored_ping_count"; - - /** - * The number of pings to test if an access point is a good connection. - */ - public static final String WIFI_WATCHDOG_PING_COUNT = "wifi_watchdog_ping_count"; - - /** - * The timeout per ping. - */ - public static final String WIFI_WATCHDOG_PING_TIMEOUT_MS = "wifi_watchdog_ping_timeout_ms"; - - /** - * The delay between pings. - */ - public static final String WIFI_WATCHDOG_PING_DELAY_MS = "wifi_watchdog_ping_delay_ms"; - - /** - * The acceptable packet loss percentage (range 0 - 100) before trying - * another AP on the same network. - */ - public static final String WIFI_WATCHDOG_ACCEPTABLE_PACKET_LOSS_PERCENTAGE = - "wifi_watchdog_acceptable_packet_loss_percentage"; - - /** - * The maximum number of access points (per network) to attempt to test. - * If this number is reached, the watchdog will no longer monitor the - * initial connection state for the network. This is a safeguard for - * networks containing multiple APs whose DNS does not respond to pings. - */ - public static final String WIFI_WATCHDOG_MAX_AP_CHECKS = "wifi_watchdog_max_ap_checks"; - - /** - * Whether the Wi-Fi watchdog is enabled for background checking even - * after it thinks the user has connected to a good access point. + * The interval in milliseconds after which Wi-Fi is considered idle. + * When idle, it is possible for the device to be switched from Wi-Fi to + * the mobile data network. + * + * @hide pending API Council approval */ - public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_ENABLED = - "wifi_watchdog_background_check_enabled"; + public static final String WIFI_IDLE_MS = "wifi_idle_ms"; /** - * The timeout for a background ping - */ - public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_TIMEOUT_MS = - "wifi_watchdog_background_check_timeout_ms"; - - /** - * The delay between background checks. - */ - public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_DELAY_MS = - "wifi_watchdog_background_check_delay_ms"; - - /** * Whether to use static IP and other static network attributes. * <p> * Set to 1 for true and 0 for false. @@ -720,16 +897,11 @@ public final class Settings { public static final String WIFI_STATIC_DNS2 = "wifi_static_dns2"; /** - * User preference for which network(s) should be used. Only the - * connectivity service should touch this. - */ - public static final String NETWORK_PREFERENCE = "network_preference"; - - /** - * Whether bluetooth is enabled/disabled - * 0=disabled. 1=enabled. + * The number of radio channels that are allowed in the local + * 802.11 regulatory domain. + * @hide */ - public static final String BLUETOOTH_ON = "bluetooth_on"; + public static final String WIFI_NUM_ALLOWED_CHANNELS = "wifi_num_allowed_channels"; /** * Determines whether remote devices may discover and/or connect to @@ -756,14 +928,15 @@ public final class Settings { public static final String LOCK_PATTERN_ENABLED = "lock_pattern_autolock"; /** - * Whether the device has been provisioned (0 = false, 1 = true) + * Whether lock pattern is visible as user enters (0 = false, 1 = true) */ - public static final String DEVICE_PROVISIONED = "device_provisioned"; + public static final String LOCK_PATTERN_VISIBLE = "lock_pattern_visible_pattern"; /** - * Whether lock pattern is visible as user enters (0 = false, 1 = true) + * Whether lock pattern will vibrate as user enters (0 = false, 1 = true) */ - public static final String LOCK_PATTERN_VISIBLE = "lock_pattern_visible_pattern"; + public static final String LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED = + "lock_pattern_tactile_feedback_enabled"; /** @@ -773,16 +946,6 @@ public final class Settings { public static final String NEXT_ALARM_FORMATTED = "next_alarm_formatted"; /** - * Comma-separated list of location providers that activities may access. - */ - public static final String LOCATION_PROVIDERS_ALLOWED = "location_providers_allowed"; - - /** - * Whether or not data roaming is enabled. (0 = false, 1 = true) - */ - public static final String DATA_ROAMING = "data_roaming"; - - /** * Scaling factor for fonts, float. */ public static final String FONT_SCALE = "font_scale"; @@ -884,10 +1047,33 @@ public final class Settings { public static final String VOLUME_ALARM = "volume_alarm"; /** + * Notification volume. This is used internally, changing this + * value will not change the volume. See AudioManager. + */ + public static final String VOLUME_NOTIFICATION = "volume_notification"; + + /** + * Whether the notifications should use the ring volume (value of 1) or + * a separate notification volume (value of 0). In most cases, users + * will have this enabled so the notification and ringer volumes will be + * the same. However, power users can disable this and use the separate + * notification volume control. + * <p> + * Note: This is a one-off setting that will be removed in the future + * when there is profile support. For this reason, it is kept hidden + * from the public APIs. + * + * @hide + */ + public static final String NOTIFICATIONS_USE_RING_VOLUME = + "notifications_use_ring_volume"; + + /** * The mapping of stream type (integer) to its setting. */ public static final String[] VOLUME_SETTINGS = { - VOLUME_VOICE, VOLUME_SYSTEM, VOLUME_RING, VOLUME_MUSIC, VOLUME_ALARM + VOLUME_VOICE, VOLUME_SYSTEM, VOLUME_RING, VOLUME_MUSIC, + VOLUME_ALARM, VOLUME_NOTIFICATION }; /** @@ -949,16 +1135,11 @@ public final class Settings { * feature converts two spaces to a "." and space. */ public static final String TEXT_AUTO_PUNCTUATE = "auto_punctuate"; - + /** * Setting to showing password characters in text editors. 1 = On, 0 = Off */ public static final String TEXT_SHOW_PASSWORD = "show_password"; - /** - * USB Mass Storage Enabled - */ - public static final String USB_MASS_STORAGE_ENABLED = - "usb_mass_storage_enabled"; public static final String SHOW_GTALK_SERVICE_STATUS = "SHOW_GTALK_SERVICE_STATUS"; @@ -969,11 +1150,6 @@ public final class Settings { public static final String WALLPAPER_ACTIVITY = "wallpaper_activity"; /** - * Host name and port for a user-selected proxy. - */ - public static final String HTTP_PROXY = "http_proxy"; - - /** * Value to specify if the user prefers the date, time and time zone * to be automatically fetched from the network (NITZ). 1=yes, 0=no */ @@ -995,13 +1171,6 @@ public final class Settings { public static final String DATE_FORMAT = "date_format"; /** - * Settings classname to launch when Settings is clicked from All - * Applications. Needed because of user testing between the old - * and new Settings apps. TODO: 881807 - */ - public static final String SETTINGS_CLASSNAME = "settings_classname"; - - /** * Whether the setup wizard has been run before (on first boot), or if * it still needs to be run. * @@ -1011,85 +1180,279 @@ public final class Settings { public static final String SETUP_WIZARD_HAS_RUN = "setup_wizard_has_run"; /** - * The Android ID (a unique 64-bit value) as a hex string. - * Identical to that obtained by calling - * GoogleLoginService.getAndroidId(); it is also placed here - * so you can get it without binding to a service. + * Scaling factor for normal window animations. Setting to 0 will disable window + * animations. */ - public static final String ANDROID_ID = "android_id"; + public static final String WINDOW_ANIMATION_SCALE = "window_animation_scale"; /** - * The Logging ID (a unique 64-bit value) as a hex string. - * Used as a pseudonymous identifier for logging. + * Scaling factor for activity transition animations. Setting to 0 will disable window + * animations. */ - public static final String LOGGING_ID = "logging_id"; + public static final String TRANSITION_ANIMATION_SCALE = "transition_animation_scale"; /** - * If this setting is set (to anything), then all references - * to Gmail on the device must change to Google Mail. + * Control whether the accelerometer will be used to change screen + * orientation. If 0, it will not be used unless explicitly requested + * by the application; if 1, it will be used by default unless explicitly + * disabled by the application. */ - public static final String USE_GOOGLE_MAIL = "use_google_mail"; + public static final String ACCELEROMETER_ROTATION = "accelerometer_rotation"; /** - * Whether the package installer should allow installation of apps downloaded from - * sources other than the Android Market (vending machine). - * - * 1 = allow installing from other sources - * 0 = only allow installing from the Android Market + * Whether the audible DTMF tones are played by the dialer when dialing. The value is + * boolean (1 or 0). */ - public static final String INSTALL_NON_MARKET_APPS = "install_non_market_apps"; + public static final String DTMF_TONE_WHEN_DIALING = "dtmf_tone"; /** - * Scaling factor for normal window animations. Setting to 0 will disable window - * animations. + * Whether the sounds effects (key clicks, lid open ...) are enabled. The value is + * boolean (1 or 0). */ - public static final String WINDOW_ANIMATION_SCALE = "window_animation_scale"; + public static final String SOUND_EFFECTS_ENABLED = "sound_effects_enabled"; + + // Settings moved to Settings.Secure /** - * Scaling factor for activity transition animations. Setting to 0 will disable window - * animations. + * @deprecated Use {@link android.provider.Settings.Secure#LOCATION_PROVIDERS_ALLOWED} + * instead */ - public static final String TRANSITION_ANIMATION_SCALE = "transition_animation_scale"; + @Deprecated + public static final String ADB_ENABLED = Secure.ADB_ENABLED; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#ANDROID_ID} instead + */ + @Deprecated + public static final String ANDROID_ID = Secure.ANDROID_ID; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#BLUETOOTH_ON} instead + */ + @Deprecated + public static final String BLUETOOTH_ON = Secure.BLUETOOTH_ON; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#DATA_ROAMING} instead + */ + @Deprecated + public static final String DATA_ROAMING = Secure.DATA_ROAMING; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#DEVICE_PROVISIONED} instead + */ + @Deprecated + public static final String DEVICE_PROVISIONED = Secure.DEVICE_PROVISIONED; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#HTTP_PROXY} instead + */ + @Deprecated + public static final String HTTP_PROXY = Secure.HTTP_PROXY; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#INSTALL_NON_MARKET_APPS} instead + */ + @Deprecated + public static final String INSTALL_NON_MARKET_APPS = Secure.INSTALL_NON_MARKET_APPS; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#LOCATION_PROVIDERS_ALLOWED} + * instead + */ + @Deprecated + public static final String LOCATION_PROVIDERS_ALLOWED = Secure.LOCATION_PROVIDERS_ALLOWED; - public static final String PARENTAL_CONTROL_ENABLED = - "parental_control_enabled"; + /** + * @deprecated Use {@link android.provider.Settings.Secure#LOGGING_ID} instead + */ + @Deprecated + public static final String LOGGING_ID = Secure.LOGGING_ID; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#NETWORK_PREFERENCE} instead + */ + @Deprecated + public static final String NETWORK_PREFERENCE = Secure.NETWORK_PREFERENCE; + /** + * @deprecated Use {@link android.provider.Settings.Secure#PARENTAL_CONTROL_ENABLED} + * instead + */ + @Deprecated + public static final String PARENTAL_CONTROL_ENABLED = Secure.PARENTAL_CONTROL_ENABLED; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#PARENTAL_CONTROL_LAST_UPDATE} + * instead + */ + @Deprecated + public static final String PARENTAL_CONTROL_LAST_UPDATE = Secure.PARENTAL_CONTROL_LAST_UPDATE; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#PARENTAL_CONTROL_REDIRECT_URL} + * instead + */ + @Deprecated public static final String PARENTAL_CONTROL_REDIRECT_URL = - "parental_control_redirect_url"; + Secure.PARENTAL_CONTROL_REDIRECT_URL; - public static final String PARENTAL_CONTROL_LAST_UPDATE = - "parental_control_last_update"; + /** + * @deprecated Use {@link android.provider.Settings.Secure#SETTINGS_CLASSNAME} instead + */ + @Deprecated + public static final String SETTINGS_CLASSNAME = Secure.SETTINGS_CLASSNAME; /** - * Whether ADB is enabled. + * @deprecated Use {@link android.provider.Settings.Secure#USB_MASS_STORAGE_ENABLED} instead */ - public static final String ADB_ENABLED = "adb_enabled"; + @Deprecated + public static final String USB_MASS_STORAGE_ENABLED = Secure.USB_MASS_STORAGE_ENABLED; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#USE_GOOGLE_MAIL} instead + */ + @Deprecated + public static final String USE_GOOGLE_MAIL = Secure.USE_GOOGLE_MAIL; + +// /** +// * @deprecated Use {@link android.provider.Settings.Secure#WIFI_MAX_DHCP_RETRY_COUNT} +// * instead +// */ + @Deprecated + public static final String WIFI_MAX_DHCP_RETRY_COUNT = Secure.WIFI_MAX_DHCP_RETRY_COUNT; + +// /** +// * @deprecated Use +// * {@link android.provider.Settings.Secure#WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS} +// * instead +// */ + @Deprecated + public static final String WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS = + Secure.WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS; /** - * Whether the audible DTMF tones are played by the dialer when dialing. The value is - * boolean (1 or 0). + * @deprecated Use + * {@link android.provider.Settings.Secure#WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON} instead */ - public static final String DTMF_TONE_WHEN_DIALING = "dtmf_tone"; + @Deprecated + public static final String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON = + Secure.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON; /** - * Whether the sounds effects (key clicks, lid open ...) are enabled. The value is - * boolean (1 or 0). + * @deprecated Use + * {@link android.provider.Settings.Secure#WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY} instead */ - public static final String SOUND_EFFECTS_ENABLED = "sound_effects_enabled"; - } + @Deprecated + public static final String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY = + Secure.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#WIFI_NUM_OPEN_NETWORKS_KEPT} + * instead + */ + @Deprecated + public static final String WIFI_NUM_OPEN_NETWORKS_KEPT = Secure.WIFI_NUM_OPEN_NETWORKS_KEPT; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#WIFI_ON} instead + */ + @Deprecated + public static final String WIFI_ON = Secure.WIFI_ON; + + /** + * @deprecated Use + * {@link android.provider.Settings.Secure#WIFI_WATCHDOG_ACCEPTABLE_PACKET_LOSS_PERCENTAGE} + * instead + */ + @Deprecated + public static final String WIFI_WATCHDOG_ACCEPTABLE_PACKET_LOSS_PERCENTAGE = + Secure.WIFI_WATCHDOG_ACCEPTABLE_PACKET_LOSS_PERCENTAGE; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#WIFI_WATCHDOG_AP_COUNT} instead + */ + @Deprecated + public static final String WIFI_WATCHDOG_AP_COUNT = Secure.WIFI_WATCHDOG_AP_COUNT; + + /** + * @deprecated Use + * {@link android.provider.Settings.Secure#WIFI_WATCHDOG_BACKGROUND_CHECK_DELAY_MS} instead + */ + @Deprecated + public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_DELAY_MS = + Secure.WIFI_WATCHDOG_BACKGROUND_CHECK_DELAY_MS; + + /** + * @deprecated Use + * {@link android.provider.Settings.Secure#WIFI_WATCHDOG_BACKGROUND_CHECK_ENABLED} instead + */ + @Deprecated + public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_ENABLED = + Secure.WIFI_WATCHDOG_BACKGROUND_CHECK_ENABLED; + + /** + * @deprecated Use + * {@link android.provider.Settings.Secure#WIFI_WATCHDOG_BACKGROUND_CHECK_TIMEOUT_MS} + * instead + */ + @Deprecated + public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_TIMEOUT_MS = + Secure.WIFI_WATCHDOG_BACKGROUND_CHECK_TIMEOUT_MS; + + /** + * @deprecated Use + * {@link android.provider.Settings.Secure#WIFI_WATCHDOG_INITIAL_IGNORED_PING_COUNT} instead + */ + @Deprecated + public static final String WIFI_WATCHDOG_INITIAL_IGNORED_PING_COUNT = + Secure.WIFI_WATCHDOG_INITIAL_IGNORED_PING_COUNT; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#WIFI_WATCHDOG_MAX_AP_CHECKS} + * instead + */ + @Deprecated + public static final String WIFI_WATCHDOG_MAX_AP_CHECKS = Secure.WIFI_WATCHDOG_MAX_AP_CHECKS; + /** + * @deprecated Use {@link android.provider.Settings.Secure#WIFI_WATCHDOG_ON} instead + */ + @Deprecated + public static final String WIFI_WATCHDOG_ON = Secure.WIFI_WATCHDOG_ON; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#WIFI_WATCHDOG_PING_COUNT} instead + */ + @Deprecated + public static final String WIFI_WATCHDOG_PING_COUNT = Secure.WIFI_WATCHDOG_PING_COUNT; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#WIFI_WATCHDOG_PING_DELAY_MS} + * instead + */ + @Deprecated + public static final String WIFI_WATCHDOG_PING_DELAY_MS = Secure.WIFI_WATCHDOG_PING_DELAY_MS; + + /** + * @deprecated Use {@link android.provider.Settings.Secure#WIFI_WATCHDOG_PING_TIMEOUT_MS} + * instead + */ + @Deprecated + public static final String WIFI_WATCHDOG_PING_TIMEOUT_MS = + Secure.WIFI_WATCHDOG_PING_TIMEOUT_MS; + } /** - * Gservices settings, containing the network names for Google's - * various services. This table holds simple name/addr pairs. - * Addresses can be accessed through the getString() method. - * @hide + * Secure system settings, containing system preferences that applications + * can read but are not allowed to write. These are for preferences that + * the user must explicitly modify through the system UI or specialized + * APIs for those values, not modified directly by applications. */ - public static final class Gservices extends NameValueTable { - public static final String SYS_PROP_SETTING_VERSION = "sys.settings_gservices_version"; + public static final class Secure extends NameValueTable { + public static final String SYS_PROP_SETTING_VERSION = "sys.settings_secure_version"; private static volatile NameValueCache mNameValueCache = null; - private static final Object mNameValueCacheLock = new Object(); /** * Look up a name in the database. @@ -1097,13 +1460,11 @@ public final class Settings { * @param name to look up in the table * @return the corresponding value, or null if not present */ - public static String getString(ContentResolver resolver, String name) { - synchronized (mNameValueCacheLock) { - if (mNameValueCache == null) { - mNameValueCache = new NameValueCache(SYS_PROP_SETTING_VERSION, CONTENT_URI); - } - return mNameValueCache.getString(resolver, name); + public synchronized static String getString(ContentResolver resolver, String name) { + if (mNameValueCache == null) { + mNameValueCache = new NameValueCache(SYS_PROP_SETTING_VERSION, CONTENT_URI); } + return mNameValueCache.getString(resolver, name); } /** @@ -1119,184 +1480,566 @@ public final class Settings { } /** - * Look up the value for name in the database, convert it to an int using Integer.parseInt - * and return it. If it is null or if a NumberFormatException is caught during the - * conversion then return defValue. + * Construct the content URI for a particular name/value pair, + * useful for monitoring changes with a ContentObserver. + * @param name to look up in the table + * @return the corresponding content URI, or null if not present */ - public static int getInt(ContentResolver resolver, String name, int defValue) { - String valString = getString(resolver, name); - int value; + public static Uri getUriFor(String name) { + return getUriFor(CONTENT_URI, name); + } + + /** + * Convenience function for retrieving a single secure settings value + * as an integer. Note that internally setting values are always + * stored as strings; this function converts the string to an integer + * for you. The default value will be returned if the setting is + * not defined or not an integer. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to retrieve. + * @param def Value to return if the setting is not defined. + * + * @return The setting's current value, or 'def' if it is not defined + * or not a valid integer. + */ + public static int getInt(ContentResolver cr, String name, int def) { + String v = getString(cr, name); try { - value = valString != null ? Integer.parseInt(valString) : defValue; + return v != null ? Integer.parseInt(v) : def; } catch (NumberFormatException e) { - value = defValue; + return def; } - return value; } /** - * Look up the value for name in the database, convert it to a long using Long.parseLong - * and return it. If it is null or if a NumberFormatException is caught during the - * conversion then return defValue. + * Convenience function for retrieving a single secure settings value + * as an integer. Note that internally setting values are always + * stored as strings; this function converts the string to an integer + * for you. + * <p> + * This version does not take a default value. If the setting has not + * been set, or the string value is not a number, + * it throws {@link SettingNotFoundException}. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to retrieve. + * + * @throws SettingNotFoundException Thrown if a setting by the given + * name can't be found or the setting value is not an integer. + * + * @return The setting's current value. */ - public static long getLong(ContentResolver resolver, String name, long defValue) { - String valString = getString(resolver, name); - long value; + public static int getInt(ContentResolver cr, String name) + throws SettingNotFoundException { + String v = getString(cr, name); try { - value = valString != null ? Long.parseLong(valString) : defValue; + return Integer.parseInt(v); } catch (NumberFormatException e) { - value = defValue; + throw new SettingNotFoundException(name); } - return value; } /** - * Construct the content URI for a particular name/value pair, - * useful for monitoring changes with a ContentObserver. - * @param name to look up in the table - * @return the corresponding content URI, or null if not present + * Convenience function for updating a single settings value as an + * integer. This will either create a new entry in the table if the + * given name does not exist, or modify the value of the existing row + * with that name. Note that internally setting values are always + * stored as strings, so this function converts the given value to a + * string before storing it. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to modify. + * @param value The new value for the setting. + * @return true if the value was set, false on database errors */ - public static Uri getUriFor(String name) { - return getUriFor(CONTENT_URI, name); + public static boolean putInt(ContentResolver cr, String name, int value) { + return putString(cr, name, Integer.toString(value)); } /** - * The content:// style URL for this table + * Convenience function for retrieving a single secure settings value + * as a {@code long}. Note that internally setting values are always + * stored as strings; this function converts the string to a {@code long} + * for you. The default value will be returned if the setting is + * not defined or not a {@code long}. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to retrieve. + * @param def Value to return if the setting is not defined. + * + * @return The setting's current value, or 'def' if it is not defined + * or not a valid {@code long}. */ - public static final Uri CONTENT_URI = - Uri.parse("content://" + AUTHORITY + "/gservices"); + public static long getLong(ContentResolver cr, String name, long def) { + String valString = getString(cr, name); + long value; + try { + value = valString != null ? Long.parseLong(valString) : def; + } catch (NumberFormatException e) { + value = def; + } + return value; + } /** - * MMS - URL to use for HTTP "x-wap-profile" header + * Convenience function for retrieving a single secure settings value + * as a {@code long}. Note that internally setting values are always + * stored as strings; this function converts the string to a {@code long} + * for you. + * <p> + * This version does not take a default value. If the setting has not + * been set, or the string value is not a number, + * it throws {@link SettingNotFoundException}. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to retrieve. + * + * @return The setting's current value. + * @throws SettingNotFoundException Thrown if a setting by the given + * name can't be found or the setting value is not an integer. */ - public static final String MMS_X_WAP_PROFILE_URL - = "mms_x_wap_profile_url"; + public static long getLong(ContentResolver cr, String name) + throws SettingNotFoundException { + String valString = getString(cr, name); + try { + return Long.parseLong(valString); + } catch (NumberFormatException e) { + throw new SettingNotFoundException(name); + } + } /** - * YouTube - "most viewed" url + * Convenience function for updating a secure settings value as a long + * integer. This will either create a new entry in the table if the + * given name does not exist, or modify the value of the existing row + * with that name. Note that internally setting values are always + * stored as strings, so this function converts the given value to a + * string before storing it. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to modify. + * @param value The new value for the setting. + * @return true if the value was set, false on database errors */ - public static final String YOUTUBE_MOST_VIEWED_URL - = "youtube_most_viewed_url"; + public static boolean putLong(ContentResolver cr, String name, long value) { + return putString(cr, name, Long.toString(value)); + } /** - * YouTube - "most recent" url + * Convenience function for retrieving a single secure settings value + * as a floating point number. Note that internally setting values are + * always stored as strings; this function converts the string to an + * float for you. The default value will be returned if the setting + * is not defined or not a valid float. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to retrieve. + * @param def Value to return if the setting is not defined. + * + * @return The setting's current value, or 'def' if it is not defined + * or not a valid float. */ - public static final String YOUTUBE_MOST_RECENT_URL - = "youtube_most_recent_url"; + public static float getFloat(ContentResolver cr, String name, float def) { + String v = getString(cr, name); + try { + return v != null ? Float.parseFloat(v) : def; + } catch (NumberFormatException e) { + return def; + } + } /** - * YouTube - "top favorites" url + * Convenience function for retrieving a single secure settings value + * as a float. Note that internally setting values are always + * stored as strings; this function converts the string to a float + * for you. + * <p> + * This version does not take a default value. If the setting has not + * been set, or the string value is not a number, + * it throws {@link SettingNotFoundException}. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to retrieve. + * + * @throws SettingNotFoundException Thrown if a setting by the given + * name can't be found or the setting value is not a float. + * + * @return The setting's current value. */ - public static final String YOUTUBE_TOP_FAVORITES_URL - = "youtube_top_favorites_url"; + public static float getFloat(ContentResolver cr, String name) + throws SettingNotFoundException { + String v = getString(cr, name); + try { + return Float.parseFloat(v); + } catch (NumberFormatException e) { + throw new SettingNotFoundException(name); + } + } /** - * YouTube - "most discussed" url + * Convenience function for updating a single settings value as a + * floating point number. This will either create a new entry in the + * table if the given name does not exist, or modify the value of the + * existing row with that name. Note that internally setting values + * are always stored as strings, so this function converts the given + * value to a string before storing it. + * + * @param cr The ContentResolver to access. + * @param name The name of the setting to modify. + * @param value The new value for the setting. + * @return true if the value was set, false on database errors */ - public static final String YOUTUBE_MOST_DISCUSSED_URL - = "youtube_most_discussed_url"; + public static boolean putFloat(ContentResolver cr, String name, float value) { + return putString(cr, name, Float.toString(value)); + } /** - * YouTube - "most responded" url + * The content:// style URL for this table */ - public static final String YOUTUBE_MOST_RESPONDED_URL - = "youtube_most_responded_url"; - + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTHORITY + "/secure"); + /** - * YouTube - "most linked" url + * Whether ADB is enabled. */ - public static final String YOUTUBE_MOST_LINKED_URL - = "youtube_most_linked_url"; - + public static final String ADB_ENABLED = "adb_enabled"; + /** - * YouTube - "top rated" url + * Setting to allow mock locations and location provider status to be injected into the + * LocationManager service for testing purposes during application development. These + * locations and status values override actual location and status information generated + * by network, gps, or other location providers. */ - public static final String YOUTUBE_TOP_RATED_URL - = "youtube_top_rated_url"; - + public static final String ALLOW_MOCK_LOCATION = "mock_location"; + /** - * YouTube - "recently featured" url + * The Android ID (a unique 64-bit value) as a hex string. + * Identical to that obtained by calling + * GoogleLoginService.getAndroidId(); it is also placed here + * so you can get it without binding to a service. */ - public static final String YOUTUBE_RECENTLY_FEATURED_URL - = "youtube_recently_featured_url"; - + public static final String ANDROID_ID = "android_id"; + /** - * YouTube - my uploaded videos + * Whether bluetooth is enabled/disabled + * 0=disabled. 1=enabled. */ - public static final String YOUTUBE_MY_VIDEOS_URL - = "youtube_my_videos_url"; - + public static final String BLUETOOTH_ON = "bluetooth_on"; + /** - * YouTube - "my favorite" videos url + * Whether or not data roaming is enabled. (0 = false, 1 = true) */ - public static final String YOUTUBE_MY_FAVORITES_URL - = "youtube_my_favorites_url"; - + public static final String DATA_ROAMING = "data_roaming"; + /** - * YouTube - "by author" videos url -- used for My videos + * Setting to record the input method used by default, holding the ID + * of the desired method. */ - public static final String YOUTUBE_BY_AUTHOR_URL - = "youtube_by_author_url"; - + public static final String DEFAULT_INPUT_METHOD = "default_input_method"; + /** - * YouTube - save a video to favorite videos url + * Whether the device has been provisioned (0 = false, 1 = true) */ - public static final String YOUTUBE_SAVE_TO_FAVORITES_URL - = "youtube_save_to_favorites_url"; - + public static final String DEVICE_PROVISIONED = "device_provisioned"; + + /** + * List of input methods that are currently enabled. This is a string + * containing the IDs of all enabled input methods, each ID separated + * by ':'. + */ + public static final String ENABLED_INPUT_METHODS = "enabled_input_methods"; + /** - * YouTube - "mobile" videos url + * Host name and port for a user-selected proxy. */ - public static final String YOUTUBE_MOBILE_VIDEOS_URL - = "youtube_mobile_videos_url"; + public static final String HTTP_PROXY = "http_proxy"; + + /** + * Whether the package installer should allow installation of apps downloaded from + * sources other than the Android Market (vending machine). + * + * 1 = allow installing from other sources + * 0 = only allow installing from the Android Market + */ + public static final String INSTALL_NON_MARKET_APPS = "install_non_market_apps"; + + /** + * Comma-separated list of location providers that activities may access. + */ + public static final String LOCATION_PROVIDERS_ALLOWED = "location_providers_allowed"; + + /** + * The Logging ID (a unique 64-bit value) as a hex string. + * Used as a pseudonymous identifier for logging. + */ + public static final String LOGGING_ID = "logging_id"; + + /** + * User preference for which network(s) should be used. Only the + * connectivity service should touch this. + */ + public static final String NETWORK_PREFERENCE = "network_preference"; + + /** + */ + public static final String PARENTAL_CONTROL_ENABLED = "parental_control_enabled"; + + /** + */ + public static final String PARENTAL_CONTROL_LAST_UPDATE = "parental_control_last_update"; + + /** + */ + public static final String PARENTAL_CONTROL_REDIRECT_URL = "parental_control_redirect_url"; + + /** + * Settings classname to launch when Settings is clicked from All + * Applications. Needed because of user testing between the old + * and new Settings apps. + */ + // TODO: 881807 + public static final String SETTINGS_CLASSNAME = "settings_classname"; + + /** + * USB Mass Storage Enabled + */ + public static final String USB_MASS_STORAGE_ENABLED = "usb_mass_storage_enabled"; + + /** + * If this setting is set (to anything), then all references + * to Gmail on the device must change to Google Mail. + */ + public static final String USE_GOOGLE_MAIL = "use_google_mail"; + + /** + * Whether to notify the user of open networks. + * <p> + * If not connected and the scan results have an open network, we will + * put this notification up. If we attempt to connect to a network or + * the open network(s) disappear, we remove the notification. When we + * show the notification, we will not show it again for + * {@link android.provider.Settings.Secure#WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY} time. + */ + public static final String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON = + "wifi_networks_available_notification_on"; + + /** + * Delay (in seconds) before repeating the Wi-Fi networks available notification. + * Connecting to a network will reset the timer. + */ + public static final String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY = + "wifi_networks_available_repeat_delay"; + + /** + * The number of radio channels that are allowed in the local + * 802.11 regulatory domain. + * @hide + */ + public static final String WIFI_NUM_ALLOWED_CHANNELS = "wifi_num_allowed_channels"; + + /** + * When the number of open networks exceeds this number, the + * least-recently-used excess networks will be removed. + */ + public static final String WIFI_NUM_OPEN_NETWORKS_KEPT = "wifi_num_open_networks_kept"; + + /** + * Whether the Wi-Fi should be on. Only the Wi-Fi service should touch this. + */ + public static final String WIFI_ON = "wifi_on"; + + /** + * The acceptable packet loss percentage (range 0 - 100) before trying + * another AP on the same network. + */ + public static final String WIFI_WATCHDOG_ACCEPTABLE_PACKET_LOSS_PERCENTAGE = + "wifi_watchdog_acceptable_packet_loss_percentage"; + + /** + * The number of access points required for a network in order for the + * watchdog to monitor it. + */ + public static final String WIFI_WATCHDOG_AP_COUNT = "wifi_watchdog_ap_count"; + + /** + * The delay between background checks. + */ + public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_DELAY_MS = + "wifi_watchdog_background_check_delay_ms"; + + /** + * Whether the Wi-Fi watchdog is enabled for background checking even + * after it thinks the user has connected to a good access point. + */ + public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_ENABLED = + "wifi_watchdog_background_check_enabled"; + + /** + * The timeout for a background ping + */ + public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_TIMEOUT_MS = + "wifi_watchdog_background_check_timeout_ms"; + + /** + * The number of initial pings to perform that *may* be ignored if they + * fail. Again, if these fail, they will *not* be used in packet loss + * calculation. For example, one network always seemed to time out for + * the first couple pings, so this is set to 3 by default. + */ + public static final String WIFI_WATCHDOG_INITIAL_IGNORED_PING_COUNT = + "wifi_watchdog_initial_ignored_ping_count"; + + /** + * The maximum number of access points (per network) to attempt to test. + * If this number is reached, the watchdog will no longer monitor the + * initial connection state for the network. This is a safeguard for + * networks containing multiple APs whose DNS does not respond to pings. + */ + public static final String WIFI_WATCHDOG_MAX_AP_CHECKS = "wifi_watchdog_max_ap_checks"; + + /** + * Whether the Wi-Fi watchdog is enabled. + */ + public static final String WIFI_WATCHDOG_ON = "wifi_watchdog_on"; + + /** + * The number of pings to test if an access point is a good connection. + */ + public static final String WIFI_WATCHDOG_PING_COUNT = "wifi_watchdog_ping_count"; + + /** + * The delay between pings. + */ + public static final String WIFI_WATCHDOG_PING_DELAY_MS = "wifi_watchdog_ping_delay_ms"; + + /** + * The timeout per ping. + */ + public static final String WIFI_WATCHDOG_PING_TIMEOUT_MS = "wifi_watchdog_ping_timeout_ms"; + + /** + * The maximum number of times we will retry a connection to an access + * point for which we have failed in acquiring an IP address from DHCP. + * A value of N means that we will make N+1 connection attempts in all. + * + * @hide pending API Council approval + */ + public static final String WIFI_MAX_DHCP_RETRY_COUNT = "wifi_max_dhcp_retry_count"; + + /** + * Maximum amount of time in milliseconds to hold a wakelock while waiting for mobile + * data connectivity to be established after a disconnect from Wi-Fi. + * + * @hide pending API Council approval + */ + public static final String WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS = + "wifi_mobile_data_transition_wakelock_timeout_ms"; + } + + /** + * Gservices settings, containing the network names for Google's + * various services. This table holds simple name/addr pairs. + * Addresses can be accessed through the getString() method. + * + * TODO: This should move to partner/google/... somewhere. + * + * @hide + */ + public static final class Gservices extends NameValueTable { + public static final String SYS_PROP_SETTING_VERSION = "sys.settings_gservices_version"; /** - * YouTube - search videos url + * Intent action broadcast when the Gservices table is updated by the server. + * This is broadcast once after settings change (so many values may have been updated). */ - public static final String YOUTUBE_SEARCH_URL - = "youtube_search_url"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String CHANGED_ACTION = + "com.google.gservices.intent.action.GSERVICES_CHANGED"; + + private static volatile NameValueCache mNameValueCache = null; + private static final Object mNameValueCacheLock = new Object(); /** - * YouTube - category search videos url + * Look up a name in the database. + * @param resolver to access the database with + * @param name to look up in the table + * @return the corresponding value, or null if not present */ - public static final String YOUTUBE_CATEGORY_SEARCH_URL - = "youtube_category_search_url"; + public static String getString(ContentResolver resolver, String name) { + synchronized (mNameValueCacheLock) { + if (mNameValueCache == null) { + mNameValueCache = new NameValueCache(SYS_PROP_SETTING_VERSION, CONTENT_URI); + } + return mNameValueCache.getString(resolver, name); + } + } /** - * YouTube - url to get the list of categories + * Store a name/value pair into the database. + * @param resolver to access the database with + * @param name to store + * @param value to associate with the name + * @return true if the value was set, false on database errors */ - public static final String YOUTUBE_CATEGORY_LIST_URL - = "youtube_category_list_url"; + public static boolean putString(ContentResolver resolver, + String name, String value) { + return putString(resolver, CONTENT_URI, name, value); + } /** - * YouTube - related videos url + * Look up the value for name in the database, convert it to an int using Integer.parseInt + * and return it. If it is null or if a NumberFormatException is caught during the + * conversion then return defValue. */ - public static final String YOUTUBE_RELATED_VIDEOS_URL - = "youtube_related_videos_url"; + public static int getInt(ContentResolver resolver, String name, int defValue) { + String valString = getString(resolver, name); + int value; + try { + value = valString != null ? Integer.parseInt(valString) : defValue; + } catch (NumberFormatException e) { + value = defValue; + } + return value; + } /** - * YouTube - individual video url + * Look up the value for name in the database, convert it to a long using Long.parseLong + * and return it. If it is null or if a NumberFormatException is caught during the + * conversion then return defValue. */ - public static final String YOUTUBE_INDIVIDUAL_VIDEO_URL - = "youtube_individual_video_url"; + public static long getLong(ContentResolver resolver, String name, long defValue) { + String valString = getString(resolver, name); + long value; + try { + value = valString != null ? Long.parseLong(valString) : defValue; + } catch (NumberFormatException e) { + value = defValue; + } + return value; + } /** - * YouTube - user's playlist url + * Construct the content URI for a particular name/value pair, + * useful for monitoring changes with a ContentObserver. + * @param name to look up in the table + * @return the corresponding content URI, or null if not present */ - public static final String YOUTUBE_MY_PLAYLISTS_URL - = "youtube_my_playlists_url"; + public static Uri getUriFor(String name) { + return getUriFor(CONTENT_URI, name); + } /** - * YouTube - user's subscriptions url + * The content:// style URL for this table */ - public static final String YOUTUBE_MY_SUBSCRIPTIONS_URL - = "youtube_my_subscriptions_url"; + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTHORITY + "/gservices"); /** - * YouTube - the url we use to contact YouTube to get a device id + * MMS - URL to use for HTTP "x-wap-profile" header */ - public static final String YOUTUBE_REGISTER_DEVICE_URL - = "youtube_register_device_url"; + public static final String MMS_X_WAP_PROFILE_URL + = "mms_x_wap_profile_url"; /** * YouTube - the flag to indicate whether to use proxy @@ -1325,7 +2068,8 @@ public final class Settings { * seconds. This allows for throttling of logs when the device is * running for large amounts of time. */ - public static final String MEMCHECK_LOG_REALTIME_INTERVAL = "memcheck_log_realtime_interval"; + public static final String MEMCHECK_LOG_REALTIME_INTERVAL = + "memcheck_log_realtime_interval"; /** * Boolean indicating whether rebooting due to system memory checks @@ -1423,7 +2167,7 @@ public final class Settings { * the device is idle within the window. */ public static final String REBOOT_WINDOW = "reboot_window"; - + /** * The minimum version of the server that is required in order for the device to accept * the server's recommendations about the initial sync settings to use. When this is unset, @@ -1446,6 +2190,12 @@ public final class Settings { public static final String GMAIL_TIMEOUT_MS = "gmail_timeout_ms"; /** + * Controls whether Gmail will request an expedited sync when a message is sent. Value must + * be an integer where non-zero means true. Defaults to 1. + */ + public static final String GMAIL_SEND_IMMEDIATELY = "gmail_send_immediately"; + + /** * Hostname of the GTalk server. */ public static final String GTALK_SERVICE_HOSTNAME = "gtalk_hostname"; @@ -1667,7 +2417,28 @@ public final class Settings { */ public static final String SETTINGS_CONTRIBUTORS_PRETTY_URL = "settings_contributors_pretty_url"; - + + /** + * URL that points to the Terms Of Service for the device. + * <p> + * This should be a pretty http URL. + */ + public static final String SETUP_GOOGLE_TOS_URL = "setup_google_tos_url"; + + /** + * URL that points to the Android privacy policy for the device. + * <p> + * This should be a pretty http URL. + */ + public static final String SETUP_ANDROID_PRIVACY_URL = "setup_android_privacy_url"; + + /** + * URL that points to the Google privacy policy for the device. + * <p> + * This should be a pretty http URL. + */ + public static final String SETUP_GOOGLE_PRIVACY_URL = "setup_google_privacy_url"; + /** * Request an MSISDN token for various Google services. */ @@ -1684,6 +2455,12 @@ public final class Settings { public static final String PARENTAL_CONTROL_CHECK_ENABLED = "parental_control_check_enabled"; + /** + * The list of applications we need to block if parental control is + * enabled. + */ + public static final String PARENTAL_CONTROL_APPS_LIST = + "parental_control_apps_list"; /** * Duration in which parental control status is valid. @@ -1712,7 +2489,7 @@ public final class Settings { */ public static final String DISK_FREE_CHANGE_REPORTING_THRESHOLD = "disk_free_change_reporting_threshold"; - + /** * Prefix for new Google services published by the checkin * server. @@ -1726,24 +2503,39 @@ public final class Settings { */ public static final String SYNC_MAX_RETRY_DELAY_IN_SECONDS = "sync_max_retry_delay_in_seconds"; - + /** * Minimum percentage of free storage on the device that is used to determine if - * the device is running low on storage. + * the device is running low on storage. * Say this value is set to 10, the device is considered running low on storage * if 90% or more of the device storage is filled up. */ - public static final String SYS_STORAGE_THRESHOLD_PERCENTAGE = + public static final String SYS_STORAGE_THRESHOLD_PERCENTAGE = "sys_storage_threshold_percentage"; - + /** - * The interval in minutes after which the amount of free storage left on the + * The interval in minutes after which the amount of free storage left on the * device is logged to the event log */ - public static final String SYS_FREE_STORAGE_LOG_INTERVAL = + public static final String SYS_FREE_STORAGE_LOG_INTERVAL = "sys_free_storage_log_interval"; /** + * The interval in milliseconds at which to check the number of SMS sent + * out without asking for use permit, to limit the un-authorized SMS + * usage. + */ + public static final String SMS_OUTGOING_CEHCK_INTERVAL_MS = + "sms_outgoing_check_interval_ms"; + + /** + * The number of outgoing SMS sent without asking for user permit + * (of {@link #SMS_OUTGOING_CEHCK_INTERVAL_MS} + */ + public static final String SMS_OUTGOING_CEHCK_MAX_COUNT = + "sms_outgoing_check_max_count"; + + /** * The interval in milliseconds at which to check packet counts on the * mobile data interface when screen is on, to detect possible data * connection problems. @@ -1757,22 +2549,22 @@ public final class Settings { * connection problems. */ public static final String PDP_WATCHDOG_LONG_POLL_INTERVAL_MS = - "pdp_watchdog_long_poll_interval_ms"; - + "pdp_watchdog_long_poll_interval_ms"; + /** * The interval in milliseconds at which to check packet counts on the * mobile data interface after {@link #PDP_WATCHDOG_TRIGGER_PACKET_COUNT} * outgoing packets has been reached without incoming packets. */ - public static final String PDP_WATCHDOG_ERROR_POLL_INTERVAL_MS = + public static final String PDP_WATCHDOG_ERROR_POLL_INTERVAL_MS = "pdp_watchdog_error_poll_interval_ms"; /** * The number of outgoing packets sent without seeing an incoming packet - * that triggers a countdown (of {@link #PDP_WATCHDOG_ERROR_POLL_COUNT} + * that triggers a countdown (of {@link #PDP_WATCHDOG_ERROR_POLL_COUNT} * device is logged to the event log */ - public static final String PDP_WATCHDOG_TRIGGER_PACKET_COUNT = + public static final String PDP_WATCHDOG_TRIGGER_PACKET_COUNT = "pdp_watchdog_trigger_packet_count"; /** @@ -1780,50 +2572,44 @@ public final class Settings { * after hitting {@link #PDP_WATCHDOG_TRIGGER_PACKET_COUNT} before * attempting data connection recovery. */ - public static final String PDP_WATCHDOG_ERROR_POLL_COUNT = + public static final String PDP_WATCHDOG_ERROR_POLL_COUNT = "pdp_watchdog_error_poll_count"; /** * The number of failed PDP reset attempts before moving to something more * drastic: re-registering to the network. */ - public static final String PDP_WATCHDOG_MAX_PDP_RESET_FAIL_COUNT = + public static final String PDP_WATCHDOG_MAX_PDP_RESET_FAIL_COUNT = "pdp_watchdog_max_pdp_reset_fail_count"; /** * Address to ping as a last sanity check before attempting any recovery. * Unset or set to "0.0.0.0" to skip this check. */ - public static final String PDP_WATCHDOG_PING_ADDRESS = - "pdp_watchdog_ping_address"; + public static final String PDP_WATCHDOG_PING_ADDRESS = "pdp_watchdog_ping_address"; /** * The "-w deadline" parameter for the ping, ie, the max time in * seconds to spend pinging. */ - public static final String PDP_WATCHDOG_PING_DEADLINE = - "pdp_watchdog_ping_deadline"; + public static final String PDP_WATCHDOG_PING_DEADLINE = "pdp_watchdog_ping_deadline"; /** - * The interval in milliseconds after which Wi-Fi is considered idle. - * When idle, it is possible for the device to be switched from Wi-Fi to - * the mobile data network. - */ - public static final String WIFI_IDLE_MS = "wifi_idle_ms"; - - /** - * The interval in milliseconds at which we forcefully release the - * transition-to-mobile-data wake lock. + * The interval in milliseconds at which to check gprs registration + * after the first registration mismatch of gprs and voice service, + * to detect possible data network registration problems. + * */ - public static final String WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS = - "wifi_mobile_data_transition_wakelock_timeout_ms"; + public static final String GPRS_REGISTER_CHECK_PERIOD_MS = + "gprs_register_check_period_ms"; /** - * The maximum number of times we will retry a connection to an access - * point for which we have failed in acquiring an IP address from DHCP. - * A value of N means that we will make N+1 connection attempts in all. + * Screen timeout in milliseconds corresponding to the + * PowerManager's POKE_LOCK_SHORT_TIMEOUT flag (i.e. the fastest + * possible screen timeout behavior.) */ - public static final String WIFI_MAX_DHCP_RETRY_COUNT = "wifi_max_dhcp_retry_count"; + public static final String SHORT_KEYLIGHT_DELAY_MS = + "short_keylight_delay_ms"; /** * @deprecated diff --git a/core/java/android/provider/Sync.java b/core/java/android/provider/Sync.java index b889293..2086a5d 100644 --- a/core/java/android/provider/Sync.java +++ b/core/java/android/provider/Sync.java @@ -489,9 +489,14 @@ public final class Sync { */ public static final Uri CONTENT_URI = Uri.parse("content://sync/settings"); - /** controls whether or not the devices listens for sync tickles */ + /** controls whether or not the device listens for sync tickles */ public static final String SETTING_LISTEN_FOR_TICKLES = "listen_for_tickles"; + /** controls whether or not the device connect to Google in background for various + * stuff, including GTalk, checkin, Market and data sync ... + */ + public static final String SETTING_BACKGROUND_DATA = "background_data"; + /** controls whether or not the individual provider is synced when tickles are received */ public static final String SETTING_SYNC_PROVIDER_PREFIX = "sync_provider_"; @@ -529,17 +534,28 @@ public final class Sync { } /** - * A convenience method to set whether or not the tickle xmpp connection - * should be established. + * A convenience method to set whether or not the device should listen to tickles. * * @param contentResolver the ContentResolver to use to access the settings table - * @param flag true if the tickle xmpp connection should be established + * @param flag true if it should listen. */ static public void setListenForNetworkTickles(ContentResolver contentResolver, boolean flag) { putBoolean(contentResolver, SETTING_LISTEN_FOR_TICKLES, flag); } + /** + * A convenience method to set whether or not the device should connect to Google + * in background. + * + * @param contentResolver the ContentResolver to use to access the settings table + * @param flag true if it should connect. + */ + static public void setBackgroundData(ContentResolver contentResolver, + boolean flag) { + putBoolean(contentResolver, SETTING_BACKGROUND_DATA, flag); + } + public static class QueryMap extends ContentQueryMap { private ContentResolver mContentResolver; @@ -570,23 +586,42 @@ public final class Sync { } /** - * Set whether or not the tickle xmpp connection should be established. + * Set whether or not the device should listen for tickles. * - * @param flag true if the tickle xmpp connection should be established + * @param flag true if it should listen. */ public void setListenForNetworkTickles(boolean flag) { Settings.setListenForNetworkTickles(mContentResolver, flag); } /** - * Check if the tickle xmpp connection should be established - * @return true if it should be stablished + * Check if the device should listen to tickles. + + * @return true if it should */ public boolean getListenForNetworkTickles() { return getBoolean(SETTING_LISTEN_FOR_TICKLES, true); } /** + * Set whether or not the device should connect to Google in background + * + * @param flag true if it should + */ + public void setBackgroundData(boolean flag) { + Settings.setBackgroundData(mContentResolver, flag); + } + + /** + * Check if the device should connect to Google in background. + + * @return true if it should + */ + public boolean getBackgroundData() { + return getBoolean(SETTING_BACKGROUND_DATA, true); + } + + /** * Convenience function for retrieving a single settings value * as a boolean. * diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index 776a266..18c64ed 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -16,7 +16,6 @@ package android.provider; -import com.android.internal.telephony.CallerInfo; import com.google.android.mms.util.SqliteWrapper; import android.annotation.SdkConstant; @@ -27,8 +26,6 @@ import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; -import android.telephony.PhoneNumberUtils; -import android.telephony.TelephonyManager; import android.telephony.gsm.SmsMessage; import android.text.TextUtils; import android.text.util.Regex; @@ -264,49 +261,6 @@ public final class Telephony { } /** - * Returns true if the address is an email address - * - * @param address the input address to be tested - * @return true if address is an email address - */ - public static boolean isEmailAddress(String address) { - /* - * The '@' char isn't a valid char in phone numbers. However, in SMS - * messages sent by carrier, the originating-address can contain - * non-dialable alphanumeric chars. For the purpose of thread id - * grouping, we don't care about those. We only care about the - * legitmate/dialable phone numbers (which we use the special phone - * number comparison) and email addresses (which we do straight up - * string comparison). - */ - return (address != null) && (address.indexOf('@') != -1); - } - - /** - * Formats an address for displaying, doing a phone number lookup in the - * Address Book, etc. - * - * @param context the context to use - * @param address the address to format - * @return a nicely formatted version of the sender to display - */ - public static String getDisplayAddress(Context context, String address) { - String result; - int index; - if (isEmailAddress(address)) { - index = address.indexOf('@'); - if (index != -1) { - result = address.substring(0, index); - } else { - result = address; - } - } else { - result = CallerInfo.getCallerId(context, address); - } - return result; - } - - /** * Contains all text based SMS messages in the SMS app's inbox. */ public static final class Inbox implements BaseColumns, TextBasedSmsColumns { @@ -1166,7 +1120,7 @@ public final class Telephony { * name-addr = [display-name] angle-addr * angle-addr = [CFWS] "<" addr-spec ">" [CFWS] */ - private static final Pattern NAME_ADDR_EMAIL_PATTERN = + public static final Pattern NAME_ADDR_EMAIL_PATTERN = Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*"); /** @@ -1174,7 +1128,7 @@ public final class Telephony { * DQUOTE *([FWS] qcontent) [FWS] DQUOTE * [CFWS] */ - private static final Pattern QUOTED_STRING_PATTERN = + public static final Pattern QUOTED_STRING_PATTERN = Pattern.compile("\\s*\"([^\"]*)\"\\s*"); public static final Cursor query( @@ -1232,81 +1186,6 @@ public final class Telephony { } /** - * Formats an address for displaying, doing a phone number lookup in the - * Address Book, etc. - * - * @param context the context to use - * @param address the address to format - * @return a nicely formatted version of the sender to display - */ - public static String getDisplayAddress(Context context, String address) { - if (address == null) { - return ""; - } - - String localNumber = TelephonyManager.getDefault().getLine1Number(); - String[] values = address.split(";"); - String result = ""; - for (int i = 0; i < values.length; i++) { - if (values[i].length() > 0) { - if (PhoneNumberUtils.compare(values[i], localNumber)) { - result = result + ";" - + context.getString(com.android.internal.R.string.me); - } else if (isEmailAddress(values[i])) { - result = result + ";" + getDisplayName(context, values[i]); - } else { - result = result + ";" + CallerInfo.getCallerId(context, values[i]); - } - } - } - - if (result.length() > 0) { - // Skip the first ';' - return result.substring(1); - } - return result; - } - - private static String getEmailDisplayName(String displayString) { - Matcher match = QUOTED_STRING_PATTERN.matcher(displayString); - if (match.matches()) { - return match.group(1); - } - - return displayString; - } - - private static String getDisplayName(Context context, String email) { - Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(email); - if (match.matches()) { - // email has display name - return getEmailDisplayName(match.group(1)); - } - - Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(), - Contacts.ContactMethods.CONTENT_EMAIL_URI, - new String[] { Contacts.ContactMethods.NAME }, - Contacts.ContactMethods.DATA + " = \'" + email + "\'", - null, null); - - if (cursor != null) { - try { - int columnIndex = cursor.getColumnIndexOrThrow( - Contacts.ContactMethods.NAME); - while (cursor.moveToNext()) { - String name = cursor.getString(columnIndex); - if (!TextUtils.isEmpty(name)) { - return name; - } - } - } finally { - cursor.close(); - } - } - return email; - } - - /** * Contains all MMS messages in the MMS app's inbox. */ public static final class Inbox implements BaseMmsColumns { @@ -1647,6 +1526,7 @@ public final class Telephony { public static final String TYPE = "type"; + public static final String CURRENT = "current"; } public static final class Intents { diff --git a/core/java/android/security/Md5MessageDigest.java b/core/java/android/security/Md5MessageDigest.java index a7221ae..4fe0cb0 100644 --- a/core/java/android/security/Md5MessageDigest.java +++ b/core/java/android/security/Md5MessageDigest.java @@ -17,8 +17,7 @@ package android.security; /** - * This is a temporary class to provide SHA-1 hash. - * It's not meant to be correct, and eventually doesn't belong in java.security + * Provides the MD5 hash encryption. */ public class Md5MessageDigest extends MessageDigest { diff --git a/core/java/android/security/MessageDigest.java b/core/java/android/security/MessageDigest.java index 93040b9..cf2d0fe 100644 --- a/core/java/android/security/MessageDigest.java +++ b/core/java/android/security/MessageDigest.java @@ -18,8 +18,22 @@ package android.security; import java.security.NoSuchAlgorithmException; +/** + * Base class for producing a message digest from different hash encryptions. + */ public abstract class MessageDigest { + /** + * Returns a digest object of the specified type. + * + * @param algorithm The type of hash function to use. Valid values are + * <em>SHA-1</em> and <em>MD5</em>. + * @return The respective MessageDigest object. Either a + * {@link android.security.Sha1MessageDigest} or + * {@link android.security.Md5MessageDigest} object. + * @throws NoSuchAlgorithmException If an invalid <var>algorithm</var> + * is given. + */ public static MessageDigest getInstance(String algorithm) throws NoSuchAlgorithmException { @@ -39,5 +53,12 @@ public abstract class MessageDigest public abstract void update(byte[] input); public abstract byte[] digest(); + + /** + * Produces a message digest for the given input. + * + * @param input The message to encrypt. + * @return The digest (hash sum). + */ public abstract byte[] digest(byte[] input); } diff --git a/core/java/android/security/Sha1MessageDigest.java b/core/java/android/security/Sha1MessageDigest.java index 3b3fd6a..aa01fa6 100644 --- a/core/java/android/security/Sha1MessageDigest.java +++ b/core/java/android/security/Sha1MessageDigest.java @@ -17,8 +17,7 @@ package android.security; /** - * This is a temporary class to provide SHA-1 hash. - * It's not meant to be correct, and eventually doesn't belong in java.security + * Provides the SHA-1 hash encyption. */ public class Sha1MessageDigest extends MessageDigest { diff --git a/core/java/android/security/package.html b/core/java/android/security/package.html index 26b8a32..dfc6303 100644 --- a/core/java/android/security/package.html +++ b/core/java/android/security/package.html @@ -1,5 +1,6 @@ <HTML> <BODY> +Utilities for encrypting messages from hash functions. {@hide} </BODY> -</HTML>
\ No newline at end of file +</HTML> diff --git a/core/java/android/server/BluetoothA2dpService.java b/core/java/android/server/BluetoothA2dpService.java new file mode 100644 index 0000000..3cbb855 --- /dev/null +++ b/core/java/android/server/BluetoothA2dpService.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * TODO: Move this to + * java/services/com/android/server/BluetoothA2dpService.java + * and make the contructor package private again. + * @hide + */ + +package android.server; + +import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothError; +import android.bluetooth.BluetoothIntent; +import android.bluetooth.IBluetoothA2dp; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.media.AudioManager; +import android.os.Binder; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.HashMap; +import java.util.Iterator; + +public class BluetoothA2dpService extends IBluetoothA2dp.Stub { + private static final String TAG = "BluetoothDeviceService"; + private static final boolean DBG = true; + + public static final String BLUETOOTH_A2DP_SERVICE = "bluetooth_a2dp"; + + private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; + private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; + + private final Context mContext; + private final IntentFilter mIntentFilter; + private HashMap<String, SinkState> mAudioDevices; + private final AudioManager mAudioManager; + + private class SinkState { + public String address; + public int state; + public SinkState(String a, int s) {address = a; state = s;} + } + + public BluetoothA2dpService(Context context) { + mContext = context; + + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + + BluetoothDevice device = + (BluetoothDevice)mContext.getSystemService(Context.BLUETOOTH_SERVICE); + if (device == null) { + throw new RuntimeException("Platform does not support Bluetooth"); + } + + if (!initNative()) { + throw new RuntimeException("Could not init BluetoothA2dpService"); + } + + mIntentFilter = new IntentFilter(BluetoothIntent.ENABLED_ACTION); + mIntentFilter.addAction(BluetoothIntent.DISABLED_ACTION); + mContext.registerReceiver(mReceiver, mIntentFilter); + + if (device.isEnabled()) { + onBluetoothEnable(); + } + } + + @Override + protected void finalize() throws Throwable { + try { + cleanupNative(); + } finally { + super.finalize(); + } + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(BluetoothIntent.ENABLED_ACTION)) { + onBluetoothEnable(); + } else if (action.equals(BluetoothIntent.DISABLED_ACTION)) { + onBluetoothDisable(); + } + } + }; + + private synchronized void onBluetoothEnable() { + mAudioDevices = new HashMap<String, SinkState>(); + String[] paths = (String[])listHeadsetsNative(); + if (paths != null) { + for (String path : paths) { + mAudioDevices.put(path, new SinkState(getAddressNative(path), + isSinkConnectedNative(path) ? BluetoothA2dp.STATE_CONNECTED : + BluetoothA2dp.STATE_DISCONNECTED)); + } + } + } + + private synchronized void onBluetoothDisable() { + mAudioDevices = null; + } + + public synchronized int connectSink(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (DBG) log("connectSink(" + address + ")"); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return BluetoothError.ERROR; + } + if (mAudioDevices == null) { + return BluetoothError.ERROR; + } + String path = lookupPath(address); + if (path == null) { + path = createHeadsetNative(address); + if (DBG) log("new bluez sink: " + address + " (" + path + ")"); + } + if (path == null) { + return BluetoothError.ERROR; + } + if (!connectSinkNative(path)) { + return BluetoothError.ERROR; + } else { + updateState(path, BluetoothA2dp.STATE_CONNECTING); + return BluetoothError.SUCCESS; + } + } + + public synchronized int disconnectSink(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (DBG) log("disconnectSink(" + address + ")"); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return BluetoothError.ERROR; + } + if (mAudioDevices == null) { + return BluetoothError.ERROR; + } + String path = lookupPath(address); + if (path == null) { + return BluetoothError.ERROR; + } + if (!disconnectSinkNative(path)) { + return BluetoothError.ERROR; + } else { + updateState(path, BluetoothA2dp.STATE_DISCONNECTING); + return BluetoothError.SUCCESS; + } + } + + public synchronized List<String> listConnectedSinks() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + List<String> connectedSinks = new ArrayList<String>(); + if (mAudioDevices == null) { + return connectedSinks; + } + for (SinkState sink : mAudioDevices.values()) { + if (sink.state == BluetoothA2dp.STATE_CONNECTED || + sink.state == BluetoothA2dp.STATE_PLAYING) { + connectedSinks.add(sink.address); + } + } + return connectedSinks; + } + + public synchronized int getSinkState(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return BluetoothError.ERROR; + } + if (mAudioDevices == null) { + return BluetoothA2dp.STATE_DISCONNECTED; + } + for (SinkState sink : mAudioDevices.values()) { + if (address.equals(sink.address)) { + return sink.state; + } + } + return BluetoothA2dp.STATE_DISCONNECTED; + } + + public synchronized void onHeadsetCreated(String path) { + updateState(path, BluetoothA2dp.STATE_DISCONNECTED); + } + + public synchronized void onHeadsetRemoved(String path) { + if (mAudioDevices == null) return; + mAudioDevices.remove(path); + } + + public synchronized void onSinkConnected(String path) { + if (mAudioDevices == null) return; + // bluez 3.36 quietly disconnects the previous sink when a new sink + // is connected, so we need to mark all previously connected sinks as + // disconnected + for (String oldPath : mAudioDevices.keySet()) { + if (path.equals(oldPath)) { + continue; + } + int state = mAudioDevices.get(oldPath).state; + if (state == BluetoothA2dp.STATE_CONNECTED || state == BluetoothA2dp.STATE_PLAYING) { + updateState(path, BluetoothA2dp.STATE_DISCONNECTED); + } + } + + mAudioManager.setBluetoothA2dpOn(true); + updateState(path, BluetoothA2dp.STATE_CONNECTED); + } + + public synchronized void onSinkDisconnected(String path) { + mAudioManager.setBluetoothA2dpOn(false); + updateState(path, BluetoothA2dp.STATE_DISCONNECTED); + } + + public synchronized void onSinkPlaying(String path) { + updateState(path, BluetoothA2dp.STATE_PLAYING); + } + + public synchronized void onSinkStopped(String path) { + updateState(path, BluetoothA2dp.STATE_CONNECTED); + } + + private synchronized final String lookupAddress(String path) { + if (mAudioDevices == null) return null; + String address = mAudioDevices.get(path).address; + if (address == null) Log.e(TAG, "Can't find address for " + path); + return address; + } + + private synchronized final String lookupPath(String address) { + if (mAudioDevices == null) return null; + + for (String path : mAudioDevices.keySet()) { + if (address.equals(mAudioDevices.get(path).address)) { + return path; + } + } + return null; + } + + private synchronized void updateState(String path, int state) { + if (mAudioDevices == null) return; + + SinkState s = mAudioDevices.get(path); + int prevState; + String address; + if (s == null) { + address = getAddressNative(path); + mAudioDevices.put(path, new SinkState(address, state)); + prevState = BluetoothA2dp.STATE_DISCONNECTED; + } else { + address = lookupAddress(path); + prevState = s.state; + s.state = state; + } + + if (DBG) log("state " + address + " (" + path + ") " + prevState + "->" + state); + + Intent intent = new Intent(BluetoothA2dp.SINK_STATE_CHANGED_ACTION); + intent.putExtra(BluetoothIntent.ADDRESS, address); + intent.putExtra(BluetoothA2dp.SINK_PREVIOUS_STATE, prevState); + intent.putExtra(BluetoothA2dp.SINK_STATE, state); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + } + + @Override + protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mAudioDevices == null) return; + pw.println("Cached audio devices:"); + for (String path : mAudioDevices.keySet()) { + SinkState sink = mAudioDevices.get(path); + pw.println(path + " " + sink.address + " " + BluetoothA2dp.stateToString(sink.state)); + } + } + + private static void log(String msg) { + Log.d(TAG, msg); + } + + private native boolean initNative(); + private native void cleanupNative(); + private synchronized native String[] listHeadsetsNative(); + private synchronized native String createHeadsetNative(String address); + private synchronized native boolean removeHeadsetNative(String path); + private synchronized native String getAddressNative(String path); + private synchronized native boolean connectSinkNative(String path); + private synchronized native boolean disconnectSinkNative(String path); + private synchronized native boolean isSinkConnectedNative(String path); + +} diff --git a/core/java/android/server/BluetoothDeviceService.java b/core/java/android/server/BluetoothDeviceService.java index 10f9f7c..9bdab9f 100644 --- a/core/java/android/server/BluetoothDeviceService.java +++ b/core/java/android/server/BluetoothDeviceService.java @@ -54,13 +54,17 @@ import java.util.HashMap; public class BluetoothDeviceService extends IBluetoothDevice.Stub { private static final String TAG = "BluetoothDeviceService"; private int mNativeData; - private Context mContext; private BluetoothEventLoop mEventLoop; private IntentFilter mIntentFilter; private boolean mIsAirplaneSensitive; private volatile boolean mIsEnabled; // local cache of isEnabledNative() private boolean mIsDiscovering; + private final Context mContext; + + private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; + private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; + static { classInitNative(); } @@ -97,7 +101,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { private native void cleanupNativeDataNative(); public boolean isEnabled() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return mIsEnabled; } private native int isEnabledNative(); @@ -106,7 +110,8 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * Disable bluetooth. Returns true on success. */ public synchronized boolean disable() { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); if (mEnableThread != null && mEnableThread.isAlive()) { return false; @@ -117,9 +122,10 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { mEventLoop.stop(); disableNative(); mIsEnabled = false; + Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.BLUETOOTH_ON, 0); mIsDiscovering = false; Intent intent = new Intent(BluetoothIntent.DISABLED_ACTION); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); return true; } @@ -131,7 +137,8 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * notified when complete. */ public synchronized boolean enable(IBluetoothDeviceCallback callback) { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); // Airplane mode can prevent Bluetooth radio from being turned on. if (mIsAirplaneSensitive && isAirplaneModeOn()) { @@ -164,6 +171,8 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { }; private EnableThread mEnableThread; + private String mOutgoingBondingDevAddress = null; + private class EnableThread extends Thread { private final IBluetoothDeviceCallback mEnableCallback; public EnableThread(IBluetoothDeviceCallback callback) { @@ -185,9 +194,11 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { if (res) { mIsEnabled = true; + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.BLUETOOTH_ON, 1); mIsDiscovering = false; Intent intent = new Intent(BluetoothIntent.ENABLED_ACTION); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); mHandler.sendMessageDelayed(mHandler.obtainMessage(REGISTER_SDP_RECORDS), 3000); } mEnableThread = null; @@ -198,19 +209,20 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { private native int disableNative(); public synchronized String getAddress() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getAddressNative(); } private native String getAddressNative(); public synchronized String getName() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getNameNative(); } private native String getNameNative(); public synchronized boolean setName(String name) { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); if (name == null) { return false; } @@ -220,19 +232,19 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { private native boolean setNameNative(String name); public synchronized String[] listBondings() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return listBondingsNative(); } private native String[] listBondingsNative(); public synchronized String getMajorClass() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getMajorClassNative(); } private native String getMajorClassNative(); public synchronized String getMinorClass() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getMinorClassNative(); } private native String getMinorClassNative(); @@ -248,7 +260,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @return The user-friendly name of the specified remote device. */ public synchronized String getRemoteName(String address) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } @@ -277,7 +289,8 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * false otherwise. */ public synchronized boolean startDiscovery(boolean resolveNames) { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); return startDiscoveryNative(resolveNames); } private native boolean startDiscoveryNative(boolean resolveNames); @@ -289,13 +302,14 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * started. */ public synchronized boolean cancelDiscovery() { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); return cancelDiscoveryNative(); } private native boolean cancelDiscoveryNative(); public synchronized boolean isDiscovering() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return mIsDiscovering; } @@ -304,19 +318,21 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { } public synchronized boolean startPeriodicDiscovery() { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); return startPeriodicDiscoveryNative(); } private native boolean startPeriodicDiscoveryNative(); public synchronized boolean stopPeriodicDiscovery() { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); return stopPeriodicDiscoveryNative(); } private native boolean stopPeriodicDiscoveryNative(); public synchronized boolean isPeriodicDiscovery() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return isPeriodicDiscoveryNative(); } private native boolean isPeriodicDiscoveryNative(); @@ -331,7 +347,8 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @param timeout_s The discoverable timeout in seconds. */ public synchronized boolean setDiscoverableTimeout(int timeout) { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); return setDiscoverableTimeoutNative(timeout); } private native boolean setDiscoverableTimeoutNative(int timeout_s); @@ -345,13 +362,13 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * value indicates an error. */ public synchronized int getDiscoverableTimeout() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getDiscoverableTimeoutNative(); } private native int getDiscoverableTimeoutNative(); public synchronized boolean isAclConnected(String address) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } @@ -378,7 +395,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @see #setMode */ public synchronized boolean isConnectable() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return isConnectableNative(); } private native boolean isConnectableNative(); @@ -401,7 +418,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @see #setMode */ public synchronized boolean isDiscoverable() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return isDiscoverableNative(); } private native boolean isDiscoverableNative(); @@ -415,7 +432,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @see #setMode */ public synchronized int getMode() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); String mode = getModeNative(); if (mode == null) { return BluetoothDevice.MODE_UNKNOWN; @@ -451,7 +468,8 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @see #getMode */ public synchronized boolean setMode(int mode) { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); switch (mode) { case BluetoothDevice.MODE_OFF: return setModeNative("off"); @@ -477,7 +495,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @return The alias of the remote device. */ public synchronized String getRemoteAlias(String address) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } @@ -496,7 +514,8 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @param alias Alias for the remote device */ public synchronized boolean setRemoteAlias(String address, String alias) { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); if (alias == null || !BluetoothDevice.checkBluetoothAddress(address)) { return false; } @@ -513,7 +532,8 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @param address Bluetooth address of remote device */ public synchronized boolean clearRemoteAlias(String address) { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } @@ -522,7 +542,8 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { private native boolean clearRemoteAliasNative(String address); public synchronized boolean disconnectRemoteDeviceAcl(String address) { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } @@ -546,7 +567,8 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @see android.bluetooth.PasskeyAgent */ public synchronized boolean createBonding(String address, IBluetoothDeviceCallback callback) { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } @@ -568,9 +590,19 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { callbacks.remove(address); return false; } + mOutgoingBondingDevAddress = address; return true; } + private native boolean createBondingNative(String address, int timeout_ms); + + /*package*/ String getOutgoingBondingDevAddress() { + return mOutgoingBondingDevAddress; + } + + /*package*/ void setOutgoingBondingDevAddress(String outgoingBondingDevAddress) { + mOutgoingBondingDevAddress = outgoingBondingDevAddress; + } /** * This method cancels a pending bonding request. @@ -593,7 +625,8 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @see #listBondings */ public synchronized boolean cancelBondingProcess(String address) { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } @@ -618,7 +651,8 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @see #listBondings */ public synchronized boolean removeBonding(String address) { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } @@ -627,7 +661,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { private native boolean removeBondingNative(String address); public synchronized boolean hasBonding(String address) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } @@ -636,7 +670,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { private native boolean hasBondingNative(String address); public synchronized String[] listAclConnections() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return listConnectionsNative(); } private native String[] listConnectionsNative(); @@ -652,7 +686,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * remote devices that this adapter is aware of. */ public synchronized String[] listRemoteDevices() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return listRemoteDevicesNative(); } private native String[] listRemoteDevicesNative(); @@ -666,7 +700,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * Bluetooth-chip version. */ public synchronized String getVersion() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getVersionNative(); } private native String getVersionNative(); @@ -683,7 +717,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @return The HCI revision of this adapter. */ public synchronized String getRevision() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getRevisionNative(); } private native String getRevisionNative(); @@ -697,7 +731,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @return Manufacturer name. */ public synchronized String getManufacturer() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getManufacturerNative(); } private native String getManufacturerNative(); @@ -716,7 +750,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @return company name */ public synchronized String getCompany() { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getCompanyNative(); } private native String getCompanyNative(); @@ -731,7 +765,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @see #getVersion */ public synchronized String getRemoteVersion(String address) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } @@ -749,7 +783,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @see #getRevision */ public synchronized String getRemoteRevision(String address) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } @@ -767,7 +801,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @see #getManufacturer */ public synchronized String getRemoteManufacturer(String address) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } @@ -785,7 +819,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @see #getCompany */ public synchronized String getRemoteCompany(String address) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } @@ -801,7 +835,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @return a String with the timestamp. */ public synchronized String lastSeen(String address) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } @@ -817,7 +851,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @return a String with the timestamp. */ public synchronized String lastUsed(String address) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } @@ -841,7 +875,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { */ public synchronized String getRemoteMajorClass(String address) { if (!BluetoothDevice.checkBluetoothAddress(address)) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return null; } return getRemoteMajorClassNative(address); @@ -863,7 +897,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @see #getRemoteClass */ public synchronized String getRemoteMinorClass(String address) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } @@ -880,7 +914,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @see #getRemoteClass */ public synchronized String[] getRemoteServiceClasses(String address) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } @@ -904,7 +938,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { */ public synchronized int getRemoteClass(String address) { if (!BluetoothDevice.checkBluetoothAddress(address)) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return -1; } return getRemoteClassNative(address); @@ -919,7 +953,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @return byte array of features. */ public synchronized byte[] getRemoteFeatures(String address) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } @@ -944,7 +978,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @see #getRemoteServiceRecord */ public synchronized int[] getRemoteServiceHandles(String address, String match) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } @@ -974,7 +1008,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { * @see #getRemoteServiceHandles */ public synchronized byte[] getRemoteServiceRecord(String address, int handle) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } @@ -985,7 +1019,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { // AIDL does not yet support short's public synchronized boolean getRemoteServiceChannel(String address, int uuid16, IBluetoothDeviceCallback callback) { - checkPermissionBluetooth(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } @@ -1011,7 +1045,8 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { private native boolean getRemoteServiceChannelNative(String address, short uuid16); public synchronized boolean setPin(String address, byte[] pin) { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); if (pin == null || pin.length <= 0 || pin.length > 16 || !BluetoothDevice.checkBluetoothAddress(address)) { return false; @@ -1036,7 +1071,8 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { private native boolean setPinNative(String address, String pin, int nativeData); public synchronized boolean cancelPin(String address) { - checkPermissionBluetoothAdmin(); + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } @@ -1061,7 +1097,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { // some random app is not sending this intent and disabling bluetooth boolean enabled = !isAirplaneModeOn(); // If bluetooth is currently expected to be on, then enable or disable bluetooth - if (Settings.System.getInt(resolver, Settings.System.BLUETOOTH_ON, 0) > 0) { + if (Settings.Secure.getInt(resolver, Settings.Secure.BLUETOOTH_ON, 0) > 0) { if (enabled) { enable(null); } else { @@ -1089,25 +1125,6 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { Settings.System.AIRPLANE_MODE_ON, 0) == 1; } - private static final String BLUETOOTH_ADMIN = android.Manifest.permission.BLUETOOTH_ADMIN; - private static final String BLUETOOTH = android.Manifest.permission.BLUETOOTH; - - private void checkPermissionBluetoothAdmin() { - if (mContext.checkCallingOrSelfPermission(BLUETOOTH_ADMIN) != - PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("Requires BLUETOOTH_ADMIN permission"); - } - } - - private void checkPermissionBluetooth() { - if (mContext.checkCallingOrSelfPermission(BLUETOOTH_ADMIN) != - PackageManager.PERMISSION_GRANTED && - mContext.checkCallingOrSelfPermission(BLUETOOTH) != - PackageManager.PERMISSION_GRANTED ) { - throw new SecurityException("Requires BLUETOOTH or BLUETOOTH_ADMIN permission"); - } - } - private static final String DISABLE_ESCO_PATH = "/sys/module/sco/parameters/disable_esco"; private static void disableEsco() { try { @@ -1124,7 +1141,7 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { pw.println("\nBluetooth ENABLED: " + getAddress() + " (" + getName() + ")"); pw.println("\nisDiscovering() = " + isDiscovering()); - BluetoothHeadset headset = new BluetoothHeadset(mContext); + BluetoothHeadset headset = new BluetoothHeadset(mContext, null); pw.println("\n--Bondings--"); String[] addresses = listBondings(); diff --git a/core/java/android/server/BluetoothEventLoop.java b/core/java/android/server/BluetoothEventLoop.java index 5722f51..2d8aacc 100644 --- a/core/java/android/server/BluetoothEventLoop.java +++ b/core/java/android/server/BluetoothEventLoop.java @@ -16,6 +16,7 @@ package android.server; +import android.bluetooth.BluetoothClass.Device; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothIntent; import android.bluetooth.IBluetoothDeviceCallback; @@ -24,8 +25,6 @@ import android.content.Intent; import android.os.RemoteException; import android.util.Log; -import java.io.IOException; -import java.lang.Thread; import java.util.HashMap; /** @@ -45,10 +44,13 @@ class BluetoothEventLoop { private HashMap<String, IBluetoothDeviceCallback> mCreateBondingCallbacks; private HashMap<String, Integer> mPasskeyAgentRequestData; private HashMap<String, IBluetoothDeviceCallback> mGetRemoteServiceChannelCallbacks; - private BluetoothDeviceService mBluetoothService; - + private HashMap<String, Boolean> mDefaultPinData; + private BluetoothDeviceService mBluetoothService; private Context mContext; + private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; + private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; + static { classInitNative(); } private static native void classInitNative(); @@ -58,6 +60,7 @@ class BluetoothEventLoop { mCreateBondingCallbacks = new HashMap(); mPasskeyAgentRequestData = new HashMap(); mGetRemoteServiceChannelCallbacks = new HashMap(); + mDefaultPinData = new HashMap(); initializeNativeDataNative(); } private native void initializeNativeDataNative(); @@ -146,27 +149,28 @@ class BluetoothEventLoop { intMode = BluetoothDevice.MODE_DISCOVERABLE; } intent.putExtra(BluetoothIntent.MODE, intMode); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } public void onDiscoveryStarted() { mBluetoothService.setIsDiscovering(true); Intent intent = new Intent(BluetoothIntent.DISCOVERY_STARTED_ACTION); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } public void onDiscoveryCompleted() { mBluetoothService.setIsDiscovering(false); Intent intent = new Intent(BluetoothIntent.DISCOVERY_COMPLETED_ACTION); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } public void onPairingRequest() { Intent intent = new Intent(BluetoothIntent.PAIRING_REQUEST_ACTION); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); } + public void onPairingCancel() { Intent intent = new Intent(BluetoothIntent.PAIRING_CANCEL_ACTION); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); } public void onRemoteDeviceFound(String address, int deviceClass, short rssi) { @@ -174,64 +178,65 @@ class BluetoothEventLoop { intent.putExtra(BluetoothIntent.ADDRESS, address); intent.putExtra(BluetoothIntent.CLASS, deviceClass); intent.putExtra(BluetoothIntent.RSSI, rssi); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } public void onRemoteDeviceDisappeared(String address) { Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_DISAPPEARED_ACTION); intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } public void onRemoteClassUpdated(String address, int deviceClass) { Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_CLASS_UPDATED_ACTION); intent.putExtra(BluetoothIntent.ADDRESS, address); intent.putExtra(BluetoothIntent.CLASS, deviceClass); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } public void onRemoteDeviceConnected(String address) { Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_CONNECTED_ACTION); intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } public void onRemoteDeviceDisconnectRequested(String address) { Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_DISCONNECT_REQUESTED_ACTION); intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } public void onRemoteDeviceDisconnected(String address) { Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_DISCONNECTED_ACTION); intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } public void onRemoteNameUpdated(String address, String name) { Intent intent = new Intent(BluetoothIntent.REMOTE_NAME_UPDATED_ACTION); intent.putExtra(BluetoothIntent.ADDRESS, address); intent.putExtra(BluetoothIntent.NAME, name); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } public void onRemoteNameFailed(String address) { Intent intent = new Intent(BluetoothIntent.REMOTE_NAME_FAILED_ACTION); intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } public void onRemoteNameChanged(String address, String name) { Intent intent = new Intent(BluetoothIntent.REMOTE_NAME_UPDATED_ACTION); intent.putExtra(BluetoothIntent.ADDRESS, address); intent.putExtra(BluetoothIntent.NAME, name); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } public void onRemoteAliasChanged(String address, String alias) { Intent intent = new Intent(BluetoothIntent.REMOTE_ALIAS_CHANGED_ACTION); intent.putExtra(BluetoothIntent.ADDRESS, address); intent.putExtra(BluetoothIntent.ALIAS, alias); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } public void onRemoteAliasCleared(String address) { Intent intent = new Intent(BluetoothIntent.REMOTE_ALIAS_CLEARED_ACTION); intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } private void onCreateBondingResult(String address, boolean result) { + mBluetoothService.setOutgoingBondingDevAddress(null); IBluetoothDeviceCallback callback = mCreateBondingCallbacks.get(address); if (callback != null) { try { @@ -240,39 +245,71 @@ class BluetoothEventLoop { BluetoothDevice.RESULT_FAILURE); } catch (RemoteException e) {} mCreateBondingCallbacks.remove(address); - } + } } + public void onBondingCreated(String address) { Intent intent = new Intent(BluetoothIntent.BONDING_CREATED_ACTION); intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } + public void onBondingRemoved(String address) { Intent intent = new Intent(BluetoothIntent.BONDING_REMOVED_ACTION); intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + + if (mDefaultPinData.containsKey(address)) { + mDefaultPinData.remove(address); + } } public void onNameChanged(String name) { Intent intent = new Intent(BluetoothIntent.NAME_CHANGED_ACTION); intent.putExtra(BluetoothIntent.NAME, name); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); } public void onPasskeyAgentRequest(String address, int nativeData) { - mPasskeyAgentRequestData.put(address, new Integer(nativeData)); + mPasskeyAgentRequestData.put(address, new Integer(nativeData)); + + if (address.equals(mBluetoothService.getOutgoingBondingDevAddress())) { + int btClass = mBluetoothService.getRemoteClass(address); + int remoteDeviceClass = Device.getDevice(btClass); + if (remoteDeviceClass == Device.AUDIO_VIDEO_WEARABLE_HEADSET || + remoteDeviceClass == Device.AUDIO_VIDEO_HANDSFREE || + remoteDeviceClass == Device.AUDIO_VIDEO_HEADPHONES || + remoteDeviceClass == Device.AUDIO_VIDEO_PORTABLE_AUDIO || + remoteDeviceClass == Device.AUDIO_VIDEO_CAR_AUDIO || + remoteDeviceClass == Device.AUDIO_VIDEO_HIFI_AUDIO) { + if (!mDefaultPinData.containsKey(address)) { + mDefaultPinData.put(address, false); + } + if (!mDefaultPinData.get(address)) { + mDefaultPinData.remove(address); + mDefaultPinData.put(address, true); + mBluetoothService.setPin(address, BluetoothDevice.convertPinToBytes("0000")); + return; + } + } + } Intent intent = new Intent(BluetoothIntent.PAIRING_REQUEST_ACTION); intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); } + public void onPasskeyAgentCancel(String address) { mPasskeyAgentRequestData.remove(address); - + if (mDefaultPinData.containsKey(address)) { + mDefaultPinData.remove(address); + mDefaultPinData.put(address, false); + } Intent intent = new Intent(BluetoothIntent.PAIRING_CANCEL_ACTION); intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); } + private void onGetRemoteServiceChannelResult(String address, int channel) { IBluetoothDeviceCallback callback = mGetRemoteServiceChannelCallbacks.get(address); if (callback != null) { diff --git a/core/java/android/server/checkin/FallbackCheckinService.java b/core/java/android/server/checkin/FallbackCheckinService.java index b450913..65921af 100644 --- a/core/java/android/server/checkin/FallbackCheckinService.java +++ b/core/java/android/server/checkin/FallbackCheckinService.java @@ -42,4 +42,8 @@ public final class FallbackCheckinService extends ICheckinService.Stub { state.isEnabled = false; p.onResult(state); } + + public void getParentalControlState(IParentalControlCallback p, String requestingApp) + throws android.os.RemoteException { + } } diff --git a/core/java/android/server/search/SearchableInfo.java b/core/java/android/server/search/SearchableInfo.java index 5b9942e..6c8f554 100644 --- a/core/java/android/server/search/SearchableInfo.java +++ b/core/java/android/server/search/SearchableInfo.java @@ -402,7 +402,7 @@ public final class SearchableInfo implements Parcelable { // initialize as an "unsearchable" object mSearchable = false; mSearchActivity = cName; - + // to access another activity's resources, I need its context. // BE SURE to release the cache sometime after construction - it's a large object to hold mCacheActivityContext = getActivityContext(context); @@ -415,6 +415,7 @@ public final class SearchableInfo implements Parcelable { mIconId = a.getResourceId(com.android.internal.R.styleable.Searchable_icon, 0); mSearchButtonText = a.getResourceId( com.android.internal.R.styleable.Searchable_searchButtonText, 0); + setSearchModeFlags(); if (DBG_INHIBIT_SUGGESTIONS == 0) { mSuggestAuthority = a.getString( diff --git a/core/java/android/speech/recognition/AbstractEmbeddedGrammarListener.java b/core/java/android/speech/recognition/AbstractEmbeddedGrammarListener.java deleted file mode 100644 index c25a7e3..0000000 --- a/core/java/android/speech/recognition/AbstractEmbeddedGrammarListener.java +++ /dev/null @@ -1,51 +0,0 @@ -/*---------------------------------------------------------------------------* - * AbstractEmbeddedGrammarListener.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * An EmbeddedGrammarListener whose methods are empty. This class exists as - * convenience for creating listener objects. - */ -public abstract class AbstractEmbeddedGrammarListener implements EmbeddedGrammarListener -{ - public void onCompileAllSlots() - { - } - - public void onError(Exception e) - { - } - - public void onLoaded() - { - } - - public void onResetAllSlots() - { - } - - public void onSaved(String path) - { - } - - public void onUnloaded() - { - } -} diff --git a/core/java/android/speech/recognition/AbstractGrammarListener.java b/core/java/android/speech/recognition/AbstractGrammarListener.java deleted file mode 100644 index fe62290..0000000 --- a/core/java/android/speech/recognition/AbstractGrammarListener.java +++ /dev/null @@ -1,39 +0,0 @@ -/*---------------------------------------------------------------------------* - * AbstractGrammarListener.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * A GrammarListener whose methods are empty. This class exists as convenience - * for creating listener objects. - */ -public abstract class AbstractGrammarListener implements GrammarListener -{ - public void onError(Exception e) - { - } - - public void onLoaded() - { - } - - public void onUnloaded() - { - } -} diff --git a/core/java/android/speech/recognition/AbstractRecognizerListener.java b/core/java/android/speech/recognition/AbstractRecognizerListener.java deleted file mode 100644 index ee2b8d1..0000000 --- a/core/java/android/speech/recognition/AbstractRecognizerListener.java +++ /dev/null @@ -1,83 +0,0 @@ -/*---------------------------------------------------------------------------* - * AbstractRecognizerListener.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -import java.util.Hashtable; -import java.util.Vector; - -/** - * A RecognizerListener whose methods are empty. This class exists as - * convenience for creating listener objects. - */ -public abstract class AbstractRecognizerListener implements RecognizerListener -{ - public void onBeginningOfSpeech() - { - } - - public void onEndOfSpeech() - { - } - - public void onRecognitionSuccess(RecognitionResult result) - { - } - - public void onRecognitionFailure(FailureReason reason) - { - } - - public void onError(Exception e) - { - } - - public void onParametersGetError(Vector<String> parameters, Exception e) - { - } - - public void onParametersSetError(Hashtable<String, String> parameters, - Exception e) - { - } - - public void onParametersGet(Hashtable<String, String> parameters) - { - } - - public void onParametersSet(Hashtable<String, String> parameters) - { - } - - public void onStartOfSpeechTimeout() - { - } - - public void onAcousticStateReset() - { - } - - public void onStarted() - { - } - - public void onStopped() - { - } -} diff --git a/core/java/android/speech/recognition/AbstractSrecGrammarListener.java b/core/java/android/speech/recognition/AbstractSrecGrammarListener.java deleted file mode 100644 index e62e4ba..0000000 --- a/core/java/android/speech/recognition/AbstractSrecGrammarListener.java +++ /dev/null @@ -1,59 +0,0 @@ -/*---------------------------------------------------------------------------* - * AbstractSrecGrammarListener.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * An SrecGrammarListener whose methods are empty. This class exists as - * convenience for creating listener objects. - */ -public abstract class AbstractSrecGrammarListener implements SrecGrammarListener -{ - public void onCompileAllSlots() - { - } - - public void onError(Exception e) - { - } - - public void onLoaded() - { - } - - public void onResetAllSlots() - { - } - - public void onSaved(String path) - { - } - - public void onUnloaded() - { - } - - public void onAddItemList() - { - } - - public void onAddItemListFailure(int index, Exception e) - { - } -} diff --git a/core/java/android/speech/recognition/AudioAlreadyInUseException.java b/core/java/android/speech/recognition/AudioAlreadyInUseException.java deleted file mode 100644 index 90698a7..0000000 --- a/core/java/android/speech/recognition/AudioAlreadyInUseException.java +++ /dev/null @@ -1,34 +0,0 @@ -/*---------------------------------------------------------------------------* - * AudioAlreadyInUseException.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Thrown when an AudioStream is passed into a component when another component - * is already using it. - */ -public class AudioAlreadyInUseException extends IllegalArgumentException -{ - private static final long serialVersionUID = 0L; - - public AudioAlreadyInUseException(String msg) - { - super(msg); - } -} diff --git a/core/java/android/speech/recognition/AudioDriverErrorException.java b/core/java/android/speech/recognition/AudioDriverErrorException.java deleted file mode 100644 index a755e7f..0000000 --- a/core/java/android/speech/recognition/AudioDriverErrorException.java +++ /dev/null @@ -1,33 +0,0 @@ -/*---------------------------------------------------------------------------* - * AudioDriverErrorException.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Thrown if an error occurs in the audio driver. - */ -public class AudioDriverErrorException extends Exception -{ - private static final long serialVersionUID = 0L; - - public AudioDriverErrorException(String msg) - { - super(msg); - } -} diff --git a/core/java/android/speech/recognition/AudioSource.java b/core/java/android/speech/recognition/AudioSource.java deleted file mode 100644 index c4cd802..0000000 --- a/core/java/android/speech/recognition/AudioSource.java +++ /dev/null @@ -1,45 +0,0 @@ -/*---------------------------------------------------------------------------* - * AudioSource.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Generates audio data. - */ -public interface AudioSource -{ - /** - * Returns an object that contains the audio samples. This object - * is passed to other components that consumes it, such a Recognizer - * or a DeviceSpeaker. - * - * @return an AudioStream instance - */ - AudioStream createAudio(); - - /** - * Tells the audio source to start collecting audio samples. - */ - void start(); - - /** - * Tells the audio source to stop collecting audio samples. - */ - void stop(); -} diff --git a/core/java/android/speech/recognition/AudioSourceListener.java b/core/java/android/speech/recognition/AudioSourceListener.java deleted file mode 100644 index 42e8ebe..0000000 --- a/core/java/android/speech/recognition/AudioSourceListener.java +++ /dev/null @@ -1,44 +0,0 @@ -/*---------------------------------------------------------------------------* - * AudioSourceListener.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Listens for Microphone events. - */ -public interface AudioSourceListener -{ - /** - * Invoked after the microphone starts recording. - */ - void onStarted(); - - /** - * Invoked after the microphone stops recording. - */ - void onStopped(); - - /** - * Invoked when an unexpected error occurs. This is normally followed by - * onStopped() if the component shuts down successfully. - * - * @param e the cause of the failure - */ - void onError(Exception e); -} diff --git a/core/java/android/speech/recognition/AudioStream.java b/core/java/android/speech/recognition/AudioStream.java deleted file mode 100644 index 36afe21..0000000 --- a/core/java/android/speech/recognition/AudioStream.java +++ /dev/null @@ -1,35 +0,0 @@ -/*---------------------------------------------------------------------------* - * AudioStream.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Stream used to read audio data. - */ -public interface AudioStream -{ - /** - * Releases resources associated with the object. - * - * @deprecated this method is deprecated and has no replacement. It will be - * removed in a future release of the API. - */ - @Deprecated - void dispose(); -} diff --git a/core/java/android/speech/recognition/Codec.java b/core/java/android/speech/recognition/Codec.java deleted file mode 100644 index 18d9e15..0000000 --- a/core/java/android/speech/recognition/Codec.java +++ /dev/null @@ -1,126 +0,0 @@ -/*---------------------------------------------------------------------------* - * Codec.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Audio formats. - */ -public abstract class Codec -{ - /** - * PCM, 16 bits, 8KHz. - */ - public static final Codec PCM_16BIT_8K = new Codec("PCM/16bit/8KHz") - { - @Override - public byte getBitsPerSample() - { - return 16; - } - - @Override - public int getSampleRate() - { - return 8000; - } - }; - /** - * PCM, 16 bits, 11KHz. - */ - public static final Codec PCM_16BIT_11K = new Codec("PCM/16bit/11KHz") - { - @Override - public byte getBitsPerSample() - { - return 16; - } - - @Override - public int getSampleRate() - { - return 11025; - } - }; - /** - * PCM, 16 bits, 22KHz. - */ - public static final Codec PCM_16BIT_22K = new Codec("PCM/16bit/22KHz") - { - @Override - public byte getBitsPerSample() - { - return 16; - } - - @Override - public int getSampleRate() - { - return 22050; - } - }; - /** - * ULAW, 8 bits, 8KHz. - */ - public static final Codec ULAW_8BIT_8K = new Codec("ULAW/8bit/8KHz") - { - @Override - public byte getBitsPerSample() - { - return 8; - } - - @Override - public int getSampleRate() - { - return 8000; - } - }; - private final String message; - - /** - * Creates a new Codec. - * - * @param message the message to associate with the codec - */ - private Codec(String message) - { - this.message = message; - } - - @Override - public String toString() - { - return message; - } - - /** - * Returns the codec sample-rate. - * - * @return the codec sample-rate - */ - public abstract int getSampleRate(); - - /** - * Returns the codec bitrate. - * - * @return the codec bitrate - */ - public abstract byte getBitsPerSample(); -} diff --git a/core/java/android/speech/recognition/DeviceSpeaker.java b/core/java/android/speech/recognition/DeviceSpeaker.java deleted file mode 100644 index bd18687..0000000 --- a/core/java/android/speech/recognition/DeviceSpeaker.java +++ /dev/null @@ -1,77 +0,0 @@ -/*---------------------------------------------------------------------------* - * DeviceSpeaker.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -import android.speech.recognition.impl.DeviceSpeakerImpl; - -/** - * A device for transforming electric signals into audible sound, most - * frequently used to reproduce speech and music. - */ -public abstract class DeviceSpeaker -{ - private static DeviceSpeaker instance; - - /** - * Returns the device speaker instance. - * - * @return an instance of a DeviceSpeaker class. - */ - public static DeviceSpeaker getInstance() - { - instance = DeviceSpeakerImpl.getInstance(); - return instance; - } - - /** - * Starts the audio playback. - * - * @param source the audio to play - * @throws IllegalStateException if the component is already started - * @throws IllegalArgumentException if source audio is null, in-use by - * another component or is empty. - * - */ - public abstract void start(AudioStream source) throws IllegalStateException, - IllegalArgumentException; - - /** - * Stops audio playback. - */ - public abstract void stop(); - - /** - * Sets the playback codec. This must be called before start() is called. - * - * @param playbackCodec the codec to use for the playback operation. - * @throws IllegalStateException if the component is already stopped - * @throws IllegalArgumentException if the specified codec is not supported - */ - public abstract void setCodec(Codec playbackCodec) throws IllegalStateException, - IllegalArgumentException; - - /** - * Sets the microphone listener. - * - * @param listener the device speaker listener. - * @throws IllegalStateException if the component is started - */ - public abstract void setListener(DeviceSpeakerListener listener) throws IllegalStateException; -} diff --git a/core/java/android/speech/recognition/DeviceSpeakerListener.java b/core/java/android/speech/recognition/DeviceSpeakerListener.java deleted file mode 100644 index e2baa2e..0000000 --- a/core/java/android/speech/recognition/DeviceSpeakerListener.java +++ /dev/null @@ -1,44 +0,0 @@ -/*---------------------------------------------------------------------------* - * DeviceSpeakerListener.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Listens for DeviceSpeaker events. - */ -public interface DeviceSpeakerListener -{ - /** - * Invoked after playback begins. - */ - void onStarted(); - - /** - * Invoked after playback terminates. - */ - void onStopped(); - - /** - * Invoked when an unexpected error occurs. This is normally followed by - * onStopped() if the component shuts down successfully. - * - * @param e the cause of the failure - */ - void onError(Exception e); -} diff --git a/core/java/android/speech/recognition/EmbeddedGrammar.java b/core/java/android/speech/recognition/EmbeddedGrammar.java deleted file mode 100644 index c6f037b..0000000 --- a/core/java/android/speech/recognition/EmbeddedGrammar.java +++ /dev/null @@ -1,43 +0,0 @@ -/*---------------------------------------------------------------------------* - * EmbeddedGrammar.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Grammar on an embedded recognizer. - */ -public interface EmbeddedGrammar extends Grammar -{ - /** - * Compiles items that were added to any of the grammar slots. - */ - void compileAllSlots(); - - /** - * Removes all words added to all slots. - */ - void resetAllSlots(); - - /** - * Saves the compiled grammar. - * - * @param url the url to save the grammar to - */ - void save(String url); -} diff --git a/core/java/android/speech/recognition/EmbeddedGrammarListener.java b/core/java/android/speech/recognition/EmbeddedGrammarListener.java deleted file mode 100644 index 5b8c1a4..0000000 --- a/core/java/android/speech/recognition/EmbeddedGrammarListener.java +++ /dev/null @@ -1,58 +0,0 @@ -/*---------------------------------------------------------------------------* - * EmbeddedGrammarListener.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Listens for EmbeddedGrammar events. - */ -public interface EmbeddedGrammarListener extends GrammarListener -{ - /** - * Invoked after the grammar is saved. - * - * @param path the path the grammar was saved to - */ - void onSaved(String path); - - /** - * Invoked when a grammar operation fails. - * - * @param e the cause of the failure.<br/> - * {@link GrammarOverflowException} if the grammar slot is full and no - * further items may be added to it.<br/> - * {@link java.lang.UnsupportedOperationException} if different words with - * the same pronunciation are added.<br/> - * {@link java.lang.IllegalStateException} if reseting or compiling the - * slots fails.<br/> - * {@link java.io.IOException} if the grammar could not be loaded or - * saved.</p> - */ - void onError(Exception e); - - /** - * Invokes after all grammar slots have been compiled. - */ - void onCompileAllSlots(); - - /** - * Invokes after all grammar slots have been reset. - */ - void onResetAllSlots(); -} diff --git a/core/java/android/speech/recognition/EmbeddedRecognizer.java b/core/java/android/speech/recognition/EmbeddedRecognizer.java deleted file mode 100644 index cd79edc..0000000 --- a/core/java/android/speech/recognition/EmbeddedRecognizer.java +++ /dev/null @@ -1,66 +0,0 @@ -/*---------------------------------------------------------------------------* - * EmbeddedRecognizer.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -import java.io.FileNotFoundException; -import java.io.IOException; -import android.speech.recognition.impl.EmbeddedRecognizerImpl; - -/** - * Embedded recognizer. - */ -public abstract class EmbeddedRecognizer implements Recognizer -{ - private static EmbeddedRecognizer instance; - - /** - * Returns the embedded recognizer. - * - * @return the embedded recognizer - */ - public static EmbeddedRecognizer getInstance() - { - instance = EmbeddedRecognizerImpl.getInstance(); - return instance; - } - - /** - * Configures the recognizer. - * - * @param config recognizer configuration file - * @throws IllegalArgumentException if config is null or an empty string - * @throws FileNotFoundException if the specified file could not be found - * @throws IOException if the specified file could not be opened - * @throws UnsatisfiedLinkError if the recognizer plugin could not be loaded - * @throws ClassNotFoundException if the recognizer plugin could not be found - */ - public abstract void configure(String config) throws IllegalArgumentException, - FileNotFoundException, IOException, UnsatisfiedLinkError, - ClassNotFoundException; - - /** - * The recognition accuracy improves over time as the recognizer adapts to - * the surrounding environment. This method enables developers to reset the - * adaptation when the environment is known to have changed. - * - * @throws IllegalArgumentException if recognizer instance is null - */ - public abstract void resetAcousticState() throws IllegalArgumentException; -} diff --git a/core/java/android/speech/recognition/Grammar.java b/core/java/android/speech/recognition/Grammar.java deleted file mode 100644 index 9f1b624..0000000 --- a/core/java/android/speech/recognition/Grammar.java +++ /dev/null @@ -1,43 +0,0 @@ -/*---------------------------------------------------------------------------* - * Grammar.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Speech recognition grammar. - */ -public interface Grammar { - /** - * Load the grammar sets the grammar state to active, indicating that can be used in a recognition process. - * Multiple grammars can be loaded, but only one at a time can be used by the recognizer. - * - */ - void load(); - - /** - * Unload the grammar sets the grammar state to inactive (inactive grammars can not be used as a parameter of a recognition). - */ - void unload(); - - /** - * (Optional operation) Releases resources associated with the object. The - * grammar may not be used past this point. - */ - void dispose(); -} diff --git a/core/java/android/speech/recognition/GrammarErrorException.java b/core/java/android/speech/recognition/GrammarErrorException.java deleted file mode 100644 index 6070758..0000000 --- a/core/java/android/speech/recognition/GrammarErrorException.java +++ /dev/null @@ -1,33 +0,0 @@ -/*---------------------------------------------------------------------------* - * GrammarErrorException.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Thrown if an error occurs in the audio driver. - */ -public class GrammarErrorException extends Exception -{ - private static final long serialVersionUID = 0L; - - public GrammarErrorException(String msg) - { - super(msg); - } -} diff --git a/core/java/android/speech/recognition/GrammarListener.java b/core/java/android/speech/recognition/GrammarListener.java deleted file mode 100644 index 871cbcb..0000000 --- a/core/java/android/speech/recognition/GrammarListener.java +++ /dev/null @@ -1,45 +0,0 @@ -/*---------------------------------------------------------------------------* - * GrammarListener.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Listens for Grammar events. - */ -public interface GrammarListener -{ - /** - * Invoked after the Grammar is loaded. - */ - void onLoaded(); - - /** - * Invoked after the Grammar is unloaded. - */ - void onUnloaded(); - - /** - * Invoked when a grammar operation fails. - * - * @param e the cause of the failure.<br/> - * {@link java.io.IOException} if the grammar could not be loaded or - * saved.</p> - */ - void onError(Exception e); -} diff --git a/core/java/android/speech/recognition/GrammarOverflowException.java b/core/java/android/speech/recognition/GrammarOverflowException.java deleted file mode 100644 index 227820b..0000000 --- a/core/java/android/speech/recognition/GrammarOverflowException.java +++ /dev/null @@ -1,33 +0,0 @@ -/*---------------------------------------------------------------------------* - * GrammarOverflowException.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Thrown if a SlotItem is added into a grammar slot that is filled to capacity. - */ -public class GrammarOverflowException extends Exception -{ - private static final long serialVersionUID = 0L; - - public GrammarOverflowException(String message) - { - super(message); - } -} diff --git a/core/java/android/speech/recognition/InvalidURLException.java b/core/java/android/speech/recognition/InvalidURLException.java deleted file mode 100644 index fec9411..0000000 --- a/core/java/android/speech/recognition/InvalidURLException.java +++ /dev/null @@ -1,34 +0,0 @@ -/*---------------------------------------------------------------------------* - * InvalidURLException.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - */ -public class InvalidURLException extends Exception { - - private static final long serialVersionUID = 0L; - - /** Creates a new instance of InvalidURLException */ - public InvalidURLException(String msg) - { - super(msg); - } - -} diff --git a/core/java/android/speech/recognition/Logger.java b/core/java/android/speech/recognition/Logger.java deleted file mode 100644 index 8a09cb3..0000000 --- a/core/java/android/speech/recognition/Logger.java +++ /dev/null @@ -1,127 +0,0 @@ -/*---------------------------------------------------------------------------* - * Logger.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -import android.speech.recognition.impl.LoggerImpl; - -/** - * Logs debugging information. - */ -public abstract class Logger -{ - /** - * Logging level - */ - public static class LogLevel - { - /** - * Does not log. - */ - public static LogLevel LEVEL_NONE = new LogLevel("Do not log"); - /** - * Logs fatal issues. This level only logs ERROR. - */ - public static LogLevel LEVEL_ERROR = new LogLevel("log UAPI_ERROR logs"); - /** - * Logs non-fatal issues. This level also logs ERROR. - */ - public static LogLevel LEVEL_WARN = - new LogLevel("log UAPI_ERROR, UAPI_WARN logs"); - /** - * Logs debugging information, such as the values of variables. This level also logs ERROR, WARN. - */ - public static LogLevel LEVEL_INFO = - new LogLevel("log UAPI_ERROR, UAPI_WARN, UAPI_INFO logs"); - /** - * Logs when loggers are created or destroyed. This level also logs INFO, WARN, ERROR. - */ - public static LogLevel LEVEL_TRACE = - new LogLevel("log UAPI_ERROR, UAPI_WARN, UAPI_INFO, UAPI_TRACE logs"); - private String message; - - /** - * Creates a new LogLevel. - * - * @param message the message associated with the LogLevel. - */ - private LogLevel(String message) - { - this.message = message; - } - - @Override - public String toString() - { - return message; - } - } - - /** - * Returns the singleton instance. - * - * @return the singleton instance - */ - public static Logger getInstance() - { - return LoggerImpl.getInstance(); - } - - /** - * Sets the logging level. - * - * @param level the logging level - */ - public abstract void setLoggingLevel(LogLevel level); - - /** - * Sets the log path. - * - * @param path the path of the log file - */ - public abstract void setPath(String path); - - /** - * Logs an error message. - * - * @param message the message to log - */ - public abstract void error(String message); - - /** - * Logs a warning message. - * - * @param message the message to log - */ - public abstract void warn(String message); - - /** - * Logs an informational message. - * - * @param message the message to log - */ - public abstract void info(String message); - - /** - * Logs a method tracing message. - * - * @param message the message to log - */ - public abstract void trace(String message); -} diff --git a/core/java/android/speech/recognition/MediaFileReader.java b/core/java/android/speech/recognition/MediaFileReader.java deleted file mode 100644 index 216511f..0000000 --- a/core/java/android/speech/recognition/MediaFileReader.java +++ /dev/null @@ -1,90 +0,0 @@ -/*---------------------------------------------------------------------------* - * MediaFileReader.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -import android.speech.recognition.impl.MediaFileReaderImpl; - -/** - * Reads audio from a file. - */ -public abstract class MediaFileReader implements AudioSource -{ - /** - * Reading mode - */ - public static class Mode - { - /** - * Read the file in "real time". - */ - public static Mode REAL_TIME = new Mode("real-time"); - /** - * Read the file all at once. - */ - public static Mode ALL_AT_ONCE = new Mode("all at once"); - private String message; - - /** - * Creates a new Mode. - * - * @param message the message associated with the reading mode. - */ - private Mode(String message) - { - this.message = message; - } - } - - /** - * Creates a new MediaFileReader to read audio samples from a file. - * - * @param filename the name of the file to read from Note: The file MUST be of type Microsoft WAVE RIFF - * format (PCM 16 bits 8000 Hz or PCM 16 bits 11025 Hz). - * @param listener listens for MediaFileReader events - * @return a new MediaFileReader - * @throws IllegalArgumentException if filename is null or is an empty string. Or if offset > file length. Or if codec is null or invalid - */ - public static MediaFileReader create(String filename, AudioSourceListener listener) throws IllegalArgumentException - { - return new MediaFileReaderImpl(filename, listener); - } - - /** - * Sets the reading mode. - * - * @param mode the reading mode - */ - public abstract void setMode(Mode mode); - - /** - * Creates an audio source. - */ - public abstract AudioStream createAudio(); - - /** - * Starts collecting audio samples. - */ - public abstract void start(); - - /** - * Stops collecting audio samples. - */ - public abstract void stop(); -} diff --git a/core/java/android/speech/recognition/MediaFileReaderListener.java b/core/java/android/speech/recognition/MediaFileReaderListener.java deleted file mode 100644 index f76e65f..0000000 --- a/core/java/android/speech/recognition/MediaFileReaderListener.java +++ /dev/null @@ -1,29 +0,0 @@ -/*---------------------------------------------------------------------------* - * MediaFileReaderListener.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -import android.speech.recognition.AudioSourceListener; - -/** - * Listens for MediaFileReader events. - */ -public interface MediaFileReaderListener extends AudioSourceListener -{ -} diff --git a/core/java/android/speech/recognition/MediaFileWriter.java b/core/java/android/speech/recognition/MediaFileWriter.java deleted file mode 100644 index b2d627c..0000000 --- a/core/java/android/speech/recognition/MediaFileWriter.java +++ /dev/null @@ -1,49 +0,0 @@ -/*---------------------------------------------------------------------------* - * MediaFileWriter.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -import android.speech.recognition.impl.MediaFileWriterImpl; - -/** - * Writes audio to a file. - */ -public abstract class MediaFileWriter -{ - /** - * Creates a new MediaFileWriter to write audio samples into a file. - * - * @param listener listens for MediaFileWriter events - * @return a new MediaFileWriter - */ - public static MediaFileWriter create(MediaFileWriterListener listener) - { - return new MediaFileWriterImpl(listener); - } - - /** - * Saves audio to a file. - * - * @param source the audio stream to write - * @param filename the file to write to - * @throws IllegalArgumentException if source is null, in-use by another - * component or contains no data. Or if filename is null or is empty. - */ - public abstract void save(AudioStream source, String filename) throws IllegalArgumentException; -} diff --git a/core/java/android/speech/recognition/MediaFileWriterListener.java b/core/java/android/speech/recognition/MediaFileWriterListener.java deleted file mode 100644 index e2104c8..0000000 --- a/core/java/android/speech/recognition/MediaFileWriterListener.java +++ /dev/null @@ -1,40 +0,0 @@ -/*---------------------------------------------------------------------------* - * MediaFileWriterListener.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Listens for MediaFileWriter events. - */ -public interface MediaFileWriterListener -{ - /** - * Invoked after the save() operation terminates - */ - void onStopped(); - - /** - * Invoked when an unexpected error occurs. This is normally followed by - * onStopped() if the component shuts down successfully. - * - * @param e the cause of the failure.<br/> - * {@link java.io.IOException} if an error occured opening or writing to the file - */ - void onError(Exception e); -} diff --git a/core/java/android/speech/recognition/Microphone.java b/core/java/android/speech/recognition/Microphone.java deleted file mode 100644 index 1b713f5..0000000 --- a/core/java/android/speech/recognition/Microphone.java +++ /dev/null @@ -1,76 +0,0 @@ -/*---------------------------------------------------------------------------* - * Microphone.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -import android.speech.recognition.impl.MicrophoneImpl; - -/** - * Records live audio. - */ -public abstract class Microphone implements AudioSource -{ - private static Microphone instance; - - /** - * Returns the microphone instance - * - * @return an instance of a Microphone class. - */ - public static Microphone getInstance() - { - instance = MicrophoneImpl.getInstance(); - return instance; - } - - /** - * Sets the recording codec. This must be called before start() is called. - * - * @param recordingCodec the codec in which the samples will be recorded. - * @throws IllegalStateException if Microphone is started - * @throws IllegalArgumentException if codec is not supported - */ - public abstract void setCodec(Codec recordingCodec) throws IllegalStateException, - IllegalArgumentException; - - /** - * Sets the microphone listener. - * - * @param listener the microphone listener. - * @throws IllegalStateException if Microphone is started - */ - public abstract void setListener(AudioSourceListener listener) throws IllegalStateException; - - /** - * Creates an audio source - */ - public abstract AudioStream createAudio(); - - /** - * Start recording audio. - * - * @throws IllegalStateException if Microphone is already started - */ - public abstract void start() throws IllegalStateException; - - /** - * Stops recording audio. - */ - public abstract void stop(); -} diff --git a/core/java/android/speech/recognition/MicrophoneListener.java b/core/java/android/speech/recognition/MicrophoneListener.java deleted file mode 100644 index f43eff9..0000000 --- a/core/java/android/speech/recognition/MicrophoneListener.java +++ /dev/null @@ -1,29 +0,0 @@ -/*---------------------------------------------------------------------------* - * MicrophoneListener.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -import android.speech.recognition.AudioSourceListener; - -/** - * Listens for Microphone events. - */ -public interface MicrophoneListener extends AudioSourceListener -{ -} diff --git a/core/java/android/speech/recognition/NBestRecognitionResult.java b/core/java/android/speech/recognition/NBestRecognitionResult.java deleted file mode 100644 index e679c19..0000000 --- a/core/java/android/speech/recognition/NBestRecognitionResult.java +++ /dev/null @@ -1,113 +0,0 @@ -/*---------------------------------------------------------------------------* - * NBestRecognitionResult.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -import java.util.Enumeration; - -/** - * N-Best recognition results. Entries are sorted in decreasing order according - * to their probability, from the most probable result to the least probable - * result. - */ -public interface NBestRecognitionResult extends RecognitionResult -{ - /** - * Recognition result entry - */ - public static interface Entry - { - /** - * Returns the semantic meaning of a recognition result (i.e. the application-specific value - * associated with what the user said). In an example where a person's name is mapped - * to a phone-number, the phone-number is the semantic meaning. - * - * @return the semantic meaning of a recognition result. - * @throws IllegalStateException if the object has been disposed - */ - String getSemanticMeaning() throws IllegalStateException; - - /** - * The confidence score of a recognition result. Values range from 0 to 100 - * (inclusive). - * - * @return the confidence score of a recognition result. - * @throws IllegalStateException if the object has been disposed - */ - byte getConfidenceScore() throws IllegalStateException; - - /** - * Returns the literal meaning of a recognition result (i.e. literally - * what the user said). In an example where a person's name is mapped to a - * phone-number, the person's name is the literal meaning. - * - * @return the literal meaning of a recognition result. - * @throws IllegalStateException if the object has been disposed - */ - String getLiteralMeaning() throws IllegalStateException; - - /** - * Returns the value associated with the specified key. - * - * @param key the key to look up - * @return the associated value or null if this entry does not contain - * any mapping for the key - */ - String get(String key); - - /** - * Returns an enumeration of the keys in this Entry. - * - * @return an enumeration of the keys in this Entry. - */ - Enumeration keys(); - } - - /** - * Returns the number of entries in the n-best list. - * - * @return the number of entries in the n-best list - */ - int getSize(); - - /** - * Returns the n-best entry that contains key-value pairs associated with the - * recognition result. - * - * @param index the index of the n-best entry - * @return null if all active GrammarConfiguration.grammarToMeaning() return - * null - * @throws ArrayIndexOutOfBoundsException if index is greater than size of - * entries - */ - Entry getEntry(int index) throws ArrayIndexOutOfBoundsException; - - /** - * Creates a new VoicetagItem if the last recognition was an enrollment - * operation. - * - * @param VoicetagId string voicetag unique id value. - * @param listener listens for Voicetag events - * @return the resulting VoicetagItem - * @throws IllegalArgumentException if VoicetagId is null or an empty string. - * @throws IllegalStateException if the last recognition was not an - * enrollment operation - */ - VoicetagItem createVoicetagItem(String VoicetagId, VoicetagItemListener listener) throws IllegalArgumentException,IllegalStateException; -} diff --git a/core/java/android/speech/recognition/ParameterErrorException.java b/core/java/android/speech/recognition/ParameterErrorException.java deleted file mode 100644 index 042ed31..0000000 --- a/core/java/android/speech/recognition/ParameterErrorException.java +++ /dev/null @@ -1,33 +0,0 @@ -/*---------------------------------------------------------------------------* - * ParameterErrorException.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Thrown if an error occurs in the audio driver. - */ -public class ParameterErrorException extends Exception -{ - private static final long serialVersionUID = 0L; - - public ParameterErrorException(String msg) - { - super(msg); - } -} diff --git a/core/java/android/speech/recognition/ParametersListener.java b/core/java/android/speech/recognition/ParametersListener.java deleted file mode 100644 index bdb551e..0000000 --- a/core/java/android/speech/recognition/ParametersListener.java +++ /dev/null @@ -1,63 +0,0 @@ -/*---------------------------------------------------------------------------* - * ParametersListener.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -import java.util.Hashtable; -import java.util.Vector; - -/** - * Listens for parameter events. - */ -public interface ParametersListener -{ - /** - * Invoked if retrieving parameters has failed. - * - * @param parameters the parameters that could not be retrieved - * @param e the failure reason - */ - void onParametersGetError(Vector<String> parameters, Exception e); - - /** - * Invoked if setting parameters has failed. - * - * @param parameters the parameters that could not be set - * @param e the failure reason - */ - void onParametersSetError(Hashtable<String, String> parameters, Exception e); - - /** - * This method is called when the parameters specified in setParameters have - * successfully been set. This method is guaranteed to be invoked after - * onParametersSetError, even if count==0. - * - * @param parameters the set parameters - */ - void onParametersSet(Hashtable<String, String> parameters); - - /** - * This method is called when the parameters specified in getParameters have - * successfully been retrieved. This method is guaranteed to be invoked after - * onParametersGetError, even if count==0. - * - * @param parameters the retrieved parameters - */ - void onParametersGet(Hashtable<String, String> parameters); -} diff --git a/core/java/android/speech/recognition/ParseErrorException.java b/core/java/android/speech/recognition/ParseErrorException.java deleted file mode 100644 index 2288a90..0000000 --- a/core/java/android/speech/recognition/ParseErrorException.java +++ /dev/null @@ -1,33 +0,0 @@ -/*---------------------------------------------------------------------------* - * ParseErrorException.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Thrown if an error occurs in the audio driver. - */ -public class ParseErrorException extends Exception -{ - private static final long serialVersionUID = 0L; - - public ParseErrorException(String msg) - { - super(msg); - } -} diff --git a/core/java/android/speech/recognition/RecognitionResult.java b/core/java/android/speech/recognition/RecognitionResult.java deleted file mode 100644 index cbbc938..0000000 --- a/core/java/android/speech/recognition/RecognitionResult.java +++ /dev/null @@ -1,27 +0,0 @@ -/*---------------------------------------------------------------------------* - * RecognitionResult.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Recognition result interface. - */ -public interface RecognitionResult -{ -} diff --git a/core/java/android/speech/recognition/Recognizer.java b/core/java/android/speech/recognition/Recognizer.java deleted file mode 100644 index ab7f8f4..0000000 --- a/core/java/android/speech/recognition/Recognizer.java +++ /dev/null @@ -1,102 +0,0 @@ -/*---------------------------------------------------------------------------* - * Recognizer.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -import java.util.Hashtable; -import java.util.Vector; - -/** - * Speech recognizer interface. - */ -public interface Recognizer -{ - /** - * Sets the recognizer event listener. - * - * @param listener listens for recognizer events - */ - void setListener(RecognizerListener listener); - - /** - * Creates an embedded grammar. - * - * @param value value of that grammarType. Could be a URL or an inline grammar. - * @return a grammar - * @throws IllegalArgumentException if value is null or listener is not of type - * GrammarListener. - */ - Grammar createGrammar(String value, GrammarListener listener) throws IllegalArgumentException; - - /** - * Begins speech recognition. - * - * @param audio the audio stream to recognizer - * @param grammars a collection of grammar sets to recognize against - * @see #recognize(AudioStream, Grammar) - * @throws IllegalStateException if any of the grammars are not loaded - * @throws IllegalArgumentException if audio is null, in-use by another - * component or empty. Or if grammars is null or grammars count is less than - * one. Or if the audio codec differs from recognizer codec. - * @throws UnsupportedOperationException if the recognizer does not support - * the number of grammars specified. - */ - void recognize(AudioStream audio, - Vector<Grammar> grammars) throws IllegalStateException, - IllegalArgumentException, UnsupportedOperationException; - - /** - * This convenience method is equivalent to invoking - * recognize(audio, grammars) with a single grammar. - * - * @param audio the audio to recognizer - * @param grammar a grammar to recognize against - * @see #recognize(AudioStream, Vector) - * @throws IllegalStateException if grammar is not loaded - * @throws IllegalArgumentException if audio is null, in-use by another - * component or is empty. Or if grammar is null or if the audio codec differs - * from the recognizer codec. - */ - void recognize(AudioStream audio, Grammar grammar) throws IllegalStateException, - IllegalArgumentException; - - /** - * Terminates a recognition if one is in-progress. - * This must not be called until the recognize method - * returns; otherwise the result is not defined. - * - * @see RecognizerListener#onStopped - */ - void stop(); - - /** - * Sets the values of recognition parameters. - * - * @param parameters the parameter key-value pairs to set - */ - void setParameters(Hashtable<String, String> parameters); - - /** - * Retrieves the values of recognition parameters. - * - * @param parameters the names of the parameters to retrieve - */ - void getParameters(Vector<String> parameters); - -} diff --git a/core/java/android/speech/recognition/RecognizerListener.java b/core/java/android/speech/recognition/RecognizerListener.java deleted file mode 100644 index d7bbda9..0000000 --- a/core/java/android/speech/recognition/RecognizerListener.java +++ /dev/null @@ -1,142 +0,0 @@ -/*---------------------------------------------------------------------------* - * RecognizerListener.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Listens for recognizer events. - */ -public interface RecognizerListener extends ParametersListener -{ - /** - * Recognition failure. - */ - public static class FailureReason - { - /** - * The audio did not generate any results. - */ - public static FailureReason NO_MATCH = - new FailureReason("The audio did not generate any results"); - /** - * Beginning of speech occured too soon. - */ - public static FailureReason SPOKE_TOO_SOON = - new FailureReason("Beginning of speech occurred too soon"); - /** - * A timeout occured before the beginning of speech. - */ - public static FailureReason BEGINNING_OF_SPEECH_TIMEOUT = - new FailureReason("A timeout occurred before the beginning of " + "speech"); - /** - * A timeout occured before the recognition could complete. - */ - public static FailureReason RECOGNITION_TIMEOUT = - new FailureReason("A timeout occurred before the recognition " + - "could complete"); - /** - * The recognizer encountered more audio than was acceptable according to - * its configuration. - */ - public static FailureReason TOO_MUCH_SPEECH = - new FailureReason("The " + - "recognizer encountered more audio than was acceptable according to " + - "its configuration"); - - public static FailureReason UNKNOWN = - new FailureReason("unknown failure reason"); - - private final String message; - - private FailureReason(String message) - { - this.message = message; - } - - @Override - public String toString() - { - return message; - } - } - - /** - * Invoked after recognition begins. - */ - void onStarted(); - - /** - * Invoked if the recognizer detects the beginning of speech. - */ - void onBeginningOfSpeech(); - - /** - * Invoked if the recognizer detects the end of speech. - */ - void onEndOfSpeech(); - - /** - * Invoked if the recognizer does not detect speech within the configured - * timeout period. - */ - void onStartOfSpeechTimeout(); - - /** - * Invoked when the recognizer acoustic state is reset. - * - * @see android.speech.recognition.EmbeddedRecognizer#resetAcousticState() - */ - void onAcousticStateReset(); - - /** - * Invoked when a recognition result is generated. - * - * @param result the recognition result. The result object can not be - * used outside of the scope of the onRecognitionSuccess() callback method. - * To be able to do so, copy it's contents to an user-defined object.<BR> - * An example of this object could be a vector of string arrays; where the - * vector represents a list of recognition result entries and each entry - * is an array of strings to hold the entry's values (the semantic - * meaning, confidence score and literal meaning). - */ - void onRecognitionSuccess(RecognitionResult result); - - /** - * Invoked when a recognition failure occurs. - * - * @param reason the failure reason - */ - void onRecognitionFailure(FailureReason reason); - - /** - * Invoked when an unexpected error occurs. This is normally followed by - * onStopped() if the component shuts down successfully. - * - * @param e the cause of the failure - */ - void onError(Exception e); - - /** - * Invoked when the recognizer stops (due to normal termination or an error). - * - * Invoking stop() on a recognizer that is already stopped will not result - * in a onStopped() event. - */ - void onStopped(); -} diff --git a/core/java/android/speech/recognition/SlotItem.java b/core/java/android/speech/recognition/SlotItem.java deleted file mode 100644 index 3abd27a..0000000 --- a/core/java/android/speech/recognition/SlotItem.java +++ /dev/null @@ -1,27 +0,0 @@ -/*---------------------------------------------------------------------------* - * SlotItem.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Item that may be inserted into an embedded grammar slot. - */ -public interface SlotItem -{ -} diff --git a/core/java/android/speech/recognition/SrecGrammar.java b/core/java/android/speech/recognition/SrecGrammar.java deleted file mode 100644 index c591e05..0000000 --- a/core/java/android/speech/recognition/SrecGrammar.java +++ /dev/null @@ -1,81 +0,0 @@ -/*---------------------------------------------------------------------------* - * SrecGrammar.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; -import java.util.Vector; - -/** - * Grammar on an SREC recognizer. - */ -public interface SrecGrammar extends EmbeddedGrammar -{ - /** - * SrecGrammar Item - */ - public class Item - { - public SlotItem _item; - public int _weight; - public String _semanticMeaning; - - /** - * Creates a grammar item. - * - * @param item the Slotitem. - * @param weight the weight of the item. Smaller values are more likely to get recognized. This should be >= 0. - * @param semanticMeaning the value that will be returned if this item is recognized. - * @throws IllegalArgumentException if item or semanticMeaning are null; if semanticMeaning is empty." - */ - public Item(SlotItem item, int weight, String semanticMeaning) - throws IllegalArgumentException - { - if (item == null) - throw new IllegalArgumentException("Item(): item can't be null."); - if (semanticMeaning == null || semanticMeaning.length()==0) - throw new IllegalArgumentException("Item(): semanticMeaning is null or empty."); - _item = item; - _weight = weight; - _semanticMeaning = semanticMeaning; - - } - } - - /** - * Adds an item to a slot. - * - * @param slotName the name of the slot - * @param item the item to add to the slot. - * @param weight the weight of the item. Smaller values are more likely to get recognized. This should be >= 0. - * @param semanticMeaning the value that will be returned if this item is recognized. - * @throws IllegalArgumentException if slotName, item or semanticMeaning are null; if semanticMeaning is not of the format "V='Jen_Parker'" - */ - public void addItem(String slotName, SlotItem item, int weight, - String semanticMeaning) throws IllegalArgumentException; - - /** - * Add a list of item to a slot. - * - * @param slotName the name of the slot - * @param items the vector of SrecGrammar.Item to add to the slot. - * @throws IllegalArgumentException if slotName,items are null or any element in the items(_item, _semanticMeaning) is null; if any semanticMeaning of the list is not of the format "key='value'" - */ - public void addItemList(String slotName, Vector<Item> items) - throws IllegalArgumentException; - -} diff --git a/core/java/android/speech/recognition/SrecGrammarListener.java b/core/java/android/speech/recognition/SrecGrammarListener.java deleted file mode 100644 index e1f7d3f..0000000 --- a/core/java/android/speech/recognition/SrecGrammarListener.java +++ /dev/null @@ -1,58 +0,0 @@ -/*---------------------------------------------------------------------------* - * SrecGrammarListener.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Listens for SrecGrammar events. - */ -public interface SrecGrammarListener extends EmbeddedGrammarListener { - - /** - * Invokes after all items of the list have been added. - */ - void onAddItemList(); - - /** - * Invoked when adding a SlotItem from a list fails. - * This callback will be trigger for each element in the list that fails to be - * add in the slot, unless there is a grammar fail operation, which will be - * reported in the onError callback. - * @param index of the list that could not be added to the slot - * @param e the cause of the failure. - */ - void onAddItemListFailure(int index, Exception e); - - - /** - * Invoked when a grammar related operation fails. - * - * @param e the cause of the failure.<br/> - * {@link GrammarOverflowException} if the grammar slot is full and no - * further items may be added to it.<br/> - * {@link java.lang.UnsupportedOperationException} if different words with - * the same pronunciation are added.<br/> - * {@link java.lang.IllegalStateException} if reseting or compiling the - * slots fails.<br/> - * {@link java.io.IOException} if the grammar could not be loaded or - * saved.</p> - */ - void onError(Exception e); - -} diff --git a/core/java/android/speech/recognition/VoicetagItem.java b/core/java/android/speech/recognition/VoicetagItem.java deleted file mode 100644 index 0b89639..0000000 --- a/core/java/android/speech/recognition/VoicetagItem.java +++ /dev/null @@ -1,82 +0,0 @@ -/*---------------------------------------------------------------------------* - * VoicetagItem.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -import android.speech.recognition.impl.VoicetagItemImpl; -import java.io.FileNotFoundException; -import java.io.IOException; -/** - * Voicetag that may be inserted into an embedded grammar slot. - */ -public abstract class VoicetagItem implements SlotItem -{ - /** - * Creates a VoicetagItem from a file - * - * @param filename filename for Voicetag - * @param listener listens for Voicetag events - * @return the resulting VoicetagItem - * @throws IllegalArgumentException if filename is null or an empty string. - * @throws FileNotFoundException if the specified filename could not be found - * @throws IOException if the specified filename could not be opened - */ - public static VoicetagItem create(String filename, VoicetagItemListener listener) throws IllegalArgumentException,FileNotFoundException,IOException - { - return VoicetagItemImpl.create(filename,listener); - } - /** - * Returns the audio used to construct the VoicetagItem. - * The audio is in PCM format and is start-pointed and end-pointed. The audio - * is only generated if the enableGetWaveform recognition parameter - * is set prior to recognition. - * - * @throws IllegalStateException if the recognition parameter 'enableGetWaveform' is not set - * @return the audio used to construct the VoicetagItem. - */ - public abstract byte[] getAudio() throws IllegalStateException; - - /** - * Sets the audio used to construct the Voicetag. The - * audio is in PCM format and is start-pointed and end-pointed. The audio is - * only generated if the enableGetWaveform recognition parameter is set - * prior to recognition. - * - * @param waveform the endpointed waveform - * @throws IllegalArgumentException if waveform is null or empty. - * @throws IllegalStateException if the recognition parameter 'enableGetWaveform' is not set - */ - public abstract void setAudio(byte[] waveform) throws IllegalArgumentException,IllegalStateException; - - /** - * Save the Voicetag Item. - * - * @param path where the Voicetag will be saved. We strongly recommend to set the filename with the same value of the VoicetagId. - * @throws IllegalArgumentException if path is null or an empty string. - */ - public abstract void save(String path) throws IllegalArgumentException,IllegalStateException; - - /** - * Load a Voicetag Item. - * - * @throws IllegalStateException if voicetag has not been created from a file. - */ - public abstract void load() throws IllegalStateException; - -} diff --git a/core/java/android/speech/recognition/VoicetagItemListener.java b/core/java/android/speech/recognition/VoicetagItemListener.java deleted file mode 100644 index 610d1c7..0000000 --- a/core/java/android/speech/recognition/VoicetagItemListener.java +++ /dev/null @@ -1,49 +0,0 @@ -/*---------------------------------------------------------------------------* - * VoicetagItemListener.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -/** - * Listens for VoicetagItem events. - */ -public interface VoicetagItemListener -{ - /** - * Invoked after the Voicetag is saved. - * - * @param path the path the Voicetag was saved to - */ - void onSaved(String path); - - /** - * Invoked after the Voicetag is loaded. - */ - void onLoaded(); - - /** - * Invoked when a grammar operation fails. - * - * @param e the cause of the failure.<br/> - * {@link java.io.IOException} if the Voicetag could not be loaded or - * saved.</p> - * {@link java.io.FileNotFoundException} if the specified file could not be found - */ - void onError(Exception e); - -} diff --git a/core/java/android/speech/recognition/WordItem.java b/core/java/android/speech/recognition/WordItem.java deleted file mode 100644 index 5c21c98..0000000 --- a/core/java/android/speech/recognition/WordItem.java +++ /dev/null @@ -1,58 +0,0 @@ -/*---------------------------------------------------------------------------* - * WordItem.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition; - -import android.speech.recognition.impl.WordItemImpl; - -/** - * Word that may be inserted into an embedded grammar slot. - */ -public abstract class WordItem implements SlotItem -{ - /** - * Creates a new WordItem. - * - * @param word the word to insert - * @param pronunciations the pronunciations to associated with the item. If the list is - * is empty (example:new String[0]) the recognizer will attempt to guess the pronunciations. - * @return the WordItem - * @throws IllegalArgumentException if word is null or if pronunciations is - * null or pronunciations contains an element equal to null or empty string. - */ - public static WordItem valueOf(String word, String[] pronunciations) throws IllegalArgumentException - { - return WordItemImpl.valueOf(word, pronunciations); - } - - /** - * Creates a new WordItem. - * - * @param word the word to insert - * @param pronunciation the pronunciation to associate with the item. If it - * is null the recognizer will attempt to guess the pronunciations. - * @return the WordItem - * @throws IllegalArgumentException if word is null or if pronunciation is - * an empty string - */ - public static WordItem valueOf(String word, String pronunciation) throws IllegalArgumentException - { - return WordItemImpl.valueOf(word, pronunciation); - } -} diff --git a/core/java/android/speech/recognition/impl/AudioStreamImpl.java b/core/java/android/speech/recognition/impl/AudioStreamImpl.java deleted file mode 100644 index 730e2d9..0000000 --- a/core/java/android/speech/recognition/impl/AudioStreamImpl.java +++ /dev/null @@ -1,84 +0,0 @@ -/*---------------------------------------------------------------------------* - * AudioStreamImpl.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition.impl; - -import android.speech.recognition.AudioStream; - -/** - */ -public class AudioStreamImpl implements AudioStream, Runnable -{ - /** - * Reference to the native object. - */ - private long nativeObject; - - /** - * Creates a new AudioStreamImpl. - * - * @param nativeObj a reference to the native object - */ - public AudioStreamImpl(long nativeObj) - { - nativeObject = nativeObj; - } - - public synchronized void run() - { - dispose(); - } - - public long getNativeObject() { - synchronized (AudioStreamImpl.class) - { - return nativeObject; - } - } - - /** - * Releases the native resources associated with the object. - */ - @SuppressWarnings("deprecation") - public void dispose() - { - synchronized (AudioStreamImpl.class) - { - if (nativeObject != 0) - { - deleteNativeObject(nativeObject); - nativeObject = 0; - } - } - } - - @Override - protected void finalize() throws Throwable - { - dispose(); - super.finalize(); - } - - /** - * Deletes a native object. - * - * @param nativeObject pointer to the native object - */ - private native void deleteNativeObject(long nativeObject); -} diff --git a/core/java/android/speech/recognition/impl/DeviceSpeakerImpl.java b/core/java/android/speech/recognition/impl/DeviceSpeakerImpl.java deleted file mode 100644 index 5d72110..0000000 --- a/core/java/android/speech/recognition/impl/DeviceSpeakerImpl.java +++ /dev/null @@ -1,164 +0,0 @@ -/*---------------------------------------------------------------------------* - * DeviceSpeakerImpl.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition.impl; - -import android.speech.recognition.AudioStream; -import android.speech.recognition.Codec; -import android.speech.recognition.DeviceSpeaker; -import android.speech.recognition.DeviceSpeakerListener; - -/** - */ -public class DeviceSpeakerImpl extends DeviceSpeaker implements Runnable -{ - private static DeviceSpeakerImpl instance; - /** - * Reference to the native object. - */ - private long nativeObject; - private DeviceSpeakerListener locallistener; - - /** - * Private constructor - */ - private DeviceSpeakerImpl() - { - System system = System.getInstance(); - nativeObject = initNativeObject(); - if (nativeObject != 0) - system.register(this); - } - - public void run() - { - dispose(); - } - - /** - * Returns the singleton instance. - * - * @return the singleton instance - */ - public static DeviceSpeakerImpl getInstance() - { - synchronized (DeviceSpeakerImpl.class) - { - if (instance == null) - instance = new DeviceSpeakerImpl(); - return instance; - } - } - - /** - * Start audio playback. - * - * @param source the audio to play - */ - public void start(AudioStream source) - { - synchronized (DeviceSpeakerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - AudioStreamImpl src = (AudioStreamImpl)source; - startProxy(nativeObject,src.getNativeObject()); - src = null; - } - } - - /** - * Stops audio playback. - */ - public void stop() - { - synchronized (DeviceSpeakerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - stopProxy(nativeObject); - } - } - - /** - * Set the playback codec. This must be called before start is called. - * @param playbackCodec the codec to use for the playback operation. - */ - public void setCodec(Codec playbackCodec) - { - synchronized (DeviceSpeakerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - setCodecProxy(nativeObject,playbackCodec); - } - } - - /** - * set the microphone listener. - * @param listener the device speaker listener. - */ - public void setListener(DeviceSpeakerListener listener) - { - synchronized (DeviceSpeakerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - locallistener = listener; - setListenerProxy(nativeObject,listener); - } - } - - /** - * Releases the native resources associated with the object. - */ - private void dispose() - { - synchronized (DeviceSpeakerImpl.class) - { - if (nativeObject != 0) - { - deleteNativeObject(nativeObject); - nativeObject = 0; - instance = null; - locallistener = null; - System.getInstance().unregister(this); - } - } - } - - @Override - protected void finalize() throws Throwable - { - dispose(); - super.finalize(); - } - - private native long initNativeObject(); - - private native void startProxy(long nativeObject, long audioNativeObject); - - private native void stopProxy(long nativeObject); - - private native void setCodecProxy(long nativeObject,Codec playbackCodec); - - private native void setListenerProxy(long nativeObject,DeviceSpeakerListener listener); - - private native void deleteNativeObject(long nativeObject); -} diff --git a/core/java/android/speech/recognition/impl/EmbeddedGrammarImpl.java b/core/java/android/speech/recognition/impl/EmbeddedGrammarImpl.java deleted file mode 100644 index 0b88cb2..0000000 --- a/core/java/android/speech/recognition/impl/EmbeddedGrammarImpl.java +++ /dev/null @@ -1,73 +0,0 @@ -/*---------------------------------------------------------------------------* - * EmbeddedGrammarImpl.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition.impl; - -import android.speech.recognition.EmbeddedGrammar; - -/** - */ -public class EmbeddedGrammarImpl extends GrammarImpl implements EmbeddedGrammar -{ - /** - * Creates a new EmbeddedGrammarImpl. - * - * @param nativeObject a reference to the native object - */ - public EmbeddedGrammarImpl(long nativeObject) - { - super(nativeObject); - } - - public void compileAllSlots() - { - synchronized (GrammarImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - compileAllSlotsProxy(nativeObject); - } - } - - public void resetAllSlots() - { - synchronized (GrammarImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - resetAllSlotsProxy(nativeObject); - } - } - - public void save(String url) - { - synchronized (GrammarImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - saveProxy(nativeObject, url.toString()); - } - } - - private native void compileAllSlotsProxy(long nativeObject); - - private native void resetAllSlotsProxy(long nativeObject); - - private native void saveProxy(long nativeObject, String url); -} diff --git a/core/java/android/speech/recognition/impl/EmbeddedRecognizerImpl.java b/core/java/android/speech/recognition/impl/EmbeddedRecognizerImpl.java deleted file mode 100644 index f04bfe4..0000000 --- a/core/java/android/speech/recognition/impl/EmbeddedRecognizerImpl.java +++ /dev/null @@ -1,246 +0,0 @@ -/*---------------------------------------------------------------------------* - * EmbeddedRecognizerImpl.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition.impl; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Hashtable; -import java.util.Vector; -import android.speech.recognition.EmbeddedRecognizer; -import android.speech.recognition.Grammar; -import android.speech.recognition.AudioStream; -import android.speech.recognition.Grammar; -import android.speech.recognition.RecognizerListener; -import android.speech.recognition.GrammarListener; - -/** - */ -public class EmbeddedRecognizerImpl extends EmbeddedRecognizer implements Runnable -{ - /** - * Reference to the native object. - */ - private long nativeObject; - /** - * The singleton instance. - */ - private static EmbeddedRecognizerImpl instance; - - /** - * Creates a new instance. - */ - EmbeddedRecognizerImpl() - { - System system = System.getInstance(); - nativeObject = getInstanceProxy(); - if (nativeObject != 0) - system.register(this); - } - - /** - * Returns the singleton instance. - * - * @return the singleton instance - */ - public synchronized static EmbeddedRecognizerImpl getInstance() - { - synchronized (EmbeddedRecognizerImpl.class) - { - if (instance == null) - instance = new EmbeddedRecognizerImpl(); - return instance; - } - } - - public void run() - { - dispose(); - } - - /** - * Releases the native resources associated with the object. - */ - private void dispose() - { - synchronized (EmbeddedRecognizerImpl.class) - { - if (instance != null) - { - deleteNativeObject(nativeObject); - nativeObject = 0; - instance = null; - System.getInstance().unregister(this); - } - } - } - - public void configure(String config) throws IllegalArgumentException, - FileNotFoundException, IOException, UnsatisfiedLinkError, - ClassNotFoundException - { - synchronized (EmbeddedRecognizerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - if (config == null) - throw new IllegalArgumentException("Configuration Is Null."); - configureProxy(nativeObject,config); - } - } - - public void setListener(RecognizerListener listener) - { - synchronized (EmbeddedRecognizerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - setListenerProxy(nativeObject,listener); - } - } - - public Grammar createGrammar(String value, GrammarListener listener) - throws IllegalArgumentException - { - synchronized (EmbeddedRecognizerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - long nativeGrammar = createEmbeddedGrammarProxy(nativeObject,value.toString(), listener); - return new SrecGrammarImpl(nativeGrammar); - } - } - - public void resetAcousticState() - { - synchronized (EmbeddedRecognizerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - resetAcousticStateProxy(nativeObject); - } - } - - public void recognize(AudioStream audio, - Vector<Grammar> grammars) - { - synchronized (EmbeddedRecognizerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - - if (audio == null) - throw new IllegalArgumentException("AudioStream cannot be null."); - - if (grammars == null || grammars.isEmpty() == true) - throw new IllegalArgumentException("Grammars are null or empty."); - int grammarCount = grammars.size(); - - long[] nativeGrammars = new long[grammarCount]; - - for (int i = 0; i < grammarCount; ++i) - nativeGrammars[i] = ((GrammarImpl) grammars.get(i)).getNativeObject(); - - recognizeProxy(nativeObject,((AudioStreamImpl)audio).getNativeObject(), nativeGrammars); - } - } - - public void recognize(AudioStream audio, - Grammar grammar) - { - synchronized (EmbeddedRecognizerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - } - Vector<Grammar> grammars = new Vector<Grammar>(); - grammars.add(grammar); - recognize(audio, grammars); - } - - public void stop() - { - synchronized (EmbeddedRecognizerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - stopProxy(nativeObject); - } - } - - public void setParameters(Hashtable<String, String> params) - { - synchronized (EmbeddedRecognizerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - setParametersProxy(nativeObject,params); - } - } - - public void getParameters(Vector<String> params) - { - synchronized (EmbeddedRecognizerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - getParametersProxy(nativeObject,params); - } - } - - /** - * Returns the native EmbeddedRecognizer. - * - * @return a reference to the native object - */ - private native long getInstanceProxy(); - - /** - * Configures the recognizer instance. - * - * @param config the recognizer configuration file - */ - private native void configureProxy(long nativeObject, String config) throws IllegalArgumentException, - FileNotFoundException, IOException, UnsatisfiedLinkError, - ClassNotFoundException; - - /** - * Sets the recognizer listener. - * - * @param listener listens for recognizer events - */ - private native void setListenerProxy(long nativeObject, RecognizerListener listener); - - private native void recognizeProxy(long nativeObject, long audioNativeObject, - long[] pGrammars); - - private native long createEmbeddedGrammarProxy(long nativeObject, String url, - GrammarListener listener); - - private native void stopProxy(long nativeObject); - - private native void deleteNativeObject(long nativeObject); - - private native void setParametersProxy(long nativeObject, Hashtable<String, String> params); - - private native void getParametersProxy(long nativeObject, Vector<String> params); - - private native void resetAcousticStateProxy(long nativeObject); - -} diff --git a/core/java/android/speech/recognition/impl/EntryImpl.java b/core/java/android/speech/recognition/impl/EntryImpl.java deleted file mode 100644 index 91b2b78..0000000 --- a/core/java/android/speech/recognition/impl/EntryImpl.java +++ /dev/null @@ -1,147 +0,0 @@ -/*---------------------------------------------------------------------------* - * EntryImpl.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition.impl; - -import android.speech.recognition.NBestRecognitionResult; -import java.util.Enumeration; - -/** - */ -public class EntryImpl implements NBestRecognitionResult.Entry, Runnable -{ - private long nativeObject; - - /** - * This implementation is a work-around to solve Q bug with - * nested classes. - * - * @param nativeObject the native NBestRecognitionResult.Entry object - */ - public EntryImpl(long nativeObject) - { - this.nativeObject = nativeObject; - } - - public void run() - { - dispose(); - } - - public byte getConfidenceScore() throws IllegalStateException - { - synchronized (EntryImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - return getConfidenceScoreProxy(nativeObject); - } - } - - public String getLiteralMeaning() throws IllegalStateException - { - synchronized (EntryImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - return getLiteralMeaningProxy(nativeObject); - } - } - - public String getSemanticMeaning() throws IllegalStateException - { - synchronized (EntryImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - return getSemanticMeaningProxy(nativeObject); - } - } - - public String get(String key) - { - synchronized (EntryImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - return getProxy(nativeObject,key); - } - } - - public Enumeration keys() - { - synchronized (EntryImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - - return new Enumeration() - { - private String[] keys = keysProxy(nativeObject); - private int indexOfNextRead = 0; - - public boolean hasMoreElements() - { - return indexOfNextRead <= keys.length-1; - } - - public Object nextElement() - { - return keys[indexOfNextRead++]; - } - }; - } - } - - - /** - * Releases the native resources associated with the object. - */ - private void dispose() - { - synchronized (EntryImpl.class) - { - if (nativeObject != 0) - { - deleteNativeObject(nativeObject); - nativeObject = 0; - } - } - } - - @Override - protected void finalize() throws Throwable - { - dispose(); - super.finalize(); - } - - private native void deleteNativeObject(long nativeObject); - - private native String getLiteralMeaningProxy(long nativeObject); - - private native String getSemanticMeaningProxy(long nativeObject); - - private native byte getConfidenceScoreProxy(long nativeObject); - - private native String getProxy(long nativeObject,String key); - - private native String[] keysProxy(long nativeObject); - -} diff --git a/core/java/android/speech/recognition/impl/GrammarImpl.java b/core/java/android/speech/recognition/impl/GrammarImpl.java deleted file mode 100644 index 563d5d9..0000000 --- a/core/java/android/speech/recognition/impl/GrammarImpl.java +++ /dev/null @@ -1,114 +0,0 @@ -/*---------------------------------------------------------------------------* - * GrammarImpl.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition.impl; - -import android.speech.recognition.Grammar; - -/** - */ -public class GrammarImpl implements Grammar, Runnable -{ - /** - * Reference to the native object. - */ - protected long nativeObject; - - /** - * Creates a new GrammarImpl. - * - * @param nativeObj a reference to the native object - */ - public GrammarImpl(long nativeObj) - { - nativeObject = nativeObj; - } - - public void run() - { - dispose(); - } - - public long getNativeObject() - { - synchronized (GrammarImpl.class) - { - return nativeObject; - } - } - - /** - * Indicates that the grammar will be used in the near future. - */ - public void load() - { - synchronized (GrammarImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - loadProxy(nativeObject); - } - } - - /** - * The grammar will be removed from use. - */ - public void unload() - { - synchronized (GrammarImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - unloadProxy(nativeObject); - } - } - - /** - * Releases the native resources associated with the object. - */ - public void dispose() - { - synchronized (GrammarImpl.class) - { - if (nativeObject != 0) - { - deleteNativeObject(nativeObject); - nativeObject = 0; - } - } - } - - @Override - protected void finalize() throws Throwable - { - dispose(); - super.finalize(); - } - - /** - * Deletes a native object. - * - * @param nativeObject pointer to the native object - */ - private native void deleteNativeObject(long nativeObject); - - private native void loadProxy(long nativeObject); - - private native void unloadProxy(long nativeObject); -} diff --git a/core/java/android/speech/recognition/impl/LoggerImpl.java b/core/java/android/speech/recognition/impl/LoggerImpl.java deleted file mode 100644 index 9933c56..0000000 --- a/core/java/android/speech/recognition/impl/LoggerImpl.java +++ /dev/null @@ -1,166 +0,0 @@ -/*---------------------------------------------------------------------------* - * LoggerImpl.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition.impl; - -import android.speech.recognition.Logger; - -/** - */ -public class LoggerImpl extends Logger implements Runnable -{ - private static LoggerImpl instance; - /** - * Reference to the native object. - */ - private long nativeObject; - - /** - * Creates a new instance of LoggerImpl. - * - * @param function the name of the enclosing function - */ - private LoggerImpl() - { - System system = System.getInstance(); - nativeObject = initNativeObject(); - if (nativeObject!=0) - system.register(this); - } - - public void run() - { - dispose(); - } - - /** - * Returns the singleton instance. - * - * @return the singleton instance - */ - public static LoggerImpl getInstance() - { - synchronized (LoggerImpl.class) - { - if (instance == null) - instance = new LoggerImpl(); - return instance; - } - } - - public void setLoggingLevel(LogLevel level) - { - synchronized (LoggerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - setLoggingLevelProxy(nativeObject,level); - } - } - - public void setPath(String path) - { - synchronized (LoggerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - setPathProxy(nativeObject,path); - } - } - - public void error(String message) - { - synchronized (LoggerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - errorProxy(nativeObject,message); - } - } - - public void warn(String message) - { - synchronized (LoggerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - warnProxy(nativeObject,message); - } - } - - public void info(String message) - { - synchronized (LoggerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - infoProxy(nativeObject,message); - } - } - - public void trace(String message) - { - synchronized (LoggerImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - traceProxy(nativeObject,message); - } - } - - /** - * Releases the native resources associated with the object. - */ - private void dispose() - { - synchronized (LoggerImpl.class) - { - if (nativeObject!=0) - { - deleteNativeObject(nativeObject); - System.getInstance().unregister(this); - } - nativeObject = 0; - instance = null; - } - } - - @Override - protected void finalize() throws Throwable - { - dispose(); - super.finalize(); - } - - private native long initNativeObject(); - - private native void setLoggingLevelProxy(long nativeObject, LogLevel level); - - private native void setPathProxy(long nativeObject, String filename); - - private native void errorProxy(long nativeObject, String message); - - private native void warnProxy(long nativeObject, String message); - - private native void infoProxy(long nativeObject, String message); - - private native void traceProxy(long nativeObject,String message); - - private native void deleteNativeObject(long nativeObject); -} diff --git a/core/java/android/speech/recognition/impl/MediaFileReaderImpl.java b/core/java/android/speech/recognition/impl/MediaFileReaderImpl.java deleted file mode 100644 index 8ce643d..0000000 --- a/core/java/android/speech/recognition/impl/MediaFileReaderImpl.java +++ /dev/null @@ -1,156 +0,0 @@ -/*---------------------------------------------------------------------------* - * MediaFileReaderImpl.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition.impl; - -import android.speech.recognition.MediaFileReader; -import android.speech.recognition.AudioStream; -import android.speech.recognition.Codec; -import android.speech.recognition.AudioSourceListener; - -/** - */ -public class MediaFileReaderImpl extends MediaFileReader implements Runnable -{ - /** - * Reference to the native object. - */ - private long nativeObject; - - /** - * Creates a new MediaFileReaderImpl. - * - * @param filename the name of the file to read from - * @param listener listens for MediaFileReader events - */ - public MediaFileReaderImpl(String filename, AudioSourceListener listener) - { - System system = System.getInstance(); - nativeObject = - createMediaFileReaderProxy(filename, listener); - if (nativeObject != 0) - system.register(this); - } - - public void run() - { - dispose(); - } - - /** - * Set the reading mode - */ - public void setMode(Mode mode) - { - synchronized (MediaFileReaderImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - setModeProxy(nativeObject,mode); - } - } - - /** - * Creates an audioStream source - */ - public AudioStream createAudio() - { - synchronized (MediaFileReaderImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - return new AudioStreamImpl(createAudioProxy(nativeObject)); - } - } - - /** - * Tells the audio source to start collecting audio samples. - */ - public void start() - { - synchronized (MediaFileReaderImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - startProxy(nativeObject); - } - } - - /** - * Stops this source from collecting audio samples. - */ - public void stop() - { - synchronized (MediaFileReaderImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - stopProxy(nativeObject); - } - } - - /** - * Releases the native resources associated with the object. - */ - public void dispose() - { - synchronized (MediaFileReaderImpl.class) - { - if (nativeObject != 0) - { - deleteNativeObject(nativeObject); - nativeObject = 0; - System.getInstance().unregister(this); - } - } - } - - @Override - protected void finalize() throws Throwable - { - dispose(); - super.finalize(); - } - - /** - * Deletes a native object. - * - * @param nativeObject pointer to the native object - */ - private native void deleteNativeObject(long nativeObject); - - /** - * Creates a native MediaFileReader. - * - * @param filename the name of the file to read from - * @param offset the offset to begin reading from - * @param codec the file audio format - * @param listener listens for MediaFileReader events - * @return a reference to the native object - */ - private native long createMediaFileReaderProxy(String filename, AudioSourceListener listener); - - private native void setModeProxy(long nativeObject,Mode mode); - - private native long createAudioProxy(long nativeObject); - - private native void startProxy(long nativeObject); - - private native void stopProxy(long nativeObject); -} diff --git a/core/java/android/speech/recognition/impl/MediaFileWriterImpl.java b/core/java/android/speech/recognition/impl/MediaFileWriterImpl.java deleted file mode 100644 index c4bd836..0000000 --- a/core/java/android/speech/recognition/impl/MediaFileWriterImpl.java +++ /dev/null @@ -1,102 +0,0 @@ -/*---------------------------------------------------------------------------* - * MediaFileWriterImpl.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition.impl; - -import android.speech.recognition.AudioStream; -import android.speech.recognition.MediaFileWriter; -import android.speech.recognition.MediaFileWriterListener; - -/** - */ -public class MediaFileWriterImpl extends MediaFileWriter implements Runnable -{ - /** - * Reference to the native object. - */ - private long nativeObject; - - /** - * Creates a new MediaFileWriterImpl. - * - * @param listener listens for MediaFileWriter events - */ - public MediaFileWriterImpl(MediaFileWriterListener listener) - { - System system = System.getInstance(); - nativeObject = createMediaFileWriterProxy(listener); - if (nativeObject != 0) - system.register(this); - } - - public void run() - { - dispose(); - } - - public void save(AudioStream source, String filename) - { - synchronized (MediaFileWriterImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - saveProxy(nativeObject,((AudioStreamImpl)source).getNativeObject(), filename); - } - } - - /** - * Releases the native resources associated with the object. - */ - public synchronized void dispose() - { - synchronized (MediaFileWriterImpl.class) - { - if (nativeObject != 0) - { - deleteNativeObject(nativeObject); - nativeObject = 0; - System.getInstance().unregister(this); - } - } - } - - @Override - protected void finalize() throws Throwable - { - dispose(); - super.finalize(); - } - - /** - * Creates a native MediaFileWriter. - * - * @param listener listens for MediaFileReader events - * @return a reference to the native object - */ - private native long createMediaFileWriterProxy(MediaFileWriterListener listener); - - /** - * Deletes a native object. - * - * @param nativeObject pointer to the native object - */ - private native void deleteNativeObject(long nativeObject); - - private native void saveProxy(long nativeObject, long audioNativeObject, String filename); -} diff --git a/core/java/android/speech/recognition/impl/MicrophoneImpl.java b/core/java/android/speech/recognition/impl/MicrophoneImpl.java deleted file mode 100644 index a915484..0000000 --- a/core/java/android/speech/recognition/impl/MicrophoneImpl.java +++ /dev/null @@ -1,165 +0,0 @@ -/*---------------------------------------------------------------------------* - * MicrophoneImpl.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition.impl; - -import android.speech.recognition.AudioStream; -import android.speech.recognition.Codec; -import android.speech.recognition.Microphone; -import android.speech.recognition.AudioSourceListener; - -/** - */ -public class MicrophoneImpl extends Microphone implements Runnable -{ - private static MicrophoneImpl instance; - /** - * Reference to the native object. - */ - private long nativeObject; - - /** - * Creates a new MicrophoneImpl. - * - * @param nativeObj a reference to the native object - */ - private MicrophoneImpl() - { - System system = System.getInstance(); - nativeObject = initNativeObject(); - if (nativeObject != 0) - system.register(this); - } - - public void run() - { - dispose(); - } - - /** - * Returns the singleton instance. - * - * @return the singleton instance - */ - public static MicrophoneImpl getInstance() - { - synchronized (MicrophoneImpl.class) - { - if (instance == null) - instance = new MicrophoneImpl(); - return instance; - } - } - - /** - * set the recording codec. This must be called before Start is called. - * @param recordingCodec the codec in which the samples will be recorded. - */ - public void setCodec(Codec recordingCodec) - { - synchronized (MicrophoneImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - setCodecProxy(nativeObject,recordingCodec); - } - } - - /** - * set the microphone listener. - * @param listener the microphone listener. - */ - public void setListener(AudioSourceListener listener) - { - synchronized (MicrophoneImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - setListenerProxy(nativeObject,listener); - } - } - - public AudioStream createAudio() - { - synchronized (MicrophoneImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - return new AudioStreamImpl(createAudioProxy(nativeObject)); - } - } - - public void start() - { - synchronized (MicrophoneImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - startProxy(nativeObject); - } - } - - public void stop() - { - synchronized (MicrophoneImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - stopProxy(nativeObject); - } - } - - /** - * Releases the native resources associated with the object. - */ - private void dispose() - { - synchronized (MicrophoneImpl.class) - { - if (nativeObject != 0) - { - deleteNativeObject(nativeObject); - nativeObject = 0; - instance = null; - System.getInstance().unregister(this); - } - } - } - - @Override - protected void finalize() throws Throwable - { - dispose(); - super.finalize(); - } - - private native long initNativeObject(); - - private native void setCodecProxy(long nativeObject,Codec recordingCodec); - - private native void setListenerProxy(long nativeObject, AudioSourceListener listener); - - private native long createAudioProxy(long nativeObject); - - private native void startProxy(long nativeObject); - - private native void stopProxy(long nativeObject); - - private native void deleteNativeObject(long nativeObject); -} diff --git a/core/java/android/speech/recognition/impl/NBestRecognitionResultImpl.java b/core/java/android/speech/recognition/impl/NBestRecognitionResultImpl.java deleted file mode 100644 index 4d2e00a..0000000 --- a/core/java/android/speech/recognition/impl/NBestRecognitionResultImpl.java +++ /dev/null @@ -1,106 +0,0 @@ -/*---------------------------------------------------------------------------* - * NBestRecognitionResultImpl.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition.impl; - -import android.speech.recognition.NBestRecognitionResult; -import android.speech.recognition.VoicetagItem; -import android.speech.recognition.VoicetagItemListener; -/** - */ -public class NBestRecognitionResultImpl implements NBestRecognitionResult -{ - /** - * Reference to the native object. - */ - private long nativeObject; - - /** - * Creates a new NBestRecognitionResultImpl. - * - * @param nativeObject a reference to the native object - */ - public NBestRecognitionResultImpl(long nativeObject) - { - this.nativeObject = nativeObject; - } - - public int getSize() - { - synchronized (NBestRecognitionResultImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - return getSizeProxy(nativeObject); - } - } - - public Entry getEntry(int index) - { - synchronized (NBestRecognitionResultImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - long nativeEntryObject = getEntryProxy(nativeObject,index); - if (nativeEntryObject==0) - return null; - else - return new EntryImpl(nativeEntryObject); - } - } - - public VoicetagItem createVoicetagItem(String VoicetagId, VoicetagItemListener listener) - { - synchronized (NBestRecognitionResultImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - if ((VoicetagId == null) || (VoicetagId.length() == 0)) - throw new IllegalArgumentException("VoicetagId may not be null or empty string."); - return new VoicetagItemImpl(createVoicetagItemProxy(nativeObject,VoicetagId,listener),false); - } - } - - /** - * Releases the native resources associated with the object. - */ - private void dispose() - { - synchronized (NBestRecognitionResultImpl.class) - { - nativeObject = 0; - } - } - - @Override - protected void finalize() throws Throwable - { - dispose(); - super.finalize(); - } - - /** - * Returns a reference to the native VoicetagItem. - */ - private native long createVoicetagItemProxy(long nativeObject, String VoicetagId, VoicetagItemListener listener); - - private native long getEntryProxy(long nativeObject, int index); - - private native int getSizeProxy(long nativeObject); -} diff --git a/core/java/android/speech/recognition/impl/SrecGrammarImpl.java b/core/java/android/speech/recognition/impl/SrecGrammarImpl.java deleted file mode 100644 index cb6f4c6..0000000 --- a/core/java/android/speech/recognition/impl/SrecGrammarImpl.java +++ /dev/null @@ -1,120 +0,0 @@ -/*---------------------------------------------------------------------------* - * SrecGrammarImpl.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition.impl; - -import android.speech.recognition.SrecGrammar; -import android.speech.recognition.SlotItem; -import android.speech.recognition.VoicetagItem; -import android.speech.recognition.WordItem; - -import java.util.Vector; - -/** - */ -public class SrecGrammarImpl extends EmbeddedGrammarImpl implements SrecGrammar -{ - /** - * Creates a new SrecGrammarImpl. - * - * @param nativeObject the native object - */ - public SrecGrammarImpl(long nativeObject) - { - super(nativeObject); - } - - public void addItem(String slotName, SlotItem item, int weight, - String semanticValue) - { - synchronized (GrammarImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - - if (slotName == null || slotName.length()==0) - throw new IllegalArgumentException("addItem() - Slot name is null or empty."); - if (item == null) - throw new IllegalArgumentException("addItem() - item can't be null."); - if (semanticValue == null || semanticValue.length()==0) - throw new IllegalArgumentException("addItem() - semanticValue is null or empty."); - - long itemNativeObject = 0; - if (item instanceof VoicetagItem) - itemNativeObject = ((VoicetagItemImpl)item).getNativeObject(); - else if (item instanceof WordItem) - itemNativeObject = ((WordItemImpl)item).getNativeObject(); - else - throw new IllegalArgumentException("SlotItem - should be a WordItem or a VoicetagItem object."); - - addItemProxy(nativeObject, slotName, itemNativeObject, weight, semanticValue); - } - } - - public void addItemList(String slotName, Vector<Item> items) - { - synchronized (GrammarImpl.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object has been disposed"); - - if (slotName == null || slotName.length()==0) - throw new IllegalArgumentException("addItemList - Slot name is null or empty."); - if (items == null || items.isEmpty() == true) - throw new IllegalArgumentException("addItemList - Items is null or empty."); - - int itemsCount = items.size(); - - long[] nativeSlots = new long[itemsCount]; - int[] nativeWeights = new int[itemsCount]; - String[] nativeSemantic = new String[itemsCount]; - - Item element = null; - long itemNativeObject = 0; - SlotItem item = null; - for (int i = 0; i < itemsCount; ++i) - { - element = items.get(i); - - item = element._item; - if (item instanceof VoicetagItem) - itemNativeObject = ((VoicetagItemImpl)item).getNativeObject(); - else if (item instanceof WordItem) - itemNativeObject = ((WordItemImpl)item).getNativeObject(); - else - { - throw new IllegalArgumentException("SlotItem ["+i+"] - should be a WordItem or a VoicetagItem object."); - } - nativeSlots[i] = itemNativeObject; - nativeWeights[i] = element._weight; - nativeSemantic[i]= element._semanticMeaning; - itemNativeObject = 0; - item = null; - } - addItemListProxy(nativeObject, slotName,nativeSlots,nativeWeights,nativeSemantic); - } - } - - private native void addItemProxy(long nativeObject, String slotName, long item, int weight, - String semanticValue); - - private native void addItemListProxy(long nativeObject, String slotName, long[] items, - int[] weights, String[] semanticValues); - -} diff --git a/core/java/android/speech/recognition/impl/System.java b/core/java/android/speech/recognition/impl/System.java deleted file mode 100644 index 23418fe..0000000 --- a/core/java/android/speech/recognition/impl/System.java +++ /dev/null @@ -1,179 +0,0 @@ -/*---------------------------------------------------------------------------* - * System.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition.impl; - -import java.lang.ref.WeakReference; -import java.util.WeakHashMap; - - -/** - */ -public class System -{ - private static boolean libraryLoaded; - private static System instance; - private static WeakHashMap<Object, WeakReference> registerMap; - /** - * Reference to the native object. - */ - private long nativeObject; - private boolean shutdownRequested; - - /** - * Creates a new instance of System - */ - private System() - { - shutdownRequested = false; - registerMap = - new WeakHashMap<Object, WeakReference>(); - initLibrary(); - nativeObject = initNativeObject(); - Runtime.getRuntime(). - addShutdownHook(new Thread() - { - @Override - public void run() - { - try - { - dispose(); - } - catch (Exception e) - { - e.printStackTrace(); - } - } - }); - - } - - /** - * Returns the singleton instance. - * - * @return the singleton instance - */ - public static System getInstance() - { - synchronized (System.class) - { - if (instance == null) - instance = new System(); - return instance; - } - } - - /** - * Loads the native library if necessary. - */ - private void initLibrary() - { - if (!libraryLoaded) - { - java.lang.System.loadLibrary("UAPI_jni"); - libraryLoaded = true; - } - } - - /** - * Registers an object for shutdown when System.dispose() is invoked. - * - * @param r the code to run on shutdown - * @throws IllegalStateException if the System is shutting down - */ - public void register(Runnable r) throws IllegalStateException - { - synchronized (System.class) - { - if (shutdownRequested) - throw new IllegalStateException("System is shutting down"); - registerMap.put(r, - new WeakReference<Runnable>(r)); - } - } - - /** - * Registers an object for shutdown when System.dispose() is invoked. - * - * @param r the code to run on shutdown - */ - public void unregister(Runnable r) - { - synchronized (System.class) - { - if (shutdownRequested) - { - // System.dispose() will end up removing all entries - return; - } - if (r!=null) registerMap.remove(r); - } - } - - /** - * Releases the native resources associated with the object. - * - * @throws java.util.concurrent.TimeoutException if the operation timeouts - * @throws IllegalThreadStateException if a native thread error occurs - */ - public void dispose() throws java.util.concurrent.TimeoutException, - IllegalThreadStateException - { - synchronized (System.class) - { - if (nativeObject == 0) - return; - shutdownRequested = true; - } - - // Traverse the list of WeakReferences - // cast to a Runnable object if the weakrerefence is not null - // then call the run method. - for (Object o: registerMap.keySet()) - { - WeakReference weakReference = registerMap.get(o); - Runnable r = (Runnable) weakReference.get(); - if (r != null) - r.run(); - } - registerMap.clear(); - - // Call the native dispose method - disposeProxy(); - synchronized (System.class) - { - nativeObject = 0; - instance = null; - } - } - - @Override - protected void finalize() throws Throwable - { - dispose(); - super.finalize(); - } - - public static native String getAPIVersion(); - - private static native long initNativeObject(); - - private static native void disposeProxy(); -} diff --git a/core/java/android/speech/recognition/impl/VoicetagItemImpl.java b/core/java/android/speech/recognition/impl/VoicetagItemImpl.java deleted file mode 100644 index f9db399..0000000 --- a/core/java/android/speech/recognition/impl/VoicetagItemImpl.java +++ /dev/null @@ -1,206 +0,0 @@ -/*---------------------------------------------------------------------------* - * VoicetagItemImpl.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition.impl; - -import android.speech.recognition.VoicetagItem; -import android.speech.recognition.VoicetagItemListener; -import java.io.FileNotFoundException; -import java.io.IOException; -/** - */ -public class VoicetagItemImpl extends VoicetagItem implements Runnable -{ - /** - * Reference to the native object. - */ - private long nativeObject; - /** - * Voicetag has a filename need to be loaded before use it. - */ - private boolean needToBeLoaded; - - /** - * Creates a new VoicetagItemImpl. - * - * @param nativeObject the pointer to the native object - */ - public VoicetagItemImpl(long nativeObject, boolean fromfile) - { - this.nativeObject = nativeObject; - needToBeLoaded = fromfile; - } - - public void run() - { - dispose(); - } - - /** - * Creates a VoicetagItem from a file - * - * @param filename filename for Voicetag - * @param listener listens for Voicetag events - * @return the resulting VoicetagItem - * @throws IllegalArgumentException if filename is null or an empty string. - * @throws FileNotFoundException if the specified filename could not be found - * @throws IOException if the specified filename could not be opened - */ - public static VoicetagItem create(String filename, VoicetagItemListener listener) throws IllegalArgumentException,FileNotFoundException,IOException - { - if ((filename == null) || (filename.length() == 0)) - throw new IllegalArgumentException("Filename may not be null or empty string."); - - VoicetagItemImpl voicetag = null; - long nativeVoicetag = createVoicetagProxy(filename,listener); - if (nativeVoicetag!=0) - { - voicetag = new VoicetagItemImpl(nativeVoicetag,true); - } - return voicetag; - } - /** - * Returns the audio used to construct the VoicetagItem. - */ - public byte[] getAudio() throws IllegalStateException - { - synchronized (VoicetagItem.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - - return getAudioProxy(nativeObject); - } - } - - /** - * Sets the audio used to construct the Voicetag. - */ - public void setAudio(byte[] waveform) throws IllegalArgumentException,IllegalStateException - { - synchronized (VoicetagItem.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - - if ((waveform == null) || (waveform.length == 0)) - throw new IllegalArgumentException("Waveform may not be null or empty."); - setAudioProxy(nativeObject,waveform); - } - } - - /** - * Save the Voicetag. - */ - public void save(String path) throws IllegalArgumentException - { - synchronized (VoicetagItem.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - if ((path == null) || (path.length() == 0)) - throw new IllegalArgumentException("Path may not be null or empty string."); - saveVoicetagProxy(nativeObject,path); - } - } - - /** - * Load a Voicetag. - */ - public void load() throws IllegalStateException - { - synchronized (VoicetagItem.class) - { - if (nativeObject == 0) - throw new IllegalStateException("Object was destroyed."); - if (!needToBeLoaded) - throw new IllegalStateException("This Voicetag was not created from a file, does not need to be loaded."); - loadVoicetagProxy(nativeObject); - } - } - - public long getNativeObject() - { - synchronized (VoicetagItem.class) - { - return nativeObject; - } - } - - /** - * Releases the native resources associated with the object. - */ - private void dispose() - { - synchronized (VoicetagItem.class) - { - if (nativeObject != 0) - { - deleteNativeObject(nativeObject); - nativeObject = 0; - } - } - } - - @Override - protected void finalize() throws Throwable - { - dispose(); - super.finalize(); - } - - - private static native long createVoicetagProxy(String filename, VoicetagItemListener listener); - /** - * (Optional operation) Returns the audio used to construct the Voicetag. The - * audio is in PCM format and is start-pointed and end-pointed. The audio is - * only generated if the enableGetWaveform recognition parameter is set - * prior to recognition. - * - * @see RecognizerParameters.enableGetWaveform - */ - private native byte[] getAudioProxy(long nativeObject); - - /** - * (Optional operation) Sets the audio used to construct the Voicetag. The - * audio is in PCM format and is start-pointed and end-pointed. The audio is - * only generated if the enableGetWaveform recognition parameter is set - * prior to recognition. - * - * @param waveform the endpointed waveform - */ - private native void setAudioProxy(long nativeObject, byte[] waveform); - - /** - * Save the Voicetag Item. - */ - private native void saveVoicetagProxy(long nativeObject, String path); - - /** - * Load a Voicetag Item. - */ - private native void loadVoicetagProxy(long nativeObject); - - /** - * Deletes a native object. - * - * @param nativeObject pointer to the native object - */ - private native void deleteNativeObject(long nativeObject); -} diff --git a/core/java/android/speech/recognition/impl/WordItemImpl.java b/core/java/android/speech/recognition/impl/WordItemImpl.java deleted file mode 100644 index f0daa34..0000000 --- a/core/java/android/speech/recognition/impl/WordItemImpl.java +++ /dev/null @@ -1,157 +0,0 @@ -/*---------------------------------------------------------------------------* - * WordItemImpl.java * - * * - * Copyright 2007, 2008 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.recognition.impl; - -import android.speech.recognition.WordItem; - -/** - */ -public class WordItemImpl extends WordItem implements Runnable -{ - /** - * Empty array that gets reused whenever the code requests that the underlying - * recognizer guess the pronunciations. - */ - private static final String[] guessPronunciations = new String[0]; - /** - * Reference to the native object. - */ - private long nativeObject; - - /** - * Creates a new WordItem. - * - * @param word the word to insert - * @throws IllegalArgumentException if word or pronunciations are null - */ - private WordItemImpl(String word, String[] pronunciations) throws IllegalArgumentException - { - initNativeObject(word, pronunciations); - } - - public void run() - { - dispose(); - } - - /** - * Creates a new WordItem. - * - * @param word the word to insert - * @param pronunciations the pronunciations to associated with the item. If the list is - * is empty (example:new String[0]) the recognizer will attempt to guess the pronunciations. - * @return the WordItem - * @throws IllegalArgumentException if word is null or if pronunciations is - * null or pronunciations contains an element equal to null or empty string. - */ - public static WordItemImpl valueOf(String word, String[] pronunciations) - throws IllegalArgumentException - { - if (word == null) - throw new IllegalArgumentException("Word may not be null"); - else if (pronunciations == null) - throw new IllegalArgumentException("Pronunciations may not be null"); - for (int i = 0, size = pronunciations.length; i < size; ++i) - { - if (pronunciations[i]==null) - { - throw new IllegalArgumentException( - "Pronunciations element may not be null"); - } - else - { - if (pronunciations[i].trim().equals("")) - throw new IllegalArgumentException( - "Pronunciations may not contain empty strings"); - } - } - return new WordItemImpl(word, pronunciations); - } - - /** - * Creates a new WordItem. - * - * @param word the word to insert - * @param pronunciation the pronunciation to associate with the item. If it - * is null the recognizer will attempt to guess the pronunciations. - * @return the WordItem - * @throws IllegalArgumentException if word is null or if pronunciation is - * an empty string - */ - public static WordItemImpl valueOf(String word, String pronunciation) - throws IllegalArgumentException - { - String[] pronunciations; - if (word == null) - throw new IllegalArgumentException("Word may not be null"); - else if (pronunciation == null) - pronunciations = guessPronunciations; - else if (pronunciation.trim().equals("")) - throw new IllegalArgumentException( - "Pronunciation may not be an empty string"); - else - pronunciations = new String[]{pronunciation}; - return new WordItemImpl(word, pronunciations); - } - - /** - * Allocates a reference to the native object. - * - * @param word the word to insert - */ - private native void initNativeObject(String word, String[] pronunciations); - - public long getNativeObject() - { - synchronized (WordItemImpl.class) - { - return nativeObject; - } - } - - /** - * Releases the native resources associated with the object. - */ - private void dispose() - { - synchronized (WordItemImpl.class) - { - if (nativeObject != 0) - { - deleteNativeObject(nativeObject); - nativeObject = 0; - } - } - } - - @Override - protected void finalize() throws Throwable - { - dispose(); - super.finalize(); - } - - /** - * Deletes a native object. - * - * @param nativeObject pointer to the native object - */ - private native void deleteNativeObject(long nativeObject); -} diff --git a/core/java/android/speech/recognition/impl/package.html b/core/java/android/speech/recognition/impl/package.html deleted file mode 100755 index 1c9bf9d..0000000 --- a/core/java/android/speech/recognition/impl/package.html +++ /dev/null @@ -1,5 +0,0 @@ -<html> -<body> - {@hide} -</body> -</html> diff --git a/core/java/android/speech/recognition/package.html b/core/java/android/speech/recognition/package.html deleted file mode 100644 index 3c59962..0000000 --- a/core/java/android/speech/recognition/package.html +++ /dev/null @@ -1,6 +0,0 @@ -<HTML> -<BODY> -{@hide} -Provides classes for speech recogntion. -</BODY> -</HTML> diff --git a/core/java/android/speech/srec/MicrophoneInputStream.java b/core/java/android/speech/srec/MicrophoneInputStream.java new file mode 100644 index 0000000..160a003 --- /dev/null +++ b/core/java/android/speech/srec/MicrophoneInputStream.java @@ -0,0 +1,106 @@ +/*---------------------------------------------------------------------------* + * MicrophoneInputStream.java * + * * + * Copyright 2007 Nuance Communciations, Inc. * + * * + * Licensed under the Apache License, Version 2.0 (the 'License'); * + * you may not use this file except in compliance with the License. * + * * + * You may obtain a copy of the License at * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an 'AS IS' BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * * + *---------------------------------------------------------------------------*/ + + +package android.speech.srec; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.IllegalStateException; + + +/** + * PCM input stream from the microphone, 16 bits per sample. + */ +public final class MicrophoneInputStream extends InputStream { + static { + System.loadLibrary("srec_jni"); + } + + private final static String TAG = "MicrophoneInputStream"; + private int mAudioRecord = 0; + private byte[] mOneByte = new byte[1]; + + /** + * MicrophoneInputStream constructor. + * @param sampleRate sample rate of the microphone, typically 11025 or 8000. + * @param fifoDepth depth of the real time fifo, measured in sampleRate clock ticks. + * This determines how long an application may delay before losing data. + */ + public MicrophoneInputStream(int sampleRate, int fifoDepth) throws IOException { + mAudioRecord = AudioRecordNew(sampleRate, fifoDepth); + if (mAudioRecord == 0) throw new IllegalStateException("not open"); + AudioRecordStart(mAudioRecord); + } + + @Override + public int read() throws IOException { + if (mAudioRecord == 0) throw new IllegalStateException("not open"); + int rtn = AudioRecordRead(mAudioRecord, mOneByte, 0, 1); + return rtn == 1 ? ((int)mOneByte[0] & 0xff) : -1; + } + + @Override + public int read(byte[] b) throws IOException { + if (mAudioRecord == 0) throw new IllegalStateException("not open"); + return AudioRecordRead(mAudioRecord, b, 0, b.length); + } + + @Override + public int read(byte[] b, int offset, int length) throws IOException { + if (mAudioRecord == 0) throw new IllegalStateException("not open"); + // TODO: should we force all reads to be a multiple of the sample size? + return AudioRecordRead(mAudioRecord, b, offset, length); + } + + /** + * Closes this stream. + */ + @Override + public void close() throws IOException { + if (mAudioRecord != 0) { + try { + AudioRecordStop(mAudioRecord); + } finally { + try { + AudioRecordDelete(mAudioRecord); + } finally { + mAudioRecord = 0; + } + } + } + } + + @Override + protected void finalize() throws Throwable { + if (mAudioRecord != 0) { + close(); + throw new IOException("someone forgot to close MicrophoneInputStream"); + } + } + + // + // AudioRecord JNI interface + // + private static native int AudioRecordNew(int sampleRate, int fifoDepth); + private static native void AudioRecordStart(int audioRecord); + private static native int AudioRecordRead(int audioRecord, byte[] b, int offset, int length) throws IOException; + private static native void AudioRecordStop(int audioRecord) throws IOException; + private static native void AudioRecordDelete(int audioRecord) throws IOException; +} diff --git a/core/java/android/speech/srec/Recognizer.java b/core/java/android/speech/srec/Recognizer.java new file mode 100644 index 0000000..749c923 --- /dev/null +++ b/core/java/android/speech/srec/Recognizer.java @@ -0,0 +1,679 @@ +/* + * --------------------------------------------------------------------------- + * Recognizer.java + * + * Copyright 2007 Nuance Communciations, Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); you may not + * use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * --------------------------------------------------------------------------- + */ + + +package android.speech.srec; + +import android.util.Config; +import android.util.Log; + +import java.io.File; +import java.io.InputStream; +import java.io.IOException; +import java.util.Locale; + +/** + * Simple, synchronous speech recognizer, using the Nuance SREC package. + * Usages proceeds as follows: + * + * <ul> + * <li>Create a <code>Recognizer</code>. + * <li>Create a <code>Recognizer.Grammar</code>. + * <li>Setup the <code>Recognizer.Grammar</code>. + * <li>Reset the <code>Recognizer.Grammar</code> slots, if needed. + * <li>Fill the <code>Recognizer.Grammar</code> slots, if needed. + * <li>Compile the <code>Recognizer.Grammar</code>, if needed. + * <li>Save the filled <code>Recognizer.Grammar</code>, if needed. + * <li>Start the <code>Recognizer</code>. + * <li>Loop over <code>advance</code> and <code>putAudio</code> until recognition complete. + * <li>Fetch and process results, or notify of failure. + * <li>Stop the <code>Recognizer</code>. + * <li>Destroy the <code>Recognizer</code>. + * </ul> + * + * <p>Below is example code</p> + * + * <pre class="prettyprint"> + * + * // create and start audio input + * InputStream audio = new MicrophoneInputStream(11025, 11025*5); + * // create a Recognizer + * String cdir = Recognizer.getConfigDir(null); + * Recognizer recognizer = new Recognizer(cdir + "/baseline11k.par"); + * // create and load a Grammar + * Recognizer.Grammar grammar = recognizer.new Grammar(cdir + "/grammars/VoiceDialer.g2g"); + * // setup the Grammar to work with the Recognizer + * grammar.setupRecognizer(); + * // fill the Grammar slots with names and save, if required + * grammar.resetAllSlots(); + * for (String name : names) grammar.addWordToSlot("@Names", name, null, 1, "V=1"); + * grammar.compile(); + * grammar.save(".../foo.g2g"); + * // start the Recognizer + * recognizer.start(); + * // loop over Recognizer events + * while (true) { + * switch (recognizer.advance()) { + * case Recognizer.EVENT_INCOMPLETE: + * case Recognizer.EVENT_STARTED: + * case Recognizer.EVENT_START_OF_VOICING: + * case Recognizer.EVENT_END_OF_VOICING: + * // let the Recognizer continue to run + * continue; + * case Recognizer.EVENT_RECOGNITION_RESULT: + * // success, so fetch results here! + * for (int i = 0; i < recognizer.getResultCount(); i++) { + * String result = recognizer.getResult(i, Recognizer.KEY_LITERAL); + * } + * break; + * case Recognizer.EVENT_NEED_MORE_AUDIO: + * // put more audio in the Recognizer + * recognizer.putAudio(audio); + * continue; + * default: + * notifyFailure(); + * break; + * } + * break; + * } + * // stop the Recognizer + * recognizer.stop(); + * // destroy the Recognizer + * recognizer.destroy(); + * // stop the audio device + * audio.close(); + * + * </pre> + */ +public final class Recognizer { + static { + System.loadLibrary("srec_jni"); + } + + private static String TAG = "Recognizer"; + + /** + * Result key corresponding to confidence score. + */ + public static final String KEY_CONFIDENCE = "conf"; + + /** + * Result key corresponding to literal text. + */ + public static final String KEY_LITERAL = "literal"; + + /** + * Result key corresponding to semantic meaning text. + */ + public static final String KEY_MEANING = "meaning"; + + // handle to SR_Vocabulary object + private int mVocabulary = 0; + + // handle to SR_Recognizer object + private int mRecognizer = 0; + + // Grammar currently associated with Recognizer via SR_GrammarSetupRecognizer + private Grammar mActiveGrammar = null; + + /** + * Get the pathname of the SREC configuration directory corresponding to the + * language indicated by the Locale. + * This directory contains dictionaries, speech models, + * configuration files, and other data needed by the Recognizer. + * @param locale <code>Locale</code> corresponding to the desired language, + * or null for default, currently <code>Locale.US</code>. + * @return Pathname of the configuration directory. + */ + public static String getConfigDir(Locale locale) { + if (locale == null) locale = Locale.US; + String dir = "/system/usr/srec/config/" + + locale.toString().replace('_', '.').toLowerCase(); + if ((new File(dir)).isDirectory()) return dir; + return null; + } + + /** + * Create an instance of a SREC speech recognizer. + * + * @param configFile pathname of the baseline*.par configuration file, + * which in turn contains references to dictionaries, speech models, + * and other data needed to configure and operate the recognizer. + * A separate config file is needed for each audio sample rate. + * Two files, baseline11k.par and baseline8k.par, which correspond to + * 11025 and 8000 hz, are present in the directory indicated by + * {@link #getConfigDir}. + * @throws IOException + */ + public Recognizer(String configFile) throws IOException { + PMemInit(); + SR_SessionCreate(configFile); + mRecognizer = SR_RecognizerCreate(); + SR_RecognizerSetup(mRecognizer); + mVocabulary = SR_VocabularyLoad(); + } + + /** + * Represents a grammar loaded into the Recognizer. + */ + public class Grammar { + private int mGrammar = 0; + + /** + * Create a <code>Grammar</code> instance. + * @param g2gFileName pathname of g2g file. + */ + public Grammar(String g2gFileName) throws IOException { + mGrammar = SR_GrammarLoad(g2gFileName); + SR_GrammarSetupVocabulary(mGrammar, mVocabulary); + } + + /** + * Reset all slots. + */ + public void resetAllSlots() { + SR_GrammarResetAllSlots(mGrammar); + } + + /** + * Add a word to a slot. + * + * @param slot slot name. + * @param word word to insert. + * @param pron pronunciation, or null to derive from word. + * @param weight weight to give the word. One is normal, 50 is low. + * @param tag semantic meaning tag string. + */ + public void addWordToSlot(String slot, String word, String pron, int weight, String tag) { + SR_GrammarAddWordToSlot(mGrammar, slot, word, pron, weight, tag); + } + + /** + * Compile all slots. + */ + public void compile() { + SR_GrammarCompile(mGrammar); + } + + /** + * Setup <code>Grammar</code> with <code>Recognizer</code>. + */ + public void setupRecognizer() { + SR_GrammarSetupRecognizer(mGrammar, mRecognizer); + mActiveGrammar = this; + } + + /** + * Save <code>Grammar</code> to g2g file. + * + * @param g2gFileName + * @throws IOException + */ + public void save(String g2gFileName) throws IOException { + SR_GrammarSave(mGrammar, g2gFileName); + } + + /** + * Release resources associated with this <code>Grammar</code>. + */ + public void destroy() { + // TODO: need to do cleanup and disassociation with Recognizer + if (mGrammar != 0) { + SR_GrammarDestroy(mGrammar); + mGrammar = 0; + } + } + + /** + * Clean up resources. + */ + protected void finalize() { + if (mGrammar != 0) { + destroy(); + throw new IllegalStateException("someone forgot to destroy Grammar"); + } + } + } + + /** + * Start recognition + */ + public void start() { + // TODO: shouldn't be here? + SR_RecognizerActivateRule(mRecognizer, mActiveGrammar.mGrammar, "trash", 1); + SR_RecognizerStart(mRecognizer); + } + + /** + * Process some audio and return the current status. + * @return recognition event, one of: + * <ul> + * <li><code>EVENT_INVALID</code> + * <li><code>EVENT_NO_MATCH</code> + * <li><code>EVENT_INCOMPLETE</code> + * <li><code>EVENT_STARTED</code> + * <li><code>EVENT_STOPPED</code> + * <li><code>EVENT_START_OF_VOICING</code> + * <li><code>EVENT_END_OF_VOICING</code> + * <li><code>EVENT_SPOKE_TOO_SOON</code> + * <li><code>EVENT_RECOGNITION_RESULT</code> + * <li><code>EVENT_START_OF_UTTERANCE_TIMEOUT</code> + * <li><code>EVENT_RECOGNITION_TIMEOUT</code> + * <li><code>EVENT_NEED_MORE_AUDIO</code> + * <li><code>EVENT_MAX_SPEECH</code> + * </ul> + */ + public int advance() { + return SR_RecognizerAdvance(mRecognizer); + } + + /** + * Put audio samples into the <code>Recognizer</code>. + * @param buf holds the audio samples. + * @param offset offset of the first sample. + * @param length number of bytes containing samples. + * @param isLast indicates no more audio data, normally false. + * @return number of bytes accepted. + */ + public int putAudio(byte[] buf, int offset, int length, boolean isLast) { + return SR_RecognizerPutAudio(mRecognizer, buf, offset, length, isLast); + } + + /** + * Read audio samples from an <code>InputStream</code> and put them in the + * <code>Recognizer</code>. + * @param audio <code>InputStream</code> containing PCM audio samples. + */ + public void putAudio(InputStream audio) throws IOException { + // make sure the audio buffer is allocated + if (mPutAudioBuffer == null) mPutAudioBuffer = new byte[512]; + // read some data + int nbytes = audio.read(mPutAudioBuffer); + // eof, so signal Recognizer + if (nbytes == -1) { + SR_RecognizerPutAudio(mRecognizer, mPutAudioBuffer, 0, 0, true); + } + // put it into the Recognizer + else if (nbytes != SR_RecognizerPutAudio(mRecognizer, mPutAudioBuffer, 0, nbytes, false)) { + throw new IOException("SR_RecognizerPutAudio failed nbytes=" + nbytes); + } + } + + // audio buffer for putAudio(InputStream) + private byte[] mPutAudioBuffer = null; + + /** + * Get the number of recognition results. Must be called after + * <code>EVENT_RECOGNITION_RESULT</code> is returned by + * <code>advance</code>, but before <code>stop</code>. + * + * @return number of results in nbest list. + */ + public int getResultCount() { + return SR_RecognizerResultGetSize(mRecognizer); + } + + /** + * Get a set of keys for the result. Must be called after + * <code>EVENT_RECOGNITION_RESULT</code> is returned by + * <code>advance</code>, but before <code>stop</code>. + * + * @param index index of result. + * @return array of keys. + */ + public String[] getResultKeys(int index) { + return SR_RecognizerResultGetKeyList(mRecognizer, index); + } + + /** + * Get a result value. Must be called after + * <code>EVENT_RECOGNITION_RESULT</code> is returned by + * <code>advance</code>, but before <code>stop</code>. + * + * @param index index of the result. + * @param key key of the result. This is typically one of + * <code>KEY_CONFIDENCE</code>, <code>KEY_LITERAL</code>, or + * <code>KEY_MEANING</code>, but the user can also define their own keys + * in a grxml file, or in the <code>tag</code> slot of + * <code>Grammar.addWordToSlot</code>. + * @return the result. + */ + public String getResult(int index, String key) { + return SR_RecognizerResultGetValue(mRecognizer, index, key); + } + + /** + * Stop the <code>Recognizer</code>. + */ + public void stop() { + SR_RecognizerStop(mRecognizer); + SR_RecognizerDeactivateRule(mRecognizer, mActiveGrammar.mGrammar, "trash"); + } + + /** + * Clean up resources. + */ + public void destroy() { + try { + if (mVocabulary != 0) SR_VocabularyDestroy(mVocabulary); + } finally { + mVocabulary = 0; + try { + if (mRecognizer != 0) SR_RecognizerUnsetup(mRecognizer); + } finally { + try { + if (mRecognizer != 0) SR_RecognizerDestroy(mRecognizer); + } finally { + mRecognizer = 0; + try { + SR_SessionDestroy(); + } finally { + PMemShutdown(); + } + } + } + } + } + + /** + * Clean up resources. + */ + protected void finalize() throws Throwable { + if (mVocabulary != 0 || mRecognizer != 0) { + destroy(); + throw new IllegalStateException("someone forgot to destroy Recognizer"); + } + } + + /* an example session captured, for reference + void doall() { + if (PMemInit ( ) + || lhs_audioinOpen ( WAVE_MAPPER, SREC_TEST_DEFAULT_AUDIO_FREQUENCY, &audio_in_handle ) + || srec_test_init_application_data ( &applicationData, argc, argv ) + || SR_SessionCreate ( "/system/usr/srec/config/en.us/baseline11k.par" ) + || SR_RecognizerCreate ( &applicationData.recognizer ) + || SR_RecognizerSetup ( applicationData.recognizer) + || ESR_SessionGetLCHAR ( L("cmdline.vocabulary"), filename, &flen ) + || SR_VocabularyLoad ( filename, &applicationData.vocabulary ) + || SR_VocabularyGetLanguage ( applicationData.vocabulary, &applicationData.locale ) + || (applicationData.nametag = NULL) + || SR_NametagsCreate ( &applicationData.nametags ) + || (LSTRCPY ( applicationData.grammars [0].grammar_path, "/system/usr/srec/config/en.us/grammars/VoiceDialer.g2g" ), 0) + || (LSTRCPY ( applicationData.grammars [0].grammarID, "BothTags" ), 0) + || (LSTRCPY ( applicationData.grammars [0].ruleName, "trash" ), 0) + || (applicationData.grammars [0].is_ve_grammar = ESR_FALSE, 0) + || SR_GrammarLoad (applicationData.grammars [0].grammar_path, &applicationData.grammars [applicationData.grammarCount].grammar ) + || SR_GrammarSetupVocabulary ( applicationData.grammars [0].grammar, applicationData.vocabulary ) + || SR_GrammarSetupRecognizer( applicationData.grammars [0].grammar, applicationData.recognizer ) + || SR_GrammarSetDispatchFunction ( applicationData.grammars [0].grammar, L("myDSMCallback"), NULL, myDSMCallback ) + || (applicationData.grammarCount++, 0) + || SR_RecognizerActivateRule ( applicationData.recognizer, applicationData.grammars [0].grammar, + applicationData.grammars [0].ruleName, 1 ) + || (applicationData.active_grammar_num = 0, 0) + || lhs_audioinStart ( audio_in_handle ) + || SR_RecognizerStart ( applicationData.recognizer ) + || strl ( applicationData.grammars [0].grammar, &applicationData, audio_in_handle, &recognition_count ) + || SR_RecognizerStop ( applicationData.recognizer ) + || lhs_audioinStop ( audio_in_handle ) + || SR_RecognizerDeactivateRule ( applicationData.recognizer, applicationData.grammars [0].grammar, applicationData.grammars [0].ruleName ) + || (applicationData.active_grammar_num = -1, 0) + || SR_GrammarDestroy ( applicationData.grammars [0].grammar ) + || (applicationData.grammarCount--, 0) + || SR_NametagsDestroy ( applicationData.nametags ) + || (applicationData.nametags = NULL, 0) + || SR_VocabularyDestroy ( applicationData.vocabulary ) + || (applicationData.vocabulary = NULL) + || SR_RecognizerUnsetup ( applicationData.recognizer) // releases acoustic models + || SR_RecognizerDestroy ( applicationData.recognizer ) + || (applicationData.recognizer = NULL) + || SR_SessionDestroy ( ) + || srec_test_shutdown_application_data ( &applicationData ) + || lhs_audioinClose ( &audio_in_handle ) + || PMemShutdown ( ) + } + */ + + + // + // PMem native methods + // + private static native void PMemInit(); + private static native void PMemShutdown(); + + + // + // SR_Session native methods + // + private static native void SR_SessionCreate(String filename); + private static native void SR_SessionDestroy(); + + + // + // SR_Recognizer native methods + // + + /** + * Reserved value. + */ + public final static int EVENT_INVALID = 0; + + /** + * <code>Recognizer</code> could not find a match for the utterance. + */ + public final static int EVENT_NO_MATCH = 1; + + /** + * <code>Recognizer</code> processed one frame of audio. + */ + public final static int EVENT_INCOMPLETE = 2; + + /** + * <code>Recognizer</code> has just been started. + */ + public final static int EVENT_STARTED = 3; + + /** + * <code>Recognizer</code> is stopped. + */ + public final static int EVENT_STOPPED = 4; + + /** + * Beginning of speech detected. + */ + public final static int EVENT_START_OF_VOICING = 5; + + /** + * End of speech detected. + */ + public final static int EVENT_END_OF_VOICING = 6; + + /** + * Beginning of utterance occured too soon. + */ + public final static int EVENT_SPOKE_TOO_SOON = 7; + + /** + * Recognition match detected. + */ + public final static int EVENT_RECOGNITION_RESULT = 8; + + /** + * Timeout occured before beginning of utterance. + */ + public final static int EVENT_START_OF_UTTERANCE_TIMEOUT = 9; + + /** + * Timeout occured before speech recognition could complete. + */ + public final static int EVENT_RECOGNITION_TIMEOUT = 10; + + /** + * Not enough samples to process one frame. + */ + public final static int EVENT_NEED_MORE_AUDIO = 11; + + /** + * More audio encountered than is allowed by 'swirec_max_speech_duration'. + */ + public final static int EVENT_MAX_SPEECH = 12; + + /** + * Produce a displayable string from an <code>advance</code> event. + * @param event + * @return String representing the event. + */ + public static String eventToString(int event) { + switch (event) { + case EVENT_INVALID: + return "EVENT_INVALID"; + case EVENT_NO_MATCH: + return "EVENT_NO_MATCH"; + case EVENT_INCOMPLETE: + return "EVENT_INCOMPLETE"; + case EVENT_STARTED: + return "EVENT_STARTED"; + case EVENT_STOPPED: + return "EVENT_STOPPED"; + case EVENT_START_OF_VOICING: + return "EVENT_START_OF_VOICING"; + case EVENT_END_OF_VOICING: + return "EVENT_END_OF_VOICING"; + case EVENT_SPOKE_TOO_SOON: + return "EVENT_SPOKE_TOO_SOON"; + case EVENT_RECOGNITION_RESULT: + return "EVENT_RECOGNITION_RESULT"; + case EVENT_START_OF_UTTERANCE_TIMEOUT: + return "EVENT_START_OF_UTTERANCE_TIMEOUT"; + case EVENT_RECOGNITION_TIMEOUT: + return "EVENT_RECOGNITION_TIMEOUT"; + case EVENT_NEED_MORE_AUDIO: + return "EVENT_NEED_MORE_AUDIO"; + case EVENT_MAX_SPEECH: + return "EVENT_MAX_SPEECH"; + } + return "EVENT_" + event; + } + + private static native void SR_RecognizerStart(int recognizer); + private static native void SR_RecognizerStop(int recognizer); + private static native int SR_RecognizerCreate(); + private static native void SR_RecognizerDestroy(int recognizer); + private static native void SR_RecognizerSetup(int recognizer); + private static native void SR_RecognizerUnsetup(int recognizer); + private static native boolean SR_RecognizerIsSetup(int recognizer); + private static native String SR_RecognizerGetParameter(int recognizer, String key); + private static native int SR_RecognizerGetSize_tParameter(int recognizer, String key); + private static native boolean SR_RecognizerGetBoolParameter(int recognizer, String key); + private static native void SR_RecognizerSetParameter(int recognizer, String key, String value); + private static native void SR_RecognizerSetSize_tParameter(int recognizer, + String key, int value); + private static native void SR_RecognizerSetBoolParameter(int recognizer, String key, + boolean value); + private static native void SR_RecognizerSetupRule(int recognizer, int grammar, + String ruleName); + private static native boolean SR_RecognizerHasSetupRules(int recognizer); + private static native void SR_RecognizerActivateRule(int recognizer, int grammar, + String ruleName, int weight); + private static native void SR_RecognizerDeactivateRule(int recognizer, int grammar, + String ruleName); + private static native void SR_RecognizerDeactivateAllRules(int recognizer); + private static native boolean SR_RecognizerIsActiveRule(int recognizer, int grammar, + String ruleName); + private static native boolean SR_RecognizerCheckGrammarConsistency(int recognizer, + int grammar); + private static native int SR_RecognizerPutAudio(int recognizer, byte[] buffer, int offset, + int length, boolean isLast); + private static native int SR_RecognizerAdvance(int recognizer); + // private static native void SR_RecognizerLoadUtterance(int recognizer, + // const LCHAR* filename); + // private static native void SR_RecognizerLoadWaveFile(int recognizer, + // const LCHAR* filename); + // private static native void SR_RecognizerSetLockFunction(int recognizer, + // SR_RecognizerLockFunction function, void* data); + private static native boolean SR_RecognizerIsSignalClipping(int recognizer); + private static native boolean SR_RecognizerIsSignalDCOffset(int recognizer); + private static native boolean SR_RecognizerIsSignalNoisy(int recognizer); + private static native boolean SR_RecognizerIsSignalTooQuiet(int recognizer); + private static native boolean SR_RecognizerIsSignalTooFewSamples(int recognizer); + private static native boolean SR_RecognizerIsSignalTooManySamples(int recognizer); + // private static native void SR_Recognizer_Change_Sample_Rate (size_t new_sample_rate); + + + // + // SR_Grammar native methods + // + private static native void SR_GrammarCompile(int grammar); + private static native void SR_GrammarAddWordToSlot(int grammar, String slot, + String word, String pronunciation, int weight, String tag); + private static native void SR_GrammarResetAllSlots(int grammar); + // private static native void SR_GrammarAddNametagToSlot(int grammar, String slot, + // const struct SR_Nametag_t* nametag, int weight, String tag); + private static native void SR_GrammarSetupVocabulary(int grammar, int vocabulary); + // private static native void SR_GrammarSetupModels(int grammar, SR_AcousticModels* models); + private static native void SR_GrammarSetupRecognizer(int grammar, int recognizer); + private static native void SR_GrammarUnsetupRecognizer(int grammar); + // private static native void SR_GrammarGetModels(int grammar,SR_AcousticModels** models); + private static native int SR_GrammarCreate(); + private static native void SR_GrammarDestroy(int grammar); + private static native int SR_GrammarLoad(String filename); + private static native void SR_GrammarSave(int grammar, String filename); + // private static native void SR_GrammarSetDispatchFunction(int grammar, + // const LCHAR* name, void* userData, SR_GrammarDispatchFunction function); + // private static native void SR_GrammarSetParameter(int grammar, const + // LCHAR* key, void* value); + // private static native void SR_GrammarSetSize_tParameter(int grammar, + // const LCHAR* key, size_t value); + // private static native void SR_GrammarGetParameter(int grammar, const + // LCHAR* key, void** value); + // private static native void SR_GrammarGetSize_tParameter(int grammar, + // const LCHAR* key, size_t* value); + // private static native void SR_GrammarCheckParse(int grammar, const LCHAR* + // transcription, SR_SemanticResult** result, size_t* resultCount); + private static native void SR_GrammarAllowOnly(int grammar, String transcription); + private static native void SR_GrammarAllowAll(int grammar); + + + // + // SR_Vocabulary native methods + // + // private static native int SR_VocabularyCreate(); + private static native int SR_VocabularyLoad(); + // private static native void SR_VocabularySave(SR_Vocabulary* self, + // const LCHAR* filename); + // private static native void SR_VocabularyAddWord(SR_Vocabulary* self, + // const LCHAR* word); + // private static native void SR_VocabularyGetLanguage(SR_Vocabulary* self, + // ESR_Locale* locale); + private static native void SR_VocabularyDestroy(int vocabulary); + private static native String SR_VocabularyGetPronunciation(int vocabulary, String word); + + + // + // SR_RecognizerResult native methods + // + private static native byte[] SR_RecognizerResultGetWaveform(int recognizer); + private static native int SR_RecognizerResultGetSize(int recognizer); + private static native int SR_RecognizerResultGetKeyCount(int recognizer, int nbest); + private static native String[] SR_RecognizerResultGetKeyList(int recognizer, int nbest); + private static native String SR_RecognizerResultGetValue(int recognizer, + int nbest, String key); + // private static native void SR_RecognizerResultGetLocale(int recognizer, ESR_Locale* locale); +} diff --git a/core/java/android/speech/srec/Srec.java b/core/java/android/speech/srec/Srec.java deleted file mode 100644 index a629214..0000000 --- a/core/java/android/speech/srec/Srec.java +++ /dev/null @@ -1,162 +0,0 @@ -/*---------------------------------------------------------------------------* - * EmbeddedRecognizerImpl.java * - * * - * Copyright 2007 Nuance Communciations, Inc. * - * * - * Licensed under the Apache License, Version 2.0 (the 'License'); * - * you may not use this file except in compliance with the License. * - * * - * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an 'AS IS' BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * * - *---------------------------------------------------------------------------*/ - -package android.speech.srec; - -import java.io.IOException; - -/** - * Simple, synchronous speech recognizer, using the SREC package. - * - * @hide - */ -public class Srec -{ - private int mNative; - - - /** - * Create an instance of a SREC speech recognizer. - * @param configFile pathname of the baseline*.par configuration file. - * @throws IOException - */ - Srec(String configFile) throws IOException { - - } - - /** - * Creates a Srec recognizer. - * @param g2gFileName pathname of a g2g grammar file. - * @return - * @throws IOException - */ - public Grammar loadGrammar(String g2gFileName) throws IOException { - return null; - } - - /** - * Represents a grammar loaded into the recognizer. - */ - public class Grammar { - private int mId = -1; - - /** - * Add a word to a slot - * @param slot slot name - * @param word word - * @param pron pronunciation, or null to derive from word - * @param weight weight to give the word - * @param meaning meaning string - */ - public void addToSlot(String slot, String word, String pron, int weight, String meaning) { - - } - - /** - * Compile all slots. - */ - public void compileSlots() { - - } - - /** - * Reset all slots. - */ - public void resetAllSlots() { - - } - - /** - * Save grammar to g2g file. - * @param g2gFileName - * @throws IOException - */ - public void save(String g2gFileName) throws IOException { - - } - - /** - * Release resources associated with this grammar. - */ - public void unload() { - - } - } - - /** - * Start recognition - */ - public void start() { - - } - - /** - * Process some audio and return the next state. - * @return true if complete - */ - public boolean process() { - return false; - } - - /** - * Get the number of recognition results. - * @return - */ - public int getResultCount() { - return 0; - } - - /** - * Get a set of keys for the result. - * @param index index of result. - * @return array of keys. - */ - public String[] getResultKeys(int index) { - return null; - } - - /** - * Get a result value - * @param index index of the result. - * @param key key of the result. - * @return the result. - */ - public String getResult(int index, String key) { - return null; - } - - /** - * Reset the recognizer to the idle state. - */ - public void reset() { - - } - - /** - * Clean up resources. - */ - public void dispose() { - - } - - protected void finalize() { - - } - -} diff --git a/core/java/android/speech/srec/UlawEncoderInputStream.java b/core/java/android/speech/srec/UlawEncoderInputStream.java new file mode 100644 index 0000000..132fe027 --- /dev/null +++ b/core/java/android/speech/srec/UlawEncoderInputStream.java @@ -0,0 +1,186 @@ +/* + * --------------------------------------------------------------------------- + * UlawEncoderInputStream.java + * + * Copyright 2008 Nuance Communciations, Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); you may not + * use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * --------------------------------------------------------------------------- + */ + +package android.speech.srec; + +import java.io.IOException; +import java.io.InputStream; + +/** + * InputStream which transforms 16 bit pcm data to ulaw data. + * + * @hide pending API council approval + */ +public final class UlawEncoderInputStream extends InputStream { + private final static String TAG = "UlawEncoderInputStream"; + + private final static int MAX_ULAW = 8192; + private final static int SCALE_BITS = 16; + + private InputStream mIn; + + private int mMax = 0; + + private final byte[] mBuf = new byte[1024]; + private int mBufCount = 0; // should be 0 or 1 + + private final byte[] mOneByte = new byte[1]; + + + public static void encode(byte[] pcmBuf, int pcmOffset, + byte[] ulawBuf, int ulawOffset, int length, int max) { + + // from 'ulaw' in wikipedia + // +8191 to +8159 0x80 + // +8158 to +4063 in 16 intervals of 256 0x80 + interval number + // +4062 to +2015 in 16 intervals of 128 0x90 + interval number + // +2014 to +991 in 16 intervals of 64 0xA0 + interval number + // +990 to +479 in 16 intervals of 32 0xB0 + interval number + // +478 to +223 in 16 intervals of 16 0xC0 + interval number + // +222 to +95 in 16 intervals of 8 0xD0 + interval number + // +94 to +31 in 16 intervals of 4 0xE0 + interval number + // +30 to +1 in 15 intervals of 2 0xF0 + interval number + // 0 0xFF + + // -1 0x7F + // -31 to -2 in 15 intervals of 2 0x70 + interval number + // -95 to -32 in 16 intervals of 4 0x60 + interval number + // -223 to -96 in 16 intervals of 8 0x50 + interval number + // -479 to -224 in 16 intervals of 16 0x40 + interval number + // -991 to -480 in 16 intervals of 32 0x30 + interval number + // -2015 to -992 in 16 intervals of 64 0x20 + interval number + // -4063 to -2016 in 16 intervals of 128 0x10 + interval number + // -8159 to -4064 in 16 intervals of 256 0x00 + interval number + // -8192 to -8160 0x00 + + // set scale factors + if (max <= 0) max = MAX_ULAW; + + int coef = MAX_ULAW * (1 << SCALE_BITS) / max; + + for (int i = 0; i < length; i++) { + int pcm = (0xff & pcmBuf[pcmOffset++]) + (pcmBuf[pcmOffset++] << 8); + pcm = (pcm * coef) >> SCALE_BITS; + + int ulaw; + if (pcm >= 0) { + ulaw = pcm <= 0 ? 0xff : + pcm <= 30 ? 0xf0 + (( 30 - pcm) >> 1) : + pcm <= 94 ? 0xe0 + (( 94 - pcm) >> 2) : + pcm <= 222 ? 0xd0 + (( 222 - pcm) >> 3) : + pcm <= 478 ? 0xc0 + (( 478 - pcm) >> 4) : + pcm <= 990 ? 0xb0 + (( 990 - pcm) >> 5) : + pcm <= 2014 ? 0xa0 + ((2014 - pcm) >> 6) : + pcm <= 4062 ? 0x90 + ((4062 - pcm) >> 7) : + pcm <= 8158 ? 0x80 + ((8158 - pcm) >> 8) : + 0x80; + } else { + ulaw = -1 <= pcm ? 0x7f : + -31 <= pcm ? 0x70 + ((pcm - -31) >> 1) : + -95 <= pcm ? 0x60 + ((pcm - -95) >> 2) : + -223 <= pcm ? 0x50 + ((pcm - -223) >> 3) : + -479 <= pcm ? 0x40 + ((pcm - -479) >> 4) : + -991 <= pcm ? 0x30 + ((pcm - -991) >> 5) : + -2015 <= pcm ? 0x20 + ((pcm - -2015) >> 6) : + -4063 <= pcm ? 0x10 + ((pcm - -4063) >> 7) : + -8159 <= pcm ? 0x00 + ((pcm - -8159) >> 8) : + 0x00; + } + ulawBuf[ulawOffset++] = (byte)ulaw; + } + } + + /** + * Compute the maximum of the absolute value of the pcm samples. + * The return value can be used to set ulaw encoder scaling. + * @param pcmBuf array containing 16 bit pcm data. + * @param offset offset of start of 16 bit pcm data. + * @param length number of pcm samples (not number of input bytes) + * @return maximum abs of pcm data values + */ + public static int maxAbsPcm(byte[] pcmBuf, int offset, int length) { + int max = 0; + for (int i = 0; i < length; i++) { + int pcm = (0xff & pcmBuf[offset++]) + (pcmBuf[offset++] << 8); + if (pcm < 0) pcm = -pcm; + if (pcm > max) max = pcm; + } + return max; + } + + /** + * Create an InputStream which takes 16 bit pcm data and produces ulaw data. + * @param in InputStream containing 16 bit pcm data. + * @param max pcm value corresponding to maximum ulaw value. + */ + public UlawEncoderInputStream(InputStream in, int max) { + mIn = in; + mMax = max; + } + + @Override + public int read(byte[] buf, int offset, int length) throws IOException { + if (mIn == null) throw new IllegalStateException("not open"); + + // return at least one byte, but try to fill 'length' + while (mBufCount < 2) { + int n = mIn.read(mBuf, mBufCount, Math.min(length * 2, mBuf.length - mBufCount)); + if (n == -1) return -1; + mBufCount += n; + } + + // compand data + int n = Math.min(mBufCount / 2, length); + encode(mBuf, 0, buf, offset, n, mMax); + + // move data to bottom of mBuf + mBufCount -= n * 2; + for (int i = 0; i < mBufCount; i++) mBuf[i] = mBuf[i + n * 2]; + + return n; + } + + @Override + public int read(byte[] buf) throws IOException { + return read(buf, 0, buf.length); + } + + @Override + public int read() throws IOException { + int n = read(mOneByte, 0, 1); + if (n == -1) return -1; + return 0xff & (int)mOneByte[0]; + } + + @Override + public void close() throws IOException { + if (mIn != null) { + InputStream in = mIn; + mIn = null; + in.close(); + } + } + + @Override + public int available() throws IOException { + return (mIn.available() + mBufCount) / 2; + } +} diff --git a/core/java/android/speech/srec/package.html b/core/java/android/speech/srec/package.html new file mode 100644 index 0000000..723b30b --- /dev/null +++ b/core/java/android/speech/srec/package.html @@ -0,0 +1,5 @@ +<HTML> +<BODY> +Simple, synchronous SREC speech recognition API. +</BODY> +</HTML> diff --git a/core/java/android/test/InstrumentationTestCase.java b/core/java/android/test/InstrumentationTestCase.java index 08a8ad1..82f2ef9 100644 --- a/core/java/android/test/InstrumentationTestCase.java +++ b/core/java/android/test/InstrumentationTestCase.java @@ -31,8 +31,7 @@ import java.lang.reflect.Modifier; import java.lang.reflect.InvocationTargetException; /** - * A test case that has access to {@link Instrumentation}. See - * <code>InstrumentationTestRunner</code>. + * A test case that has access to {@link Instrumentation}. */ public class InstrumentationTestCase extends TestCase { @@ -57,7 +56,13 @@ public class InstrumentationTestCase extends TestCase { } /** - * Utility method for launching an activity. + * Utility method for launching an activity. + * + * <p>The {@link Intent} used to launch the Activity is: + * action = {@link Intent#ACTION_MAIN} + * extras = null, unless a custom bundle is provided here + * All other fields are null or empty. + * * @param pkg The package hosting the activity to be launched. * @param activityCls The activity class to launch. * @param extras Optional extra stuff to pass to the activity. @@ -69,20 +74,61 @@ public class InstrumentationTestCase extends TestCase { Class<T> activityCls, Bundle extras) { Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setClassName(pkg, activityCls.getName()); if (extras != null) { intent.putExtras(extras); } + return launchActivityWithIntent(pkg, activityCls, intent); + } + + /** + * Utility method for launching an activity with a specific Intent. + * @param pkg The package hosting the activity to be launched. + * @param activityCls The activity class to launch. + * @param intent The intent to launch with + * @return The activity, or null if non launched. + */ + @SuppressWarnings("unchecked") + public final <T extends Activity> T launchActivityWithIntent( + String pkg, + Class<T> activityCls, + Intent intent) { + intent.setClassName(pkg, activityCls.getName()); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); T activity = (T) getInstrumentation().startActivitySync(intent); getInstrumentation().waitForIdleSync(); return activity; } + + /** + * Helper for running portions of a test on the UI thread. + * + * Note, in most cases it is simpler to annotate the test method with + * {@link android.test.UiThreadTest}, which will run the entire test method on the UI thread. + * Use this method if you need to switch in and out of the UI thread to perform your test. + * + * @param r runnable containing test code in the {@link Runnable#run()} method + */ + public void runTestOnUiThread(final Runnable r) throws Throwable { + final Throwable[] exceptions = new Throwable[1]; + getInstrumentation().runOnMainSync(new Runnable() { + public void run() { + try { + r.run(); + } catch (Throwable throwable) { + exceptions[0] = throwable; + } + } + }); + if (exceptions[0] != null) { + throw exceptions[0]; + } + } /** * Runs the current unit test. If the unit test is annotated with * {@link android.test.UiThreadTest}, the test is run on the UI thread. */ + @Override protected void runTest() throws Throwable { String fName = getName(); assertNotNull(fName); diff --git a/core/java/android/test/suitebuilder/annotation/LargeTest.java b/core/java/android/test/suitebuilder/annotation/LargeTest.java new file mode 100644 index 0000000..a6269e7 --- /dev/null +++ b/core/java/android/test/suitebuilder/annotation/LargeTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.test.suitebuilder.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a test that should run as part of the large tests. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface LargeTest { +} diff --git a/core/java/android/test/suitebuilder/annotation/MediumTest.java b/core/java/android/test/suitebuilder/annotation/MediumTest.java new file mode 100644 index 0000000..8afeb91 --- /dev/null +++ b/core/java/android/test/suitebuilder/annotation/MediumTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.test.suitebuilder.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a test that should run as part of the medium tests. + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface MediumTest { +} diff --git a/core/java/android/test/suitebuilder/annotation/SmallTest.java b/core/java/android/test/suitebuilder/annotation/SmallTest.java new file mode 100644 index 0000000..ad530e2 --- /dev/null +++ b/core/java/android/test/suitebuilder/annotation/SmallTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.test.suitebuilder.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a test that should run as part of the small tests. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface SmallTest { +} diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java index 2ee4f62..843754b 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -92,7 +92,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback int ellipsizedWidth) { boolean trust; - if (ellipsize == null) { + if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) { replaceWith(source, paint, outerwidth, align, spacingmult, spacingadd); @@ -145,7 +145,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback boolean trust; - if (ellipsize == null) { + if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) { mEllipsizedWidth = outerwidth; mEllipsizedStart = 0; mEllipsizedCount = 0; diff --git a/core/java/android/text/InputFilter.java b/core/java/android/text/InputFilter.java index e1563ae..2f55677 100644 --- a/core/java/android/text/InputFilter.java +++ b/core/java/android/text/InputFilter.java @@ -33,6 +33,11 @@ public interface InputFilter * as this is what happens when you delete text. Also beware that * you should not attempt to make any changes to <code>dest</code> * from this method; you may only examine it for context. + * + * Note: If <var>source</var> is an instance of {@link Spanned} or + * {@link Spannable}, the span objects in the <var>source</var> should be + * copied into the filtered result (i.e. the non-null return value). + * {@link TextUtils#copySpansFrom} can be used for convenience. */ public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend); diff --git a/core/java/android/text/InputType.java b/core/java/android/text/InputType.java new file mode 100644 index 0000000..0ffe4ac --- /dev/null +++ b/core/java/android/text/InputType.java @@ -0,0 +1,227 @@ +package android.text; + +import android.text.TextUtils; + +/** + * Bit definitions for an integer defining the basic content type of text + * held in an {@link Editable} object. + */ +public interface InputType { + /** + * Mask of bits that determine the overall class + * of text being given. Currently supported classes are: + * {@link #TYPE_CLASS_TEXT}, {@link #TYPE_CLASS_NUMBER}, + * {@link #TYPE_CLASS_PHONE}, {@link #TYPE_CLASS_DATETIME}. + * If the class is not one you + * understand, assume {@link #TYPE_CLASS_TEXT} with NO variation + * or flags. + */ + public static final int TYPE_MASK_CLASS = 0x0000000f; + + /** + * Mask of bits that determine the variation of + * the base content class. + */ + public static final int TYPE_MASK_VARIATION = 0x00000ff0; + + /** + * Mask of bits that provide addition bit flags + * of options. + */ + public static final int TYPE_MASK_FLAGS = 0x00fff000; + + /** + * Special content type for when no explicit type has been specified. + * This should be interpreted to mean that the target input connection + * is not rich, it can not process and show things like candidate text nor + * retrieve the current text, so the input method will need to run in a + * limited "generate key events" mode. + */ + public static final int TYPE_NULL = 0x00000000; + + // ---------------------------------------------------------------------- + // ---------------------------------------------------------------------- + // ---------------------------------------------------------------------- + + /** + * Class for normal text. This class supports the following flags (only + * one of which should be set): + * {@link #TYPE_TEXT_FLAG_CAP_CHARACTERS}, + * {@link #TYPE_TEXT_FLAG_CAP_WORDS}, and. + * {@link #TYPE_TEXT_FLAG_CAP_SENTENCES}. It also supports the + * following variations: + * {@link #TYPE_TEXT_VARIATION_NORMAL}, and + * {@link #TYPE_TEXT_VARIATION_URI}. If you do not recognize the + * variation, normal should be assumed. + */ + public static final int TYPE_CLASS_TEXT = 0x00000001; + + /** + * Flag for {@link #TYPE_CLASS_TEXT}: capitalize all characters. Overrides + * {@link #TYPE_TEXT_FLAG_CAP_WORDS} and + * {@link #TYPE_TEXT_FLAG_CAP_SENTENCES}. This value is explicitly defined + * to be the same as {@link TextUtils#CAP_MODE_CHARACTERS}. + */ + public static final int TYPE_TEXT_FLAG_CAP_CHARACTERS = 0x00001000; + + /** + * Flag for {@link #TYPE_CLASS_TEXT}: capitalize first character of + * all words. Overrides {@link #TYPE_TEXT_FLAG_CAP_SENTENCES}. This + * value is explicitly defined + * to be the same as {@link TextUtils#CAP_MODE_WORDS}. + */ + public static final int TYPE_TEXT_FLAG_CAP_WORDS = 0x00002000; + + /** + * Flag for {@link #TYPE_CLASS_TEXT}: capitalize first character of + * each sentence. This value is explicitly defined + * to be the same as {@link TextUtils#CAP_MODE_SENTENCES}. + */ + public static final int TYPE_TEXT_FLAG_CAP_SENTENCES = 0x00004000; + + /** + * Flag for {@link #TYPE_CLASS_TEXT}: the user is entering free-form + * text that should have auto-correction applied to it. + */ + public static final int TYPE_TEXT_FLAG_AUTO_CORRECT = 0x00008000; + + /** + * Flag for {@link #TYPE_CLASS_TEXT}: the text editor is performing + * auto-completion of the text being entered based on its own semantics, + * which it will present to the user as they type. This generally means + * that the input method should not be showing candidates itself, but can + * expect for the editor to supply its own completions/candidates from + * {@link android.view.inputmethod.InputMethodSession#displayCompletions + * InputMethodSession.displayCompletions()} as a result of the editor calling + * {@link android.view.inputmethod.InputMethodManager#displayCompletions + * InputMethodManager.displayCompletions()}. + */ + public static final int TYPE_TEXT_FLAG_AUTO_COMPLETE = 0x00010000; + + /** + * Flag for {@link #TYPE_CLASS_TEXT}: multiple lines of text can be + * entered into the field. + */ + public static final int TYPE_TEXT_FLAG_MULTI_LINE = 0x00020000; + + // ---------------------------------------------------------------------- + + /** + * Default variation of {@link #TYPE_CLASS_TEXT}: plain old normal text. + */ + public static final int TYPE_TEXT_VARIATION_NORMAL = 0x00000000; + + /** + * Variation of {@link #TYPE_CLASS_TEXT}: entering a URI. + */ + public static final int TYPE_TEXT_VARIATION_URI = 0x00000010; + + /** + * Variation of {@link #TYPE_CLASS_TEXT}: entering an e-mail address. + */ + public static final int TYPE_TEXT_VARIATION_EMAIL_ADDRESS = 0x00000020; + + /** + * Variation of {@link #TYPE_CLASS_TEXT}: entering the subject line of + * an e-mail. + */ + public static final int TYPE_TEXT_VARIATION_EMAIL_SUBJECT = 0x00000030; + + /** + * Variation of {@link #TYPE_CLASS_TEXT}: entering the content of + * an e-mail. + */ + public static final int TYPE_TEXT_VARIATION_EMAIL_CONTENT = 0x00000040; + + /** + * Variation of {@link #TYPE_CLASS_TEXT}: entering the name of a person. + */ + public static final int TYPE_TEXT_VARIATION_PERSON_NAME = 0x00000050; + + /** + * Variation of {@link #TYPE_CLASS_TEXT}: entering a postal mailing + * address. + */ + public static final int TYPE_TEXT_VARIATION_POSTAL_ADDRESS = 0x00000060; + + /** + * Variation of {@link #TYPE_CLASS_TEXT}: entering a password. + */ + public static final int TYPE_TEXT_VARIATION_PASSWORD = 0x00000070; + + /** + * Variation of {@link #TYPE_CLASS_TEXT}: entering a search string + * for a web search. + */ + public static final int TYPE_TEXT_VARIATION_WEB_SEARCH = 0x00000080; + + /** + * Variation of {@link #TYPE_CLASS_TEXT}: entering text inside of + * a web form. + */ + public static final int TYPE_TEXT_VARIATION_WEB_EDIT_TEXT = 0x00000090; + + // ---------------------------------------------------------------------- + // ---------------------------------------------------------------------- + // ---------------------------------------------------------------------- + + /** + * Class for numeric text. This class supports the following flag: + * {@link #TYPE_NUMBER_FLAG_SIGNED} and + * {@link #TYPE_NUMBER_FLAG_DECIMAL}. + */ + public static final int TYPE_CLASS_NUMBER = 0x00000002; + + /** + * Flag of {@link #TYPE_CLASS_NUMBER}: the number is signed, allowing + * a positive or negative sign at the start. + */ + public static final int TYPE_NUMBER_FLAG_SIGNED = 0x00001000; + + /** + * Flag of {@link #TYPE_CLASS_NUMBER}: the number is decimal, allowing + * a decimal point to provide fractional values. + */ + public static final int TYPE_NUMBER_FLAG_DECIMAL = 0x00002000; + + // ---------------------------------------------------------------------- + // ---------------------------------------------------------------------- + // ---------------------------------------------------------------------- + + /** + * Class for a phone number. This class currently supports no variations + * or flags. + */ + public static final int TYPE_CLASS_PHONE = 0x00000003; + + // ---------------------------------------------------------------------- + // ---------------------------------------------------------------------- + // ---------------------------------------------------------------------- + + /** + * Class for dates and times. It supports the + * following variations: + * {@link #TYPE_DATETIME_VARIATION_NORMAL} + * {@link #TYPE_DATETIME_VARIATION_DATE}, and + * {@link #TYPE_DATETIME_VARIATION_TIME},. + */ + public static final int TYPE_CLASS_DATETIME = 0x00000004; + + /** + * Default variation of {@link #TYPE_CLASS_DATETIME}: allows entering + * both a date and time. + */ + public static final int TYPE_DATETIME_VARIATION_NORMAL = 0x00000000; + + /** + * Default variation of {@link #TYPE_CLASS_DATETIME}: allows entering + * only a date. + */ + public static final int TYPE_DATETIME_VARIATION_DATE = 0x00000010; + + /** + * Default variation of {@link #TYPE_CLASS_DATETIME}: allows entering + * only a time. + */ + public static final int TYPE_DATETIME_VARIATION_TIME = 0x00000020; +} diff --git a/core/java/android/text/LoginFilter.java b/core/java/android/text/LoginFilter.java index dd2d77f..27c703f 100644 --- a/core/java/android/text/LoginFilter.java +++ b/core/java/android/text/LoginFilter.java @@ -83,8 +83,21 @@ public abstract class LoginFilter implements InputFilter { } onStop(); + + if (!changed) { + return null; + } - return changed ? new String(out, 0, outidx) : null; + String s = new String(out, 0, outidx); + + if (source instanceof Spanned) { + SpannableString sp = new SpannableString(s); + TextUtils.copySpansFrom((Spanned) source, + start, end, null, sp, 0); + return sp; + } else { + return s; + } } /** diff --git a/core/java/android/text/Selection.java b/core/java/android/text/Selection.java index 0f4916a..44469ec 100644 --- a/core/java/android/text/Selection.java +++ b/core/java/android/text/Selection.java @@ -417,10 +417,13 @@ public class Selection { } } + private static final class START { }; + private static final class END { }; + /* * Public constants */ - public static final Object SELECTION_START = new Object(); - public static final Object SELECTION_END = new Object(); + public static final Object SELECTION_START = new START(); + public static final Object SELECTION_END = new END(); } diff --git a/core/java/android/text/Spanned.java b/core/java/android/text/Spanned.java index 2b4b4d2..bd0a16b 100644 --- a/core/java/android/text/Spanned.java +++ b/core/java/android/text/Spanned.java @@ -26,6 +26,12 @@ public interface Spanned extends CharSequence { /** + * Bitmask of bits that are relevent for controlling point/mark behavior + * of spans. + */ + public static final int SPAN_POINT_MARK_MASK = 0x33; + + /** * 0-length spans with type SPAN_MARK_MARK behave like text marks: * they remain at their original offset when text is inserted * at that offset. @@ -92,6 +98,14 @@ extends CharSequence public static final int SPAN_EXCLUSIVE_INCLUSIVE = SPAN_POINT_POINT; /** + * This flag is set on spans that are being used to apply temporary + * styling information on the composing text of an input method, so that + * they can be found and removed when the composing text is being + * replaced. + */ + public static final int SPAN_COMPOSING = 0x100; + + /** * The bits numbered SPAN_USER_SHIFT and above are available * for callers to use to store scalar data associated with their * span object. diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index 2d18575..ceb9f4f 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -907,7 +907,8 @@ extends Layout mLineDirections[j] = linedirs; - if (ellipsize != null) { + // 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, ellipsiswidth, ellipsize, j, textwidth, paint); @@ -950,7 +951,7 @@ extends Layout ellipsisStart = 0; ellipsisCount = i; - } else if (where == TextUtils.TruncateAt.END) { + } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE) { float sum = 0; int i; diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index e791aaf..64356d5 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -22,6 +22,7 @@ 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; @@ -42,13 +43,14 @@ import android.text.style.TextAppearanceSpan; import android.text.style.TypefaceSpan; 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; -public class TextUtils -{ +public class TextUtils { private TextUtils() { /* cannot be instantiated */ } private static String[] EMPTY_STRING_ARRAY = new String[]{}; @@ -827,6 +829,30 @@ public class TextUtils }; /** + * Debugging tool to print the spans in a CharSequence. The output will + * be printed one span per line. If the CharSequence is not a Spanned, + * then the entire string will be printed on a single line. + */ + public static void dumpSpans(CharSequence cs, Printer printer, String prefix) { + if (cs instanceof Spanned) { + Spanned sp = (Spanned) cs; + Object[] os = sp.getSpans(0, cs.length(), Object.class); + + for (int i = 0; i < os.length; i++) { + Object o = os[i]; + printer.println(prefix + cs.subSequence(sp.getSpanStart(o), + sp.getSpanEnd(o)) + ": " + + Integer.toHexString(System.identityHashCode(o)) + + " " + o.getClass().getCanonicalName() + + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o) + + ") fl=#" + sp.getSpanFlags(o)); + } + } else { + printer.println(prefix + cs + ": (no spans)"); + } + } + + /** * Return a new CharSequence in which each of the source strings is * replaced by the corresponding element of the destinations. */ @@ -1021,6 +1047,7 @@ public class TextUtils START, MIDDLE, END, + MARQUEE, } public interface EllipsizeCallback { @@ -1460,7 +1487,7 @@ public class TextUtils case '&': sb.append("&"); //$NON-NLS-1$ break; - case '\\': + case '\'': sb.append("'"); //$NON-NLS-1$ break; case '"': @@ -1565,6 +1592,132 @@ public class TextUtils return true; } + /** + * Capitalization mode for {@link #getCapsMode}: capitalize all + * characters. This value is explicitly defined to be the same as + * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}. + */ + 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 + * {@link InputType#TYPE_TEXT_FLAG_CAP_WORDS}. + */ + 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 + * {@link InputType#TYPE_TEXT_FLAG_CAP_SENTENCES}. + */ + 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 + * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and + * {@link #CAP_MODE_SENTENCES}. + * + * @return Returns the actual capitalization modes that can be in effect + * at the current position, which is any combination of + * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and + * {@link #CAP_MODE_SENTENCES}. + */ + public static int getCapsMode(CharSequence cs, int off, int reqModes) { + int i; + char c; + int mode = 0; + + if ((reqModes&CAP_MODE_CHARACTERS) != 0) { + mode |= CAP_MODE_CHARACTERS; + } + if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) { + return mode; + } + + // Back over allowed opening punctuation. + + for (i = off; i > 0; i--) { + c = cs.charAt(i - 1); + + if (c != '"' && c != '\'' && + Character.getType(c) != Character.START_PUNCTUATION) { + break; + } + } + + // Start of paragraph, with optional whitespace. + + int j = i; + while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) { + j--; + } + if (j == 0 || cs.charAt(j - 1) == '\n') { + return mode | CAP_MODE_WORDS; + } + + // Or start of word if we are that style. + + if ((reqModes&CAP_MODE_SENTENCES) == 0) { + if (i != j) mode |= CAP_MODE_WORDS; + return mode; + } + + // There must be a space if not the start of paragraph. + + if (i == j) { + return mode; + } + + // Back over allowed closing punctuation. + + for (; j > 0; j--) { + c = cs.charAt(j - 1); + + if (c != '"' && c != '\'' && + Character.getType(c) != Character.END_PUNCTUATION) { + break; + } + } + + if (j > 0) { + c = cs.charAt(j - 1); + + if (c == '.' || c == '?' || c == '!') { + // Do not capitalize if the word ends with a period but + // also contains a period, in which case it is an abbreviation. + + if (c == '.') { + for (int k = j - 2; k >= 0; k--) { + c = cs.charAt(k); + + if (c == '.') { + return mode; + } + + if (!Character.isLetter(c)) { + break; + } + } + } + + return mode | CAP_MODE_SENTENCES; + } + } + + return mode; + } + private static Object sLock = new Object(); private static char[] sTemp = null; } diff --git a/core/java/android/pim/DateFormat.java b/core/java/android/text/format/DateFormat.java index 802e045..3437978 100644 --- a/core/java/android/pim/DateFormat.java +++ b/core/java/android/text/format/DateFormat.java @@ -14,19 +14,21 @@ * limitations under the License. */ -package android.pim; +package android.text.format; import android.content.Context; import android.provider.Settings; -import android.provider.Settings.SettingNotFoundException; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.SpannedString; +import com.android.internal.R; + import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.TimeZone; +import java.text.SimpleDateFormat; /** Utility class for producing strings with formatted date/time. @@ -187,12 +189,32 @@ public class DateFormat { public static final char YEAR = 'y'; /** - * @return true if the user has set the system to use a 24 hour time - * format, else false. + * Returns true if user preference is set to 24-hour format. + * @param context the context to use for the content resolver + * @return true if 24 hour time format is selected, false otherwise. */ public static boolean is24HourFormat(Context context) { String value = Settings.System.getString(context.getContentResolver(), Settings.System.TIME_12_24); + + if (value == null) { + java.text.DateFormat natural = + java.text.DateFormat.getTimeInstance( + java.text.DateFormat.LONG, + context.getResources().getConfiguration().locale); + + if (natural instanceof SimpleDateFormat) { + SimpleDateFormat sdf = (SimpleDateFormat) natural; + String pattern = sdf.toPattern(); + + if (pattern.indexOf('H') >= 0) { + return true; + } else { + return false; + } + } + } + boolean b24 = !(value == null || value.equals("12")); return b24; } @@ -205,7 +227,15 @@ public class DateFormat { */ public static final java.text.DateFormat getTimeFormat(Context context) { boolean b24 = is24HourFormat(context); - return new java.text.SimpleDateFormat(b24 ? "H:mm" : "h:mm a"); + int res; + + if (b24) { + res = R.string.twenty_four_hour_time_format; + } else { + res = R.string.twelve_hour_time_format; + } + + return new java.text.SimpleDateFormat(context.getString(res)); } /** @@ -228,13 +258,29 @@ public class DateFormat { public static final java.text.DateFormat getLongDateFormat(Context context) { String value = getDateFormatString(context); if (value.indexOf('M') < value.indexOf('d')) { - value = "MMMM dd, yyyy"; + value = context.getString(R.string.full_date_month_first); } else { - value = "dd MMMM, yyyy"; + value = context.getString(R.string.full_date_day_first); } return new java.text.SimpleDateFormat(value); } - + + /** + * Returns a {@link java.text.DateFormat} object that can format the date + * in medium form (such as Dec. 31, 1999) based on user preference. + * @param context the application context + * @return the {@link java.text.DateFormat} object that formats the date in long form. + */ + public static final java.text.DateFormat getMediumDateFormat(Context context) { + String value = getDateFormatString(context); + if (value.indexOf('M') < value.indexOf('d')) { + value = context.getString(R.string.medium_date_month_first); + } else { + value = context.getString(R.string.medium_date_day_first); + } + return new java.text.SimpleDateFormat(value); + } + /** * Gets the current date format stored as a char array. The array will contain * 3 elements ({@link #DATE}, {@link #MONTH}, and {@link #YEAR}) in the order @@ -274,15 +320,33 @@ public class DateFormat { String value = Settings.System.getString(context.getContentResolver(), Settings.System.DATE_FORMAT); if (value == null || value.length() < 6) { + /* + * No need to localize -- this is an emergency fallback in case + * the setting is missing, but it should always be set. + */ value = "MM-dd-yyyy"; } return value; } + /** + * Given a format string and a time in milliseconds since Jan 1, 1970 GMT, returns a + * CharSequence containing the requested date. + * @param inFormat the format string, as described in {@link android.text.format.DateFormat} + * @param inTimeInMillis in milliseconds since Jan 1, 1970 GMT + * @return a {@link CharSequence} containing the requested text + */ public static final CharSequence format(CharSequence inFormat, long inTimeInMillis) { return format(inFormat, new Date(inTimeInMillis)); } + /** + * Given a format string and a {@link java.util.Date} object, returns a CharSequence containing + * the requested date. + * @param inFormat the format string, as described in {@link android.text.format.DateFormat} + * @param inDate the date to format + * @return a {@link CharSequence} containing the requested text + */ public static final CharSequence format(CharSequence inFormat, Date inDate) { Calendar c = new GregorianCalendar(); @@ -291,6 +355,13 @@ public class DateFormat { return format(inFormat, c); } + /** + * Given a format string and a {@link java.util.Calendar} object, returns a CharSequence + * containing the requested date. + * @param inFormat the format string, as described in {@link android.text.format.DateFormat} + * @param inDate the date to format + * @return a {@link CharSequence} containing the requested text + */ public static final CharSequence format(CharSequence inFormat, Calendar inDate) { SpannableStringBuilder s = new SpannableStringBuilder(inFormat); int c; diff --git a/core/java/android/pim/DateUtils.java b/core/java/android/text/format/DateUtils.java index 2a01f12..48f65c6 100644 --- a/core/java/android/pim/DateUtils.java +++ b/core/java/android/text/format/DateUtils.java @@ -14,25 +14,26 @@ * limitations under the License. */ -package android.pim; +package android.text.format; + +import com.android.internal.R; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.pim.DateException; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.TimeZone; -import com.android.internal.R; - /** + * This class contains various date-related utilities for creating text for things like + * elapsed time and date ranges, strings for days of the week and months, and AM/PM text etc. */ public class DateUtils { - private static final String TAG = "DateUtils"; - private static final Object sLock = new Object(); private static final int[] sDaysLong = new int[] { com.android.internal.R.string.day_of_week_long_sunday, @@ -125,9 +126,7 @@ public class DateUtils com.android.internal.R.string.am, com.android.internal.R.string.pm, }; - private static int sFirstDay; private static Configuration sLastConfig; - private static String sStatusDateFormat; private static String sStatusTimeFormat; private static String sElapsedFormatMMSS; private static String sElapsedFormatHMMSS; @@ -153,17 +152,18 @@ public class DateUtils public static final int FORMAT_NO_YEAR = 0x00008; public static final int FORMAT_SHOW_DATE = 0x00010; public static final int FORMAT_NO_MONTH_DAY = 0x00020; - public static final int FORMAT_24HOUR = 0x00040; - public static final int FORMAT_CAP_AMPM = 0x00080; - public static final int FORMAT_NO_NOON = 0x00100; - public static final int FORMAT_CAP_NOON = 0x00200; - public static final int FORMAT_NO_MIDNIGHT = 0x00400; - public static final int FORMAT_CAP_MIDNIGHT = 0x00800; - public static final int FORMAT_UTC = 0x01000; - public static final int FORMAT_ABBREV_TIME = 0x02000; - public static final int FORMAT_ABBREV_WEEKDAY = 0x04000; - public static final int FORMAT_ABBREV_MONTH = 0x08000; - public static final int FORMAT_NUMERIC_DATE = 0x10000; + public static final int FORMAT_12HOUR = 0x00040; + public static final int FORMAT_24HOUR = 0x00080; + public static final int FORMAT_CAP_AMPM = 0x00100; + public static final int FORMAT_NO_NOON = 0x00200; + public static final int FORMAT_CAP_NOON = 0x00400; + public static final int FORMAT_NO_MIDNIGHT = 0x00800; + public static final int FORMAT_CAP_MIDNIGHT = 0x01000; + public static final int FORMAT_UTC = 0x02000; + public static final int FORMAT_ABBREV_TIME = 0x04000; + public static final int FORMAT_ABBREV_WEEKDAY = 0x08000; + public static final int FORMAT_ABBREV_MONTH = 0x10000; + public static final int FORMAT_NUMERIC_DATE = 0x20000; public static final int FORMAT_ABBREV_ALL = (FORMAT_ABBREV_TIME | FORMAT_ABBREV_WEEKDAY | FORMAT_ABBREV_MONTH); public static final int FORMAT_CAP_NOON_MIDNIGHT = (FORMAT_CAP_NOON | FORMAT_CAP_MIDNIGHT); @@ -172,10 +172,6 @@ public class DateUtils // Date and time format strings that are constant and don't need to be // translated. public static final String HOUR_MINUTE_24 = "%H:%M"; - public static final String HOUR_MINUTE_AMPM = "%-l:%M%P"; - public static final String HOUR_MINUTE_CAP_AMPM = "%-l:%M%p"; - public static final String HOUR_AMPM = "%-l%P"; - public static final String HOUR_CAP_AMPM = "%-l%p"; public static final String MONTH_FORMAT = "%B"; public static final String ABBREV_MONTH_FORMAT = "%b"; public static final String NUMERIC_MONTH_FORMAT = "%m"; @@ -238,7 +234,7 @@ public class DateUtils /** * Request the full spelled-out name. - * For use with the 'abbrev' parameter of {@link #getDayOfWeekStr} and {@link #getMonthStr}. + * For use with the 'abbrev' parameter of {@link #getDayOfWeekString} and {@link #getMonthString}. * @more * <p>e.g. "Sunday" or "January" */ @@ -246,7 +242,7 @@ public class DateUtils /** * Request an abbreviated version of the name. - * For use with the 'abbrev' parameter of {@link #getDayOfWeekStr} and {@link #getMonthStr}. + * For use with the 'abbrev' parameter of {@link #getDayOfWeekString} and {@link #getMonthString}. * @more * <p>e.g. "Sun" or "Jan" */ @@ -254,7 +250,7 @@ public class DateUtils /** * Request a shorter abbreviated version of the name. - * For use with the 'abbrev' parameter of {@link #getDayOfWeekStr} and {@link #getMonthStr}. + * For use with the 'abbrev' parameter of {@link #getDayOfWeekString} and {@link #getMonthString}. * @more * <p>e.g. "Su" or "Jan" * <p>In some languages, the results returned for LENGTH_SHORT may be the same as @@ -264,7 +260,7 @@ public class DateUtils /** * Request an even shorter abbreviated version of the name. - * For use with the 'abbrev' parameter of {@link #getDayOfWeekStr} and {@link #getMonthStr}. + * For use with the 'abbrev' parameter of {@link #getDayOfWeekString} and {@link #getMonthString}. * @more * <p>e.g. "M", "Tu", "Th" or "J" * <p>In some languages, the results returned for LENGTH_SHORTEST may be the same as @@ -274,7 +270,7 @@ public class DateUtils /** * Request an even shorter abbreviated version of the name. - * For use with the 'abbrev' parameter of {@link #getDayOfWeekStr} and {@link #getMonthStr}. + * For use with the 'abbrev' parameter of {@link #getDayOfWeekString} and {@link #getMonthString}. * @more * <p>e.g. "S", "T", "T" or "J" * <p>In some languages, the results returned for LENGTH_SHORTEST may be the same as @@ -282,11 +278,10 @@ public class DateUtils */ public static final int LENGTH_SHORTEST = 50; - /** * Return a string for the day of the week. - * @param dayOfWeek One of {@link #Calendar.SUNDAY Calendar.SUNDAY}, - * {@link #Calendar.MONDAY Calendar.MONDAY}, etc. + * @param dayOfWeek One of {@link Calendar#SUNDAY Calendar.SUNDAY}, + * {@link Calendar#MONDAY Calendar.MONDAY}, etc. * @param abbrev One of {@link #LENGTH_LONG}, {@link #LENGTH_SHORT}, {@link #LENGTH_SHORTER} * or {@link #LENGTH_SHORTEST}. For forward compatibility, anything else * will return the same as {#LENGTH_MEDIUM}. @@ -308,9 +303,10 @@ public class DateUtils } /** - * Return a string for AM or PM. + * Return a localized string for AM or PM. * @param ampm Either {@link Calendar#AM Calendar.AM} or {@link Calendar#PM Calendar.PM}. * @throws IndexOutOfBoundsException if the ampm is out of bounds. + * @return Localized version of "AM" or "PM". */ public static String getAMPMString(int ampm) { Resources r = Resources.getSystem(); @@ -318,12 +314,13 @@ public class DateUtils } /** - * Return a string for the day of the week. - * @param month One of {@link #Calendar.JANUARY Calendar.JANUARY}, - * {@link #Calendar.FEBRUARY Calendar.FEBRUARY}, etc. + * Return a localized string for the day of the week. + * @param month One of {@link Calendar#JANUARY Calendar.JANUARY}, + * {@link Calendar#FEBRUARY Calendar.FEBRUARY}, etc. * @param abbrev One of {@link #LENGTH_LONG}, {@link #LENGTH_SHORT}, {@link #LENGTH_SHORTER} * or {@link #LENGTH_SHORTEST}. For forward compatibility, anything else * will return the same as {#LENGTH_MEDIUM}. + * @return Localized day of the week. */ public static String getMonthString(int month, int abbrev) { // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER. @@ -344,6 +341,12 @@ public class DateUtils return r.getString(list[month - Calendar.JANUARY]); } + /** + * Returns a string describing the elapsed time since startTime. + * @param startTime some time in the past. + * @return a String object containing the elapsed time. + * @see #getRelativeTimeSpanString(long, long, long) + */ public static CharSequence getRelativeTimeSpanString(long startTime) { return getRelativeTimeSpanString(startTime, System.currentTimeMillis(), MINUTE_IN_MILLIS); } @@ -363,67 +366,50 @@ public class DateUtils public static CharSequence getRelativeTimeSpanString(long time, long now, long minResolution) { Resources r = Resources.getSystem(); - // TODO: Assembling strings by hand like this is bad style for i18n. - boolean past = (now > time); - String prefix = past ? null : r.getString(com.android.internal.R.string.in); - String postfix = past ? r.getString(com.android.internal.R.string.ago) : null; - return getRelativeTimeSpanString(time, now, minResolution, prefix, postfix); - } - - public static CharSequence getRelativeTimeSpanString(long time, long now, long minResolution, - String prefix, String postfix) { - Resources r = Resources.getSystem(); - + boolean past = (now >= time); long duration = Math.abs(now - time); + int resId; + long count; if (duration < MINUTE_IN_MILLIS && minResolution < MINUTE_IN_MILLIS) { - long count = duration / SECOND_IN_MILLIS; - String singular = r.getString(com.android.internal.R.string.second); - String plural = r.getString(com.android.internal.R.string.seconds); - return pluralizedSpan(count, singular, plural, prefix, postfix); - } - - if (duration < HOUR_IN_MILLIS && minResolution < HOUR_IN_MILLIS) { - long count = duration / MINUTE_IN_MILLIS; - String singular = r.getString(com.android.internal.R.string.minute); - String plural = r.getString(com.android.internal.R.string.minutes); - return pluralizedSpan(count, singular, plural, prefix, postfix); - } - - if (duration < DAY_IN_MILLIS && minResolution < DAY_IN_MILLIS) { - long count = duration / HOUR_IN_MILLIS; - String singular = r.getString(com.android.internal.R.string.hour); - String plural = r.getString(com.android.internal.R.string.hours); - return pluralizedSpan(count, singular, plural, prefix, postfix); - } - - if (duration < WEEK_IN_MILLIS && minResolution < WEEK_IN_MILLIS) { - return getRelativeDayString(r, time, now); - } - - return dateString(time); - } - - - private static final String pluralizedSpan(long count, String singular, String plural, - String prefix, String postfix) { - StringBuilder s = new StringBuilder(); - - if (prefix != null) { - s.append(prefix); - s.append(" "); + count = duration / SECOND_IN_MILLIS; + if (past) { + resId = com.android.internal.R.plurals.num_seconds_ago; + } else { + resId = com.android.internal.R.plurals.in_num_seconds; + } + } else if (duration < HOUR_IN_MILLIS && minResolution < HOUR_IN_MILLIS) { + count = duration / MINUTE_IN_MILLIS; + if (past) { + resId = com.android.internal.R.plurals.num_minutes_ago; + } else { + resId = com.android.internal.R.plurals.in_num_minutes; + } + } else if (duration < DAY_IN_MILLIS && minResolution < DAY_IN_MILLIS) { + count = duration / HOUR_IN_MILLIS; + if (past) { + resId = com.android.internal.R.plurals.num_hours_ago; + } else { + resId = com.android.internal.R.plurals.in_num_hours; + } + } else if (duration < WEEK_IN_MILLIS && minResolution < WEEK_IN_MILLIS) { + count = duration / DAY_IN_MILLIS; + if (past) { + resId = com.android.internal.R.plurals.num_days_ago; + } else { + resId = com.android.internal.R.plurals.in_num_days; + } + } else { + // Longer than a week ago, so just show the date. + int flags = FORMAT_SHOW_DATE | FORMAT_SHOW_YEAR | FORMAT_ABBREV_MONTH; + + // We know that we won't be showing the time, so it is safe to pass + // in a null context. + return formatDateRange(null, time, time, flags); } - s.append(count); - s.append(' '); - s.append(count == 0 || count > 1 ? plural : singular); - - if (postfix != null) { - s.append(" "); - s.append(postfix); - } - - return s.toString(); + String format = r.getQuantityString(resId, (int) count); + return String.format(format, count); } /** @@ -457,12 +443,16 @@ public class DateUtils } else if (days == 0) { return r.getString(com.android.internal.R.string.today); } - - if (!past) { - return r.getString(com.android.internal.R.string.daysDurationFuturePlural, days); + + int resId; + if (past) { + resId = com.android.internal.R.plurals.num_days_ago; } else { - return r.getString(com.android.internal.R.string.daysDurationPastPlural, days); + resId = com.android.internal.R.plurals.in_num_days; } + + String format = r.getQuantityString(resId, days); + return String.format(format, days); } private static void initFormatStrings() { @@ -472,7 +462,6 @@ public class DateUtils if (sLastConfig == null || !sLastConfig.equals(cfg)) { sLastConfig = cfg; sStatusTimeFormat = r.getString(com.android.internal.R.string.status_bar_time_format); - sStatusDateFormat = r.getString(com.android.internal.R.string.status_bar_date_format); sElapsedFormatMMSS = r.getString(com.android.internal.R.string.elapsed_time_short_format_mm_ss); sElapsedFormatHMMSS = r.getString(com.android.internal.R.string.elapsed_time_short_format_h_mm_ss); } @@ -488,20 +477,11 @@ public class DateUtils initFormatStrings(); return DateFormat.format(sStatusTimeFormat, millis); } - - /** - * Format a date so it appears like it would in the status bar clock. - * @deprecated use {@link #DateFormat.getDateFormat(Context)} instead. - * @hide - */ - public static final CharSequence dateString(long startTime) { - initFormatStrings(); - return DateFormat.format(sStatusDateFormat, startTime); - } /** - * Formats an elapsed time like MM:SS or H:MM:SS + * Formats an elapsed time in the form "MM:SS" or "H:MM:SS" * for display on the call-in-progress screen. + * @param elapsedSeconds the elapsed time in seconds. */ public static String formatElapsedTime(long elapsedSeconds) { initFormatStrings(); @@ -584,7 +564,7 @@ public class DateUtils return (char) (digit + '0'); } - /* + /** * Format a date / time such that if the then is on the same day as now, it shows * just the time and if it's a different day, it shows just the date. * @@ -623,7 +603,7 @@ public class DateUtils /** * @hide - * @deprecated use {@link android.pim.Time} + * @deprecated use {@link android.text.format.Time} */ public static Calendar newCalendar(boolean zulu) { @@ -652,7 +632,7 @@ public class DateUtils /** * @hide - * @deprecated use {@link android.pim.Time} + * @deprecated use {@link android.text.format.Time} */ private static final int ctoi(String str, int index) throws DateException @@ -667,7 +647,7 @@ public class DateUtils /** * @hide - * @deprecated use {@link android.pim.Time} + * @deprecated use {@link android.text.format.Time} */ private static final int check(int lowerBound, int upperBound, int value) throws DateException @@ -681,7 +661,7 @@ public class DateUtils /** * @hide - * @deprecated use {@link android.pim.Time} + * @deprecated use {@link android.text.format.Time} * Return true if this date string is local time */ public static boolean isUTC(String s) @@ -702,7 +682,7 @@ public class DateUtils // Returns if the Z was present, meaning that the time is in UTC /** * @hide - * @deprecated use {@link android.pim.Time} + * @deprecated use {@link android.text.format.Time} */ public static boolean parseDateTime(String str, Calendar cal) throws DateException @@ -748,7 +728,7 @@ public class DateUtils * Given a timezone string which can be null, and a dateTime string, * set that time into a calendar. * @hide - * @deprecated use {@link android.pim.Time} + * @deprecated use {@link android.text.format.Time} */ public static void parseDateTime(String tz, String dateTime, Calendar out) throws DateException @@ -778,7 +758,7 @@ public class DateUtils * * @param cal the date and time to write * @hide - * @deprecated use {@link android.pim.Time} + * @deprecated use {@link android.text.format.Time} */ public static String writeDateTime(Calendar cal) { @@ -796,7 +776,7 @@ public class DateUtils * be written at the end as per RFC2445. Otherwise, the time is * considered in localtime. * @hide - * @deprecated use {@link android.pim.Time} + * @deprecated use {@link android.text.format.Time} */ public static String writeDateTime(Calendar cal, boolean zulu) { @@ -819,7 +799,7 @@ public class DateUtils * has already been called on sb to the appropriate length * which is sb.setLength(zulu ? 16 : 15) * @hide - * @deprecated use {@link android.pim.Time} + * @deprecated use {@link android.text.format.Time} */ public static String writeDateTime(Calendar cal, StringBuilder sb) { @@ -866,7 +846,7 @@ public class DateUtils /** * @hide - * @deprecated use {@link android.pim.Time} + * @deprecated use {@link android.text.format.Time} */ public static void assign(Calendar lval, Calendar rval) { @@ -876,8 +856,31 @@ public class DateUtils } /** - * Creates a string describing a date/time range. The flags argument - * is a bitmask of options from the following list: + * Formats a date or a time range according to the local conventions. + * + * <p> + * Example output strings (date formats in these examples are shown using + * the US date format convention but that may change depending on the + * local settings): + * <ul> + * <li>10:15am</li> + * <li>3:00pm - 4:00pm</li> + * <li>3pm - 4pm</li> + * <li>3PM - 4PM</li> + * <li>08:00 - 17:00</li> + * <li>Oct 9</li> + * <li>Tue, Oct 9</li> + * <li>October 9, 2007</li> + * <li>Oct 9 - 10</li> + * <li>Oct 9 - 10, 2007</li> + * <li>Oct 28 - Nov 3, 2007</li> + * <li>Dec 31, 2007 - Jan 1, 2008</li> + * <li>Oct 9, 8:00am - Oct 10, 5:00pm</li> + * <li>12/31/2007 - 01/01/2008</li> + * </ul> + * + * <p> + * The flags argument is a bitmask of options from the following list: * * <ul> * <li>FORMAT_SHOW_TIME</li> @@ -886,6 +889,7 @@ public class DateUtils * <li>FORMAT_NO_YEAR</li> * <li>FORMAT_SHOW_DATE</li> * <li>FORMAT_NO_MONTH_DAY</li> + * <li>FORMAT_12HOUR</li> * <li>FORMAT_24HOUR</li> * <li>FORMAT_CAP_AMPM</li> * <li>FORMAT_NO_NOON</li> @@ -946,15 +950,25 @@ public class DateUtils * shown instead of "midnight". * * <p> + * If FORMAT_12HOUR is set and the time is shown, then the time is + * shown in the 12-hour time format. You should not normally set this. + * Instead, let the time format be chosen automatically according to the + * system settings. If both FORMAT_12HOUR and FORMAT_24HOUR are set, then + * FORMAT_24HOUR takes precedence. + * + * <p> * If FORMAT_24HOUR is set and the time is shown, then the time is - * shown in the 24-hour time format. + * shown in the 24-hour time format. You should not normally set this. + * Instead, let the time format be chosen automatically according to the + * system settings. If both FORMAT_12HOUR and FORMAT_24HOUR are set, then + * FORMAT_24HOUR takes precedence. * * <p> * If FORMAT_UTC is set, then the UTC timezone is used for the start * and end milliseconds. * * <p> - * If FORMAT_ABBREV_TIME is set and FORMAT_24HOUR is not set, then the + * If FORMAT_ABBREV_TIME is set and 12-hour time format is used, then the * start and end times (if shown) are abbreviated by not showing the minutes * if they are zero. For example, instead of "3:00pm" the time would be * abbreviated to "3pm". @@ -976,30 +990,15 @@ public class DateUtils * instead of using the name of the month. For example, "12/31/2008" * instead of "December 31, 2008". * - * <p> - * Example output strings: - * <ul> - * <li>10:15am</li> - * <li>3:00pm - 4:00pm</li> - * <li>3pm - 4pm</li> - * <li>3PM - 4PM</li> - * <li>08:00 - 17:00</li> - * <li>Oct 9</li> - * <li>Tue, Oct 9</li> - * <li>October 9, 2007</li> - * <li>Oct 9 - 10</li> - * <li>Oct 9 - 10, 2007</li> - * <li>Oct 28 - Nov 3, 2007</li> - * <li>Dec 31, 2007 - Jan 1, 2008</li> - * <li>Oct 9, 8:00am - Oct 10, 5:00pm</li> - * </ul> + * @param context the context is required only if the time is shown * @param startMillis the start time in UTC milliseconds * @param endMillis the end time in UTC milliseconds * @param flags a bit mask of options * - * @return a string with the formatted date/time range. + * @return a string containing the formatted date/time range. */ - public static String formatDateRange(long startMillis, long endMillis, int flags) { + public static String formatDateRange(Context context, long startMillis, + long endMillis, int flags) { Resources res = Resources.getSystem(); boolean showTime = (flags & FORMAT_SHOW_TIME) != 0; boolean showWeekDay = (flags & FORMAT_SHOW_WEEKDAY) != 0; @@ -1008,7 +1007,6 @@ public class DateUtils boolean useUTC = (flags & FORMAT_UTC) != 0; boolean abbrevWeekDay = (flags & FORMAT_ABBREV_WEEKDAY) != 0; boolean abbrevMonth = (flags & FORMAT_ABBREV_MONTH) != 0; - boolean use24Hour = (flags & FORMAT_24HOUR) != 0; boolean noMonthDay = (flags & FORMAT_NO_MONTH_DAY) != 0; boolean numericDate = (flags & FORMAT_NUMERIC_DATE) != 0; @@ -1039,7 +1037,7 @@ public class DateUtils // the end of Nov 11?). // If we are not showing the time then also adjust the end date // for multiple-day events. This is to allow us to display, for - // example, "Nov 10 -11" for an event with an start date of Nov 10 + // example, "Nov 10 -11" for an event with a start date of Nov 10 // and an end date of Nov 12 at 00:00. // If the start and end time are the same, then skip this and don't // adjust the date. @@ -1075,6 +1073,16 @@ public class DateUtils if (showTime) { String startTimeFormat = ""; String endTimeFormat = ""; + boolean force24Hour = (flags & FORMAT_24HOUR) != 0; + boolean force12Hour = (flags & FORMAT_12HOUR) != 0; + boolean use24Hour; + if (force24Hour) { + use24Hour = true; + } else if (force12Hour) { + use24Hour = false; + } else { + use24Hour = DateFormat.is24HourFormat(context); + } if (use24Hour) { startTimeFormat = HOUR_MINUTE_24; endTimeFormat = HOUR_MINUTE_24; @@ -1090,28 +1098,28 @@ public class DateUtils boolean endOnTheHour = endDate.minute == 0 && endDate.second == 0; if (abbrevTime && startOnTheHour) { if (capAMPM) { - startTimeFormat = HOUR_CAP_AMPM; + startTimeFormat = res.getString(com.android.internal.R.string.hour_cap_ampm); } else { - startTimeFormat = HOUR_AMPM; + startTimeFormat = res.getString(com.android.internal.R.string.hour_ampm); } } else { if (capAMPM) { - startTimeFormat = HOUR_MINUTE_CAP_AMPM; + startTimeFormat = res.getString(com.android.internal.R.string.hour_minute_cap_ampm); } else { - startTimeFormat = HOUR_MINUTE_AMPM; + startTimeFormat = res.getString(com.android.internal.R.string.hour_minute_ampm); } } if (abbrevTime && endOnTheHour) { if (capAMPM) { - endTimeFormat = HOUR_CAP_AMPM; + endTimeFormat = res.getString(com.android.internal.R.string.hour_cap_ampm); } else { - endTimeFormat = HOUR_AMPM; + endTimeFormat = res.getString(com.android.internal.R.string.hour_ampm); } } else { if (capAMPM) { - endTimeFormat = HOUR_MINUTE_CAP_AMPM; + endTimeFormat = res.getString(com.android.internal.R.string.hour_minute_cap_ampm); } else { - endTimeFormat = HOUR_MINUTE_AMPM; + endTimeFormat = res.getString(com.android.internal.R.string.hour_minute_ampm); } } @@ -1346,63 +1354,117 @@ public class DateUtils } /** + * Formats a date or a time according to the local conventions. There are + * lots of options that allow the caller to control, for example, if the + * time is shown, if the day of the week is shown, if the month name is + * abbreviated, if noon is shown instead of 12pm, and so on. For the + * complete list of options, see the documentation for + * {@link #formatDateRange}. + * <p> + * Example output strings (date formats in these examples are shown using + * the US date format convention but that may change depending on the + * local settings): + * <ul> + * <li>10:15am</li> + * <li>3:00pm</li> + * <li>3pm</li> + * <li>3PM</li> + * <li>08:00</li> + * <li>17:00</li> + * <li>noon</li> + * <li>Noon</li> + * <li>midnight</li> + * <li>Midnight</li> + * <li>Oct 31</li> + * <li>Oct 31, 2007</li> + * <li>October 31, 2007</li> + * <li>10am, Oct 31</li> + * <li>17:00, Oct 31</li> + * <li>Wed</li> + * <li>Wednesday</li> + * <li>10am, Wed, Oct 31</li> + * <li>Wed, Oct 31</li> + * <li>Wednesday, Oct 31</li> + * <li>Wed, Oct 31, 2007</li> + * <li>Wed, October 31</li> + * <li>10/31/2007</li> + * </ul> + * + * @param context the context is required only if the time is shown + * @param millis a point in time in UTC milliseconds + * @param flags a bit mask of formatting options + * @return a string containing the formatted date/time. + */ + public static String formatDateTime(Context context, long millis, int flags) { + return formatDateRange(context, millis, millis, flags); + } + + /** * @return a relative time string to display the time expressed by millis. Times * are counted starting at midnight, which means that assuming that the current * time is March 31st, 0:30: - * "millis=0:10 today" will be displayed as "0:10" - * "millis=11:30pm the day before" will be displayed as "Mar 30" - * A similar scheme is used to dates that are a week, a month or more than a year old. + * <ul> + * <li>"millis=0:10 today" will be displayed as "0:10"</li> + * <li>"millis=11:30pm the day before" will be displayed as "Mar 30"</li> + * </ul> + * If the given millis is in a different year, then the full date is + * returned in numeric format (e.g., "10/12/2008"). * * @param withPreposition If true, the string returned will include the correct - * preposition ("at 9:20am", "in 2008" or "on May 29"). + * preposition ("at 9:20am", "on 10/12/2008" or "on May 29"). */ public static CharSequence getRelativeTimeSpanString(Context c, long millis, boolean withPreposition) { - long span = System.currentTimeMillis() - millis; + long now = System.currentTimeMillis(); + long span = now - millis; Resources res = c.getResources(); if (sNowTime == null) { sNowTime = new Time(); sThenTime = new Time(); - sMonthDayFormat = res.getString(com.android.internal.R.string.abbrev_month_day); } - sNowTime.setToNow(); + sNowTime.set(now); sThenTime.set(millis); + String result; + int prepositionId; if (span < DAY_IN_MILLIS && sNowTime.weekDay == sThenTime.weekDay) { // Same day - return getPrepositionDate(res, sThenTime, R.string.preposition_for_time, - HOUR_MINUTE_CAP_AMPM, withPreposition); + int flags = FORMAT_SHOW_TIME; + result = formatDateRange(c, millis, millis, flags); + prepositionId = R.string.preposition_for_time; } else if (sNowTime.year != sThenTime.year) { // Different years - // TODO: take locale into account so that the display will adjust correctly. - return getPrepositionDate(res, sThenTime, R.string.preposition_for_year, - NUMERIC_MONTH_FORMAT + "/" + MONTH_DAY_FORMAT + "/" + YEAR_FORMAT_TWO_DIGITS, - withPreposition); + int flags = FORMAT_SHOW_DATE | FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE; + result = formatDateRange(c, millis, millis, flags); + + // This is a date (like "10/31/2008" so use the date preposition) + prepositionId = R.string.preposition_for_date; } else { // Default - return getPrepositionDate(res, sThenTime, R.string.preposition_for_date, - sMonthDayFormat, withPreposition); + int flags = FORMAT_SHOW_DATE | FORMAT_ABBREV_MONTH; + result = formatDateRange(c, millis, millis, flags); + prepositionId = R.string.preposition_for_date; } + if (withPreposition) { + result = res.getString(prepositionId, result); + } + return result; } /** - * @return A date string suitable for display based on the format and including the - * date preposition if withPreposition is true. + * Convenience function to return relative time string without preposition. + * @param c context for resources + * @param millis time in milliseconds + * @return {@link CharSequence} containing relative time. + * @see #getRelativeTimeSpanString(Context, long, boolean) */ - private static String getPrepositionDate(Resources res, Time thenTime, int id, - String formatString, boolean withPreposition) { - String result = thenTime.format(formatString); - return withPreposition ? res.getString(id, result) : result; - } - public static CharSequence getRelativeTimeSpanString(Context c, long millis) { return getRelativeTimeSpanString(c, millis, false /* no preposition */); } private static Time sNowTime; private static Time sThenTime; - private static String sMonthDayFormat; } diff --git a/core/java/android/content/Formatter.java b/core/java/android/text/format/Formatter.java index 8ad9f40..1b30aa0 100644 --- a/core/java/android/content/Formatter.java +++ b/core/java/android/text/format/Formatter.java @@ -14,18 +14,18 @@ * limitations under the License. */ -package android.content; +package android.text.format; + +import android.content.Context; /** * Utility class to aid in formatting common values that are not covered - * by the standard java.util.Formatter - * @hide + * by the standard java.util.Formatter. */ public final class Formatter { /** - * Formats a content size to be in the form of bytes, kilobytes, - * megabytes, etc + * Formats a content size to be in the form of bytes, kilobytes, megabytes, etc * * @param context Context to use to load the localized units * @param number size value to be formated @@ -63,4 +63,21 @@ public final class Formatter { } return String.format("%.0f%s", result, context.getText(suffix).toString()); } + + /** + * Returns a string in the canonical IP format ###.###.###.### from a packed integer containing + * the IP address. The IP address is expected to be in little-endian format (LSB first). That + * is, 0x01020304 will return "4.3.2.1". + * + * @param addr the IP address as a packed integer with LSB first. + * @return string with canonical IP address format. + */ + public static String formatIpAddress(int addr) { + StringBuffer buf = new StringBuffer(); + buf.append(addr & 0xff).append('.'). + append((addr >>>= 8) & 0xff).append('.'). + append((addr >>>= 8) & 0xff).append('.'). + append((addr >>>= 8) & 0xff); + return buf.toString(); + } } diff --git a/core/java/android/pim/Time.java b/core/java/android/text/format/Time.java index 59ba87b..5bf9b20 100644 --- a/core/java/android/pim/Time.java +++ b/core/java/android/text/format/Time.java @@ -14,15 +14,14 @@ * limitations under the License. */ -package android.pim; - +package android.text.format; +import android.content.res.Resources; +import java.util.Locale; import java.util.TimeZone; /** - * {@hide} - * * The Time class is a faster replacement for the java.util.Calendar and * java.util.GregorianCalendar classes. An instance of the Time class represents * a moment in time, specified with second precision. It is modelled after @@ -30,6 +29,10 @@ import java.util.TimeZone; * functionality. */ public class Time { + private static final String Y_M_D_T_H_M_S_000 = "%Y-%m-%dT%H:%M:%S.000"; + private static final String Y_M_D_T_H_M_S_000_Z = "%Y-%m-%dT%H:%M:%S.000Z"; + private static final String Y_M_D = "%Y-%m-%d"; + public static final String TIMEZONE_UTC = "UTC"; /** @@ -90,6 +93,7 @@ public class Time { * <li><b>positive</b> - in dst</li> * <li><b>0</b> - not in dst</li> * <li><b>negative</b> - unknown</li> + * </ul> */ public int isDst; @@ -125,9 +129,26 @@ public class Time { public static final int FRIDAY = 5; public static final int SATURDAY = 6; + /* + * The Locale for which date formatting strings have been loaded. + */ + private static Locale sLocale; + private static String[] sShortMonths; + private static String[] sLongMonths; + private static String[] sShortWeekdays; + private static String[] sLongWeekdays; + private static String sTimeOnlyFormat; + private static String sDateOnlyFormat; + private static String sDateTimeFormat; + private static String sAm; + private static String sPm; + private static String sDateCommand = "%a %b %e %H:%M:%S %Z %Y"; + /** * Construct a Time object in the timezone named by the string * argument "timezone". The time is initialized to Jan 1, 1970. + * @param timezone string containing the timezone to use. + * @see TimeZone */ public Time(String timezone) { if (timezone == null) { @@ -142,7 +163,7 @@ public class Time { } /** - * Construct a Time object in the local timezone. The time is initialized to + * Construct a Time object in the default timezone. The time is initialized to * Jan 1, 1970. */ public Time() { @@ -191,6 +212,8 @@ public class Time { * Return the maximum possible value for the given field given the value of * the other fields. Requires that it be normalized for MONTH_DAY and * YEAR_DAY. + * @param field one of the constants for HOUR, MINUTE, SECOND, etc. + * @return the maximum value for the field. */ public int getActualMaximum(int field) { switch (field) { @@ -230,6 +253,7 @@ public class Time { /** * Clears all values, setting the timezone to the given timezone. Sets isDst * to a negative value to mean "unknown". + * @param timezone the timezone to use. */ public void clear(String timezone) { if (timezone == null) { @@ -259,8 +283,76 @@ public class Time { * Print the current value given the format string provided. See man * strftime for what means what. The final string must be less than 256 * characters. - */ - native public String format(String format); + * @param format a string containing the desired format. + * @return a String containing the current time expressed in the current locale. + */ + public String format(String format) { + synchronized (Time.class) { + Locale locale = Locale.getDefault(); + + if (sLocale == null || locale == null || !(locale.equals(sLocale))) { + Resources r = Resources.getSystem(); + + sShortMonths = new String[] { + r.getString(com.android.internal.R.string.month_medium_january), + r.getString(com.android.internal.R.string.month_medium_february), + r.getString(com.android.internal.R.string.month_medium_march), + r.getString(com.android.internal.R.string.month_medium_april), + r.getString(com.android.internal.R.string.month_medium_may), + r.getString(com.android.internal.R.string.month_medium_june), + r.getString(com.android.internal.R.string.month_medium_july), + r.getString(com.android.internal.R.string.month_medium_august), + r.getString(com.android.internal.R.string.month_medium_september), + r.getString(com.android.internal.R.string.month_medium_october), + r.getString(com.android.internal.R.string.month_medium_november), + r.getString(com.android.internal.R.string.month_medium_december), + }; + sLongMonths = new String[] { + r.getString(com.android.internal.R.string.month_long_january), + r.getString(com.android.internal.R.string.month_long_february), + r.getString(com.android.internal.R.string.month_long_march), + r.getString(com.android.internal.R.string.month_long_april), + r.getString(com.android.internal.R.string.month_long_may), + r.getString(com.android.internal.R.string.month_long_june), + r.getString(com.android.internal.R.string.month_long_july), + r.getString(com.android.internal.R.string.month_long_august), + r.getString(com.android.internal.R.string.month_long_september), + r.getString(com.android.internal.R.string.month_long_october), + r.getString(com.android.internal.R.string.month_long_november), + r.getString(com.android.internal.R.string.month_long_december), + }; + sShortWeekdays = new String[] { + r.getString(com.android.internal.R.string.day_of_week_medium_sunday), + r.getString(com.android.internal.R.string.day_of_week_medium_monday), + r.getString(com.android.internal.R.string.day_of_week_medium_tuesday), + r.getString(com.android.internal.R.string.day_of_week_medium_wednesday), + r.getString(com.android.internal.R.string.day_of_week_medium_thursday), + r.getString(com.android.internal.R.string.day_of_week_medium_friday), + r.getString(com.android.internal.R.string.day_of_week_medium_saturday), + }; + sLongWeekdays = new String[] { + r.getString(com.android.internal.R.string.day_of_week_long_sunday), + r.getString(com.android.internal.R.string.day_of_week_long_monday), + r.getString(com.android.internal.R.string.day_of_week_long_tuesday), + r.getString(com.android.internal.R.string.day_of_week_long_wednesday), + r.getString(com.android.internal.R.string.day_of_week_long_thursday), + r.getString(com.android.internal.R.string.day_of_week_long_friday), + r.getString(com.android.internal.R.string.day_of_week_long_saturday), + }; + sTimeOnlyFormat = r.getString(com.android.internal.R.string.time_of_day); + sDateOnlyFormat = r.getString(com.android.internal.R.string.month_day_year); + sDateTimeFormat = r.getString(com.android.internal.R.string.date_and_time); + sAm = r.getString(com.android.internal.R.string.am); + sPm = r.getString(com.android.internal.R.string.pm); + + sLocale = locale; + } + + return format1(format); + } + } + + native private String format1(String format); /** * Return the current time in YYYYMMDDTHHMMSS<tz> format @@ -269,33 +361,79 @@ public class Time { native public String toString(); /** - * Parse a time in the current zone in YYYYMMDDTHHMMSS format. - */ - native public void parse(String s); - - /** - * Parse a time in RFC 2445 format. Returns whether or not the time is in - * UTC (ends with Z). + * Parses a date-time string in either the RFC 2445 format or an abbreviated + * format that does not include the "time" field. For example, all of the + * following strings are valid: + * + * <ul> + * <li>"20081013T160000Z"</li> + * <li>"20081013T160000"</li> + * <li>"20081013"</li> + * </ul> + * + * Returns whether or not the time is in UTC (ends with Z). If the string + * ends with "Z" then the timezone is set to UTC. If the date-time string + * included only a date and no time field, then the <code>allDay</code> + * field of this Time class is set to true and the <code>hour</code>, + * <code>minute</code>, and <code>second</code> fields are set to zero; + * otherwise (a time field was included in the date-time string) + * <code>allDay</code> is set to false. The fields <code>weekDay</code>, + * <code>yearDay</code>, and <code>gmtoff</code> are always set to zero, + * and the field <code>isDst</code> is set to -1 (unknown). To set those + * fields, call {@link #normalize(boolean)} after parsing. + * + * To parse a date-time string and convert it to UTC milliseconds, do + * something like this: + * + * <pre> + * Time time = new Time(); + * String date = "20081013T160000Z"; + * time.parse(date); + * long millis = time.normalize(false); + * </pre> * * @param s the string to parse * @return true if the resulting time value is in UTC time */ - public boolean parse2445(String s) { - if (nativeParse2445(s)) { + public boolean parse(String s) { + if (nativeParse(s)) { timezone = TIMEZONE_UTC; return true; } return false; } - native private boolean nativeParse2445(String s); + /** + * Parse a time in the current zone in YYYYMMDDTHHMMSS format. + */ + native private boolean nativeParse(String s); /** * Parse a time in RFC 3339 format. This method also parses simple dates - * (that is, strings that contain no time or time offset). If the string - * contains a time and time offset, then the time offset will be used to - * convert the time value to UTC. + * (that is, strings that contain no time or time offset). For example, + * all of the following strings are valid: + * + * <ul> + * <li>"2008-10-13T16:00:00.000Z"</li> + * <li>"2008-10-13T16:00:00.000+07:00"</li> + * <li>"2008-10-13T16:00:00.000-07:00"</li> + * <li>"2008-10-13"</li> + * </ul> + * + * <p> + * If the string contains a time and time offset, then the time offset will + * be used to convert the time value to UTC. + * </p> + * + * <p> + * If the given string contains just a date (with no time field), then + * the {@link #allDay} field is set to true and the {@link #hour}, + * {@link #minute}, and {@link #second} fields are set to zero. + * </p> + * + * <p> * Returns true if the resulting time value is in UTC time. + * </p> * * @param s the string to parse * @return true if the resulting time value is in UTC time @@ -408,8 +546,8 @@ public class Time { } /** - * Set the fields. Sets weekDay, yearDay and gmtoff to 0. Call - * normalize() if you need those. + * Sets the fields. Sets weekDay, yearDay and gmtoff to 0, and isDst to -1. + * Call {@link #normalize(boolean)} if you need those. */ public void set(int second, int minute, int hour, int monthDay, int month, int year) { this.allDay = false; @@ -425,6 +563,15 @@ public class Time { this.gmtoff = 0; } + /** + * Sets the date from the given fields. Also sets allDay to true. + * Sets weekDay, yearDay and gmtoff to 0, and isDst to -1. + * Call {@link #normalize(boolean)} if you need those. + * + * @param monthDay the day of the month (in the range [1,31]) + * @param month the zero-based month number (in the range [0,11]) + * @param year the year + */ public void set(int monthDay, int month, int year) { this.allDay = true; this.second = 0; @@ -439,10 +586,25 @@ public class Time { this.gmtoff = 0; } + /** + * Returns true if the time represented by this Time object occurs before + * the given time. + * + * @param that a given Time object to compare against + * @return true if this time is less than the given time + */ public boolean before(Time that) { return Time.compare(this, that) < 0; } + + /** + * Returns true if the time represented by this Time object occurs after + * the given time. + * + * @param that a given Time object to compare against + * @return true if this time is greater than the given time + */ public boolean after(Time that) { return Time.compare(this, that) > 0; } @@ -459,14 +621,18 @@ public class Time { * object must already be normalized because this method uses the * yearDay and weekDay fields. * + * <p> * In IS0 8601, weeks start on Monday. * The first week of the year (week 1) is defined by ISO 8601 as the * first week with four or more of its days in the starting year. * Or equivalently, the week containing January 4. Or equivalently, * the week with the year's first Thursday in it. + * </p> * + * <p> * The week number can be calculated by counting Thursdays. Week N * contains the Nth Thursday of the year. + * </p> * * @return the ISO week number. */ @@ -486,13 +652,24 @@ public class Time { return temp.yearDay / 7 + 1; } + /** + * Return a string in the RFC 3339 format. + * <p> + * If allDay is true, expresses the time as Y-M-D</p> + * <p> + * Otherwise, if the timezone is UTC, expresses the time as Y-M-D-T-H-M-S UTC</p> + * <p> + * Otherwise the time is expressed the time as Y-M-D-T-H-M-S +- GMT</p> + * @param allDay + * @return string in the RFC 3339 format. + */ public String format3339(boolean allDay) { if (allDay) { - return format("%Y-%m-%d"); + return format(Y_M_D); } else if (TIMEZONE_UTC.equals(timezone)) { - return format("%Y-%m-%dT%H:%M:%S.000Z"); + return format(Y_M_D_T_H_M_S_000_Z); } else { - String base = format("%Y-%m-%dT%H:%M:%S.000"); + String base = format(Y_M_D_T_H_M_S_000); String sign = (gmtoff < 0) ? "-" : "+"; int offset = (int)Math.abs(gmtoff); int minutes = (offset % 3600) / 60; @@ -502,6 +679,13 @@ public class Time { } } + /** + * Returns true if the day of the given time is the epoch on the Julian Calendar + * (January 1, 1970 on the Gregorian calendar). + * + * @param time the time to test + * @return true if epoch. + */ public static boolean isEpoch(Time time) { long millis = time.toMillis(true); return getJulianDay(millis, 0) == EPOCH_JULIAN_DAY; @@ -536,6 +720,7 @@ public class Time { * GMT offset than whatever is currently stored in this Time object anyway. * After this method returns all the fields will be normalized and the time * will be set to 12am at the beginning of the given Julian day. + * </p> * * <p> * The only exception to this is if 12am does not exist for that day because @@ -543,6 +728,7 @@ public class Time { * hour at 12am on April 25, 2008 and there are a few other places that * also change daylight saving time at 12am. In those cases, the time * will be set to 1am. + * </p> * * @param julianDay the Julian day in the timezone for this Time object * @return the UTC milliseconds for the beginning of the Julian day diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java index ac2e499..652413e 100644 --- a/core/java/android/text/method/ArrowKeyMovementMethod.java +++ b/core/java/android/text/method/ArrowKeyMovementMethod.java @@ -155,34 +155,11 @@ implements MovementMethod return false; } - public boolean onTrackballEvent(TextView widget, Spannable buffer, - MotionEvent event) { - boolean handled = false; - int x = (int) event.getX(); - int y = (int) event.getY(); - - for (; y < 0; y++) { - handled |= up(widget, buffer); - } - for (; y > 0; y--) { - handled |= down(widget, buffer); - } - - for (; x < 0; x++) { - handled |= left(widget, buffer); - } - for (; x > 0; x--) { - handled |= right(widget, buffer); - } - - if (handled) { - MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); - MetaKeyKeyListener.resetLockedMeta(buffer); - } - - return handled; + public boolean onTrackballEvent(TextView widget, Spannable text, + MotionEvent event) { + return false; } - + public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { boolean handled = Touch.onTouchEvent(widget, buffer, event); diff --git a/core/java/android/text/method/BaseKeyListener.java b/core/java/android/text/method/BaseKeyListener.java index 3e92b7b..a875368 100644 --- a/core/java/android/text/method/BaseKeyListener.java +++ b/core/java/android/text/method/BaseKeyListener.java @@ -18,9 +18,9 @@ package android.text.method; import android.view.KeyEvent; import android.view.View; -import android.os.Message; -import android.util.Log; +import android.text.InputType; import android.text.*; +import android.text.method.TextKeyListener.Capitalize; import android.widget.TextView; public abstract class BaseKeyListener @@ -99,6 +99,25 @@ implements KeyListener { return true; } + static int makeTextContentType(Capitalize caps, boolean autoText) { + int contentType = InputType.TYPE_CLASS_TEXT; + switch (caps) { + case CHARACTERS: + contentType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; + break; + case WORDS: + contentType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS; + break; + case SENTENCES: + contentType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; + break; + } + if (autoText) { + contentType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; + } + return contentType; + } + public boolean onKeyDown(View view, Editable content, int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_DEL) { diff --git a/core/java/android/text/method/CharacterPickerDialog.java b/core/java/android/text/method/CharacterPickerDialog.java index d787132..3c406751 100644 --- a/core/java/android/text/method/CharacterPickerDialog.java +++ b/core/java/android/text/method/CharacterPickerDialog.java @@ -69,7 +69,7 @@ public class CharacterPickerDialog extends Dialog WindowManager.LayoutParams params = getWindow().getAttributes(); params.token = mView.getApplicationWindowToken(); - params.type = params.TYPE_APPLICATION_PANEL; + params.type = params.TYPE_APPLICATION_ATTACHED_DIALOG; setTitle(R.string.select_character); setContentView(R.layout.character_picker); diff --git a/core/java/android/text/method/DateKeyListener.java b/core/java/android/text/method/DateKeyListener.java index 0ca0332..7c11434 100644 --- a/core/java/android/text/method/DateKeyListener.java +++ b/core/java/android/text/method/DateKeyListener.java @@ -17,12 +17,18 @@ package android.text.method; import android.view.KeyEvent; +import android.text.InputType; /** * For entering dates in a text field. */ public class DateKeyListener extends NumberKeyListener { + public int getInputType() { + return InputType.TYPE_CLASS_DATETIME + | InputType.TYPE_DATETIME_VARIATION_DATE; + } + @Override protected char[] getAcceptedChars() { diff --git a/core/java/android/text/method/DateTimeKeyListener.java b/core/java/android/text/method/DateTimeKeyListener.java index 304d326..f8ebc40 100644 --- a/core/java/android/text/method/DateTimeKeyListener.java +++ b/core/java/android/text/method/DateTimeKeyListener.java @@ -16,6 +16,7 @@ package android.text.method; +import android.text.InputType; import android.view.KeyEvent; /** @@ -23,6 +24,11 @@ import android.view.KeyEvent; */ public class DateTimeKeyListener extends NumberKeyListener { + public int getInputType() { + return InputType.TYPE_CLASS_DATETIME + | InputType.TYPE_DATETIME_VARIATION_NORMAL; + } + @Override protected char[] getAcceptedChars() { diff --git a/core/java/android/text/method/DialerKeyListener.java b/core/java/android/text/method/DialerKeyListener.java index e805ad7..b121e60 100644 --- a/core/java/android/text/method/DialerKeyListener.java +++ b/core/java/android/text/method/DialerKeyListener.java @@ -18,7 +18,7 @@ package android.text.method; import android.view.KeyEvent; import android.view.KeyCharacterMap.KeyData; -import android.util.SparseIntArray; +import android.text.InputType; import android.text.Spannable; /** @@ -40,6 +40,10 @@ public class DialerKeyListener extends NumberKeyListener return sInstance; } + public int getInputType() { + return InputType.TYPE_CLASS_PHONE; + } + /** * Overrides the superclass's lookup method to prefer the number field * from the KeyEvent. diff --git a/core/java/android/text/method/DigitsKeyListener.java b/core/java/android/text/method/DigitsKeyListener.java index 99a3f1a..f0f072c 100644 --- a/core/java/android/text/method/DigitsKeyListener.java +++ b/core/java/android/text/method/DigitsKeyListener.java @@ -16,6 +16,7 @@ package android.text.method; +import android.text.InputType; import android.text.Spanned; import android.text.SpannableStringBuilder; import android.view.KeyEvent; @@ -109,6 +110,17 @@ public class DigitsKeyListener extends NumberKeyListener return dim; } + public int getInputType() { + int contentType = InputType.TYPE_CLASS_NUMBER; + if (mSign) { + contentType |= InputType.TYPE_NUMBER_FLAG_SIGNED; + } + if (mDecimal) { + contentType |= InputType.TYPE_NUMBER_FLAG_DECIMAL; + } + return contentType; + } + @Override public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { diff --git a/core/java/android/text/method/KeyListener.java b/core/java/android/text/method/KeyListener.java index 05ab72d..4ae6191 100644 --- a/core/java/android/text/method/KeyListener.java +++ b/core/java/android/text/method/KeyListener.java @@ -16,14 +16,39 @@ package android.text.method; +import android.text.Editable; import android.view.KeyEvent; import android.view.View; -import android.os.Message; -import android.text.*; -import android.widget.TextView; -public interface KeyListener -{ +/** + * Interface for converting text key events into edit operations on an + * Editable class. Note that for must cases this interface has been + * superceded by general soft input methods as defined by + * {@link android.view.inputmethod.InputMethod}; it should only be used + * for cases where an application has its own on-screen keypad and also wants + * to process hard keyboard events to match it. + */ +public interface KeyListener { + /** + * Return the type of text that this key listener is manipulating, + * as per {@link android.text.InputType}. This is used to + * determine the mode of the soft keyboard that is shown for the editor. + * + * <p>If you return + * {@link android.text.InputType#TYPE_NULL} + * then <em>no</em> soft keyboard will provided. In other words, you + * must be providing your own key pad for on-screen input and the key + * listener will be used to handle input from a hard keyboard. + * + * <p>If you + * return any other value, a soft input method will be created when the + * user puts focus in the editor, which will provide a keypad and also + * consume hard key events. This means that the key listener will generally + * not be used, instead the soft input method will take care of managing + * key input as per the content type returned here. + */ + public int getInputType(); + /** * If the key listener wants to handle this key, return true, * otherwise return false and the caller (i.e. the widget host) @@ -39,4 +64,9 @@ public interface KeyListener */ public boolean onKeyUp(View view, Editable text, int keyCode, KeyEvent event); + + /** + * Remove the given shift states from the edited text. + */ + public void clearMetaKeyState(View view, Editable content, int states); } diff --git a/core/java/android/text/method/MetaKeyKeyListener.java b/core/java/android/text/method/MetaKeyKeyListener.java index 2d75b87..f0305d9 100644 --- a/core/java/android/text/method/MetaKeyKeyListener.java +++ b/core/java/android/text/method/MetaKeyKeyListener.java @@ -221,6 +221,12 @@ public abstract class MetaKeyKeyListener { content.setSpan(what, 0, 0, RELEASED); } + public void clearMetaKeyState(View view, Editable content, int states) { + if ((states&META_SHIFT_ON) != 0) resetLock(content, CAP); + if ((states&META_ALT_ON) != 0) resetLock(content, ALT); + if ((states&META_SYM_ON) != 0) resetLock(content, SYM); + } + /** * The meta key has been pressed but has not yet been used. */ diff --git a/core/java/android/text/method/MultiTapKeyListener.java b/core/java/android/text/method/MultiTapKeyListener.java index 7137d40..6d94788 100644 --- a/core/java/android/text/method/MultiTapKeyListener.java +++ b/core/java/android/text/method/MultiTapKeyListener.java @@ -18,14 +18,11 @@ package android.text.method; import android.view.KeyEvent; import android.view.View; -import android.os.Message; import android.os.Handler; import android.os.SystemClock; import android.text.*; import android.text.method.TextKeyListener.Capitalize; -import android.widget.TextView; import android.util.SparseArray; -import android.util.SparseIntArray; /** * This is the standard key listener for alphabetic input on 12-key @@ -77,6 +74,10 @@ public class MultiTapKeyListener extends BaseKeyListener return sInstance[off]; } + public int getInputType() { + return makeTextContentType(mCapitalize, mAutoText); + } + public boolean onKeyDown(View view, Editable content, int keyCode, KeyEvent event) { int selStart, selEnd; diff --git a/core/java/android/text/method/QwertyKeyListener.java b/core/java/android/text/method/QwertyKeyListener.java index ae7ba8f..863b2e2 100644 --- a/core/java/android/text/method/QwertyKeyListener.java +++ b/core/java/android/text/method/QwertyKeyListener.java @@ -58,6 +58,10 @@ public class QwertyKeyListener extends BaseKeyListener { return sInstance[off]; } + public int getInputType() { + return makeTextContentType(mAutoCap, mAutoText); + } + public boolean onKeyDown(View view, Editable content, int keyCode, KeyEvent event) { int selStart, selEnd; @@ -219,7 +223,7 @@ public class QwertyKeyListener extends BaseKeyListener { if ((pref & TextKeyListener.AUTO_TEXT) != 0 && mAutoText && (i == ' ' || i == '\t' || i == '\n' || i == ',' || i == '.' || i == '!' || i == '?' || - i == '"' || i == ')' || i == ']') && + i == '"' || Character.getType(i) == Character.END_PUNCTUATION) && content.getSpanEnd(TextKeyListener.INHIBIT_REPLACEMENT) != oldStart) { int x; @@ -257,7 +261,16 @@ public class QwertyKeyListener extends BaseKeyListener { content.charAt(selEnd - 2) == ' ') { char c = content.charAt(selEnd - 3); - if (Character.isLetter(c)) { + for (int j = selEnd - 3; j > 0; j--) { + if (c == '"' || + Character.getType(c) == Character.END_PUNCTUATION) { + c = content.charAt(j - 1); + } else { + break; + } + } + + if (Character.isLetter(c) || Character.isDigit(c)) { content.replace(selEnd - 2, selEnd - 1, "."); } } diff --git a/core/java/android/text/method/ScrollingMovementMethod.java b/core/java/android/text/method/ScrollingMovementMethod.java index 0438e1e..db470be 100644 --- a/core/java/android/text/method/ScrollingMovementMethod.java +++ b/core/java/android/text/method/ScrollingMovementMethod.java @@ -171,34 +171,16 @@ implements MovementMethod return false; } + public boolean onTrackballEvent(TextView widget, Spannable text, + MotionEvent event) { + return false; + } + public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { return Touch.onTouchEvent(widget, buffer, event); } - public boolean onTrackballEvent(TextView widget, Spannable buffer, - MotionEvent event) { - boolean handled = false; - int x = (int) event.getX(); - int y = (int) event.getY(); - - for (; y < 0; y++) { - handled |= up(widget, buffer); - } - for (; y > 0; y--) { - handled |= down(widget, buffer); - } - - for (; x < 0; x++) { - handled |= left(widget, buffer); - } - for (; x > 0; x--) { - handled |= right(widget, buffer); - } - - return handled; - } - public void initialize(TextView widget, Spannable text) { } public boolean canSelectArbitrarily() { diff --git a/core/java/android/text/method/TextKeyListener.java b/core/java/android/text/method/TextKeyListener.java index 012e41d..b1c380a 100644 --- a/core/java/android/text/method/TextKeyListener.java +++ b/core/java/android/text/method/TextKeyListener.java @@ -26,6 +26,7 @@ import android.text.*; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.View; +import android.text.InputType; import java.lang.ref.WeakReference; @@ -114,76 +115,15 @@ public class TextKeyListener extends BaseKeyListener implements SpanWatcher { return true; } - // Back over allowed opening punctuation. - - for (i = off; i > 0; i--) { - c = cs.charAt(i - 1); - - if (c != '"' && c != '(' && c != '[' && c != '\'') { - break; - } - } - - // Start of paragraph, with optional whitespace. - - int j = i; - while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) { - j--; - } - if (j == 0 || cs.charAt(j - 1) == '\n') { - return true; - } - - // Or start of word if we are that style. - - if (cap == Capitalize.WORDS) { - return i != j; - } - - // There must be a space if not the start of paragraph. - - if (i == j) { - return false; - } - - // Back over allowed closing punctuation. - - for (; j > 0; j--) { - c = cs.charAt(j - 1); - - if (c != '"' && c != ')' && c != ']' && c != '\'') { - break; - } - } - - if (j > 0) { - c = cs.charAt(j - 1); - - if (c == '.' || c == '?' || c == '!') { - // Do not capitalize if the word ends with a period but - // also contains a period, in which case it is an abbreviation. - - if (c == '.') { - for (int k = j - 2; k >= 0; k--) { - c = cs.charAt(k); - - if (c == '.') { - return false; - } - - if (!Character.isLetter(c)) { - break; - } - } - } - - return true; - } - } - - return false; + return TextUtils.getCapsMode(cs, off, cap == Capitalize.WORDS + ? TextUtils.CAP_MODE_WORDS : TextUtils.CAP_MODE_SENTENCES) + != 0; } + public int getInputType() { + return makeTextContentType(mAutoCap, mAutoText); + } + @Override public boolean onKeyDown(View view, Editable content, int keyCode, KeyEvent event) { @@ -251,6 +191,10 @@ public class TextKeyListener extends BaseKeyListener implements SpanWatcher { private static class NullKeyListener implements KeyListener { + public int getInputType() { + return InputType.TYPE_NULL; + } + public boolean onKeyDown(View view, Editable content, int keyCode, KeyEvent event) { return false; @@ -261,6 +205,9 @@ public class TextKeyListener extends BaseKeyListener implements SpanWatcher { return false; } + public void clearMetaKeyState(View view, Editable content, int states) { + } + public static NullKeyListener getInstance() { if (sInstance != null) return sInstance; diff --git a/core/java/android/text/method/TimeKeyListener.java b/core/java/android/text/method/TimeKeyListener.java index 9ba1fe6..3fbfd8c 100644 --- a/core/java/android/text/method/TimeKeyListener.java +++ b/core/java/android/text/method/TimeKeyListener.java @@ -17,12 +17,18 @@ package android.text.method; import android.view.KeyEvent; +import android.text.InputType; /** * For entering times in a text field. */ public class TimeKeyListener extends NumberKeyListener { + public int getInputType() { + return InputType.TYPE_CLASS_DATETIME + | InputType.TYPE_DATETIME_VARIATION_TIME; + } + @Override protected char[] getAcceptedChars() { diff --git a/core/java/android/text/method/Touch.java b/core/java/android/text/method/Touch.java index bd01728..8b097c5 100644 --- a/core/java/android/text/method/Touch.java +++ b/core/java/android/text/method/Touch.java @@ -17,6 +17,7 @@ package android.text.method; import android.text.Layout; +import android.text.Layout.Alignment; import android.text.Spannable; import android.view.MotionEvent; import android.view.View; @@ -41,15 +42,31 @@ public class Touch { int left = Integer.MAX_VALUE; int right = 0; + Alignment a = null; for (int i = top; i <= bottom; i++) { left = (int) Math.min(left, layout.getLineLeft(i)); right = (int) Math.max(right, layout.getLineRight(i)); + + if (a == null) { + a = layout.getParagraphAlignment(i); + } } padding = widget.getTotalPaddingLeft() + widget.getTotalPaddingRight(); - x = Math.min(x, right - (widget.getWidth() - padding)); - x = Math.max(x, left); + int width = widget.getWidth(); + int diff = 0; + + if (right - left < width - padding) { + if (a == Alignment.ALIGN_CENTER) { + diff = (width - padding - (right - left)) / 2; + } else if (a == Alignment.ALIGN_OPPOSITE) { + diff = width - padding - (right - left); + } + } + + x = Math.min(x, right - (width - padding) - diff); + x = Math.max(x, left - diff); widget.scrollTo(x, y); } diff --git a/core/java/android/text/style/DynamicDrawableSpan.java b/core/java/android/text/style/DynamicDrawableSpan.java index 3bcc335..dd89b68 100644 --- a/core/java/android/text/style/DynamicDrawableSpan.java +++ b/core/java/android/text/style/DynamicDrawableSpan.java @@ -16,11 +16,12 @@ package android.text.style; -import java.lang.ref.WeakReference; - -import android.graphics.drawable.Drawable; import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +import java.lang.ref.WeakReference; /** * @@ -35,48 +36,51 @@ extends ReplacementSpan */ public abstract Drawable getDrawable(); + @Override public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { - Drawable b = getCachedDrawable(); + Drawable d = getCachedDrawable(); + Rect rect = d.getBounds(); if (fm != null) { - fm.ascent = -b.getIntrinsicHeight(); - fm.descent = 0; + fm.ascent = -rect.bottom; + fm.descent = 0; fm.top = fm.ascent; fm.bottom = 0; } - return b.getIntrinsicWidth(); + return rect.right; } + @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { Drawable b = getCachedDrawable(); canvas.save(); - canvas.translate(x, bottom-b.getIntrinsicHeight());; + canvas.translate(x, bottom - b.getBounds().bottom); b.draw(canvas); canvas.restore(); } private Drawable getCachedDrawable() { - WeakReference wr = mDrawableRef; - Drawable b = null; + WeakReference<Drawable> wr = mDrawableRef; + Drawable d = null; if (wr != null) - b = (Drawable) wr.get(); + d = wr.get(); - if (b == null) { - b = getDrawable(); - mDrawableRef = new WeakReference(b); + if (d == null) { + d = getDrawable(); + mDrawableRef = new WeakReference<Drawable>(d); } - return b; + return d; } - private WeakReference mDrawableRef; + private WeakReference<Drawable> mDrawableRef; } diff --git a/core/java/android/text/util/Linkify.java b/core/java/android/text/util/Linkify.java index 79ecfbd..d61e888 100644 --- a/core/java/android/text/util/Linkify.java +++ b/core/java/android/text/util/Linkify.java @@ -69,7 +69,7 @@ public class Linkify { public static final int PHONE_NUMBERS = 0x04; /** - * Bit field indicating that phone numbers should be matched in methods that + * Bit field indicating that street addresses should be matched in methods that * take an options mask */ public static final int MAP_ADDRESSES = 0x08; @@ -78,8 +78,7 @@ public class Linkify { * Bit mask indicating that all available patterns should be matched in * methods that take an options mask */ - public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS - | MAP_ADDRESSES; + public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS | MAP_ADDRESSES; /** * Don't treat anything with fewer than this many digits as a @@ -109,8 +108,7 @@ public class Linkify { * Filters out URL matches that don't have enough digits to be a * phone number. */ - public static final MatchFilter sPhoneNumberMatchFilter = - new MatchFilter() { + public static final MatchFilter sPhoneNumberMatchFilter = new MatchFilter() { public final boolean acceptMatch(CharSequence s, int start, int end) { int digitCount = 0; @@ -133,8 +131,7 @@ public class Linkify { * '+1 (919) 555-1212' * becomes '+19195551212' */ - public static final TransformFilter sPhoneNumberTransformFilter = - new TransformFilter() { + public static final TransformFilter sPhoneNumberTransformFilter = new TransformFilter() { public final String transformUrl(final Matcher match, String url) { return Regex.digitsAndPlusOnly(match); } @@ -300,8 +297,7 @@ public class Linkify { * prepended to the url of links that do not have * a scheme specified in the link text */ - public static final void addLinks(TextView text, Pattern pattern, - String scheme) { + public static final void addLinks(TextView text, Pattern pattern, String scheme) { addLinks(text, pattern, scheme, null, null); } @@ -341,8 +337,7 @@ public class Linkify { * prepended to the url of links that do not have * a scheme specified in the link text */ - public static final boolean addLinks(Spannable text, Pattern pattern, - String scheme) { + public static final boolean addLinks(Spannable text, Pattern pattern, String scheme) { return addLinks(text, pattern, scheme, null, null); } @@ -388,8 +383,7 @@ public class Linkify { return hasMatches; } - private static final void applyLink(String url, int start, int end, - Spannable text) { + private static final void applyLink(String url, int start, int end, Spannable text) { URLSpan span = new URLSpan(url); text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -402,13 +396,22 @@ public class Linkify { } boolean hasPrefix = false; + for (int i = 0; i < prefixes.length; i++) { if (url.regionMatches(true, 0, prefixes[i], 0, prefixes[i].length())) { hasPrefix = true; + + // Fix capitalization if necessary + if (!url.regionMatches(false, 0, prefixes[i], 0, + prefixes[i].length())) { + url = prefixes[i] + url.substring(prefixes[i].length()); + } + break; } } + if (!hasPrefix) { url = prefixes[0] + url; } @@ -438,30 +441,35 @@ public class Linkify { } } - private static final void gatherMapLinks(ArrayList<LinkSpec> links, - Spannable s) { + private static final void gatherMapLinks(ArrayList<LinkSpec> links, Spannable s) { String string = s.toString(); String address; int base = 0; + while ((address = WebView.findAddress(string)) != null) { int start = string.indexOf(address); + if (start < 0) { break; } + LinkSpec spec = new LinkSpec(); int length = address.length(); int end = start + length; + spec.start = base + start; spec.end = base + end; string = string.substring(end); base += end; String encodedAddress = null; + try { encodedAddress = URLEncoder.encode(address,"UTF-8"); } catch (UnsupportedEncodingException e) { continue; } + spec.url = "geo:0,0?q=" + encodedAddress; links.add(spec); } diff --git a/core/java/android/text/util/Regex.java b/core/java/android/text/util/Regex.java index 55ad140..4c128ad 100644 --- a/core/java/android/text/util/Regex.java +++ b/core/java/android/text/util/Regex.java @@ -65,7 +65,7 @@ public class Regex { */ public static final Pattern WEB_URL_PATTERN = Pattern.compile( - "((?:(http|https):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" + "((?:(http|https|Http|Https):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2}))+(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2}))+)?\\@)?)?" + "((?:(?:[a-zA-Z0-9][a-zA-Z0-9\\-]*\\.)+" // named host @@ -103,7 +103,9 @@ public class Regex { + "(?:\\:\\d{1,5})?)" // plus option port number + "(\\/(?:(?:[a-zA-Z0-9\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query params + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?" - + "\\b"); // and finally, a word boundary this is to stop foo.sure from matching as foo.su + + "(?:\\b|$)"); // and finally, a word boundary or end of + // input. This is to stop foo.sure from + // matching as foo.su public static final Pattern IP_ADDRESS_PATTERN = Pattern.compile( @@ -134,10 +136,20 @@ public class Regex { * might be phone numbers in arbitrary text, not for validating whether * something is in fact a phone number. It will miss many things that * are legitimate phone numbers. + * + * <p> The pattern matches the following: + * <ul> + * <li>Optionally, a + sign followed immediately by one or more digits. Spaces, dots, or dashes + * may follow. + * <li>Optionally, sets of digits in parentheses, separated by spaces, dots, or dashes. + * <li>A string starting and ending with a digit, containing digits, spaces, dots, and/or dashes. + * </ul> */ public static final Pattern PHONE_PATTERN - = Pattern.compile( - "(?:\\+[0-9]+)|(?:[0-9()][0-9()\\- \\.][0-9()\\- \\.]+[0-9])"); + = Pattern.compile( // sdd = space, dot, or dash + "(\\+[0-9]+[\\- \\.]*)?" // +<digits><sdd>* + + "(\\([0-9]+\\)[\\- \\.]*)?" // (<digits>)<sdd>* + + "([0-9][0-9\\- \\.][0-9\\- \\.]+[0-9])"); // <digit><digit|sdd>+<digit> /** * Convenience method to take all of the non-null matching groups in a diff --git a/core/java/android/util/TimeUtils.java b/core/java/android/util/TimeUtils.java index 3c4e337..0fc70d5 100644 --- a/core/java/android/util/TimeUtils.java +++ b/core/java/android/util/TimeUtils.java @@ -19,6 +19,7 @@ package android.util; import android.content.res.Resources; import android.content.res.XmlResourceParser; +import org.apache.harmony.luni.internal.util.ZoneInfoDB; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -28,14 +29,18 @@ import java.util.Date; import com.android.internal.util.XmlUtils; +/** + * A class containing utility methods related to time zones. + */ public class TimeUtils { + private static final String TAG = "TimeUtils"; + /** * Tries to return a time zone that would have had the specified offset * and DST value at the specified moment in the specified country. * Returns null if no suitable zone could be found. */ - public static TimeZone getTimeZone(int offset, boolean dst, long when, - String country) { + public static TimeZone getTimeZone(int offset, boolean dst, long when, String country) { if (country == null) { return null; } @@ -50,18 +55,18 @@ public class TimeUtils { String currentName = current.getID(); int currentOffset = current.getOffset(when); boolean currentDst = current.inDaylightTime(d); - + try { XmlUtils.beginDocument(parser, "timezones"); - + while (true) { XmlUtils.nextElement(parser); - + String element = parser.getName(); if (element == null || !(element.equals("timezone"))) { break; } - + String code = parser.getAttributeValue(null, "code"); if (country.equals(code)) { @@ -95,15 +100,34 @@ public class TimeUtils { } } } catch (XmlPullParserException e) { - Log.e("TimeUtils", - "Got exception while getting preferred time zone.", e); + Log.e(TAG, "Got exception while getting preferred time zone.", e); } catch (IOException e) { - Log.e("TimeUtils", - "Got exception while getting preferred time zone.", e); + Log.e(TAG, "Got exception while getting preferred time zone.", e); } finally { parser.close(); } - + return best; } + + /** + * Returns a String indicating the version of the time zone database currently + * in use. The format of the string is dependent on the underlying time zone + * database implementation, but will typically contain the year in which the database + * was updated plus a letter from a to z indicating changes made within that year. + * + * <p>Time zone database updates should be expected to occur periodically due to + * political and legal changes that cannot be anticipated in advance. Therefore, + * when computing the UTC time for a future event, applications should be aware that + * the results may differ following a time zone database update. This method allows + * applications to detect that a database change has occurred, and to recalculate any + * cached times accordingly. + * + * <p>The time zone database may be assumed to change only when the device runtime + * is restarted. Therefore, it is not necessary to re-query the database version + * during the lifetime of an activity. + */ + public static String getTimeZoneDatabaseVersion() { + return ZoneInfoDB.getVersion(); + } } diff --git a/core/java/android/view/ContextMenu.java b/core/java/android/view/ContextMenu.java index 9bfda40..dd1d7db 100644 --- a/core/java/android/view/ContextMenu.java +++ b/core/java/android/view/ContextMenu.java @@ -24,7 +24,7 @@ import android.widget.AdapterView; * Extension of {@link Menu} for context menus providing functionality to modify * the header of the context menu. * <p> - * Context menus do not support item shortcuts, item icons, and sub menus. + * Context menus do not support item shortcuts and item icons. * <p> * To show a context menu on long click, most clients will want to call * {@link Activity#registerForContextMenu} and override diff --git a/core/java/android/view/Gravity.java b/core/java/android/view/Gravity.java index ff9ab18..36d8ce6 100644 --- a/core/java/android/view/Gravity.java +++ b/core/java/android/view/Gravity.java @@ -23,7 +23,7 @@ import android.graphics.Rect; */ public class Gravity { - /** Contstant indicating that no gravity has been set **/ + /** Constant indicating that no gravity has been set **/ public static final int NO_GRAVITY = 0x0000; /** Raw bit indicating the gravity for an axis has been specified. */ @@ -33,6 +33,9 @@ public class Gravity public static final int AXIS_PULL_BEFORE = 0x0002; /** Raw bit controlling how the right/bottom edge is placed. */ public static final int AXIS_PULL_AFTER = 0x0004; + /** Raw bit controlling whether the right/bottom edge is clipped to its + * container, based on the gravity direction being applied. */ + public static final int AXIS_CLIP = 0x0008; /** Bits defining the horizontal axis. */ public static final int AXIS_X_SHIFT = 0; @@ -66,10 +69,18 @@ public class Gravity * and horizontal axis, not changing its size. */ public static final int CENTER = CENTER_VERTICAL|CENTER_HORIZONTAL; - /** Grow the horizontal and vertical size of the obejct if needed so it + /** Grow the horizontal and vertical size of the object if needed so it * completely fills its container. */ public static final int FILL = FILL_VERTICAL|FILL_HORIZONTAL; + /** Flag to clip the edges of the object to its container along the + * vertical axis. */ + public static final int CLIP_VERTICAL = AXIS_CLIP<<AXIS_Y_SHIFT; + + /** Flag to clip the edges of the object to its container along the + * horizontal axis. */ + public static final int CLIP_HORIZONTAL = AXIS_CLIP<<AXIS_X_SHIFT; + /** * Binary mask to get the horizontal gravity of a gravity. */ @@ -81,6 +92,20 @@ public class Gravity public static final int VERTICAL_GRAVITY_MASK = (AXIS_SPECIFIED | AXIS_PULL_BEFORE | AXIS_PULL_AFTER) << AXIS_Y_SHIFT; + /** Special constant to enable clipping to an overall display along the + * vertical dimension. This is not applied by default by + * {@link #apply(int, int, int, Rect, int, int, Rect)}; you must do so + * yourself by calling {@link #applyDisplay}. + */ + public static final int DISPLAY_CLIP_VERTICAL = 0x10000000; + + /** Special constant to enable clipping to an overall display along the + * horizontal dimension. This is not applied by default by + * {@link #apply(int, int, int, Rect, int, int, Rect)}; you must do so + * yourself by calling {@link #applyDisplay}. + */ + public static final int DISPLAY_CLIP_HORIZONTAL = 0x01000000; + /** * Apply a gravity constant to an object. * @@ -122,27 +147,143 @@ public class Gravity */ public static void apply(int gravity, int w, int h, Rect container, int xAdj, int yAdj, Rect outRect) { - if ((gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_X_SHIFT)) - == ((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_X_SHIFT)) { - outRect.left = container.left; - outRect.right = container.right; - } else { - outRect.left = applyMovement( - gravity>>AXIS_X_SHIFT, w, container.left, container.right, xAdj); - outRect.right = outRect.left + w; + switch (gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_X_SHIFT)) { + case 0: + outRect.left = container.left + + ((container.right - container.left - w)/2) + xAdj; + outRect.right = outRect.left + w; + if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT)) + == (AXIS_CLIP<<AXIS_X_SHIFT)) { + if (outRect.left < container.left) { + outRect.left = container.left; + } + if (outRect.right > container.right) { + outRect.right = container.right; + } + } + break; + case AXIS_PULL_BEFORE<<AXIS_X_SHIFT: + outRect.left = container.left + xAdj; + outRect.right = outRect.left + w; + if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT)) + == (AXIS_CLIP<<AXIS_X_SHIFT)) { + if (outRect.right > container.right) { + outRect.right = container.right; + } + } + break; + case AXIS_PULL_AFTER<<AXIS_X_SHIFT: + outRect.right = container.right - xAdj; + outRect.left = outRect.right - w; + if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT)) + == (AXIS_CLIP<<AXIS_X_SHIFT)) { + if (outRect.left < container.left) { + outRect.left = container.left; + } + } + break; + default: + outRect.left = container.left + xAdj; + outRect.right = container.right + xAdj; + break; + } + + switch (gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_Y_SHIFT)) { + case 0: + outRect.top = container.top + + ((container.bottom - container.top - h)/2) + yAdj; + outRect.bottom = outRect.top + h; + if ((gravity&(AXIS_CLIP<<AXIS_Y_SHIFT)) + == (AXIS_CLIP<<AXIS_Y_SHIFT)) { + if (outRect.top < container.top) { + outRect.top = container.top; + } + if (outRect.bottom > container.bottom) { + outRect.bottom = container.bottom; + } + } + break; + case AXIS_PULL_BEFORE<<AXIS_Y_SHIFT: + outRect.top = container.top + yAdj; + outRect.bottom = outRect.top + h; + if ((gravity&(AXIS_CLIP<<AXIS_Y_SHIFT)) + == (AXIS_CLIP<<AXIS_Y_SHIFT)) { + if (outRect.bottom > container.bottom) { + outRect.bottom = container.bottom; + } + } + break; + case AXIS_PULL_AFTER<<AXIS_Y_SHIFT: + outRect.bottom = container.bottom - yAdj; + outRect.top = outRect.bottom - h; + if ((gravity&(AXIS_CLIP<<AXIS_Y_SHIFT)) + == (AXIS_CLIP<<AXIS_Y_SHIFT)) { + if (outRect.top < container.top) { + outRect.top = container.top; + } + } + break; + default: + outRect.top = container.top + yAdj; + outRect.bottom = container.bottom + yAdj; + break; } + } - if ((gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_Y_SHIFT)) - == ((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_Y_SHIFT)) { - outRect.top = container.top; - outRect.bottom = container.bottom; + /** + * Apply addition gravity behavior based on the overall "display" that an + * object exists in. This can be used after + * {@link #apply(int, int, int, Rect, int, int, Rect)} to place the object + * within a visible display. By default this moves or clips the object + * to be visible in the display; the gravity flags + * {@link #DISPLAY_CLIP_HORIZONTAL} and {@link #DISPLAY_CLIP_VERTICAL} + * can be used to change this behavior. + * + * @param gravity Gravity constants to modify the placement within the + * display. + * @param display The rectangle of the display in which the object is + * being placed. + * @param inoutObj Supplies the current object position; returns with it + * modified if needed to fit in the display. + */ + public static void applyDisplay(int gravity, Rect display, Rect inoutObj) { + if ((gravity&DISPLAY_CLIP_VERTICAL) != 0) { + if (inoutObj.top < display.top) inoutObj.top = display.top; + if (inoutObj.bottom > display.bottom) inoutObj.bottom = display.bottom; } else { - outRect.top = applyMovement( - gravity>>AXIS_Y_SHIFT, h, container.top, container.bottom, yAdj); - outRect.bottom = outRect.top + h; + int off = 0; + if (inoutObj.top < display.top) off = display.top-inoutObj.top; + else if (inoutObj.bottom > display.bottom) off = display.bottom-inoutObj.bottom; + if (off != 0) { + if (inoutObj.height() > (display.bottom-display.top)) { + inoutObj.top = display.top; + inoutObj.bottom = display.bottom; + } else { + inoutObj.top += off; + inoutObj.bottom += off; + } + } + } + + if ((gravity&DISPLAY_CLIP_HORIZONTAL) != 0) { + if (inoutObj.left < display.left) inoutObj.left = display.left; + if (inoutObj.right > display.right) inoutObj.right = display.right; + } else { + int off = 0; + if (inoutObj.left < display.left) off = display.left-inoutObj.left; + else if (inoutObj.right > display.right) off = display.right-inoutObj.right; + if (off != 0) { + if (inoutObj.width() > (display.right-display.left)) { + inoutObj.left = display.left; + inoutObj.right = display.right; + } else { + inoutObj.left += off; + inoutObj.right += off; + } + } } } - + /** * <p>Indicate whether the supplied gravity has a vertical pull.</p> * @@ -162,18 +303,4 @@ public class Gravity public static boolean isHorizontal(int gravity) { return gravity > 0 && (gravity & HORIZONTAL_GRAVITY_MASK) != 0; } - - private static int applyMovement(int mode, int size, - int start, int end, int adj) { - if ((mode & AXIS_PULL_BEFORE) != 0) { - return start + adj; - } - - if ((mode & AXIS_PULL_AFTER) != 0) { - return end - size - adj; - } - - return start + ((end - start - size)/2) + adj; - } } - diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl index b4a3067..99d5c0c 100644 --- a/core/java/android/view/IWindow.aidl +++ b/core/java/android/view/IWindow.aidl @@ -17,6 +17,7 @@ package android.view; +import android.graphics.Rect; import android.view.KeyEvent; import android.view.MotionEvent; @@ -42,7 +43,8 @@ oneway interface IWindow { */ void executeCommand(String command, String parameters, in ParcelFileDescriptor descriptor); - void resized(int w, int h, boolean reportDraw); + void resized(int w, int h, in Rect coveredInsets, in Rect visibleInsets, + boolean reportDraw); void dispatchKey(in KeyEvent event); void dispatchPointer(in MotionEvent event, long eventTime); void dispatchTrackball(in MotionEvent event, long eventTime); diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index e6d52e2..d89c7b4 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -17,6 +17,9 @@ package android.view; +import com.android.internal.view.IInputContext; +import com.android.internal.view.IInputMethodClient; + import android.content.res.Configuration; import android.view.IApplicationToken; import android.view.IOnKeyguardExitResult; @@ -42,8 +45,10 @@ interface IWindowManager boolean stopViewServer(); // Transaction #2 boolean isViewServerRunning(); // Transaction #3 - IWindowSession openSession(IBinder token); - + IWindowSession openSession(in IInputMethodClient client, + in IInputContext inputContext); + boolean inputMethodClientHasFocus(IInputMethodClient client); + // These can only be called when injecting events to your own window, // or by holding the INJECT_EVENTS permission. boolean injectKeyEvent(in KeyEvent ev, boolean sync); @@ -74,6 +79,8 @@ interface IWindowManager void moveAppToken(int index, IBinder token); void moveAppTokensToTop(in List<IBinder> tokens); void moveAppTokensToBottom(in List<IBinder> tokens); + void addWindowToken(IBinder token, int type); + void removeWindowToken(IBinder token); // these require DISABLE_KEYGUARD permission void disableKeyguard(IBinder token, String tag); diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index c2c0b97..7276f17 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -31,7 +31,7 @@ import android.view.Surface; */ interface IWindowSession { int add(IWindow window, in WindowManager.LayoutParams attrs, - in int viewVisibility, out Rect outCoveredInsets); + in int viewVisibility, out Rect outContentInsets); void remove(IWindow window); /** @@ -46,10 +46,22 @@ interface IWindowSession { * @param requestedWidth The width the window wants to be. * @param requestedHeight The height the window wants to be. * @param viewVisibility Window root view's visibility. - * @param outFrame Object in which is placed the new position/size on - * screen. - * @param outCoveredInsets Object in which is placed the insets for the areas covered by - * system windows (e.g. status bar) + * @param insetsPending Set to true if the client will be later giving + * internal insets; as a result, the window will not impact other window + * layouts until the insets are given. + * @param outFrame Rect in which is placed the new position/size on + * screen. + * @param outContentInsets Rect in which is placed the offsets from + * <var>outFrame</var> in which the content of the window should be + * placed. This can be used to modify the window layout to ensure its + * contents are visible to the user, taking into account system windows + * like the status bar or a soft keyboard. + * @param outVisibleInsets Rect in which is placed the offsets from + * <var>outFrame</var> in which the window is actually completely visible + * to the user. This can be used to temporarily scroll the window's + * contents to make sure the user can see it. This is different than + * <var>outContentInsets</var> in that these insets change transiently, + * so complex relayout of the window should not happen based on them. * @param outSurface Object in which is placed the new display surface. * * @return int Result flags: {@link WindowManagerImpl#RELAYOUT_SHOW_FOCUS}, @@ -57,16 +69,41 @@ interface IWindowSession { */ int relayout(IWindow window, in WindowManager.LayoutParams attrs, int requestedWidth, int requestedHeight, int viewVisibility, - out Rect outFrame, out Rect outCoveredInsets, out Surface outSurface); + boolean insetsPending, out Rect outFrame, out Rect outContentInsets, + out Rect outVisibleInsets, out Surface outSurface); + /** + * Give the window manager a hint of the part of the window that is + * completely transparent, allowing it to work with the surface flinger + * to optimize compositing of this part of the window. + */ + void setTransparentRegion(IWindow window, in Region region); + + /** + * Tell the window manager about the content and visible insets of the + * given window, which can be used to adjust the <var>outContentInsets</var> + * and <var>outVisibleInsets</var> values returned by + * {@link #relayout relayout()} for windows behind this one. + * + * @param touchableInsets Controls which part of the window inside of its + * frame can receive pointer events, as defined by + * {@link android.view.ViewTreeObserver.InternalInsetsInfo}. + */ + void setInsets(IWindow window, int touchableInsets, in Rect contentInsets, + in Rect visibleInsets); + + /** + * Return the current display size in which the window is being laid out, + * accounting for screen decorations around it. + */ + void getDisplayFrame(IWindow window, out Rect outDisplayFrame); + void finishDrawing(IWindow window); void finishKey(IWindow window); MotionEvent getPendingPointerMove(IWindow window); MotionEvent getPendingTrackballMove(IWindow window); - void setTransparentRegion(IWindow window, in Region region); - void setInTouchMode(boolean showFocus); boolean getInTouchMode(); } diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index 24b0316..1575aad 100644 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -111,15 +111,27 @@ public class KeyEvent implements Parcelable { public static final int KEYCODE_MENU = 82; public static final int KEYCODE_NOTIFICATION = 83; public static final int KEYCODE_SEARCH = 84; + public static final int KEYCODE_PLAYPAUSE = 85; + public static final int KEYCODE_STOP = 86; + public static final int KEYCODE_NEXTSONG = 87; + public static final int KEYCODE_PREVIOUSSONG = 88; + public static final int KEYCODE_REWIND = 89; + public static final int KEYCODE_FORWARD = 90; + private static final int LAST_KEYCODE = KEYCODE_FORWARD; // NOTE: If you add a new keycode here you must also add it to: // isSystem() - // include/ui/KeycodeLabels.h + // frameworks/base/include/ui/KeycodeLabels.h // tools/puppet_master/PuppetMaster/nav_keys.py - // apps/common/res/values/attrs.xml + // frameworks/base/core/res/res/values/attrs.xml // commands/monkey/Monkey.java // emulator? - + + /** + * @deprecated There are now more than MAX_KEYCODE keycodes. + * Use {@link #getMaxKeyCode()} instead. + */ + @Deprecated public static final int MAX_KEYCODE = 84; /** @@ -207,6 +219,18 @@ public class KeyEvent implements Parcelable { public static final int FLAG_WOKE_HERE = 0x1; /** + * This mask is set if the key event was generated by a software keyboard. + */ + public static final int FLAG_SOFT_KEYBOARD = 0x2; + + /** + * Returns the maximum keycode. + */ + public static int getMaxKeyCode() { + return LAST_KEYCODE; + } + + /** * Get the character that is produced by putting accent on the character * c. * For example, getDeadChar('`', 'e') returns è. @@ -402,6 +426,24 @@ public class KeyEvent implements Parcelable { } /** + * Copy an existing key event, modifying its action. + * + * @param origEvent The existing event to be copied. + * @param action The new action code of the event. + */ + public KeyEvent(KeyEvent origEvent, int action) { + mDownTime = origEvent.mDownTime; + mEventTime = origEvent.mEventTime; + mAction = action; + mKeyCode = origEvent.mKeyCode; + mRepeatCount = origEvent.mRepeatCount; + mMetaState = origEvent.mMetaState; + mDeviceId = origEvent.mDeviceId; + mScancode = origEvent.mScancode; + mFlags = origEvent.mFlags; + } + + /** * Don't use in new code, instead explicitly check * {@link #getAction()}. * @@ -432,6 +474,12 @@ public class KeyEvent implements Parcelable { case KEYCODE_VOLUME_DOWN: case KEYCODE_POWER: case KEYCODE_HEADSETHOOK: + case KEYCODE_PLAYPAUSE: + case KEYCODE_STOP: + case KEYCODE_NEXTSONG: + case KEYCODE_PREVIOUSSONG: + case KEYCODE_REWIND: + case KEYCODE_FORWARD: case KEYCODE_CAMERA: case KEYCODE_FOCUS: case KEYCODE_SEARCH: diff --git a/core/java/android/view/Menu.java b/core/java/android/view/Menu.java index f2ec076..ca2140b 100644 --- a/core/java/android/view/Menu.java +++ b/core/java/android/view/Menu.java @@ -32,8 +32,7 @@ import android.content.Intent; * <p> * Different menu types support different features: * <ol> - * <li><b>Context menus</b>: Do not support item shortcuts, item icons, and sub - * menus. + * <li><b>Context menus</b>: Do not support item shortcuts and item icons. * <li><b>Options menus</b>: The <b>icon menus</b> do not support item check * marks and only show the item's * {@link MenuItem#setTitleCondensed(CharSequence) condensed title}. The @@ -380,6 +379,24 @@ public interface Menu { public int size(); /** + * Gets the menu item at the given index. + * + * @param index The index of the menu item to return. + * @return The menu item. + * @exception IndexOutOfBoundsException + * when {@code index < 0 || >= size()} + * @hide pending API council + */ + public MenuItem getItem(int index); + + /** + * Closes the menu, if open. + * + * @hide pending API council + */ + public void close(); + + /** * Execute the menu item action associated with the given shortcut * character. * diff --git a/core/java/android/view/MenuItem.java b/core/java/android/view/MenuItem.java index 92cf4af..fcebec5 100644 --- a/core/java/android/view/MenuItem.java +++ b/core/java/android/view/MenuItem.java @@ -1,3 +1,19 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package android.view; import android.app.Activity; diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index ab05e16..882a079 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -27,10 +27,36 @@ import android.util.Config; * it is being used for. */ public final class MotionEvent implements Parcelable { + /** + * Constant for {@link #getAction}: A pressed gesture has started, the + * motion contains the initial starting location. + */ public static final int ACTION_DOWN = 0; + /** + * Constant for {@link #getAction}: A pressed gesture has finished, the + * motion contains the final release location as well as any intermediate + * points since the last down or move event. + */ public static final int ACTION_UP = 1; + /** + * Constant for {@link #getAction}: A change has happened during a + * press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}). + * The motion contains the most recent point, as well as any intermediate + * points since the last down or move event. + */ public static final int ACTION_MOVE = 2; + /** + * Constant for {@link #getAction}: The current gesture has been aborted. + * You will not receive any more points in it. You should treat this as + * an up event, but not perform any action that you normally would. + */ public static final int ACTION_CANCEL = 3; + /** + * Constant for {@link #getAction}: A movement has happened outside of the + * normal bounds of the UI element. This does not provide a full gesture, + * but only the initial location of the movement/touch. + */ + public static final int ACTION_OUTSIDE = 4; private static final boolean TRACK_RECYCLED_LOCATION = false; diff --git a/core/java/android/view/OrientationListener.java b/core/java/android/view/OrientationListener.java index 0add025..974c2e8 100644 --- a/core/java/android/view/OrientationListener.java +++ b/core/java/android/view/OrientationListener.java @@ -34,6 +34,7 @@ public abstract class OrientationListener implements SensorListener { private SensorManager mSensorManager; private int mOrientation = ORIENTATION_UNKNOWN; private boolean mEnabled = false; + private int mRate; /** * Returned from onOrientationChanged when the device orientation cannot be determined @@ -50,6 +51,21 @@ public abstract class OrientationListener implements SensorListener { */ public OrientationListener(Context context) { mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); + mRate = SensorManager.SENSOR_DELAY_NORMAL; + } + + /** + * Creates a new OrientationListener. + * + * @param context for the OrientationListener. + * @param rate at which sensor events are processed (see also + * {@link android.hardware.SensorManager SensorManager}). Use the default + * value of {@link android.hardware.SensorManager#SENSOR_DELAY_NORMAL + * SENSOR_DELAY_NORMAL} for simple screen orientation change detection. + */ + public OrientationListener(Context context, int rate) { + mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); + mRate = rate; } /** @@ -59,7 +75,7 @@ public abstract class OrientationListener implements SensorListener { public void enable() { if (mEnabled == false) { if (localLOGV) Log.d(TAG, "OrientationListener enabled"); - mSensorManager.registerListener(this, SensorManager.SENSOR_ACCELEROMETER); + mSensorManager.registerListener(this, SensorManager.SENSOR_ACCELEROMETER, mRate); mEnabled = true; } } @@ -106,7 +122,6 @@ public abstract class OrientationListener implements SensorListener { public void onAccuracyChanged(int sensor, int accuracy) { // TODO Auto-generated method stub - } /** diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 57689f2..0d9e221 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -82,6 +82,8 @@ public class SurfaceView extends View { final ArrayList<SurfaceHolder.Callback> mCallbacks = new ArrayList<SurfaceHolder.Callback>(); + final int[] mLocation = new int[2]; + final ReentrantLock mSurfaceLock = new ReentrantLock(); final Surface mSurface = new Surface(); boolean mDrawingStopped = true; @@ -90,8 +92,9 @@ public class SurfaceView extends View { = new WindowManager.LayoutParams(); IWindowSession mSession; MyWindow mWindow; + final Rect mVisibleInsets = new Rect(); final Rect mWinFrame = new Rect(); - final Rect mCoveredInsets = new Rect(); + final Rect mContentInsets = new Rect(); static final int KEEP_SCREEN_ON_MSG = 1; static final int GET_NEW_SURFACE_MSG = 2; @@ -309,7 +312,7 @@ public class SurfaceView extends View { mLayout.type = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA; mLayout.gravity = Gravity.LEFT|Gravity.TOP; mSession.add(mWindow, mLayout, - mVisible ? VISIBLE : GONE, mCoveredInsets); + mVisible ? VISIBLE : GONE, mContentInsets); } if (visibleChanged && (!visible || mNewSurfaceNeeded)) { @@ -321,8 +324,9 @@ public class SurfaceView extends View { mSurfaceLock.lock(); mDrawingStopped = !visible; final int relayoutResult = mSession.relayout( - mWindow, mLayout, mWidth, mHeight, - visible ? VISIBLE : GONE, mWinFrame, mCoveredInsets, mSurface); + mWindow, mLayout, mWidth, mHeight, + visible ? VISIBLE : GONE, false, mWinFrame, mContentInsets, + mVisibleInsets, mSurface); if (localLOGV) Log.i(TAG, "New surface: " + mSurface + ", vis=" + visible + ", frame=" + mWinFrame); mSurfaceFrame.left = 0; @@ -390,7 +394,8 @@ public class SurfaceView extends View { } private class MyWindow extends IWindow.Stub { - public void resized(int w, int h, boolean reportDraw) { + public void resized(int w, int h, Rect coveredInsets, + Rect visibleInsets, boolean reportDraw) { if (localLOGV) Log.v( "SurfaceView", SurfaceView.this + " got resized: w=" + w + " h=" + h + ", cur w=" + mCurWidth + " h=" + mCurHeight); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 30402f8..f948b33 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -38,13 +38,18 @@ import android.os.IBinder; import android.os.Message; import android.os.Parcel; import android.os.Parcelable; +import android.os.RemoteException; import android.os.SystemClock; +import android.os.SystemProperties; import android.util.AttributeSet; import android.util.EventLog; import android.util.Log; import android.util.SparseArray; import android.view.ContextMenu.ContextMenuInfo; import android.view.animation.Animation; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.EditorInfo; import android.widget.ScrollBarDrawable; import com.android.internal.R; @@ -58,9 +63,9 @@ import java.util.Arrays; * The <code>View</code> class represents the basic UI building block. A view * occupies a rectangular area on the screen and is responsible for drawing and * event handling. <code>View</code> is the base class for <em>widgets</em>, - * used to create interactive graphical user interfaces. + * used to create interactive graphical user interfaces. * </p> - * + * * <a name="Using"></a> * <h3>Using Views</h3> * <p> @@ -98,7 +103,7 @@ import java.util.Arrays; * views yourself unless you are actually implementing a * {@link android.view.ViewGroup}. * </em></p> - * + * * <a name="Lifecycle"></a> * <h3>Implementing a Custom View</h3> * @@ -111,7 +116,7 @@ import java.util.Arrays; * <thead> * <tr><th>Category</th> <th>Methods</th> <th>Description</th></tr> * </thead> - * + * * <tbody> * <tr> * <td rowspan="2">Creation</td> @@ -127,7 +132,7 @@ import java.util.Arrays; * <td>Called after a view and all of its children has been inflated * from XML.</td> * </tr> - * + * * <tr> * <td rowspan="3">Layout</td> * <td><code>{@link #onMeasure}</code></td> @@ -146,7 +151,7 @@ import java.util.Arrays; * <td>Called when the size of this view has changed. * </td> * </tr> - * + * * <tr> * <td>Drawing</td> * <td><code>{@link #onDraw}</code></td> @@ -164,31 +169,31 @@ import java.util.Arrays; * <td><code>{@link #onKeyUp}</code></td> * <td>Called when a key up event occurs. * </td> - * </tr> + * </tr> * <tr> * <td><code>{@link #onTrackballEvent}</code></td> * <td>Called when a trackball motion event occurs. * </td> - * </tr> + * </tr> * <tr> * <td><code>{@link #onTouchEvent}</code></td> * <td>Called when a touch screen motion event occurs. * </td> - * </tr> - * + * </tr> + * * <tr> * <td rowspan="2">Focus</td> * <td><code>{@link #onFocusChanged}</code></td> * <td>Called when the view gains or loses focus. * </td> * </tr> - * + * * <tr> * <td><code>{@link #onWindowFocusChanged}</code></td> * <td>Called when the window containing the view gains or loses focus. * </td> * </tr> - * + * * <tr> * <td rowspan="3">Attaching</td> * <td><code>{@link #onAttachedToWindow()}</code></td> @@ -200,26 +205,26 @@ import java.util.Arrays; * <td><code>{@link #onDetachedFromWindow}</code></td> * <td>Called when the view is detached from its window. * </td> - * </tr> + * </tr> * * <tr> * <td><code>{@link #onWindowVisibilityChanged}</code></td> * <td>Called when the visibility of the window containing the view * has changed. * </td> - * </tr> + * </tr> * </tbody> - * + * * </table> * </p> - * + * * <a name="IDs"></a> * <h3>IDs</h3> * Views may have an integer id associated with them. These ids are typically * assigned in the layout XML files, and are used to find specific views within * the view tree. A common pattern is to: * <ul> - * <li>Define a Button in the layout file and assign it a unique ID. + * <li>Define a Button in the layout file and assign it a unique ID. * <pre> * <Button id="@+id/my_button" * android:layout_width="wrap_content" @@ -232,11 +237,11 @@ import java.util.Arrays; * </pre></li> * </ul> * <p> - * View IDs need not be unique throughout the tree, but it is good practice to + * View IDs need not be unique throughout the tree, but it is good practice to * ensure that they are at least unique within the part of the tree you are * searching. * </p> - * + * * <a name="Position"></a> * <h3>Position</h3> * <p> @@ -264,7 +269,7 @@ import java.util.Arrays; * is similar to the following computation: <code>getLeft() + getWidth()</code> * (see <a href="#SizePaddingMargins">Size</a> for more information about the width.) * </p> - * + * * <a name="SizePaddingMargins"></a> * <h3>Size, padding and margins</h3> * <p> @@ -286,7 +291,7 @@ import java.util.Arrays; * dimensions define the actual size of the view on screen, at drawing time and * after layout. These values may, but do not have to, be different from the * measured width and height. The width and height can be obtained by calling - * {@link #getWidth()} and {@link #getHeight()}. + * {@link #getWidth()} and {@link #getHeight()}. * </p> * * <p> @@ -297,7 +302,7 @@ import java.util.Arrays; * 2 pixels to the right of the left edge. Padding can be set using the * {@link #setPadding(int, int, int, int)} method and queried by calling * {@link #getPaddingLeft()}, {@link #getPaddingTop()}, - * {@link #getPaddingRight()} and {@link #getPaddingBottom()}. + * {@link #getPaddingRight()} and {@link #getPaddingBottom()}. * </p> * * <p> @@ -306,7 +311,7 @@ import java.util.Arrays; * {@link android.view.ViewGroup} and * {@link android.view.ViewGroup.MarginLayoutParams} for further information. * </p> - * + * * <a name="Layout"></a> * <h3>Layout</h3> * <p> @@ -319,7 +324,7 @@ import java.util.Arrays; * this pass each parent is responsible for positioning all of its children * using the sizes computed in the measure pass. * </p> - * + * * <p> * When a view's measure() method returns, its {@link #getMeasuredWidth()} and * {@link #getMeasuredHeight()} values must be set, along with those for all of @@ -332,7 +337,7 @@ import java.util.Arrays; * measure() on them again with actual numbers if the sum of all the children's * unconstrained sizes is too big or too small. * </p> - * + * * <p> * The measure pass uses two classes to communicate dimensions. The * {@link MeasureSpec} class is used by views to tell their parents how they @@ -350,7 +355,7 @@ import java.util.Arrays; * For example, AbsoluteLayout has its own subclass of LayoutParams which adds * an X and Y value. * </p> - * + * * <p> * MeasureSpecs are used to push requirements down the tree from parent to * child. A MeasureSpec can be in one of three modes: @@ -367,13 +372,13 @@ import java.util.Arrays; * within this size. * </ul> * </p> - * + * * <p> * To intiate a layout, call {@link #requestLayout}. This method is typically * called by a view on itself when it believes that is can no longer fit within * its current bounds. * </p> - * + * * <a name="Drawing"></a> * <h3>Drawing</h3> * <p> @@ -381,17 +386,18 @@ import java.util.Arrays; * intersects the the invalid region. Because the tree is traversed in-order, * this means that parents will draw before (i.e., behind) their children, with * siblings drawn in the order they appear in the tree. + * If you set a background drawable for a View, then the View will draw it for you + * before calling back to its <code>onDraw()</code> method. * </p> - * + * * <p> - * The framework will not draw views that are not in the invalid region, and also - * will take care of drawing the views background for you. + * Note that the framework will not draw views that are not in the invalid region. * </p> - * + * * <p> * To force a view to draw, call {@link #invalidate()}. * </p> - * + * * <a name="EventHandlingThreading"></a> * <h3>Event Handling and Threading</h3> * <p> @@ -408,13 +414,13 @@ import java.util.Arrays; * as appropriate.</li> * </ol> * </p> - * + * * <p><em>Note: The entire view tree is single threaded. You must always be on * the UI thread when calling any method on any view.</em> * If you are doing work on other threads and want to update the state of a view * from that thread, you should use a {@link Handler}. * </p> - * + * * <a name="FocusHandling"></a> * <h3>Focus Handling</h3> * <p> @@ -479,7 +485,7 @@ import java.util.Arrays; * offset as well as mechanisms for drawing scrollbars. See * {@link #scrollBy(int, int)}, {@link #scrollTo(int, int)} for more details. * </p> - * + * * <a name="Tags"></a> * <h3>Tags</h3> * <p> @@ -488,7 +494,7 @@ import java.util.Arrays; * often used as a convenience to store data related to views in the views * themselves rather than by putting them in a separate structure. * </p> - * + * * <a name="Animation"></a> * <h3>Animation</h3> * <p> @@ -543,7 +549,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * setFlags. */ private static final int FOCUSABLE = 0x00000001; - + /** * Mask for use with setFlags indicating bits used for focus. */ @@ -599,7 +605,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * {@hide} */ static final int ENABLED_MASK = 0x00000020; - + /** * This view won't draw. {@link #onDraw} won't be called and further * optimizations @@ -615,7 +621,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * {@hide} */ static final int DRAW_MASK = 0x00000080; - + /** * <p>This view doesn't show scrollbars.</p> * {@hide} @@ -752,22 +758,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * The scrollbar style to display the scrollbars inside the content area, - * without increasing the padding. The scrollbars will be overlaid with + * without increasing the padding. The scrollbars will be overlaid with * translucency on the view's content. */ public static final int SCROLLBARS_INSIDE_OVERLAY = 0; - + /** * The scrollbar style to display the scrollbars inside the padded area, - * increasing the padding of the view. The scrollbars will not overlap the + * increasing the padding of the view. The scrollbars will not overlap the * content area of the view. */ public static final int SCROLLBARS_INSIDE_INSET = 0x01000000; /** * The scrollbar style to display the scrollbars at the edge of the view, - * without increasing the padding. The scrollbars will be overlaid with - * translucency. + * without increasing the padding. The scrollbars will be overlaid with + * translucency. */ public static final int SCROLLBARS_OUTSIDE_OVERLAY = 0x02000000; @@ -789,7 +795,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * {@hide} */ static final int SCROLLBARS_OUTSIDE_MASK = 0x02000000; - + /** * Mask for scrollbar style. * {@hide} @@ -841,7 +847,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Use with {@link #focusSearch}. Move focus down. */ public static final int FOCUS_DOWN = 0x00000082; - + /** * Base View state sets */ @@ -850,7 +856,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Indicates the view has no states set. States are used with * {@link android.graphics.drawable.Drawable} to change the drawing of the * view depending on its state. - * + * * @see android.graphics.drawable.Drawable * @see #getDrawableState() */ @@ -1003,7 +1009,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Indicates the view is pressed and its window has the focus. - * + * * @see #PRESSED_STATE_SET * @see #WINDOW_FOCUSED_STATE_SET */ @@ -1021,7 +1027,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Indicates the view is pressed, selected and its window has the focus. - * + * * @see #PRESSED_STATE_SET * @see #SELECTED_STATE_SET * @see #WINDOW_FOCUSED_STATE_SET @@ -1040,7 +1046,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Indicates the view is pressed, focused and its window has the focus. - * + * * @see #PRESSED_STATE_SET * @see #FOCUSED_STATE_SET * @see #WINDOW_FOCUSED_STATE_SET @@ -1050,7 +1056,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Indicates the view is pressed, focused and selected. - * + * * @see #PRESSED_STATE_SET * @see #SELECTED_STATE_SET * @see #FOCUSED_STATE_SET @@ -1060,7 +1066,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Indicates the view is pressed, focused, selected and its window has the focus. - * + * * @see #PRESSED_STATE_SET * @see #FOCUSED_STATE_SET * @see #SELECTED_STATE_SET @@ -1071,7 +1077,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Indicates the view is pressed and enabled. - * + * * @see #PRESSED_STATE_SET * @see #ENABLED_STATE_SET */ @@ -1080,7 +1086,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Indicates the view is pressed, enabled and its window has the focus. - * + * * @see #PRESSED_STATE_SET * @see #ENABLED_STATE_SET * @see #WINDOW_FOCUSED_STATE_SET @@ -1090,7 +1096,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Indicates the view is pressed, enabled and selected. - * + * * @see #PRESSED_STATE_SET * @see #ENABLED_STATE_SET * @see #SELECTED_STATE_SET @@ -1101,7 +1107,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Indicates the view is pressed, enabled, selected and its window has the * focus. - * + * * @see #PRESSED_STATE_SET * @see #ENABLED_STATE_SET * @see #SELECTED_STATE_SET @@ -1112,18 +1118,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Indicates the view is pressed, enabled and focused. - * + * * @see #PRESSED_STATE_SET * @see #ENABLED_STATE_SET * @see #FOCUSED_STATE_SET */ protected static final int[] PRESSED_ENABLED_FOCUSED_STATE_SET = stateSetUnion(PRESSED_ENABLED_STATE_SET, FOCUSED_STATE_SET); - + /** * Indicates the view is pressed, enabled, focused and its window has the * focus. - * + * * @see #PRESSED_STATE_SET * @see #ENABLED_STATE_SET * @see #FOCUSED_STATE_SET @@ -1134,7 +1140,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Indicates the view is pressed, enabled, focused and selected. - * + * * @see #PRESSED_STATE_SET * @see #ENABLED_STATE_SET * @see #SELECTED_STATE_SET @@ -1146,7 +1152,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Indicates the view is pressed, enabled, focused, selected and its window * has the focus. - * + * * @see #PRESSED_STATE_SET * @see #ENABLED_STATE_SET * @see #SELECTED_STATE_SET @@ -1242,6 +1248,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * @hide */ protected static final int[] PRESSED_SINGLE_STATE_SET = {R.attr.state_single, R.attr.state_pressed}; + + /** + * Temporary Rect currently for use in setBackground(). This will probably + * be extended in the future to hold our own class with more than just + * a Rect. :) + */ + static final ThreadLocal<Rect> sThreadLocal = new ThreadLocal<Rect>(); /** * The animation currently associated with this view. @@ -1264,20 +1277,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { protected int mMeasuredHeight; /** - * Used to store a pair of coordinates, for instance returned values - * returned by {@link #getLocationInWindow(int[])}. - * - * This field should be made private, so it is hidden from the SDK. - * {@hide} - */ - protected final int[] mLocation = new int[2]; - - /** * The view's identifier. * {@hide} * * @see #setId(int) - * @see #getId() + * @see #getId() */ @ViewDebug.ExportedProperty(resolveId = true) int mID = NO_ID; @@ -1287,7 +1291,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * {@hide} * * @see #setTag(Object) - * @see #getTag() + * @see #getTag() */ protected Object mTag; @@ -1320,7 +1324,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { private static final int LAYOUT_REQUIRED = 0x00002000; private static final int PRESSED = 0x00004000; - + /** {@hide} */ static final int DRAWING_CACHE_VALID = 0x00008000; /** @@ -1329,7 +1333,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * {@hide} */ static final int ANIMATION_STARTED = 0x00010000; - + private static final int SAVE_STATE_CALLED = 0x00020000; /** @@ -1339,8 +1343,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback { */ static final int ALPHA_SET = 0x00040000; + /** + * Set by {@link #setScrollContainer(boolean)}. + */ + static final int SCROLL_CONTAINER = 0x00080000; + + /** + * Set by {@link #setScrollContainer(boolean)}. + */ + static final int SCROLL_CONTAINER_ADDED = 0x00100000; + // Note: flag 0x00000040 is available - + /** * The parent this view is attached to. * {@hide} @@ -1357,10 +1371,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * {@hide} */ + @ViewDebug.ExportedProperty int mPrivateFlags; - + /** - * Count of how many windows this view has been attached to. + * Count of how many windows this view has been attached to. */ int mWindowAttachCount; @@ -1376,6 +1391,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * The view flags hold various views states. * {@hide} */ + @ViewDebug.ExportedProperty int mViewFlags; /** @@ -1450,7 +1466,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { */ @ViewDebug.ExportedProperty protected int mPaddingBottom; - + /** * Cache the paddingRight set by the user to append to the scrollbar's size. */ @@ -1462,7 +1478,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { */ @ViewDebug.ExportedProperty int mUserPaddingBottom; - + private int mOldWidthMeasureSpec = Integer.MIN_VALUE; private int mOldHeightMeasureSpec = Integer.MIN_VALUE; @@ -1479,28 +1495,28 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * {@hide} */ protected OnFocusChangeListener mOnFocusChangeListener; - + /** * Listener used to dispatch click events. * This field should be made private, so it is hidden from the SDK. * {@hide} */ protected OnClickListener mOnClickListener; - + /** * Listener used to dispatch long click events. * This field should be made private, so it is hidden from the SDK. * {@hide} */ protected OnLongClickListener mOnLongClickListener; - + /** * Listener used to build the context menu. * This field should be made private, so it is hidden from the SDK. * {@hide} */ protected OnCreateContextMenuListener mOnCreateContextMenuListener; - + private OnKeyListener mOnKeyListener; private OnTouchListener mOnTouchListener; @@ -1519,11 +1535,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback { private Bitmap mDrawingCache; /** - * Used for local (within a stackframe) calls that need a rect temporarily - */ - private final Rect mTempRect = new Rect(); - - /** * When this view has focus and the next focus is {@link #FOCUS_LEFT}, * the user may specify which view to go to next. */ @@ -1542,13 +1553,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback { private int mNextFocusUpId = View.NO_ID; /** - * When this view has focus and the next focus is {@link #FOCUS_DOWN}, + * When this view has focus and the next focus is {@link #FOCUS_DOWN}, * the user may specify which view to go to next. */ private int mNextFocusDownId = View.NO_ID; private CheckForLongPress mPendingCheckForLongPress; - + /** * Whether the long press's action has been invoked. The tap's action is invoked on the * up event while a long press is invoked as soon as the long press duration is reached, so @@ -1561,12 +1572,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * The minimum height of the view. We'll try our best to have the height * of this view to at least this amount. */ + @ViewDebug.ExportedProperty private int mMinHeight; - + /** * The minimum width of the view. We'll try our best to have the width * of this view to at least this amount. */ + @ViewDebug.ExportedProperty private int mMinWidth; /** @@ -1574,7 +1587,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * but should be handled by another view. */ private TouchDelegate mTouchDelegate = null; - + /** * Solid color to use as a background when creating the drawing cache. Enables * the cache to use 16 bit bitmaps instead of 32 bit. @@ -1591,7 +1604,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Simple constructor to use when creating a view from code. - * + * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. */ @@ -1607,11 +1620,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * that were specified in the XML file. This version uses a default style of * 0, so the only attribute values applied are those in the Context's Theme * and the given AttributeSet. - * + * * <p> * The method onFinishInflate() will be called after all children have been * added. - * + * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. @@ -1629,7 +1642,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * <code>R.attr.buttonStyle</code> for <var>defStyle</var>; this allows * the theme's button style to modify all of the base view attributes (in * particular its background) as well as the Button class's attributes. - * + * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. @@ -1663,7 +1676,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { int scrollbarStyle = SCROLLBARS_INSIDE_OVERLAY; viewFlagValues |= SOUND_EFFECTS_ENABLED; - viewFlagMasks |= SOUND_EFFECTS_ENABLED; + viewFlagMasks |= SOUND_EFFECTS_ENABLED; final int N = a.getIndexCount(); for (int i = 0; i < N; i++) { @@ -1833,7 +1846,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if (viewFlagMasks != 0) { setFlags(viewFlagValues, viewFlagMasks); } - + // Needs to be called after mViewFlags is set if (scrollbarStyle != SCROLLBARS_INSIDE_OVERLAY) { recomputePadding(); @@ -1866,7 +1879,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * being inflated from XML. This method is automatically called when the XML * is inflated. * </p> - * + * * @param a the styled attributes set to initialize the fading edges from */ protected void initializeFadingEdge(TypedArray a) { @@ -1875,7 +1888,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { mScrollCache.fadingEdgeLength = a.getDimensionPixelSize( R.styleable.View_fadingEdgeLength, ViewConfiguration.getFadingEdgeLength()); } - + /** * Returns the size of the vertical faded edges used to indicate that more * content in this view is visible. @@ -1907,7 +1920,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { initScrollCache(); mScrollCache.fadingEdgeLength = length; } - + /** * Returns the size of the horizontal faded edges used to indicate that more * content in this view is visible. @@ -1978,7 +1991,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * being inflated from XML. This method is automatically called when the XML * is inflated. * </p> - * + * * @param a the styled attributes set to initialize the scrollbars from */ protected void initializeScrollbars(TypedArray a) { @@ -1999,8 +2012,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if (thumb != null) { mScrollCache.scrollBar.setHorizontalThumbDrawable(thumb); } - - boolean alwaysDraw = a.getBoolean(R.styleable.View_scrollbarAlwaysDrawHorizontalTrack, + + boolean alwaysDraw = a.getBoolean(R.styleable.View_scrollbarAlwaysDrawHorizontalTrack, false); if (alwaysDraw) { mScrollCache.scrollBar.setAlwaysDrawHorizontalTrack(true); @@ -2013,13 +2026,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if (thumb != null) { mScrollCache.scrollBar.setVerticalThumbDrawable(thumb); } - - alwaysDraw = a.getBoolean(R.styleable.View_scrollbarAlwaysDrawVerticalTrack, + + alwaysDraw = a.getBoolean(R.styleable.View_scrollbarAlwaysDrawVerticalTrack, false); if (alwaysDraw) { mScrollCache.scrollBar.setAlwaysDrawVerticalTrack(true); } - + // Re-apply user/background padding so that scrollbar(s) get added recomputePadding(); } @@ -2037,16 +2050,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Register a callback to be invoked when focus of this view changed. - * + * * @param l The callback that will run. */ public void setOnFocusChangeListener(OnFocusChangeListener l) { mOnFocusChangeListener = l; } - + /** * Returns the focus-change callback registered for this view. - * + * * @return The callback, or null if one is not registered. */ public OnFocusChangeListener getOnFocusChangeListener() { @@ -2056,7 +2069,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Register a callback to be invoked when this view is clicked. If this view is not * clickable, it becomes clickable. - * + * * @param l The callback that will run * * @see #setClickable(boolean) @@ -2067,11 +2080,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } mOnClickListener = l; } - + /** * Register a callback to be invoked when this view is clicked and held. If this view is not * long clickable, it becomes long clickable. - * + * * @param l The callback that will run * * @see #setLongClickable(boolean) @@ -2084,9 +2097,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } /** - * Register a callback to be invoked when the context menu for this view is + * Register a callback to be invoked when the context menu for this view is * being built. If this view is not long clickable, it becomes long clickable. - * + * * @param l The callback that will run * */ @@ -2096,10 +2109,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } mOnCreateContextMenuListener = l; } - + /** * Call this view's OnClickListener, if it is defined. - * + * * @return True there was an assigned OnClickListener that was called, false * otherwise is returned. */ @@ -2112,11 +2125,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { return false; } - + /** * Call this view's OnLongClickListener, if it is defined. Invokes the context menu * if the OnLongClickListener did not consume the event. - * + * * @return True there was an assigned OnLongClickListener that was called, false * otherwise is returned. */ @@ -2130,10 +2143,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } return handled; } - + /** * Bring up the context menu for this view. - * + * * @return Whether a context menu was displayed. */ public boolean showContextMenu() { @@ -2155,7 +2168,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { public void setOnTouchListener(OnTouchListener l) { mOnTouchListener = l; } - + /** * Give this view focus. This will cause {@link #onFocusChanged} to be called. * @@ -2191,7 +2204,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Request that a rectangle of this view be visible on the screen, * scrolling if necessary just enough. * - * A View should call this if it maintains some notion of which part + * <p>A View should call this if it maintains some notion of which part * of its content is interesting. For example, a text editing view * should call this when its cursor moves. * @@ -2206,11 +2219,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Request that a rectangle of this view be visible on the screen, * scrolling if necessary just enough. * - * A View should call this if it maintains some notion of which part + * <p>A View should call this if it maintains some notion of which part * of its content is interesting. For example, a text editing view * should call this when its cursor moves. * - * When <code>immediate</code> is set to true, scrolling will not be + * <p>When <code>immediate</code> is set to true, scrolling will not be * animated. * * @param rectangle The rectangle. @@ -2221,9 +2234,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { View child = this; ViewParent parent = mParent; boolean scrolled = false; - while (parent instanceof ViewGroup) { - ViewGroup vgParent = (ViewGroup) parent; - scrolled |= vgParent.requestChildRectangleOnScreen(child, + while (parent != null) { + scrolled |= parent.requestChildRectangleOnScreen(child, rectangle, immediate); // offset rect so next call has the rectangle in the @@ -2231,12 +2243,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback { rectangle.offset(child.getLeft(), child.getTop()); rectangle.offset(-child.getScrollX(), -child.getScrollY()); + if (!(parent instanceof View)) { + break; + } + child = (View) parent; parent = child.getParent(); } return scrolled; } - + /** * Called when this view wants to give up focus. This will cause * {@link #onFocusChanged} to be called. @@ -2257,7 +2273,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { refreshDrawableState(); } } - + /** * Called to clear the focus of a view that is about to be removed. * Doesn't call clearChildFocus, which prevents this view from taking @@ -2271,7 +2287,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { refreshDrawableState(); } } - + /** * Called internally by the view system when a new view is getting focus. * This is what clears the old focus. @@ -2292,7 +2308,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Returns true if this view has focus iteself, or is the ancestor of the * view that has focus. - * + * * @return True if this view has or contains focus, false otherwise. */ @ViewDebug.ExportedProperty @@ -2315,17 +2331,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback { public boolean hasFocusable() { return (mViewFlags & VISIBILITY_MASK) == VISIBLE && isFocusable(); } - + /** * Called by the view system when the focus state of this view changes. * When the focus change event is caused by directional navigation, direction * and previouslyFocusedRect provide insight into where the focus is coming from. + * When overriding, be sure to call up through to the super class so that + * the standard focus handling will occur. * * @param gainFocus True if the View has focus; false otherwise. - * @param direction The direction focus has moved when requestFocus() - * is called to give this view focus. Values are - * View.FOCUS_UP, View.FOCUS_DOWN, View.FOCUS_LEFT or - * View.FOCUS_RIGHT. It may not always apply, in which + * @param direction The direction focus has moved when requestFocus() + * is called to give this view focus. Values are + * View.FOCUS_UP, View.FOCUS_DOWN, View.FOCUS_LEFT or + * View.FOCUS_RIGHT. It may not always apply, in which * case use the default. * @param previouslyFocusedRect The rectangle, in this view's coordinate * system, of the previously focused view. If applicable, this will be @@ -2333,20 +2351,29 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * from (in addition to direction). Will be <code>null</code> otherwise. */ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + InputMethodManager imm = InputMethodManager.peekInstance(); if (!gainFocus) { if (isPressed()) { setPressed(false); } + if (imm != null && mAttachInfo != null + && mAttachInfo.mHasWindowFocus) { + imm.focusOut(this); + } + } else if (imm != null && mAttachInfo != null + && mAttachInfo.mHasWindowFocus) { + imm.focusIn(this); } + invalidate(); if (mOnFocusChangeListener != null) { mOnFocusChangeListener.onFocusChange(this, gainFocus); } } - + /** * Returns true if this view has focus - * + * * @return True if this view has focus, false otherwise. */ @ViewDebug.ExportedProperty @@ -2357,7 +2384,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Find the view in the hierarchy rooted at this view that currently has * focus. - * + * * @return The view that currently has focus, or null if no focused view can * be found. */ @@ -2366,6 +2393,28 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } /** + * Change whether this view is one of the set of scrollable containers in + * its window. This will be used to determine whether the window can + * resize or must pan when a soft input area is open -- scrollable + * containers allow the window to use resize mode since the container + * will appropriately shrink. + */ + public void setScrollContainer(boolean isScrollContainer) { + if (isScrollContainer) { + if (mAttachInfo != null && (mPrivateFlags&SCROLL_CONTAINER_ADDED) == 0) { + mAttachInfo.mScrollContainers.add(this); + mPrivateFlags |= SCROLL_CONTAINER_ADDED; + } + mPrivateFlags |= SCROLL_CONTAINER; + } else { + if ((mPrivateFlags&SCROLL_CONTAINER_ADDED) != 0) { + mAttachInfo.mScrollContainers.remove(this); + } + mPrivateFlags &= ~(SCROLL_CONTAINER|SCROLL_CONTAINER_ADDED); + } + } + + /** * Returns the quality of the drawing cache. * * @return One of {@link #DRAWING_CACHE_QUALITY_AUTO}, @@ -2388,7 +2437,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * @param quality One of {@link #DRAWING_CACHE_QUALITY_AUTO}, * {@link #DRAWING_CACHE_QUALITY_LOW}, or {@link #DRAWING_CACHE_QUALITY_HIGH} * - * @see #getDrawingCacheQuality() + * @see #getDrawingCacheQuality() * @see #setDrawingCacheEnabled(boolean) * @see #isDrawingCacheEnabled() * @@ -2418,7 +2467,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * * @param keepScreenOn Supply true to set {@link #KEEP_SCREEN_ON}. * - * @see #getKeepScreenOn() + * @see #getKeepScreenOn() * * @attr ref android.R.styleable#View_keepScreenOn */ @@ -2508,10 +2557,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Returns the visibility of this view and all of its ancestors - * + * * @return True if this view and all of its ancestors are {@link #VISIBLE} */ - public boolean isShown() { + public boolean isShown() { View current = this; //noinspection ConstantConditions do { @@ -2522,7 +2571,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if (parent == null) { return false; // We are not attached to the view root } - if (parent instanceof ViewRoot) { + if (!(parent instanceof View)) { return true; } current = (View) parent; @@ -2534,7 +2583,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Apply the insets for system windows to this view, if the FITS_SYSTEM_WINDOWS flag * is set - * + * * @param insets Insets for system windows * * @return True if this view applied the insets, false otherwise @@ -2545,14 +2594,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback { mPaddingTop = insets.top; mPaddingRight = insets.right; mPaddingBottom = insets.bottom; + requestLayout(); return true; } return false; } - + /** * Returns the visibility status for this view. - * + * * @return One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}. * @attr ref android.R.styleable#View_visibility */ @@ -2567,7 +2617,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Set the enabled state of this view. - * + * * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}. * @attr ref android.R.styleable#View_visibility */ @@ -2578,40 +2628,40 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Returns the enabled status for this view. The interpretation of the * enabled state varies by subclass. - * + * * @return True if this view is enabled, false otherwise. */ @ViewDebug.ExportedProperty public boolean isEnabled() { return (mViewFlags & ENABLED_MASK) == ENABLED; } - + /** * Set the enabled state of this view. The interpretation of the enabled * state varies by subclass. - * + * * @param enabled True if this view is enabled, false otherwise. */ public void setEnabled(boolean enabled) { setFlags(enabled ? ENABLED : DISABLED, ENABLED_MASK); - + /* * The View most likely has to change its appearance, so refresh * the drawable state. */ refreshDrawableState(); - + // Invalidate too, since the default behavior for views is to be // be drawn at 50% alpha rather than to change the drawable. invalidate(); } - + /** * Set whether this view can receive the focus. * * Setting this to false will also ensure that this view is not focusable * in touch mode. - * + * * @param focusable If true, this view can receive the focus. * * @see #setFocusableInTouchMode(boolean) @@ -2631,7 +2681,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * * @param focusableInTouchMode If true, this view can receive the focus while * in touch mode. - * + * * @see #setFocusable(boolean) * @attr ref android.R.styleable#View_focusableInTouchMode */ @@ -2676,18 +2726,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } /** - * If this view doesn't do any drawing on its own, set this flag to + * If this view doesn't do any drawing on its own, set this flag to * allow further optimizations. By default, this flag is not set on * View, but could be set on some View subclasses such as ViewGroup. - * + * * Typically, if you override {@link #onDraw} you should clear this flag. - * + * * @param willNotDraw whether or not this View draw on its own */ public void setWillNotDraw(boolean willNotDraw) { setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); } - + /** * Returns whether or not this View draws on its own. * @@ -2739,7 +2789,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * is clickable it will change its state to "pressed" on every click. * Subclasses should set the view clickable to visually react to * user's clicks. - * + * * @param clickable true to make the view clickable, false otherwise * * @see #isClickable() @@ -2766,7 +2816,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * clickable it reacts to the user holding down the button for a longer * duration than a tap. This event can either launch the listener or a * context menu. - * + * * @param longClickable true to make the view long clickable, false otherwise * @see #isLongClickable() * @attr ref android.R.styleable#View_longClickable @@ -2777,10 +2827,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Sets the pressed that for this view. - * + * * @see #isClickable() * @see #setClickable(boolean) - * + * * @param pressed Pass true to set the View's internal state to "pressed", or false to reverts * the View's internal state from a previously set "pressed" state. */ @@ -2793,12 +2843,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback { refreshDrawableState(); dispatchSetPressed(pressed); } - + /** * Dispatch setPressed to all of this View's children. - * + * * @see #setPressed(boolean) - * + * * @param pressed The new pressed state */ protected void dispatchSetPressed(boolean pressed) { @@ -2818,7 +2868,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { public boolean isPressed() { return (mPrivateFlags & PRESSED) == PRESSED; } - + /** * Indicates whether this view will save its state (that is, * whether its {@link #onSaveInstanceState} method will be called). @@ -2839,12 +2889,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * view still must have an id assigned to it (via {@link #setId setId()}) * for its state to be saved. This flag can only disable the * saving of this view; any child views may still have their state saved. - * + * * @param enabled Set to false to <em>disable</em> state saving, or true * (the default) to allow it. * * @see #isSaveEnabled() - * @see #setId(int) + * @see #setId(int) * @see #onSaveInstanceState() * @attr ref android.R.styleable#View_saveEnabled */ @@ -2855,7 +2905,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Returns whether this View is able to take focus. - * + * * @return True if this view can take focus, or false otherwise. * @attr ref android.R.styleable#View_focusable */ @@ -2880,9 +2930,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Find the nearest view in the specified direction that can take focus. * This does not actually give focus to that view. - * + * * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT - * + * * @return The nearest focusable in the specified direction, or null if none * can be found. */ @@ -2893,7 +2943,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { return null; } } - + /** * This method is the last chance for the focused view and its ancestors to * respond to an arrow key. This is called when the focused view did not @@ -2943,7 +2993,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } return result; } - + /** * Find and return all focusable views that are descendants of this view, * possibly including this view if it is focusable itself. @@ -2961,7 +3011,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Add any focusable views that are descendants of this view (possibly * including this view if it is focusable itself) to views. If we are in touch mode, * only add views that are also focusable in touch mode. - * + * * @param views Focusable views found so far * @param direction The direction of the focus */ @@ -2972,11 +3022,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { views.add(this); } - + /** * Find and return all touchable views that are descendants of this view, * possibly including this view if it is touchable itself. - * + * * @return A list of touchable views */ public ArrayList<View> getTouchables() { @@ -2987,13 +3037,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Add any touchable views that are descendants of this view (possibly - * including this view if it is touchable itself) to views. - * + * including this view if it is touchable itself) to views. + * * @param views Touchable views found so far */ public void addTouchables(ArrayList<View> views) { final int viewFlags = mViewFlags; - + if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) && (viewFlags & ENABLED_MASK) == ENABLED) { views.add(this); @@ -3024,14 +3074,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Call this to try to give focus to a specific view or to one of its * descendants and give it a hint about what direction focus is heading. - * + * * A view will not actually take focus if it is not focusable ({@link #isFocusable} returns false), * or if it is focusable and it is not focusable in touch mode ({@link #isFocusableInTouchMode}) * while the device is in touch mode. * * See also {@link #focusSearch}, which is what you call to say that you * have focus, and you want your parent to look for the next one. - * + * * This is equivalent to calling {@link #requestFocus(int, Rect)} with * <code>null</code> set for the previously focused rectangle. * @@ -3096,11 +3146,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Call this to try to give focus to a specific view or to one of its descendants. This is a * special variant of {@link #requestFocus() } that will allow views that are not focuable in * touch mode to request focus when they are touched. - * + * * @return Whether this view or one of its descendants actually took focus. - * + * * @see #isInTouchMode() - * + * */ public final boolean requestFocusFromTouch() { // Leave touch mode if we need to @@ -3115,7 +3165,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } return requestFocus(View.FOCUS_DOWN); } - + /** * @return Whether any ancestor of this view blocks descendant focus. */ @@ -3131,42 +3181,74 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } return false; } - + + /** + * capture information of this view for later analysis: developement only + * check dynamic switch to make sure we only dump view + * when ViewDebug.SYSTEM_PROPERTY_CAPTURE_VIEW) is set + */ + private static void captureViewInfo(String subTag, View v) { + if (v == null || + SystemProperties.getInt(ViewDebug.SYSTEM_PROPERTY_CAPTURE_VIEW, 0) == 0) { + return; + } + ViewDebug.dumpCapturedView(subTag, v); + } + + /** + * Dispatch a key event before it is processed by any input method + * associated with the view hierarchy. This can be used to intercept + * key events in special situations before the IME consumes them; a + * typical example would be handling the BACK key to update the application's + * UI instead of allowing the IME to see it and close itself. + * + * @param event The key event to be dispatched. + * @return True if the event was handled, false otherwise. + */ + public boolean dispatchKeyEventPreIme(KeyEvent event) { + return onKeyPreIme(event.getKeyCode(), event); + } + /** * Dispatch a key event to the next view on the focus path. This path runs * from the top of the view tree down to the currently focused view. If this * view has focus, it will dispatch to itself. Otherwise it will dispatch * the next node down the focus path. This method also fires any key * listeners. - * + * * @param event The key event to be dispatched. * @return True if the event was handled, false otherwise. */ public boolean dispatchKeyEvent(KeyEvent event) { // If any attached key listener a first crack at the event. //noinspection SimplifiableIfStatement + + if (android.util.Config.LOGV) { + captureViewInfo("captureViewKeyEvent", this); + } + if (mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnKeyListener.onKey(this, event.getKeyCode(), event)) { return true; } - + return event.dispatch(this); } /** * Dispatches a key shortcut event. - * + * * @param event The key event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchKeyShortcutEvent(KeyEvent event) { return onKeyShortcut(event.getKeyCode(), event); } - + /** * Pass the touch screen motion event down to the target view, or this * view if it is the target. - * + * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ @@ -3180,7 +3262,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Pass a trackball motion event down to the focused view. - * + * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ @@ -3192,7 +3274,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Called when the window containing this view gains or loses window focus. * ViewGroups should override to route to their children. - * + * * @param hasFocus True if the window containing this view now has focus, * false otherwise. */ @@ -3206,23 +3288,29 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * your view and its window must have focus. If a window is displayed * on top of yours that takes input focus, then your own window will lose * focus but the view focus will remain unchanged. - * + * * @param hasWindowFocus True if the window containing this view now has * focus, false otherwise. */ public void onWindowFocusChanged(boolean hasWindowFocus) { + InputMethodManager imm = InputMethodManager.peekInstance(); if (!hasWindowFocus) { if (isPressed()) { setPressed(false); } + if (imm != null && (mPrivateFlags & FOCUSED) != 0) { + imm.focusOut(this); + } + } else if (imm != null && (mPrivateFlags & FOCUSED) != 0) { + imm.focusIn(this); } refreshDrawableState(); } - + /** * Returns true if this view is in a window that currently has window focus. * Note that this is not the same as the view itself having focus. - * + * * @return True if this view is in a window that currently has window focus. */ public boolean hasWindowFocus() { @@ -3232,9 +3320,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Dispatch a window visibility change down the view hierarchy. * ViewGroups should override to route to their children. - * + * * @param visibility The new visibility of the window. - * + * * @see #onWindowVisibilityChanged */ public void dispatchWindowVisibilityChanged(int visibility) { @@ -3248,22 +3336,59 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * to the window manager; this does <em>not</em> tell you whether or not * your window is obscured by other windows on the screen, even if it * is itself visible. - * + * * @param visibility The new visibility of the window. */ protected void onWindowVisibilityChanged(int visibility) { } - + /** * Returns the current visibility of the window this view is attached to * (either {@link #GONE}, {@link #INVISIBLE}, or {@link #VISIBLE}). - * + * * @return Returns the current visibility of the view's window. */ public int getWindowVisibility() { return mAttachInfo != null ? mAttachInfo.mWindowVisibility : GONE; } - + + /** + * Retrieve the overall visible display size in which the window this view is + * attached to has been positioned in. This takes into account screen + * decorations above the window, for both cases where the window itself + * is being position inside of them or the window is being placed under + * then and covered insets are used for the window to position its content + * inside. In effect, this tells you the available area where content can + * be placed and remain visible to users. + * + * <p>This function requires an IPC back to the window manager to retrieve + * the requested information, so should not be used in performance critical + * code like drawing. + * + * @param outRect Filled in with the visible display frame. If the view + * is not attached to a window, this is simply the raw display size. + */ + public void getWindowVisibleDisplayFrame(Rect outRect) { + if (mAttachInfo != null) { + try { + mAttachInfo.mSession.getDisplayFrame(mAttachInfo.mWindow, outRect); + } catch (RemoteException e) { + return; + } + // XXX This is really broken, and probably all needs to be done + // in the window manager, and we need to know more about whether + // we want the area behind or in front of the IME. + final Rect insets = mAttachInfo.mVisibleInsets; + outRect.left += insets.left; + outRect.top += insets.top; + outRect.right -= insets.right; + outRect.bottom -= insets.bottom; + return; + } + Display d = WindowManagerImpl.getDefault().getDefaultDisplay(); + outRect.set(0, 0, d.getWidth(), d.getHeight()); + } + /** * Private function to aggregate all per-view attributes in to the view * root. @@ -3278,7 +3403,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { mAttachInfo.mKeepScreenOn = true; } } - + void needGlobalAttributesUpdate(boolean force) { AttachInfo ai = mAttachInfo; if (ai != null) { @@ -3287,7 +3412,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } } } - + /** * Returns whether the device is currently in touch mode. Touch mode is entered * once the user begins interacting with the device by touch, and affects various @@ -3306,21 +3431,38 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Returns the context the view is running in, through which it can - * access the current theme, resources, etc. - * + * access the current theme, resources, etc. + * * @return The view's Context. */ + @ViewDebug.CapturedViewProperty public final Context getContext() { return mContext; } /** + * Handle a key event before it is processed by any input method + * associated with the view hierarchy. This can be used to intercept + * key events in special situations before the IME consumes them; a + * typical example would be handling the BACK key to update the application's + * UI instead of allowing the IME to see it and close itself. + * + * @param keyCode The value in event.getKeyCode(). + * @param event Description of the key event. + * @return If you handled the event, return true. If you want to allow the + * event to be handled by the next receiver, return false. + */ + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + return false; + } + + /** * Default implementation of {@link KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent) * KeyEvent.Callback.onKeyMultiple()}: perform press of the view * when {@link KeyEvent#KEYCODE_DPAD_CENTER} or {@link KeyEvent#KEYCODE_ENTER} * is released, if the view is enabled and clickable. - * - * @param keyCode A key code that represents the button pressed, from + * + * @param keyCode A key code that represents the button pressed, from * {@link android.view.KeyEvent}. * @param event The KeyEvent object that defines the button action. */ @@ -3354,8 +3496,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * KeyEvent.Callback.onKeyMultiple()}: perform clicking of the view * when {@link KeyEvent#KEYCODE_DPAD_CENTER} or * {@link KeyEvent#KEYCODE_ENTER} is released. - * - * @param keyCode A key code that represents the button pressed, from + * + * @param keyCode A key code that represents the button pressed, from * {@link android.view.KeyEvent}. * @param event The KeyEvent object that defines the button action. */ @@ -3390,8 +3532,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Default implementation of {@link KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent) * KeyEvent.Callback.onKeyMultiple()}: always returns false (doesn't handle * the event). - * - * @param keyCode A key code that represents the button pressed, from + * + * @param keyCode A key code that represents the button pressed, from * {@link android.view.KeyEvent}. * @param repeatCount The number of times the action was made. * @param event The KeyEvent object that defines the button action. @@ -3402,7 +3544,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Called when an unhandled key shortcut event occurs. - * + * * @param keyCode The value in event.getKeyCode(). * @param event Description of the key event. * @return If you handled the event, return true. If you want to allow the @@ -3411,11 +3553,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback { public boolean onKeyShortcut(int keyCode, KeyEvent event) { return false; } - + + /** + * Create a new InputConnection for an InputMethod to interact + * with the view. The default implementation returns null, since it doesn't + * support input methods. You can override this to implement such support. + * This is only needed for views that take focus and text input. + * + * @param outAttrs Fill in with attribute information about the connection. + */ + public InputConnection createInputConnection(EditorInfo outAttrs) { + return null; + } + /** * Show the context menu for this view. It is not safe to hold on to the * menu after returning from this method. - * + * * @param menu The context menu to populate */ public void createContextMenu(ContextMenu menu) { @@ -3517,7 +3671,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if (mPendingCheckForLongPress != null) { removeCallbacks(mPendingCheckForLongPress); } - + // Only perform take click actions if we were in the pressed state if (!focusTaken) { performClick(); @@ -3548,10 +3702,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); - + // Be lenient about moving outside of buttons int slop = ViewConfiguration.getTouchSlop(); - if ((x < 0 - slop) || (x >= getWidth() + slop) || + if ((x < 0 - slop) || (x >= getWidth() + slop) || (y < 0 - slop) || (y >= getHeight() + slop)) { // Outside button if ((mPrivateFlags & PRESSED) != 0) { @@ -3598,7 +3752,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { public void setTouchDelegate(TouchDelegate delegate) { mTouchDelegate = delegate; } - + /** * Gets the TouchDelegate for this View. */ @@ -3608,7 +3762,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Set flags controlling behavior of this view. - * + * * @param flags Constant indicating the value which should be set * @param mask Constant indicating the bit range that should be changed */ @@ -3648,7 +3802,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { mPrivateFlags |= DRAWN; needGlobalAttributesUpdate(true); - + // a view becoming visible is worth notifying the parent // about in case nothing has focus. even if this specific view // isn't focusable, it may contain something that is, so let @@ -3668,6 +3822,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if (((mViewFlags & VISIBILITY_MASK) == GONE) && hasFocus()) { clearFocus(); } + if (mAttachInfo != null) { + mAttachInfo.mViewVisibilityChanged = true; + } } /* Check if the VISIBLE bit has changed */ @@ -3681,6 +3838,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback { clearFocus(); } } + if (mAttachInfo != null) { + mAttachInfo.mViewVisibilityChanged = true; + } } if ((changed & WILL_NOT_CACHE_DRAWING) != 0) { @@ -3712,7 +3872,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { mPrivateFlags &= ~SKIP_DRAW; mPrivateFlags |= ONLY_DRAWS_BACKGROUND; } else { - mPrivateFlags |= SKIP_DRAW; + mPrivateFlags |= SKIP_DRAW; } } else { mPrivateFlags &= ~SKIP_DRAW; @@ -3720,7 +3880,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { requestLayout(); invalidate(); } - + if ((changed & KEEP_SCREEN_ON) != 0) { if (mParent != null) { mParent.recomputeViewAttributes(this); @@ -3740,10 +3900,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * This is called in response to an internal scroll in this view (i.e., the - * view scrolled its own contents). This is typically as a result of + * view scrolled its own contents). This is typically as a result of * {@link #scrollBy(int, int)} or {@link #scrollTo(int, int)} having been * called. - * + * * @param l Current horizontal scroll origin. * @param t Current vertical scroll origin. * @param oldl Previous horizontal scroll origin. @@ -3757,7 +3917,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * This is called during layout when the size of this view has changed. If * you were just added to the view hierarchy, you're called with the old * values of 0. - * + * * @param w Current width of this view. * @param h Current height of this view. * @param oldw Old width of this view. @@ -3765,7 +3925,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { */ protected void onSizeChanged(int w, int h, int oldw, int oldh) { } - + /** * Called by draw to draw the child views. This may be overridden * by derived classes to gain control just before its children are drawn @@ -3778,7 +3938,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Gets the parent of this view. Note that the parent is a * ViewParent and not necessarily a View. - * + * * @return Parent of this view. */ public final ViewParent getParent() { @@ -3790,7 +3950,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * the displayed part of your view. You do not need to draw any pixels * farther left, since those are outside of the frame of your view on * screen. - * + * * @return The left edge of the displayed part of your view, in pixels. */ public final int getScrollX() { @@ -3801,7 +3961,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Return the scrolled top position of this view. This is the top edge of * the displayed part of your view. You do not need to draw any pixels above * it, since those are outside of the frame of your view on screen. - * + * * @return The top edge of the displayed part of your view, in pixels. */ public final int getScrollY() { @@ -3810,7 +3970,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Return the width of the your view. - * + * * @return The width of your view, in pixels. */ @ViewDebug.ExportedProperty @@ -3820,7 +3980,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Return the height of your view. - * + * * @return The height of your view, in pixels. */ @ViewDebug.ExportedProperty @@ -3832,7 +3992,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Return the visible drawing bounds of your view. Fills in the output * rectangle with the values from getScrollX(), getScrollY(), * getWidth(), and getHeight(). - * + * * @param outRect The (scrolled) drawing bounds of the view. */ public void getDrawingRect(Rect outRect) { @@ -3846,69 +4006,73 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * The width of this view as measured in the most recent call to measure(). * This should be used during measurement and layout calculations only. Use * {@link #getWidth()} to see how wide a view is after layout. - * + * * @return The measured width of this view. */ public final int getMeasuredWidth() { return mMeasuredWidth; } - + /** * The height of this view as measured in the most recent call to measure(). * This should be used during measurement and layout calculations only. Use * {@link #getHeight()} to see how tall a view is after layout. - * + * * @return The measured height of this view. */ public final int getMeasuredHeight() { return mMeasuredHeight; } - + /** * Top position of this view relative to its parent. - * + * * @return The top of this view, in pixels. */ + @ViewDebug.CapturedViewProperty public final int getTop() { return mTop; } - + /** * Bottom position of this view relative to its parent. - * + * * @return The bottom of this view, in pixels. */ + @ViewDebug.CapturedViewProperty public final int getBottom() { return mBottom; } - + /** * Left position of this view relative to its parent. - * + * * @return The left edge of this view, in pixels. */ + @ViewDebug.CapturedViewProperty public final int getLeft() { return mLeft; } - + /** * Right position of this view relative to its parent. - * + * * @return The right edge of this view, in pixels. */ + @ViewDebug.CapturedViewProperty public final int getRight() { return mRight; } /** * Hit rectangle in parent's coordinates - * + * * @param outRect The hit rectangle of the view. */ public void getHitRect(Rect outRect) { outRect.set(mLeft, mTop, mRight, mBottom); } - + /** * When a view has focus and the user navigates away from it, the next view is searched for * starting from the rectangle filled in by this method. @@ -3929,7 +4093,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * coordinates, offset it by -globalOffset (e.g. r.offset(-globalOffset.x, * -globalOffset.y)) If the view is completely clipped or translated out, * return false. - * + * * @param r If true is returned, r holds the global coordinates of the * visible portion of this view. * @param globalOffset If true is returned, globalOffset holds the dx,dy @@ -3962,7 +4126,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } return false; } - + /** * Offset this view's vertical location by the specified number of pixels. * @@ -3975,18 +4139,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Offset this view's horizontal location by the specified amount of pixels. - * + * * @param offset the numer of pixels to offset the view by */ public void offsetLeftAndRight(int offset) { mLeft += offset; mRight += offset; } - + /** * Get the LayoutParams associated with this view. All views should have - * layout parameters. These supply parameters to the <i>parent</i> of this - * view specifying how it should be arranged. There are many subclasses of + * layout parameters. These supply parameters to the <i>parent</i> of this + * view specifying how it should be arranged. There are many subclasses of * ViewGroup.LayoutParams, and these correspond to the different subclasses * of ViewGroup that are responsible for arranging their children. * @return The LayoutParams associated with this view @@ -4012,7 +4176,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { mLayoutParams = params; requestLayout(); } - + /** * Set the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be @@ -4047,7 +4211,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * visible, {@link #onDraw} will be called at some point in the future. * This must be called from a UI thread. To call from a non-UI thread, call * {@link #postInvalidate()}. - * + * * WARNING: This method is destructive to dirty. * @param dirty the rectangle representing the bounds of the dirty region */ @@ -4058,13 +4222,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) { mPrivateFlags &= ~DRAWING_CACHE_VALID; - ViewParent p = mParent; - if (p != null) { + final ViewParent p = mParent; + final AttachInfo ai = mAttachInfo; + if (p != null && ai != null) { final int scrollX = mScrollX; final int scrollY = mScrollY; - mTempRect.set(dirty.left - scrollX, dirty.top - scrollY, - dirty.right - scrollX, dirty.bottom - scrollY); - p.invalidateChild(this, mTempRect); + final Rect r = ai.mTmpInvalRect; + r.set(dirty.left - scrollX, dirty.top - scrollY, + dirty.right - scrollX, dirty.bottom - scrollY); + mParent.invalidateChild(this, r); } } } @@ -4087,12 +4253,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) { mPrivateFlags &= ~DRAWING_CACHE_VALID; - ViewParent p = mParent; - if (p != null && l < r && t < b) { + final ViewParent p = mParent; + final AttachInfo ai = mAttachInfo; + if (p != null && ai != null && l < r && t < b) { final int scrollX = mScrollX; final int scrollY = mScrollY; - mTempRect.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY); - p.invalidateChild(this, mTempRect); + final Rect tmpr = ai.mTmpInvalRect; + tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY); + p.invalidateChild(this, tmpr); } } } @@ -4109,25 +4277,27 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) { mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID; - ViewParent p = mParent; - if (p != null) { - mTempRect.set(0, 0, mRight - mLeft, mBottom - mTop); + final ViewParent p = mParent; + final AttachInfo ai = mAttachInfo; + if (p != null && ai != null) { + final Rect r = ai.mTmpInvalRect; + r.set(0, 0, mRight - mLeft, mBottom - mTop); // Don't call invalidate -- we don't want to internally scroll // our own bounds - p.invalidateChild(this, mTempRect); + p.invalidateChild(this, r); } } } - + /** * @return A handler associated with the thread running the View. This * handler can be used to pump events in the UI events queue. */ - protected Handler getHandler() { + public Handler getHandler() { if (mAttachInfo != null) { return mAttachInfo.mHandler; } - return null; + return null; } /** @@ -4260,7 +4430,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Cause an invalidate to happen on a subsequent cycle through the event * loop. Waits for the specified amount of time. - * + * * @param delayMilliseconds the duration in milliseconds to delay the * invalidation by */ @@ -4304,7 +4474,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Called by a parent to request that a child update its values for mScrollX * and mScrollY if necessary. This will typically be done if the child is * animating a scroll using a {@link android.widget.Scroller Scroller} - * object. + * object. */ public void computeScroll() { } @@ -4337,7 +4507,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { public void setHorizontalFadingEdgeEnabled(boolean horizontalFadingEdgeEnabled) { if (isHorizontalFadingEdgeEnabled() != horizontalFadingEdgeEnabled) { if (horizontalFadingEdgeEnabled) { - initScrollCache(); + initScrollCache(); } mViewFlags ^= FADING_EDGE_HORIZONTAL; @@ -4504,12 +4674,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * inset. When inset, they add to the padding of the view. And the scrollbars * can be drawn inside the padding area or on the edge of the view. For example, * if a view has a background drawable and you want to draw the scrollbars - * inside the padding specified by the drawable, you can use + * inside the padding specified by the drawable, you can use * SCROLLBARS_INSIDE_OVERLAY or SCROLLBARS_INSIDE_INSET. If you want them to * appear at the edge of the view, ignoring the padding, then you can use - * SCROLLBARS_OUTSIDE_OVERLAY or SCROLLBARS_OUTSIDE_INSET.</p> - * @param style the style of the scrollbars. Should be one of - * SCROLLBARS_INSIDE_OVERLAY, SCROLLBARS_INSIDE_INSET, + * SCROLLBARS_OUTSIDE_OVERLAY or SCROLLBARS_OUTSIDE_INSET.</p> + * @param style the style of the scrollbars. Should be one of + * SCROLLBARS_INSIDE_OVERLAY, SCROLLBARS_INSIDE_INSET, * SCROLLBARS_OUTSIDE_OVERLAY or SCROLLBARS_OUTSIDE_INSET. * @see #SCROLLBARS_INSIDE_OVERLAY * @see #SCROLLBARS_INSIDE_INSET @@ -4517,12 +4687,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * @see #SCROLLBARS_OUTSIDE_INSET */ public void setScrollBarStyle(int style) { - if (style != (mViewFlags & SCROLLBARS_STYLE_MASK)) { + if (style != (mViewFlags & SCROLLBARS_STYLE_MASK)) { mViewFlags = (mViewFlags & ~SCROLLBARS_STYLE_MASK) | (style & SCROLLBARS_STYLE_MASK); recomputePadding(); } } - + /** * <p>Returns the current scrollbar style.</p> * @return the current scrollbar style @@ -4534,7 +4704,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { public int getScrollBarStyle() { return mViewFlags & SCROLLBARS_STYLE_MASK; } - + /** * <p>Compute the horizontal range that the horizontal scrollbar * represents.</p> @@ -4670,10 +4840,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { final ScrollabilityCache cache = mScrollCache; if (cache != null) { final int viewFlags = mViewFlags; - - final boolean drawHorizontalScrollBar = + + final boolean drawHorizontalScrollBar = (viewFlags & SCROLLBARS_HORIZONTAL) == SCROLLBARS_HORIZONTAL; - final boolean drawVerticalScrollBar = + final boolean drawVerticalScrollBar = (viewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL; if (drawVerticalScrollBar || drawHorizontalScrollBar) { @@ -4728,12 +4898,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback { final int scrollY = mScrollY; final int inside = (viewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0; final int top = scrollY + height - size - (mUserPaddingBottom & inside); - - final int verticalScrollBarGap = + + final int verticalScrollBarGap = (viewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL ? getVerticalScrollbarWidth() : 0; - - scrollBar.setBounds(scrollX + (mPaddingLeft & inside) + getScrollBarPaddingLeft(), top, + + scrollBar.setBounds(scrollX + (mPaddingLeft & inside) + getScrollBarPaddingLeft(), top, scrollX + width - (mUserPaddingRight & inside) - verticalScrollBarGap, top + size); scrollBar.setParameters( computeHorizontalScrollRange(), @@ -4773,8 +4943,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { final int inside = (mViewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0; // TODO: Deal with RTL languages to position scrollbar on left final int left = scrollX + width - size - (mUserPaddingRight & inside); - - scrollBar.setBounds(left, scrollY + (mPaddingTop & inside), + + scrollBar.setBounds(left, scrollY + (mPaddingTop & inside), left + size, scrollY + height - (mUserPaddingBottom & inside)); scrollBar.setParameters( computeVerticalScrollRange(), @@ -4832,7 +5002,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { removeCallbacks(mPendingCheckForLongPress); } } - + /** * @return The number of times this view has been attached to a window */ @@ -4855,7 +5025,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * {@link #getWindowToken}, except if the window this view in is a panel * window (attached to another containing window), then the token of * the containing window is returned instead. - * + * * @return Returns the associated window token, either * {@link #getWindowToken()} or the containing window's token. */ @@ -4892,6 +5062,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { info.mTreeObserver.merge(mFloatingTreeObserver); mFloatingTreeObserver = null; } + if ((mPrivateFlags&SCROLL_CONTAINER) != 0) { + mAttachInfo.mScrollContainers.add(this); + mPrivateFlags |= SCROLL_CONTAINER_ADDED; + } performCollectViewAttributes(visibility); onAttachedToWindow(); int vis = info.mWindowVisibility; @@ -4909,16 +5083,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback { onWindowVisibilityChanged(GONE); } } - + onDetachedFromWindow(); + if ((mPrivateFlags&SCROLL_CONTAINER_ADDED) != 0) { + mAttachInfo.mScrollContainers.remove(this); + mPrivateFlags &= ~SCROLL_CONTAINER_ADDED; + } mAttachInfo = null; } /** * Store this view hierarchy's frozen state into the given container. - * + * * @param container The SparseArray in which to save the view's state. - * + * * @see #restoreHierarchyState * @see #dispatchSaveInstanceState * @see #onSaveInstanceState @@ -4931,9 +5109,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Called by {@link #saveHierarchyState} to store the state for this view and its children. * May be overridden to modify how freezing happens to a view's children; for example, some * views may want to not store state for their children. - * + * * @param container The SparseArray in which to save the view's state. - * + * * @see #dispatchRestoreInstanceState * @see #saveHierarchyState * @see #onSaveInstanceState @@ -4966,7 +5144,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * in a text view (but usually not the text itself since that is stored in a * content provider or other persistent storage), the currently selected * item in a list view. - * + * * @return Returns a Parcelable object containing the view's current dynamic * state, or null if there is nothing interesting to save. The * default implementation returns null. @@ -4982,9 +5160,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Restore this view hierarchy's frozen state from the given container. - * + * * @param container The SparseArray which holds previously frozen states. - * + * * @see #saveHierarchyState * @see #dispatchRestoreInstanceState * @see #onRestoreInstanceState @@ -4997,9 +5175,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Called by {@link #restoreHierarchyState} to retrieve the state for this view and its * children. May be overridden to modify how restoreing happens to a view's children; for * example, some views may want to not store state for their children. - * + * * @param container The SparseArray which holds previously saved state. - * + * * @see #dispatchSaveInstanceState * @see #restoreHierarchyState * @see #onRestoreInstanceState @@ -5024,10 +5202,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Hook allowing a view to re-apply a representation of its internal state that had previously * been generated by {@link #onSaveInstanceState}. This function will never be called with a * null state. - * + * * @param state The frozen state that had previously been returned by * {@link #onSaveInstanceState}. - * + * * @see #onSaveInstanceState * @see #restoreHierarchyState * @see #dispatchRestoreInstanceState @@ -5038,7 +5216,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { throw new IllegalArgumentException("Wrong state class -- expecting View State"); } } - + /** * <p>Return the time at which the drawing of the view hierarchy started.</p> * @@ -5095,7 +5273,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * * @see #isDrawingCacheEnabled() * @see #getDrawingCache() - * @see #buildDrawingCache() + * @see #buildDrawingCache() */ public void setDrawingCacheEnabled(boolean enabled) { setFlags(enabled ? DRAWING_CACHE_ENABLED : 0, DRAWING_CACHE_ENABLED); @@ -5126,7 +5304,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * * @see #setDrawingCacheEnabled(boolean) * @see #isDrawingCacheEnabled() - * @see #buildDrawingCache() + * @see #buildDrawingCache() * @see #destroyDrawingCache() */ public Bitmap getDrawingCache() { @@ -5148,7 +5326,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * * @see #setDrawingCacheEnabled(boolean) * @see #buildDrawingCache() - * @see #getDrawingCache() + * @see #getDrawingCache() */ public void destroyDrawingCache() { if (mDrawingCache != null) { @@ -5156,31 +5334,31 @@ public class View implements Drawable.Callback, KeyEvent.Callback { mDrawingCache = null; } } - + /** * Setting a solid background color for the drawing cache's bitmaps will improve * perfromance and memory usage. Note, though that this should only be used if this * view will always be drawn on top of a solid color. - * + * * @param color The background color to use for the drawing cache's bitmap - * + * * @see #setDrawingCacheEnabled(boolean) * @see #buildDrawingCache() - * @see #getDrawingCache() + * @see #getDrawingCache() */ public void setDrawingCacheBackgroundColor(int color) { mDrawingCacheBackgroundColor = color; } - + /** * @see #setDrawingCacheBackgroundColor(int) - * + * * @return The background color to used for the drawing cache's bitmap */ public int getDrawingCacheBackgroundColor() { return mDrawingCacheBackgroundColor; } - + /** * <p>Forces the drawing cache to be built if the drawing cache is invalid.</p> * @@ -5204,7 +5382,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { final int height = mBottom - mTop; final int drawingCacheBackgroundColor = mDrawingCacheBackgroundColor; - final boolean opaque = drawingCacheBackgroundColor != 0 || + final boolean opaque = drawingCacheBackgroundColor != 0 || (mBGDrawable != null && mBGDrawable.getOpacity() == PixelFormat.OPAQUE); if (width <= 0 || height <= 0 || @@ -5242,9 +5420,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback { quality = Bitmap.Config.RGB_565; } - mDrawingCache = bitmap = Bitmap.createBitmap(width, height, quality); + // Try to cleanup memory + if (mDrawingCache != null) { + mDrawingCache.recycle(); + } - clear = drawingCacheBackgroundColor != 0; + try { + mDrawingCache = bitmap = Bitmap.createBitmap(width, height, quality); + } catch (OutOfMemoryError e) { + // If there is not enough memory to create the bitmap cache, just + // ignore the issue as bitmap caches are not required to draw the + // view hierarchy + mDrawingCache = null; + return; + } + + clear = drawingCacheBackgroundColor != 0; } Canvas canvas; @@ -5293,7 +5484,103 @@ public class View implements Drawable.Callback, KeyEvent.Callback { mPrivateFlags |= DRAWING_CACHE_VALID; } } - + + /** + * Indicates whether this View is currently in edit mode. A View is usually + * in edit mode when displayed within a developer tool. For instance, if + * this View is being drawn by a visual user interface builder, this method + * should return true. + * + * Subclasses should check the return value of this method to provide + * different behaviors if their normal behavior might interfere with the + * host environment. For instance: the class spawns a thread in its + * constructor, the drawing code relies on device-specific features, etc. + * + * This method is usually checked in the drawing code of custom widgets. + * + * @return True if this View is in edit mode, false otherwise. + */ + public boolean isInEditMode() { + return false; + } + + /** + * If the View draws content inside its padding and enables fading edges, + * it needs to support padding offsets. Padding offsets are added to the + * fading edges to extend the length of the fade so that it covers pixels + * drawn inside the padding. + * + * Subclasses of this class should override this method if they need + * to draw content inside the padding. + * + * @return True if padding offset must be applied, false otherwise. + * + * @see #getLeftPaddingOffset() + * @see #getRightPaddingOffset() + * @see #getTopPaddingOffset() + * @see #getBottomPaddingOffset() + * + * @since CURRENT + */ + protected boolean isPaddingOffsetRequired() { + return false; + } + + /** + * Amount by which to extend the left fading region. Called only when + * {@link #isPaddingOffsetRequired()} returns true. + * + * @return The left padding offset in pixels. + * + * @see #isPaddingOffsetRequired() + * + * @since CURRENT + */ + protected int getLeftPaddingOffset() { + return 0; + } + + /** + * Amount by which to extend the right fading region. Called only when + * {@link #isPaddingOffsetRequired()} returns true. + * + * @return The right padding offset in pixels. + * + * @see #isPaddingOffsetRequired() + * + * @since CURRENT + */ + protected int getRightPaddingOffset() { + return 0; + } + + /** + * Amount by which to extend the top fading region. Called only when + * {@link #isPaddingOffsetRequired()} returns true. + * + * @return The top padding offset in pixels. + * + * @see #isPaddingOffsetRequired() + * + * @since CURRENT + */ + protected int getTopPaddingOffset() { + return 0; + } + + /** + * Amount by which to extend the bottom fading region. Called only when + * {@link #isPaddingOffsetRequired()} returns true. + * + * @return The bottom padding offset in pixels. + * + * @see #isPaddingOffsetRequired() + * + * @since CURRENT + */ + protected int getBottomPaddingOffset() { + return 0; + } /** * Manually render this view (and all of its children) to the given Canvas. @@ -5379,13 +5666,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback { float rightFadeStrength = 0.0f; // Step 2, save the canvas' layers - final int paddingLeft = mPaddingLeft; - final int paddingTop = mPaddingTop; + int paddingLeft = mPaddingLeft; + int paddingTop = mPaddingTop; - final int left = mScrollX + paddingLeft; - final int right = left + mRight - mLeft - mPaddingRight - paddingLeft; - final int top = mScrollY + paddingTop; - final int bottom = top + mBottom - mTop - mPaddingBottom - paddingTop; + final boolean offsetRequired = isPaddingOffsetRequired(); + if (offsetRequired) { + paddingLeft += getLeftPaddingOffset(); + paddingTop += getTopPaddingOffset(); + } + + int left = mScrollX + paddingLeft; + int right = left + mRight - mLeft - mPaddingRight - paddingLeft; + int top = mScrollY + paddingTop; + int bottom = top + mBottom - mTop - mPaddingBottom - paddingTop; + + if (offsetRequired) { + right += getRightPaddingOffset(); + bottom += getBottomPaddingOffset(); + } final ScrollabilityCache scrollabilityCache = mScrollCache; int length = scrollabilityCache.fadingEdgeLength; @@ -5416,23 +5714,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } saveCount = canvas.getSaveCount(); - + int solidColor = getSolidColor(); if (solidColor == 0) { final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG; - + if (drawTop) { canvas.saveLayer(left, top, right, top + length, null, flags); } - + if (drawBottom) { canvas.saveLayer(left, bottom - length, right, bottom, null, flags); } - + if (drawLeft) { canvas.saveLayer(left, top, left + length, bottom, null, flags); } - + if (drawRight) { canvas.saveLayer(right - length, top, right, bottom, null, flags); } @@ -5495,10 +5793,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * and needs to draw fading edges. Returning a non-zero color enables the view system to * optimize the drawing of the fading edges. If you do return a non-zero color, the alpha * should be set to 0xFF. - * + * * @see #setVerticalFadingEdgeEnabled * @see #setHorizontalFadingEdgeEnabled - * + * * @return The known solid color background for this view, or 0 if the color may vary */ public int getSolidColor() { @@ -5550,12 +5848,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback { private static String printPrivateFlags(int privateFlags) { String output = ""; int numFlags = 0; - + if ((privateFlags & WANTS_FOCUS) == WANTS_FOCUS) { output += "WANTS_FOCUS"; numFlags++; } - + if ((privateFlags & FOCUSED) == FOCUSED) { if (numFlags > 0) { output += " "; @@ -5563,7 +5861,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { output += "FOCUSED"; numFlags++; } - + if ((privateFlags & SELECTED) == SELECTED) { if (numFlags > 0) { output += " "; @@ -5571,15 +5869,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback { output += "SELECTED"; numFlags++; } - + if ((privateFlags & IS_ROOT_NAMESPACE) == IS_ROOT_NAMESPACE) { if (numFlags > 0) { output += " "; } output += "IS_ROOT_NAMESPACE"; numFlags++; - } - + } + if ((privateFlags & HAS_BOUNDS) == HAS_BOUNDS) { if (numFlags > 0) { output += " "; @@ -5587,7 +5885,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { output += "HAS_BOUNDS"; numFlags++; } - + if ((privateFlags & DRAWN) == DRAWN) { if (numFlags > 0) { output += " "; @@ -5611,17 +5909,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Assign a size and position to a view and all of its * descendants - * - * <p>This is the second phase of the layout mechanism. + * + * <p>This is the second phase of the layout mechanism. * (The first is measuring). In this phase, each parent calls * layout on all of its children to position them. * This is typically done using the child measurements * that were stored in the measure pass(). - * + * * Derived classes with children should override * onLayout. In that method, they should * call layout on each of their their children. - * + * * @param l Left position, relative to parent * @param t Top position, relative to parent * @param r Right position, relative to parent @@ -5639,11 +5937,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } mPrivateFlags &= ~FORCE_LAYOUT; } - + /** * Called from layout when this view should * assign a size and position to each of its children. - * + * * Derived classes with children should override * this method and call layout on each of * their their children. @@ -5655,12 +5953,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback { */ protected void onLayout(boolean changed, int left, int top, int right, int bottom) { } - + /** * Assign a size and position to this view. - * + * * This is called from layout. - * + * * @param left Left position, relative to parent * @param top Top position, relative to parent * @param right Right position, relative to parent @@ -5699,7 +5997,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { int newWidth = right - left; int newHeight = bottom - top; - + if (newWidth != oldWidth || newHeight != oldHeight) { onSizeChanged(newWidth, newHeight, oldWidth, oldHeight); } @@ -5725,7 +6023,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Finalize inflating a view from XML. This is called as the last phase * of inflation, after all child views have been added. - * + * * <p>Even if the subclass overrides onFinishInflate, they should always be * sure to call the super method, so that we get called. */ @@ -5734,7 +6032,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Returns the resources associated with this view. - * + * * @return Resources object. */ public Resources getResources() { @@ -5787,9 +6085,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Unschedule any events associated with the given Drawable. This can be * used when selecting a new Drawable into a view, so that the previous * one is completely unscheduled. - * + * * @param who The Drawable to unschedule. - * + * * @see #drawableStateChanged */ public void unscheduleDrawable(Drawable who) { @@ -5803,17 +6101,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * override this function and return true for any Drawable it is * displaying. This allows animations for those drawables to be * scheduled. - * + * * <p>Be sure to call through to the super class when overriding this * function. - * + * * @param who The Drawable to verify. Return true if it is one you are * displaying, else return the result of calling through to the * super class. - * + * * @return boolean If true than the Drawable is being displayed in the * view; else false and it is not allowed to animate. - * + * * @see #unscheduleDrawable * @see #drawableStateChanged */ @@ -5824,7 +6122,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * This function is called whenever the state of the view changes in such * a way that it impacts the state of drawables being shown. - * + * * <p>Be sure to call through to the superclass when overriding this * function. * @@ -5836,12 +6134,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback { d.setState(getDrawableState()); } } - + /** * Call this to force a view to update its drawable state. This will cause * drawableStateChanged to be called on this view. Views that are interested * in the new state should call getDrawableState. - * + * * @see #drawableStateChanged * @see #getDrawableState */ @@ -5858,9 +6156,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Return an array of resource IDs of the drawable states representing the * current state of the view. - * + * * @return The current drawable state - * + * * @see Drawable#setState * @see #drawableStateChanged * @see #onCreateDrawableState @@ -5880,14 +6178,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * this view. This is called by the view * system when the cached Drawable state is determined to be invalid. To * retrieve the current state, you should use {@link #getDrawableState}. - * + * * @param extraSpace if non-zero, this is the number of extra entries you * would like in the returned array in which you can place your own * states. - * + * * @return Returns an array holding the current {@link Drawable} state of * the view. - * + * * @see #mergeDrawableStates */ protected int[] onCreateDrawableState(int extraSpace) { @@ -5897,22 +6195,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } int[] drawableState; - + int privateFlags = mPrivateFlags; - boolean isPressed = (privateFlags & PRESSED) != 0; - int viewStateIndex = (isPressed ? 1 : 0); + int viewStateIndex = (((privateFlags & PRESSED) != 0) ? 1 : 0); - boolean isEnabled = (mViewFlags & ENABLED_MASK) == ENABLED; - viewStateIndex = (viewStateIndex << 1) + (isEnabled ? 1 : 0); + viewStateIndex = (viewStateIndex << 1) + + (((mViewFlags & ENABLED_MASK) == ENABLED) ? 1 : 0); - boolean isFocused = isFocused(); - viewStateIndex = (viewStateIndex << 1) + (isFocused ? 1 : 0); - - boolean isSelected = (privateFlags & SELECTED) != 0; - viewStateIndex = (viewStateIndex << 1) + (isSelected ? 1 : 0); - - boolean hasWindowFocus = hasWindowFocus(); + viewStateIndex = (viewStateIndex << 1) + (isFocused() ? 1 : 0); + + viewStateIndex = (viewStateIndex << 1) + + (((privateFlags & SELECTED) != 0) ? 1 : 0); + + final boolean hasWindowFocus = hasWindowFocus(); viewStateIndex = (viewStateIndex << 1) + (hasWindowFocus ? 1 : 0); drawableState = VIEW_STATE_SETS[viewStateIndex]; @@ -5920,9 +6216,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback { //noinspection ConstantIfStatement if (false) { Log.i("View", "drawableStateIndex=" + viewStateIndex); - Log.i("View", toString() + " pressed=" + isPressed - + " en=" + isEnabled + " fo=" + isFocused - + " sl=" + isSelected + " wf=" + hasWindowFocus + Log.i("View", toString() + + " pressed=" + ((privateFlags & PRESSED) != 0) + + " en=" + ((mViewFlags & ENABLED_MASK) == ENABLED) + + " fo=" + hasFocus() + + " sl=" + ((privateFlags & SELECTED) != 0) + + " wf=" + hasWindowFocus + ": " + Arrays.toString(drawableState)); } @@ -5940,22 +6239,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback { return fullState; } - + /** * Merge your own state values in <var>additionalState</var> into the base * state values <var>baseState</var> that were returned by * {@link #onCreateDrawableState}. - * + * * @param baseState The base state values returned by * {@link #onCreateDrawableState}, which will be modified to also hold your * own additional state values. - * + * * @param additionalState The additional state values you would like * added to <var>baseState</var>; this array is not modified. - * + * * @return As a convenience, the <var>baseState</var> array you originally * passed into the function is returned. - * + * * @see #onCreateDrawableState */ protected static int[] mergeDrawableStates(int[] baseState, int[] additionalState) { @@ -5967,7 +6266,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { System.arraycopy(additionalState, 0, baseState, i + 1, additionalState.length); return baseState; } - + /** * Sets the background color for this view. * @param color the color of the background @@ -5975,7 +6274,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { public void setBackgroundColor(int color) { setBackgroundDrawable(new ColorDrawable(color)); } - + /** * Set the background to a given resource. The resource should refer to * a Drawable object. @@ -6002,13 +6301,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * padding. However, when a background is removed, this View's padding isn't * touched. If setting the padding is desired, please use * {@link #setPadding(int, int, int, int)}. - * + * * @param d The Drawable to use as the background, or null to remove the * background */ public void setBackgroundDrawable(Drawable d) { boolean requestLayout = false; - + mBackgroundResource = 0; /* @@ -6021,11 +6320,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } if (d != null) { - final Rect padding = mTempRect; + Rect padding = sThreadLocal.get(); + if (padding == null) { + padding = new Rect(); + sThreadLocal.set(padding); + } if (d.getPadding(padding)) { setPadding(padding.left, padding.top, padding.right, padding.bottom); } - + // Compare the minimum sizes of the old Drawable and the new. If there isn't an old or // if it has a different minimum size, we should layout again if (mBGDrawable == null || mBGDrawable.getMinimumHeight() != d.getMinimumHeight() || @@ -6048,7 +6351,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } else { /* Remove the background */ mBGDrawable = null; - + if ((mPrivateFlags & ONLY_DRAWS_BACKGROUND) != 0) { /* * This view ONLY drew the background before and we're removing @@ -6065,7 +6368,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * padding. This is noted in the Javadocs. Hence, we don't need to * requestLayout(), the invalidate() below is sufficient. */ - + // The old background's minimum size could have affected this // View's layout, so let's requestLayout requestLayout = true; @@ -6091,7 +6394,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { // TODO: Deal with RTL languages return 0; } - + /* * Returns the pixels occupied by the vertical scrollbar, if not overlaid */ @@ -6114,7 +6417,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } /** - * Sets the padding. The view may add on the space required to display + * Sets the padding. The view may add on the space required to display * the scrollbars, depending on the style and visibility of the scrollbars. * So the values returned from {@link #getPaddingLeft}, {@link #getPaddingTop}, * {@link #getPaddingRight} and {@link #getPaddingBottom} may be different @@ -6132,10 +6435,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { */ public void setPadding(int left, int top, int right, int bottom) { boolean changed = false; - + mUserPaddingRight = right; mUserPaddingBottom = bottom; - + if (mPaddingLeft != left + getScrollBarPaddingLeft()) { changed = true; mPaddingLeft = left; @@ -6216,17 +6519,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback { dispatchSetSelected(selected); } } - + /** * Dispatch setSelected to all of this View's children. - * + * * @see #setSelected(boolean) - * + * * @param selected The new selected state */ protected void dispatchSetSelected(boolean selected) { } - + /** * Indicates the selection state of this view. * @@ -6265,6 +6568,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * @return the topmost view containing this view */ public View getRootView() { + if (mAttachInfo != null) { + final View v = mAttachInfo.mRootView; + if (v != null) { + return v; + } + } + View parent = this; while (parent.mParent != null && parent.mParent instanceof View) { @@ -6305,22 +6615,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback { location[0] = mLeft; location[1] = mTop; - if (!(mParent instanceof View)) { - return; + ViewParent viewParent = mParent; + while (viewParent instanceof View) { + final View view = (View)viewParent; + location[0] += view.mLeft - view.mScrollX; + location[1] += view.mTop - view.mScrollY; + viewParent = view.mParent; } - - View parent = (View) mParent; - - while (parent != null) { - location[0] += parent.mLeft - parent.mScrollX; - location[1] += parent.mTop - parent.mScrollY; - - final ViewParent viewParent = parent.mParent; - if (viewParent != null && viewParent instanceof View) { - parent = (View) viewParent; - } else { - parent = null; - } + + if (viewParent instanceof ViewRoot) { + // *cough* + final ViewRoot vr = (ViewRoot)viewParent; + location[1] -= vr.mCurScrollY; } } @@ -6380,13 +6686,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Sets the identifier for this view. The identifier does not have to be * unique in this view's hierarchy. The identifier should be a positive * number. - * + * * @see #NO_ID * @see #getId * @see #findViewById * * @param id a number used to identify the view - * + * * @attr ref android.R.styleable#View_id */ public void setId(int id) { @@ -6421,11 +6727,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * * @return a positive integer used to identify the view or {@link #NO_ID} * if the view has no ID - * + * * @see #setId * @see #findViewById * @attr ref android.R.styleable#View_id */ + @ViewDebug.CapturedViewProperty public int getId() { return mID; } @@ -6433,7 +6740,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Returns this view's tag. * - * @return the Object stored in this view as a tag + * @return the Object stored in this view as a tag */ @ViewDebug.ExportedProperty public Object getTag() { @@ -6473,7 +6780,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { */ protected void debug(int depth) { String output = debugIndent(depth - 1); - + output += "+ " + this; int id = getId(); if (id != -1) { @@ -6484,18 +6791,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback { output += " (tag=" + tag + ")"; } Log.d(VIEW_LOG_TAG, output); - + if ((mPrivateFlags & FOCUSED) != 0) { - output = debugIndent(depth) + " FOCUSED"; + output = debugIndent(depth) + " FOCUSED"; Log.d(VIEW_LOG_TAG, output); } - + output = debugIndent(depth); output += "frame={" + mLeft + ", " + mTop + ", " + mRight + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY + "} "; Log.d(VIEW_LOG_TAG, output); - + if (mPaddingLeft != 0 || mPaddingTop != 0 || mPaddingRight != 0 || mPaddingBottom != 0) { output = debugIndent(depth); @@ -6503,12 +6810,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback { + ", " + mPaddingRight + ", " + mPaddingBottom + "}"; Log.d(VIEW_LOG_TAG, output); } - + output = debugIndent(depth); output += "mMeasureWidth=" + mMeasuredWidth + " mMeasureHeight=" + mMeasuredHeight; Log.d(VIEW_LOG_TAG, output); - + output = debugIndent(depth); if (mLayoutParams == null) { output += "BAD! no layout params"; @@ -6516,7 +6823,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { output = mLayoutParams.debug(output); } Log.d(VIEW_LOG_TAG, output); - + output = debugIndent(depth); output += "flags={"; output += View.printFlags(mViewFlags); @@ -6583,8 +6890,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { */ public void forceLayout() { mPrivateFlags |= FORCE_LAYOUT; - } - + } + /** * <p> * This is called to find out how big a view should be. The parent @@ -6596,14 +6903,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * {@link #onMeasure(int, int)}, called by this method. Therefore, only * {@link #onMeasure(int, int)} can and must be overriden by subclasses. * </p> - * - * + * + * * @param widthMeasureSpec Horizontal space requirements as imposed by the * parent * @param heightMeasureSpec Vertical space requirements as imposed by the * parent * - * @see #onMeasure(int, int) + * @see #onMeasure(int, int) */ public final void measure(int widthMeasureSpec, int heightMeasureSpec) { if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || @@ -6642,7 +6949,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * should be overriden by subclasses to provide accurate and efficient * measurement of their contents. * </p> - * + * * <p> * <strong>CONTRACT:</strong> When overriding this method, you * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the @@ -6651,35 +6958,35 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * {@link #measure(int, int)}. Calling the superclass' * {@link #onMeasure(int, int)} is a valid use. * </p> - * + * * <p> * The base class implementation of measure defaults to the background size, * unless a larger size is allowed by the MeasureSpec. Subclasses should * override {@link #onMeasure(int, int)} to provide better measurements of * their content. * </p> - * + * * <p> * If this method is overridden, it is the subclass's responsibility to make * sure the measured height and width are at least the view's minimum height * and width ({@link #getSuggestedMinimumHeight()} and * {@link #getSuggestedMinimumWidth()}). * </p> - * + * * @param widthMeasureSpec horizontal space requirements as imposed by the parent. * The requirements are encoded with * {@link android.view.View.MeasureSpec}. * @param heightMeasureSpec vertical space requirements as imposed by the parent. * The requirements are encoded with * {@link android.view.View.MeasureSpec}. - * + * * @see #getMeasuredWidth() * @see #getMeasuredHeight() * @see #setMeasuredDimension(int, int) * @see #getSuggestedMinimumHeight() * @see #getSuggestedMinimumWidth() * @see android.view.View.MeasureSpec#getMode(int) - * @see android.view.View.MeasureSpec#getSize(int) + * @see android.view.View.MeasureSpec#getSize(int) */ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), @@ -6704,7 +7011,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Utility to reconcile a desired size with constraints imposed by a MeasureSpec. * Will take the desired size, unless a different size is imposed by the constraints. - * + * * @param size How big the view wants to be * @param measureSpec Constraints imposed by the parent * @return The size this view should be. @@ -6726,12 +7033,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } return result; } - + /** * Utility to return a default size. Uses the supplied size if the * MeasureSpec imposed no contraints. Will get larger if allowed * by the MeasureSpec. - * + * * @param size Default size for this view * @param measureSpec Constraints imposed by the parent * @return The size this view should be. @@ -6740,7 +7047,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); - + switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; @@ -6761,19 +7068,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * <p> * When being used in {@link #onMeasure(int, int)}, the caller should still * ensure the returned height is within the requirements of the parent. - * + * * @return The suggested minimum height of the view. */ protected int getSuggestedMinimumHeight() { int suggestedMinHeight = mMinHeight; - + if (mBGDrawable != null) { final int bgMinHeight = mBGDrawable.getMinimumHeight(); if (suggestedMinHeight < bgMinHeight) { suggestedMinHeight = bgMinHeight; } } - + return suggestedMinHeight; } @@ -6785,19 +7092,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * <p> * When being used in {@link #onMeasure(int, int)}, the caller should still * ensure the returned width is within the requirements of the parent. - * + * * @return The suggested minimum width of the view. */ protected int getSuggestedMinimumWidth() { int suggestedMinWidth = mMinWidth; - + if (mBGDrawable != null) { final int bgMinWidth = mBGDrawable.getMinimumWidth(); if (suggestedMinWidth < bgMinWidth) { suggestedMinWidth = bgMinWidth; } } - + return suggestedMinWidth; } @@ -6805,7 +7112,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Sets the minimum height of the view. It is not guaranteed the view will * be able to achieve this minimum height (for example, if its parent layout * constrains it with less available height). - * + * * @param minHeight The minimum height the view will try to be. */ public void setMinimumHeight(int minHeight) { @@ -6816,7 +7123,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Sets the minimum width of the view. It is not guaranteed the view will * be able to achieve this minimum width (for example, if its parent layout * constrains it with less available width). - * + * * @param minWidth The minimum width the view will try to be. */ public void setMinimumWidth(int minWidth) { @@ -6832,10 +7139,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { public Animation getAnimation() { return mCurrentAnimation; } - + /** * Start the specified animation now. - * + * * @param animation the animation to start now */ public void startAnimation(Animation animation) { @@ -6843,23 +7150,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback { setAnimation(animation); invalidate(); } - + /** * Cancels any animations for this view. */ public void clearAnimation() { mCurrentAnimation = null; } - + /** * Sets the next animation to play for this view. * If you want the animation to play immediately, use * startAnimation. This method provides allows fine-grained * control over the start time and invalidation, but you * must make sure that 1) the animation has a start time set, and - * 2) the view will be invalidated when the animation is supposed to + * 2) the view will be invalidated when the animation is supposed to * start. - * + * * @param animation The next animation, or null. */ public void setAnimation(Animation animation) { @@ -6914,25 +7221,26 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * SurfaceView is always considered transparent, but its children are not, * therefore all View objects remove themselves from the global transparent * region (passed as a parameter to this function). - * + * * @param region The transparent region for this ViewRoot (window). - * + * * @return Returns true if the effective visibility of the view at this * point is opaque, regardless of the transparent region; returns false * if it is possible for underlying windows to be seen behind the view. - * + * * {@hide} */ public boolean gatherTransparentRegion(Region region) { - if (region != null) { + final AttachInfo attachInfo = mAttachInfo; + if (region != null && attachInfo != null) { final int pflags = mPrivateFlags; if ((pflags & SKIP_DRAW) == 0) { // The SKIP_DRAW flag IS NOT set, so this view draws. We need to // remove it from the transparent region. - getLocationInWindow(mLocation); - region.op(mLocation[0], mLocation[1], - mLocation[0] + mRight - mLeft, mLocation[1] + mBottom - mTop, - Region.Op.DIFFERENCE); + final int[] location = attachInfo.mTransparentLocation; + getLocationInWindow(location); + region.op(location[0], location[1], location[0] + mRight - mLeft, + location[1] + mBottom - mTop, Region.Op.DIFFERENCE); } else if ((pflags & ONLY_DRAWS_BACKGROUND) != 0 && mBGDrawable != null) { // The ONLY_DRAWS_BACKGROUND flag IS set and the background drawable // exists, so we remove the background drawable's non-transparent @@ -6952,7 +7260,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * * The sound effect will only be played if sound effects are enabled by the user, and * {@link #isSoundEffectsEnabled()} is true. - * + * * @param soundConstant One of the constants defined in {@link SoundEffectConstants} */ protected void playSoundEffect(int soundConstant) { @@ -6967,7 +7275,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * update a Region being computed for {@link #gatherTransparentRegion} so * that any non-transparent parts of the Drawable are removed from the * given transparent region. - * + * * @param dr The Drawable whose transparency is to be applied to the region. * @param region A Region holding the current transparency information, * where any parts of the region that are set are considered to be @@ -6982,7 +7290,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } final Region r = dr.getTransparentRegion(); final Rect db = dr.getBounds(); - if (r != null) { + final AttachInfo attachInfo = mAttachInfo; + if (r != null && attachInfo != null) { final int w = getRight()-getLeft(); final int h = getBottom()-getTop(); if (db.left > 0) { @@ -7001,8 +7310,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback { //Log.i("VIEW", "Drawable bottom " + db.bottom + " < view " + h); r.op(0, db.bottom, w, h, Region.Op.UNION); } - getLocationInWindow(mLocation); - r.translate(mLocation[0], mLocation[1]); + final int[] location = attachInfo.mTransparentLocation; + getLocationInWindow(location); + r.translate(location[0], location[1]); region.op(r, Region.Op.INTERSECT); } else { region.op(db, Region.Op.DIFFERENCE); @@ -7177,10 +7487,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } class CheckForLongPress implements Runnable { - + private int mOriginalWindowAttachCount; - - public void run() { + + public void run() { if (isPressed() && (mParent != null) && hasWindowFocus() && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick()) { @@ -7188,7 +7498,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } } } - + public void rememberWindowAttachCount() { mOriginalWindowAttachCount = mWindowAttachCount; } @@ -7279,7 +7589,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Called when the context menu for this view is being built. It is not * safe to hold onto the menu after this method returns. - * + * * @param menu The context menu that is being built * @param v The view for which the context menu is being built * @param menuInfo Extra information about the item for which the @@ -7297,12 +7607,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Base class for derived classes that want to save and restore their own - * state in {@link #onSaveInstanceState}. + * state in {@link android.view.View#onSaveInstanceState()}. */ public static class BaseSavedState extends AbsSavedState { /** * Constructor used when reading from a parcel. Reads the state of the superclass. - * + * * @param source */ public BaseSavedState(Parcel source) { @@ -7311,7 +7621,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Constructor called by derived classes when creating their SavedState objects - * + * * @param superState The state of the superclass of this view */ public BaseSavedState(Parcelable superState) { @@ -7340,11 +7650,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback { void playSoundEffect(int effectId); } - IBinder mWindowToken; + final IWindowSession mSession; + + final IWindow mWindow; + + final IBinder mWindowToken; + + final SoundEffectPlayer mSoundEffectPlayer; + + /** + * The top view of the hierarchy. + */ + View mRootView; + IBinder mPanelParentWindowToken; Surface mSurface; - IWindowSession mSession; - SoundEffectPlayer mSoundEffectPlayer; /** * Left position of this view's window @@ -7357,6 +7677,37 @@ public class View implements Drawable.Callback, KeyEvent.Callback { int mWindowTop; /** + * For windows that are full-screen but using insets to layout inside + * of the screen decorations, these are the current insets for the + * content of the window. + */ + final Rect mContentInsets = new Rect(); + + /** + * For windows that are full-screen but using insets to layout inside + * of the screen decorations, these are the current insets for the + * actual visible parts of the window. + */ + final Rect mVisibleInsets = new Rect(); + + /** + * The internal insets given by this window. This value is + * supplied by the client (through + * {@link ViewTreeObserver.OnComputeInternalInsetsListener}) and will + * be given to the window manager when changed to be used in laying + * out windows behind it. + */ + final ViewTreeObserver.InternalInsetsInfo mGivenInternalInsets + = new ViewTreeObserver.InternalInsetsInfo(); + + /** + * All views in the window's hierarchy that serve as scroll containers, + * used to determine if the window can be resized or must be panned + * to adjust for a soft input area. + */ + final ArrayList<View> mScrollContainers = new ArrayList<View>(); + + /** * Indicates whether the view's window currently has the focus. */ boolean mHasWindowFocus; @@ -7365,7 +7716,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * The current visibility of the window. */ int mWindowVisibility; - + /** * Indicates the time at which drawing started to occur. */ @@ -7375,7 +7726,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Indicates whether the view's window is currently in touch mode. */ boolean mInTouchMode; - + /** * Indicates that ViewRoot should trigger a global layout change * the next time it performs a traversal @@ -7387,12 +7738,29 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * recomputed. */ boolean mAttributesChanged; - + /** * Set during a traveral if any views want to keep the screen on. */ boolean mKeepScreenOn; - + + /** + * Set if the visibility of any views has changed. + */ + boolean mViewVisibilityChanged; + + /** + * Global to the view hierarchy used as a temporary for dealing with + * x/y points in the transparent region computations. + */ + final int[] mTransparentLocation = new int[2]; + + /** + * Global to the view hierarchy used as a temporary for dealing with + * x/y points in the ViewGroup.invalidateChild implementation. + */ + final int[] mInvalidateChildLocation = new int[2]; + /** * The view tree observer used to dispatch global events like * layout, pre-draw, touch mode change, etc. @@ -7422,17 +7790,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback { */ static final int INVALIDATE_RECT_MSG = 0x2; - AttachInfo(Handler handler) { - this(handler, null); - } - + /** + * Temporary for use in computing invalidate rectangles while + * calling up the hierarchy. + */ + final Rect mTmpInvalRect = new Rect(); + /** * Creates a new set of attachment information with the specified * events handler and thread. * * @param handler the events handler the view must use */ - AttachInfo(Handler handler, SoundEffectPlayer effectPlayer) { + AttachInfo(IWindowSession session, IWindow window, + Handler handler, SoundEffectPlayer effectPlayer) { + mSession = session; + mWindow = window; + mWindowToken = window.asBinder(); mHandler = handler; mSoundEffectPlayer = effectPlayer; } diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 38be806..b7110ce 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -46,6 +46,13 @@ public class ViewConfiguration { private static final int LONG_PRESS_TIMEOUT = 500; /** + * Defines the duration in milliseconds a user needs to hold down the + * appropriate button to bring up the global actions dialog (power off, + * lock screen, etc). + */ + private static final int GLOBAL_ACTIONS_KEY_TIMEOUT = 500; + + /** * Defines the duration in milliseconds we will wait to see if a touch event * is a top or a scroll. If the user does not move within this interval, it is * considered to be a tap. @@ -66,14 +73,6 @@ public class ViewConfiguration { private static final int ZOOM_CONTROLS_TIMEOUT = 3000; /** - * Defines the duration in milliseconds a user needs to hold down the - * appropriate button to bring up the global actions dialog (power off, - * lock screen, etc). - */ - private static final int GLOBAL_ACTIONS_KEY_TIMEOUT = 1000; - - - /** * Inset in pixels to look for touchable content when the user touches the edge of the screen */ private static final int EDGE_SLOP = 12; diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index 1bf46b4..883c7bd 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -19,6 +19,7 @@ package android.view; import android.util.Log; import android.content.res.Resources; import android.graphics.Bitmap; +import android.os.Environment; import java.io.File; import java.io.BufferedWriter; @@ -60,7 +61,19 @@ public class ViewDebug { * check that this value is set to true as not to affect performance. */ public static final boolean TRACE_RECYCLER = false; - + + /** + * The system property of dynamic switch for capturing view information + * when it is set, we dump interested fields and methods for the view on focus + */ + static final String SYSTEM_PROPERTY_CAPTURE_VIEW = "debug.captureview"; + + /** + * The system property of dynamic switch for capturing event information + * when it is set, we log key events, touch/motion and trackball events + */ + static final String SYSTEM_PROPERTY_CAPTURE_EVENT = "debug.captureevent"; + /** * This annotation can be used to mark fields and methods to be dumped by * the view server. Only non-void methods with no arguments can be annotated @@ -143,6 +156,29 @@ public class ViewDebug { */ String to(); } + + /** + * This annotation can be used to mark fields and methods to be dumped when + * the view is captured. Methods with this annotation must have no arguments + * and must return <some type of data>. + * + * @hide pending API Council approval + */ + @Target({ ElementType.FIELD, ElementType.METHOD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface CapturedViewProperty { + /** + * When retrieveReturn is true, we need to retrieve second level methods + * e.g., we need myView.getFirstLevelMethod().getSecondLevelMethod() + * we will set retrieveReturn = true on the annotation of + * myView.getFirstLevelMethod() + * @return true if we need the second level methods + */ + boolean retrieveReturn() default false; + } + + private static HashMap<Class<?>, Method[]> mCapturedViewMethodsForClasses = null; + private static HashMap<Class<?>, Field[]> mCapturedViewFieldsForClasses = null; // Maximum delay in ms after which we stop trying to capture a View's drawing private static final int CAPTURE_TIMEOUT = 4000; @@ -154,7 +190,7 @@ public class ViewDebug { private static HashMap<Class<?>, Field[]> sFieldsForClasses; private static HashMap<Class<?>, Method[]> sMethodsForClasses; - + /** * Defines the type of hierarhcy trace to output to the hierarchy traces file. */ @@ -250,8 +286,8 @@ public class ViewDebug { /** * Starts tracing the view recycler of the specified view. The trace is identified by a prefix, - * used to build the traces files names: <code>/tmp/view-recycler/PREFIX.traces</code> and - * <code>/tmp/view-recycler/PREFIX.recycler</code>. + * used to build the traces files names: <code>/EXTERNAL/view-recycler/PREFIX.traces</code> and + * <code>/EXTERNAL/view-recycler/PREFIX.recycler</code>. * * Only one view recycler can be traced at the same time. After calling this method, any * other invocation will result in a <code>IllegalStateException</code> unless @@ -287,10 +323,10 @@ public class ViewDebug { /** * Stops the current view recycer tracing. * - * Calling this method creates the file <code>/tmp/view-recycler/PREFIX.traces</code> + * Calling this method creates the file <code>/EXTERNAL/view-recycler/PREFIX.traces</code> * containing all the traces (or method calls) relative to the specified view's recycler. * - * Calling this method creates the file <code>/tmp/view-recycler/PREFIX.recycler</code> + * Calling this method creates the file <code>/EXTERNAL/view-recycler/PREFIX.recycler</code> * containing all of the views used by the recycler of the view supplied to * {@link #startRecyclerTracing(String, View)}. * @@ -310,7 +346,7 @@ public class ViewDebug { " stopRecyclerTracing()!"); } - File recyclerDump = new File("/tmp/view-recycler/"); + File recyclerDump = new File(Environment.getExternalStorageDirectory(), "view-recycler/"); recyclerDump.mkdirs(); recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".recycler"); @@ -329,7 +365,7 @@ public class ViewDebug { return; } - recyclerDump = new File("/tmp/view-recycler/"); + recyclerDump = new File(Environment.getExternalStorageDirectory(), "view-recycler/"); recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".traces"); try { final FileOutputStream file = new FileOutputStream(recyclerDump); @@ -384,14 +420,14 @@ public class ViewDebug { /** * Starts tracing the view hierarchy of the specified view. The trace is identified by a prefix, - * used to build the traces files names: <code>/tmp/view-hierarchy/PREFIX.traces</code> and - * <code>/tmp/view-hierarchy/PREFIX.tree</code>. + * used to build the traces files names: <code>/EXTERNAL/view-hierarchy/PREFIX.traces</code> and + * <code>/EXTERNAL/view-hierarchy/PREFIX.tree</code>. * * Only one view hierarchy can be traced at the same time. After calling this method, any * other invocation will result in a <code>IllegalStateException</code> unless * {@link #stopHierarchyTracing()} is invoked before. * - * Calling this method creates the file <code>/tmp/view-hierarchy/PREFIX.traces</code> + * Calling this method creates the file <code>/EXTERNAL/view-hierarchy/PREFIX.traces</code> * containing all the traces (or method calls) relative to the specified view's hierarchy. * * This method will return immediately if TRACE_HIERARCHY is false. @@ -413,7 +449,7 @@ public class ViewDebug { " a new trace!"); } - File hierarchyDump = new File("/tmp/view-hierarchy/"); + File hierarchyDump = new File(Environment.getExternalStorageDirectory(), "view-hierarchy/"); hierarchyDump.mkdirs(); hierarchyDump = new File(hierarchyDump, prefix + ".traces"); @@ -431,10 +467,11 @@ public class ViewDebug { /** * Stops the current view hierarchy tracing. This method closes the file - * <code>/tmp/view-hierarchy/PREFIX.traces</code>. + * <code>/EXTERNAL/view-hierarchy/PREFIX.traces</code>. * - * Calling this method creates the file <code>/tmp/view-hierarchy/PREFIX.tree</code> containing - * the view hierarchy of the view supplied to {@link #startHierarchyTracing(String, View)}. + * Calling this method creates the file <code>/EXTERNAL/view-hierarchy/PREFIX.tree</code> + * containing the view hierarchy of the view supplied to + * {@link #startHierarchyTracing(String, View)}. * * This method will return immediately if TRACE_HIERARCHY is false. * @@ -459,7 +496,7 @@ public class ViewDebug { } sHierarchyTraces = null; - File hierarchyDump = new File("/tmp/view-hierarchy/"); + File hierarchyDump = new File(Environment.getExternalStorageDirectory(), "view-hierarchy/"); hierarchyDump.mkdirs(); hierarchyDump = new File(hierarchyDump, sHierarchyTracePrefix + ".tree"); @@ -484,7 +521,7 @@ public class ViewDebug { sHierarhcyRoot = null; } - + static void dispatchCommand(View view, String command, String parameters, OutputStream clientStream) throws IOException { @@ -714,10 +751,10 @@ public class ViewDebug { final ArrayList<Method> foundMethods = new ArrayList<Method>(); methods = klass.getDeclaredMethods(); - + int count = methods.length; for (int i = 0; i < count; i++) { - final Method method = methods[i]; + final Method method = methods[i]; if (method.getParameterTypes().length == 0 && method.isAnnotationPresent(ExportedProperty.class) && method.getReturnType() != Void.class) { @@ -746,7 +783,7 @@ public class ViewDebug { klass = klass.getSuperclass(); } while (klass != Object.class); } - + private static void exportMethods(Object view, BufferedWriter out, Class<?> klass, String prefix) throws IOException { @@ -767,8 +804,12 @@ public class ViewDebug { final Resources resources = ((View) view).getContext().getResources(); final int id = (Integer) methodValue; if (id >= 0) { - methodValue = resources.getResourceTypeName(id) + '/' + - resources.getResourceEntryName(id); + try { + methodValue = resources.getResourceTypeName(id) + '/' + + resources.getResourceEntryName(id); + } catch (Resources.NotFoundException e) { + methodValue = "UNKNOWN"; + } } else { methodValue = "NO_ID"; } @@ -839,8 +880,12 @@ public class ViewDebug { final Resources resources = ((View) view).getContext().getResources(); final int id = field.getInt(view); if (id >= 0) { - fieldValue = resources.getResourceTypeName(id) + '/' + - resources.getResourceEntryName(id); + try { + fieldValue = resources.getResourceTypeName(id) + '/' + + resources.getResourceEntryName(id); + } catch (Resources.NotFoundException e) { + fieldValue = "UNKNOWN"; + } } else { fieldValue = "NO_ID"; } @@ -924,4 +969,160 @@ public class ViewDebug { } return true; } + + private static Field[] capturedViewGetPropertyFields(Class<?> klass) { + if (mCapturedViewFieldsForClasses == null) { + mCapturedViewFieldsForClasses = new HashMap<Class<?>, Field[]>(); + } + final HashMap<Class<?>, Field[]> map = mCapturedViewFieldsForClasses; + + Field[] fields = map.get(klass); + if (fields != null) { + return fields; + } + + final ArrayList<Field> foundFields = new ArrayList<Field>(); + fields = klass.getFields(); + + int count = fields.length; + for (int i = 0; i < count; i++) { + final Field field = fields[i]; + if (field.isAnnotationPresent(CapturedViewProperty.class)) { + field.setAccessible(true); + foundFields.add(field); + } + } + + fields = foundFields.toArray(new Field[foundFields.size()]); + map.put(klass, fields); + + return fields; + } + + private static Method[] capturedViewGetPropertyMethods(Class<?> klass) { + if (mCapturedViewMethodsForClasses == null) { + mCapturedViewMethodsForClasses = new HashMap<Class<?>, Method[]>(); + } + final HashMap<Class<?>, Method[]> map = mCapturedViewMethodsForClasses; + + Method[] methods = map.get(klass); + if (methods != null) { + return methods; + } + + final ArrayList<Method> foundMethods = new ArrayList<Method>(); + methods = klass.getMethods(); + + int count = methods.length; + for (int i = 0; i < count; i++) { + final Method method = methods[i]; + if (method.getParameterTypes().length == 0 && + method.isAnnotationPresent(CapturedViewProperty.class) && + method.getReturnType() != Void.class) { + method.setAccessible(true); + foundMethods.add(method); + } + } + + methods = foundMethods.toArray(new Method[foundMethods.size()]); + map.put(klass, methods); + + return methods; + } + + private static String capturedViewExportMethods(Object obj, Class<?> klass, + String prefix) { + + if (obj == null) { + return "null"; + } + + StringBuilder sb = new StringBuilder(); + final Method[] methods = capturedViewGetPropertyMethods(klass); + + int count = methods.length; + for (int i = 0; i < count; i++) { + final Method method = methods[i]; + try { + Object methodValue = method.invoke(obj, (Object[]) null); + final Class<?> returnType = method.getReturnType(); + + CapturedViewProperty property = method.getAnnotation(CapturedViewProperty.class); + if (property.retrieveReturn()) { + //we are interested in the second level data only + sb.append(capturedViewExportMethods(methodValue, returnType, method.getName() + "#")); + } else { + sb.append(prefix); + sb.append(method.getName()); + sb.append("()="); + + if (methodValue != null) { + final String value = methodValue.toString().replace("\n", "\\n"); + sb.append(value); + } else { + sb.append("null"); + } + sb.append("; "); + } + } catch (IllegalAccessException e) { + //Exception IllegalAccess, it is OK here + //we simply ignore this method + } catch (InvocationTargetException e) { + //Exception InvocationTarget, it is OK here + //we simply ignore this method + } + } + return sb.toString(); + } + + private static String capturedViewExportFields(Object obj, Class<?> klass, String prefix) { + + if (obj == null) { + return "null"; + } + + StringBuilder sb = new StringBuilder(); + final Field[] fields = capturedViewGetPropertyFields(klass); + + int count = fields.length; + for (int i = 0; i < count; i++) { + final Field field = fields[i]; + try { + Object fieldValue = field.get(obj); + + sb.append(prefix); + sb.append(field.getName()); + sb.append("="); + + if (fieldValue != null) { + final String value = fieldValue.toString().replace("\n", "\\n"); + sb.append(value); + } else { + sb.append("null"); + } + sb.append(' '); + } catch (IllegalAccessException e) { + //Exception IllegalAccess, it is OK here + //we simply ignore this field + } + } + return sb.toString(); + } + + /** + * dump view info for id based instrument test generation + * (and possibly further data analysis). The results are dumped + * to the log. + * @param tag for log + * @param view for dump + * + * @hide pending API Council approval + */ + public static void dumpCapturedView(String tag, Object view) { + Class<?> klass = view.getClass(); + StringBuilder sb = new StringBuilder(klass.getName() + ": "); + sb.append(capturedViewExportFields(view, klass, "")); + sb.append(capturedViewExportMethods(view, klass, "")); + Log.d(tag, sb.toString()); + } } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 9063821..e26a19e 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -73,7 +73,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager private View mFocused; // The current transformation to apply on the child being drawn - private final Transformation mChildTransformation = new Transformation(); + private Transformation mChildTransformation; // Target of Motion events private View mMotionTarget; @@ -148,9 +148,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * {@link #getChildStaticTransformation(View, android.view.animation.Transformation)} should * set this flags in {@link #mGroupFlags}. * - * This flag needs to be removed until we can add a setter for it. People - * can't be directly stuffing values in to mGroupFlags!!! - * * {@hide} */ protected static final int FLAG_SUPPORT_STATIC_TRANSFORMATIONS = 0x800; @@ -466,27 +463,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** - * Called when a child of this group wants a particular rectangle to be - * positioned onto the screen. {@link ViewGroup}s overriding this can trust - * that: - * <ul> - * <li>child will be a direct child of this group</li> - * <li>rectangle will be in the child's coordinates</li> - * </ul> - * - * <p>{@link ViewGroup}s overriding this should uphold the contract:</p> - * <ul> - * <li>nothing will change if the rectangle is already visible</li> - * <li>the view port will be scrolled only just enough to make the - * rectangle visible</li> - * <ul> - * - * @param child The direct child making the request. - * @param rectangle The rectangle in the child's coordinates the child - * wishes to be on the screen. - * @param immediate True to forbid animated or delayed scrolling, - * false otherwise - * @return Whether the group scrolled to handle the operation + * {@inheritDoc} */ public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { return false; @@ -727,6 +704,19 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * {@inheritDoc} */ @Override + public boolean dispatchKeyEventPreIme(KeyEvent event) { + if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) { + return super.dispatchKeyEventPreIme(event); + } else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) { + return mFocused.dispatchKeyEventPreIme(event); + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override public boolean dispatchKeyEvent(KeyEvent event) { if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) { return super.dispatchKeyEvent(event); @@ -740,6 +730,19 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * {@inheritDoc} */ @Override + public boolean dispatchKeyShortcutEvent(KeyEvent event) { + if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) { + return super.dispatchKeyShortcutEvent(event); + } else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) { + return mFocused.dispatchKeyShortcutEvent(event); + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override public boolean dispatchTrackballEvent(MotionEvent event) { if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) { return super.dispatchTrackballEvent(event); @@ -1314,7 +1317,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int flags = mGroupFlags; if ((flags & FLAG_CLEAR_TRANSFORMATION) == FLAG_CLEAR_TRANSFORMATION) { - mChildTransformation.clear(); + if (mChildTransformation != null) { + mChildTransformation.clear(); + } mGroupFlags &= ~FLAG_CLEAR_TRANSFORMATION; } @@ -1328,6 +1333,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager child.onAnimationStart(); } + if (mChildTransformation == null) { + mChildTransformation = new Transformation(); + } more = a.getTransformation(drawingTime, mChildTransformation); transformToApply = mChildTransformation; @@ -1347,6 +1355,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) == FLAG_SUPPORT_STATIC_TRANSFORMATIONS) { + if (mChildTransformation == null) { + mChildTransformation = new Transformation(); + } final boolean hasTransform = getChildStaticTransformation(child, mChildTransformation); if (hasTransform) { final int transformType = mChildTransformation.getTransformationType(); @@ -1507,7 +1518,27 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * When this property is set to true, this ViewGroup supports static transformations on + * children; this causes + * {@link #getChildStaticTransformation(View, android.view.animation.Transformation)} to be + * invoked when a child is drawn. + * + * Any subclass overriding + * {@link #getChildStaticTransformation(View, android.view.animation.Transformation)} should + * set this property to true. + * + * @param enabled True to enable static transformations on children, false otherwise. + * + * @see #FLAG_SUPPORT_STATIC_TRANSFORMATIONS + */ + protected void setStaticTransformationsEnabled(boolean enabled) { + setBooleanFlag(FLAG_SUPPORT_STATIC_TRANSFORMATIONS, enabled); + } + + /** * {@inheritDoc} + * + * @see #setStaticTransformationsEnabled(boolean) */ protected boolean getChildStaticTransformation(View child, Transformation t) { return false; @@ -1969,7 +2000,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (view.getAnimation() != null) { addDisappearingView(view); - } else if (mAttachInfo != null) { + } else if (view.mAttachInfo != null) { view.dispatchDetachedFromWindow(); } @@ -2107,7 +2138,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (animate && child.getAnimation() != null) { addDisappearingView(child); - } else if (mAttachInfo != null) { + } else if (child.mAttachInfo != null) { child.dispatchDetachedFromWindow(); } @@ -2205,7 +2236,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** - * Detaches all views from theparent. Detaching a view should be temporary and followed + * Detaches all views from the parent. Detaching a view should be temporary and followed * either by a call to {@link #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)} * or a call to {@link #removeDetachedView(View, boolean)}. When a view is detached, * its parent is null and cannot be retrieved by a call to {@link #getChildAt(int)}. @@ -2242,13 +2273,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager ViewParent parent = this; - final int[] location = mLocation; - location[CHILD_LEFT_INDEX] = child.mLeft; - location[CHILD_TOP_INDEX] = child.mTop; - - do { - parent = parent.invalidateChildInParent(location, dirty); - } while (parent != null); + final AttachInfo attachInfo = mAttachInfo; + if (attachInfo != null) { + final int[] location = attachInfo.mInvalidateChildLocation; + location[CHILD_LEFT_INDEX] = child.mLeft; + location[CHILD_TOP_INDEX] = child.mTop; + + do { + parent = parent.invalidateChildInParent(location, dirty); + } while (parent != null); + } } /** @@ -2917,7 +2951,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (disappearingChildren.contains(view)) { disappearingChildren.remove(view); - if (mAttachInfo != null) { + if (view.mAttachInfo != null) { view.dispatchDetachedFromWindow(); } diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index 1a5d495..b456c5d 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -182,4 +182,30 @@ public interface ViewParent { * intercept touch events. */ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept); + + /** + * Called when a child of this group wants a particular rectangle to be + * positioned onto the screen. {@link ViewGroup}s overriding this can trust + * that: + * <ul> + * <li>child will be a direct child of this group</li> + * <li>rectangle will be in the child's coordinates</li> + * </ul> + * + * <p>{@link ViewGroup}s overriding this should uphold the contract:</p> + * <ul> + * <li>nothing will change if the rectangle is already visible</li> + * <li>the view port will be scrolled only just enough to make the + * rectangle visible</li> + * <ul> + * + * @param child The direct child making the request. + * @param rectangle The rectangle in the child's coordinates the child + * wishes to be on the screen. + * @param immediate True to forbid animated or delayed scrolling, + * false otherwise + * @return Whether the group scrolled to handle the operation + */ + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, + boolean immediate); } diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java index ca67404..db0b368 100644 --- a/core/java/android/view/ViewRoot.java +++ b/core/java/android/view/ViewRoot.java @@ -16,18 +16,26 @@ package android.view; +import com.android.internal.view.IInputMethodCallback; +import com.android.internal.view.IInputMethodSession; + import android.graphics.Canvas; import android.graphics.PixelFormat; +import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.Region; import android.os.*; import android.os.Process; +import android.os.SystemProperties; import android.util.AndroidRuntimeException; import android.util.Config; import android.util.Log; import android.util.EventLog; +import android.util.SparseArray; import android.view.View.MeasureSpec; +import android.view.inputmethod.InputMethodManager; +import android.widget.Scroller; import android.content.pm.PackageManager; import android.content.Context; import android.app.ActivityManagerNative; @@ -51,15 +59,19 @@ import static javax.microedition.khronos.opengles.GL10.*; * {@hide} */ @SuppressWarnings({"EmptyCatchBlock"}) -final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.SoundEffectPlayer { +public final class ViewRoot extends Handler implements ViewParent, + View.AttachInfo.SoundEffectPlayer { private static final String TAG = "ViewRoot"; private static final boolean DBG = false; @SuppressWarnings({"ConstantConditionalExpression"}) private static final boolean LOCAL_LOGV = false ? Config.LOGD : Config.LOGV; /** @noinspection PointlessBooleanExpression*/ private static final boolean DEBUG_DRAW = false || LOCAL_LOGV; + private static final boolean DEBUG_LAYOUT = false || LOCAL_LOGV; + private static final boolean DEBUG_INPUT_RESIZE = false || LOCAL_LOGV; private static final boolean DEBUG_ORIENTATION = false || LOCAL_LOGV; - private static final boolean DEBUG_TRACKBALL = LOCAL_LOGV; + private static final boolean DEBUG_TRACKBALL = false || LOCAL_LOGV; + private static final boolean DEBUG_IMF = false || LOCAL_LOGV; private static final boolean WATCH_POINTER = false; static final boolean PROFILE_DRAWING = false; @@ -75,85 +87,103 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun */ static final int MAX_TRACKBALL_DELAY = 250; - private static long sInstanceCount = 0; + static long sInstanceCount = 0; - private static IWindowSession sWindowSession; + static IWindowSession sWindowSession; - private static final Object mStaticInit = new Object(); - private static boolean mInitialized = false; + static final Object mStaticInit = new Object(); + static boolean mInitialized = false; static final ThreadLocal<Handler> sUiThreads = new ThreadLocal<Handler>(); static final RunQueue sRunQueue = new RunQueue(); - private long mLastTrackballTime = 0; - private final TrackballAxis mTrackballAxisX = new TrackballAxis(); - private final TrackballAxis mTrackballAxisY = new TrackballAxis(); + long mLastTrackballTime = 0; + final TrackballAxis mTrackballAxisX = new TrackballAxis(); + final TrackballAxis mTrackballAxisY = new TrackballAxis(); - private final Thread mThread; + final int[] mTmpLocation = new int[2]; + + final InputMethodCallback mInputMethodCallback; + final SparseArray<Object> mPendingEvents = new SparseArray<Object>(); + int mPendingEventSeq = 0; + + final Thread mThread; - private final WindowLeaked mLocation; + final WindowLeaked mLocation; - private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams(); + final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams(); final W mWindow; - private View mView; - private View mFocusedView; - private int mViewVisibility; - private boolean mAppVisible = true; + View mView; + View mFocusedView; + int mViewVisibility; + boolean mAppVisible = true; - private final Region mTransparentRegion; - private final Region mPreviousTransparentRegion; + final Region mTransparentRegion; + final Region mPreviousTransparentRegion; - private int mWidth; - private int mHeight; - private Rect mDirty; // will be a graphics.Region soon + int mWidth; + int mHeight; + Rect mDirty; // will be a graphics.Region soon - private final View.AttachInfo mAttachInfo; + final View.AttachInfo mAttachInfo; - private final Rect mTempRect; // used in the transaction to not thrash the heap. + final Rect mTempRect; // used in the transaction to not thrash the heap. + final Rect mVisRect; // used to retrieve visible rect of focused view. + final Point mVisPoint; // used to retrieve global offset of focused view. - private boolean mTraversalScheduled; - private boolean mWillDrawSoon; - private boolean mLayoutRequested; - private boolean mFirst; - private boolean mReportNextDraw; - private boolean mFullRedrawNeeded; - private boolean mNewSurfaceNeeded; + boolean mTraversalScheduled; + boolean mWillDrawSoon; + boolean mLayoutRequested; + boolean mFirst; + boolean mReportNextDraw; + boolean mFullRedrawNeeded; + boolean mNewSurfaceNeeded; + boolean mHasHadWindowFocus; - private boolean mWindowAttributesChanged = false; + boolean mWindowAttributesChanged = false; // These can be accessed by any thread, must be protected with a lock. - private Surface mSurface; + Surface mSurface; - private boolean mAdded; - private boolean mAddedTouchMode; + boolean mAdded; + boolean mAddedTouchMode; /*package*/ int mAddNesting; // These are accessed by multiple threads. - private final Rect mWinFrame; // frame given by window manager. - - private final Rect mCoveredInsets = new Rect(); - private final Rect mNewCoveredInsets = new Rect(); - - private EGL10 mEgl; - private EGLDisplay mEglDisplay; - private EGLContext mEglContext; - private EGLSurface mEglSurface; - private GL11 mGL; - private Canvas mGlCanvas; - private boolean mUseGL; - private boolean mGlWanted; + final Rect mWinFrame; // frame given by window manager. + + final Rect mPendingVisibleInsets = new Rect(); + final Rect mPendingContentInsets = new Rect(); + final ViewTreeObserver.InternalInsetsInfo mLastGivenInsets + = new ViewTreeObserver.InternalInsetsInfo(); + + boolean mScrollMayChange; + int mSoftInputMode; + View mLastScrolledFocus; + int mScrollY; + int mCurScrollY; + Scroller mScroller; + + EGL10 mEgl; + EGLDisplay mEglDisplay; + EGLContext mEglContext; + EGLSurface mEglSurface; + GL11 mGL; + Canvas mGlCanvas; + boolean mUseGL; + boolean mGlWanted; /** * see {@link #playSoundEffect(int)} */ - private AudioManager mAudioManager; + AudioManager mAudioManager; - public ViewRoot() { + public ViewRoot(Context context) { super(); ++sInstanceCount; @@ -164,9 +194,10 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun synchronized (mStaticInit) { if (!mInitialized) { try { + InputMethodManager imm = InputMethodManager.getInstance(context); sWindowSession = IWindowManager.Stub.asInterface( ServiceManager.getService("window")) - .openSession(new Binder()); + .openSession(imm.getClient(), imm.getInputContext()); mInitialized = true; } catch (RemoteException e) { } @@ -180,8 +211,11 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun mHeight = -1; mDirty = new Rect(); mTempRect = new Rect(); + mVisRect = new Rect(); + mVisPoint = new Point(); mWinFrame = new Rect(); mWindow = new W(this); + mInputMethodCallback = new InputMethodCallback(this); mViewVisibility = View.GONE; mTransparentRegion = new Region(); mPreviousTransparentRegion = new Region(); @@ -194,7 +228,7 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun handler = new RootHandler(); sUiThreads.set(handler); } - mAttachInfo = new View.AttachInfo(handler, this); + mAttachInfo = new View.AttachInfo(sWindowSession, mWindow, handler, this); } @Override @@ -350,8 +384,10 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun synchronized (this) { if (mView == null) { mWindowAttributes.copyFrom(attrs); + mSoftInputMode = attrs.softInputMode; mWindowAttributesChanged = true; mView = view; + mAttachInfo.mRootView = view; if (panelParentView != null) { mAttachInfo.mPanelParentWindowToken = panelParentView.getApplicationWindowToken(); @@ -366,16 +402,20 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun try { res = sWindowSession.add(mWindow, attrs, - getHostVisibility(), mCoveredInsets); + getHostVisibility(), mAttachInfo.mContentInsets); } catch (RemoteException e) { mAdded = false; mView = null; + mAttachInfo.mRootView = null; unscheduleTraversals(); throw new RuntimeException("Adding window failed", e); } + mPendingContentInsets.set(mAttachInfo.mContentInsets); + mPendingVisibleInsets.set(0, 0, 0, 0); if (Config.LOGV) Log.v("ViewRoot", "Added window " + mWindow); if (res < WindowManagerImpl.ADD_OKAY) { mView = null; + mAttachInfo.mRootView = null; mAdded = false; unscheduleTraversals(); switch (res) { @@ -427,9 +467,13 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun return mLocation; } - public void setLayoutParams(WindowManager.LayoutParams attrs) { + void setLayoutParams(WindowManager.LayoutParams attrs, boolean newView) { synchronized (this) { mWindowAttributes.copyFrom(attrs); + if (newView) { + mSoftInputMode = attrs.softInputMode; + requestLayout(); + } mWindowAttributesChanged = true; scheduleTraversals(); } @@ -467,6 +511,11 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun public void invalidateChild(View child, Rect dirty) { checkThread(); if (LOCAL_LOGV) Log.v(TAG, "Invalidate child: " + dirty); + if (mCurScrollY != 0) { + mTempRect.set(dirty); + mTempRect.offset(0, -mCurScrollY); + dirty = mTempRect; + } mDirty.union(dirty); if (!mWillDrawSoon) { scheduleTraversals(); @@ -486,6 +535,8 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun if (child != mView) { throw new RuntimeException("child is not mine, honest!"); } + // Note: don't apply scroll offset, because we want to know its + // visibility in the virtual canvas being given to the view hierarchy. return r.intersect(0, 0, mWidth, mHeight); } @@ -528,7 +579,7 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun boolean windowResizesToFitContent = false; boolean fullRedrawNeeded = mFullRedrawNeeded; boolean newSurface = false; - WindowManager.LayoutParams lp = (WindowManager.LayoutParams) host.getLayoutParams(); + WindowManager.LayoutParams lp = mWindowAttributes; int desiredWindowWidth; int desiredWindowHeight; @@ -544,7 +595,7 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun WindowManager.LayoutParams params = null; if (mWindowAttributesChanged) { mWindowAttributesChanged = false; - params = mWindowAttributes; + params = lp; } if (mFirst) { @@ -559,9 +610,7 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun // is attached to the window. Note that at this point the surface // object is not initialized to its backing store, but soon it // will be (assuming the window is visible). - attachInfo.mWindowToken = mWindow.asBinder(); attachInfo.mSurface = mSurface; - attachInfo.mSession = sWindowSession; attachInfo.mHasWindowFocus = false; attachInfo.mWindowVisibility = viewVisibility; attachInfo.mRecomputeGlobalAttributes = false; @@ -590,16 +639,35 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun destroyGL(); } } + if (viewVisibility == View.GONE) { + // After making a window gone, we will count it as being + // shown for the first time the next time it gets focus. + mHasHadWindowFocus = false; + } } + boolean insetsChanged = false; + if (mLayoutRequested) { if (mFirst) { - host.fitSystemWindows(mCoveredInsets); + host.fitSystemWindows(mAttachInfo.mContentInsets); // make sure touch mode code executes by setting cached value // to opposite of the added touch mode. mAttachInfo.mInTouchMode = !mAddedTouchMode; ensureTouchModeLocally(mAddedTouchMode); } else { + if (!mAttachInfo.mContentInsets.equals(mPendingContentInsets)) { + mAttachInfo.mContentInsets.set(mPendingContentInsets); + host.fitSystemWindows(mAttachInfo.mContentInsets); + insetsChanged = true; + if (DEBUG_LAYOUT) Log.v(TAG, "Content insets changing to: " + + mAttachInfo.mContentInsets); + } + if (!mAttachInfo.mVisibleInsets.equals(mPendingVisibleInsets)) { + mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets); + if (DEBUG_LAYOUT) Log.v(TAG, "Visible insets changing to: " + + mAttachInfo.mVisibleInsets); + } if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) { windowResizesToFitContent = true; @@ -614,7 +682,7 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); // Ask host how big it wants to be - if (DEBUG_ORIENTATION) Log.v("ViewRoot", + if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v("ViewRoot", "Measuring " + host + " in display " + desiredWindowWidth + "x" + desiredWindowHeight + "..."); host.measure(childWidthMeasureSpec, childHeightMeasureSpec); @@ -633,11 +701,37 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun attachInfo.mKeepScreenOn = false; host.dispatchCollectViewAttributes(0); if (attachInfo.mKeepScreenOn != oldVal) { - params = mWindowAttributes; + params = lp; //Log.i(TAG, "Keep screen on changed: " + attachInfo.mKeepScreenOn); } } + if (mFirst || attachInfo.mViewVisibilityChanged) { + attachInfo.mViewVisibilityChanged = false; + int resizeMode = mSoftInputMode & + WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST; + // If we are in auto resize mode, then we need to determine + // what mode to use now. + if (resizeMode == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED) { + final int N = attachInfo.mScrollContainers.size(); + for (int i=0; i<N; i++) { + if (attachInfo.mScrollContainers.get(i).isShown()) { + resizeMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + } + } + if (resizeMode == 0) { + resizeMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN; + } + if ((lp.softInputMode & + WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST) != resizeMode) { + lp.softInputMode = (lp.softInputMode & + ~WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST) | + resizeMode; + params = lp; + } + } + } + if (params != null && (host.mPrivateFlags & View.REQUEST_TRANSPARENT_REGIONS) != 0) { if (!PixelFormat.formatHasAlpha(params.format)) { params.format = PixelFormat.TRANSLUCENT; @@ -647,10 +741,26 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun boolean windowShouldResize = mLayoutRequested && windowResizesToFitContent && (mWidth != host.mMeasuredWidth || mHeight != host.mMeasuredHeight); + final boolean computesInternalInsets = + attachInfo.mTreeObserver.hasComputeInternalInsetsListeners(); + boolean insetsPending = false; int relayoutResult = 0; - if (mFirst || windowShouldResize || viewVisibilityChanged || params != null) { + if (mFirst || windowShouldResize || insetsChanged + || viewVisibilityChanged || params != null) { if (viewVisibility == View.VISIBLE) { + // If this window is giving internal insets to the window + // manager, and it is being added or changing its visibility, + // then we want to first give the window manager "fake" + // insets to cause it to effectively ignore the content of + // the window during layout. This avoids it briefly causing + // other windows to resize/move based on the raw frame of the + // window, waiting until we can finish laying out this window + // and get back to the window manager with the ultimately + // computed insets. + insetsPending = computesInternalInsets + && (mFirst || viewVisibilityChanged); + if (mWindowAttributes.memoryType == WindowManager.LayoutParams.MEMORY_TYPE_GPU) { if (params == null) { params = mWindowAttributes; @@ -661,7 +771,8 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun final Rect frame = mWinFrame; boolean initialized = false; - boolean coveredInsetsChanged = false; + boolean contentInsetsChanged = false; + boolean visibleInsetsChanged = false; try { boolean hadSurface = mSurface.isValid(); int fl = 0; @@ -673,31 +784,57 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun } relayoutResult = sWindowSession.relayout( mWindow, params, host.mMeasuredWidth, host.mMeasuredHeight, - viewVisibility, frame, mNewCoveredInsets, mSurface); + viewVisibility, insetsPending, frame, + mPendingContentInsets, mPendingVisibleInsets, mSurface); if (params != null) { params.flags = fl; } - coveredInsetsChanged = !mNewCoveredInsets.equals(mCoveredInsets); - if (coveredInsetsChanged) { - mCoveredInsets.set(mNewCoveredInsets); - host.fitSystemWindows(mCoveredInsets); + if (DEBUG_LAYOUT) Log.v(TAG, "relayout: frame=" + frame.toShortString() + + " content=" + mPendingContentInsets.toShortString() + + " visible=" + mPendingVisibleInsets.toShortString() + + " surface=" + mSurface); + + contentInsetsChanged = !mPendingContentInsets.equals( + mAttachInfo.mContentInsets); + visibleInsetsChanged = !mPendingVisibleInsets.equals( + mAttachInfo.mVisibleInsets); + if (contentInsetsChanged) { + mAttachInfo.mContentInsets.set(mPendingContentInsets); + host.fitSystemWindows(mAttachInfo.mContentInsets); + if (DEBUG_LAYOUT) Log.v(TAG, "Content insets changing to: " + + mAttachInfo.mContentInsets); + } + if (visibleInsetsChanged) { + mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets); + if (DEBUG_LAYOUT) Log.v(TAG, "Visible insets changing to: " + + mAttachInfo.mVisibleInsets); } - if (!hadSurface && mSurface.isValid()) { - // If we are creating a new surface, then we need to - // completely redraw it. Also, when we get to the - // point of drawing it we will hold off and schedule - // a new traversal instead. This is so we can tell the - // window manager about all of the windows being displayed - // before actually drawing them, so it can display then - // all at once. - newSurface = true; - fullRedrawNeeded = true; - - if (mGlWanted && !mUseGL) { - initializeGL(); - initialized = mGlCanvas != null; + if (!hadSurface) { + if (mSurface.isValid()) { + // If we are creating a new surface, then we need to + // completely redraw it. Also, when we get to the + // point of drawing it we will hold off and schedule + // a new traversal instead. This is so we can tell the + // window manager about all of the windows being displayed + // before actually drawing them, so it can display then + // all at once. + newSurface = true; + fullRedrawNeeded = true; + + if (mGlWanted && !mUseGL) { + initializeGL(); + initialized = mGlCanvas != null; + } + } + } else if (!mSurface.isValid()) { + // If the surface has been removed, then reset the scroll + // positions. + mLastScrolledFocus = null; + mScrollY = mCurScrollY = 0; + if (mScroller != null) { + mScroller.abortAnimation(); } } } catch (RemoteException e) { @@ -721,10 +858,16 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun boolean focusChangedDueToTouchMode = ensureTouchModeLocally( (relayoutResult&WindowManagerImpl.RELAYOUT_IN_TOUCH_MODE) != 0); if (focusChangedDueToTouchMode || mWidth != host.mMeasuredWidth - || mHeight != host.mMeasuredHeight || coveredInsetsChanged) { + || mHeight != host.mMeasuredHeight || contentInsetsChanged) { childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); + if (DEBUG_LAYOUT) Log.v(TAG, "Ooops, something changed! mWidth=" + + mWidth + " measuredWidth=" + host.mMeasuredWidth + + " mHeight=" + mHeight + + " measuredHeight" + host.mMeasuredHeight + + " coveredInsetsChanged=" + contentInsetsChanged); + // Ask host how big it wants to be host.measure(childWidthMeasureSpec, childHeightMeasureSpec); @@ -749,6 +892,9 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun } if (measureAgain) { + if (DEBUG_LAYOUT) Log.v(TAG, + "And hey let's measure once more: width=" + width + + " height=" + height); host.measure(childWidthMeasureSpec, childHeightMeasureSpec); } @@ -756,12 +902,14 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun } } - boolean triggerGlobalLayoutListener = mLayoutRequested + final boolean didLayout = mLayoutRequested; + boolean triggerGlobalLayoutListener = didLayout || attachInfo.mRecomputeGlobalAttributes; - if (mLayoutRequested) { + if (didLayout) { mLayoutRequested = false; - if (DEBUG_ORIENTATION) Log.v( - "ViewRoot", "Setting frame " + host + " to (" + + mScrollMayChange = true; + if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v( + "ViewRoot", "Laying out " + host + " to (" + host.mMeasuredWidth + ", " + host.mMeasuredHeight + ")"); long startTime; if (PROFILE_LAYOUT) { @@ -780,10 +928,10 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun if ((host.mPrivateFlags & View.REQUEST_TRANSPARENT_REGIONS) != 0) { // start out transparent // TODO: AVOID THAT CALL BY CACHING THE RESULT? - host.getLocationInWindow(host.mLocation); - mTransparentRegion.set(host.mLocation[0], host.mLocation[1], - host.mLocation[0] + host.mRight - host.mLeft, - host.mLocation[1] + host.mBottom - host.mTop); + host.getLocationInWindow(mTmpLocation); + mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1], + mTmpLocation[0] + host.mRight - host.mLeft, + mTmpLocation[1] + host.mBottom - host.mTop); host.gatherTransparentRegion(mTransparentRegion); if (!mTransparentRegion.equals(mPreviousTransparentRegion)) { @@ -809,6 +957,24 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun attachInfo.mTreeObserver.dispatchOnGlobalLayout(); } + if (computesInternalInsets) { + ViewTreeObserver.InternalInsetsInfo insets = attachInfo.mGivenInternalInsets; + final Rect givenContent = attachInfo.mGivenInternalInsets.contentInsets; + final Rect givenVisible = attachInfo.mGivenInternalInsets.visibleInsets; + givenContent.left = givenContent.top = givenContent.right + = givenContent.bottom = givenVisible.left = givenVisible.top + = givenVisible.right = givenVisible.bottom = 0; + attachInfo.mTreeObserver.dispatchOnComputeInternalInsets(insets); + if (insetsPending || !mLastGivenInsets.equals(insets)) { + mLastGivenInsets.set(insets); + try { + sWindowSession.setInsets(mWindow, insets.mTouchableInsets, + insets.contentInsets, insets.visibleInsets); + } catch (RemoteException e) { + } + } + } + if (mFirst) { // handle first focus request if (mView != null && !mView.hasFocus()) { @@ -841,7 +1007,7 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun } } else { // We were supposed to report when we are done drawing. Since we canceled the - // draw, rememeber it here. + // draw, remember it here. if ((relayoutResult&WindowManagerImpl.RELAYOUT_FIRST_TIME) != 0) { mReportNextDraw = true; } @@ -903,6 +1069,21 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun return; } + scrollToRectOrFocus(null, false); + + int yoff; + final boolean scrolling = mScroller != null + && mScroller.computeScrollOffset(); + if (scrolling) { + yoff = mScroller.getCurrY(); + } else { + yoff = mScrollY; + } + if (mCurScrollY != yoff) { + mCurScrollY = yoff; + fullRedrawNeeded = true; + } + Rect dirty = mDirty; if (mUseGL) { if (!dirty.isEmpty()) { @@ -914,7 +1095,9 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun mGL.glEnable(GL_SCISSOR_TEST); mAttachInfo.mDrawingTime = SystemClock.uptimeMillis(); + canvas.translate(0, -yoff); mView.draw(canvas); + canvas.translate(0, yoff); mEgl.eglSwapBuffers(mEglDisplay, mEglSurface); checkEglErrors(); @@ -928,6 +1111,10 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun } } } + if (scrolling) { + mFullRedrawNeeded = true; + scheduleTraversals(); + } return; } @@ -973,11 +1160,18 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun // background. This automatically respects the clip/dirty region if (!canvas.isOpaque()) { canvas.drawColor(0, PorterDuff.Mode.CLEAR); + } else if (yoff != 0) { + // If we are applying an offset, we need to clear the area + // where the offset doesn't appear to avoid having garbage + // left in the blank areas. + canvas.drawColor(0, PorterDuff.Mode.CLEAR); } dirty.setEmpty(); mAttachInfo.mDrawingTime = SystemClock.uptimeMillis(); + canvas.translate(0, -yoff); mView.draw(canvas); + canvas.translate(0, yoff); if (SHOW_FPS) { int now = (int)SystemClock.elapsedRealtime(); @@ -999,12 +1193,125 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun Log.v("ViewRoot", "Surface " + surface + " unlockCanvasAndPost"); } } + + if (scrolling) { + mFullRedrawNeeded = true; + scheduleTraversals(); + } } + boolean scrollToRectOrFocus(Rect rectangle, boolean immediate) { + final View.AttachInfo attachInfo = mAttachInfo; + final Rect ci = attachInfo.mContentInsets; + final Rect vi = attachInfo.mVisibleInsets; + int scrollY = 0; + boolean handled = false; + + if (vi.left > ci.left || vi.top > ci.top + || vi.right > ci.right || vi.bottom > ci.bottom) { + // We'll assume that we aren't going to change the scroll + // offset, since we want to avoid that unless it is actually + // going to make the focus visible... otherwise we scroll + // all over the place. + scrollY = mScrollY; + // We can be called for two different situations: during a draw, + // to update the scroll position if the focus has changed (in which + // case 'rectangle' is null), or in response to a + // requestChildRectangleOnScreen() call (in which case 'rectangle' + // is non-null and we just want to scroll to whatever that + // rectangle is). + View focus = mFocusedView; + if (focus != mLastScrolledFocus) { + // If the focus has changed, then ignore any requests to scroll + // to a rectangle; first we want to make sure the entire focus + // view is visible. + rectangle = null; + } + if (focus == mLastScrolledFocus && !mScrollMayChange + && rectangle == null) { + // Optimization: if the focus hasn't changed since last + // time, and no layout has happened, then just leave things + // as they are. + if (DEBUG_INPUT_RESIZE) Log.v(TAG, "Keeping scroll y=" + + mScrollY + " vi=" + vi.toShortString()); + } else if (focus != null) { + // We need to determine if the currently focused view is + // within the visible part of the window and, if not, apply + // a pan so it can be seen. + mLastScrolledFocus = focus; + mScrollMayChange = false; + // Try to find the rectangle from the focus view. + if (focus.getGlobalVisibleRect(mVisRect, null)) { + if (DEBUG_INPUT_RESIZE) Log.v(TAG, "Root w=" + + mView.getWidth() + " h=" + mView.getHeight() + + " ci=" + ci.toShortString() + + " vi=" + vi.toShortString()); + if (rectangle == null) { + focus.getFocusedRect(mTempRect); + if (DEBUG_INPUT_RESIZE) Log.v(TAG, "Focus " + focus + + ": focusRect=" + mTempRect.toShortString()); + ((ViewGroup) mView).offsetDescendantRectToMyCoords( + focus, mTempRect); + if (DEBUG_INPUT_RESIZE) Log.v(TAG, + "Focus in window: focusRect=" + + mTempRect.toShortString() + + " visRect=" + mVisRect.toShortString()); + } else { + mTempRect.set(rectangle); + if (DEBUG_INPUT_RESIZE) Log.v(TAG, + "Request scroll to rect: " + + mTempRect.toShortString() + + " visRect=" + mVisRect.toShortString()); + } + if (mTempRect.intersect(mVisRect)) { + if (DEBUG_INPUT_RESIZE) Log.v(TAG, + "Focus window visible rect: " + + mTempRect.toShortString()); + if (mTempRect.height() > + (mView.getHeight()-vi.top-vi.bottom)) { + // If the focus simply is not going to fit, then + // best is probably just to leave things as-is. + if (DEBUG_INPUT_RESIZE) Log.v(TAG, + "Too tall; leaving scrollY=" + scrollY); + } else if ((mTempRect.top-scrollY) < vi.top) { + scrollY -= vi.top - (mTempRect.top-scrollY); + if (DEBUG_INPUT_RESIZE) Log.v(TAG, + "Top covered; scrollY=" + scrollY); + } else if ((mTempRect.bottom-scrollY) + > (mView.getHeight()-vi.bottom)) { + scrollY += (mTempRect.bottom-scrollY) + - (mView.getHeight()-vi.bottom); + if (DEBUG_INPUT_RESIZE) Log.v(TAG, + "Bottom covered; scrollY=" + scrollY); + } + handled = true; + } + } + } + } + + if (scrollY != mScrollY) { + if (DEBUG_INPUT_RESIZE) Log.v(TAG, "Pan scroll changed: old=" + + mScrollY + " , new=" + scrollY); + if (!immediate) { + if (mScroller == null) { + mScroller = new Scroller(mView.getContext()); + } + mScroller.startScroll(0, mScrollY, 0, scrollY-mScrollY); + } else if (mScroller != null) { + mScroller.abortAnimation(); + } + mScrollY = scrollY; + } + + return handled; + } + public void requestChildFocus(View child, View focused) { checkThread(); if (mFocusedView != focused) { mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(mFocusedView, focused); + scheduleTraversals(); } mFocusedView = focused; } @@ -1063,6 +1370,7 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun mView.dispatchDetachedFromWindow(); } mView = null; + mAttachInfo.mRootView = null; if (mUseGL) { destroyGL(); } @@ -1081,16 +1389,17 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun } - private final static int DO_TRAVERSAL = 1000; - private final static int DIE = 1001; - private final static int RESIZED = 1002; - private final static int RESIZED_REPORT = 1003; - private final static int WINDOW_FOCUS_CHANGED = 1004; - private final static int DISPATCH_KEY = 1005; - private final static int DISPATCH_POINTER = 1006; - private final static int DISPATCH_TRACKBALL = 1007; - private final static int DISPATCH_APP_VISIBILITY = 1008; - private final static int DISPATCH_GET_NEW_SURFACE = 1009; + public final static int DO_TRAVERSAL = 1000; + public final static int DIE = 1001; + public final static int RESIZED = 1002; + public final static int RESIZED_REPORT = 1003; + public final static int WINDOW_FOCUS_CHANGED = 1004; + public final static int DISPATCH_KEY = 1005; + public final static int DISPATCH_POINTER = 1006; + public final static int DISPATCH_TRACKBALL = 1007; + public final static int DISPATCH_APP_VISIBILITY = 1008; + public final static int DISPATCH_GET_NEW_SURFACE = 1009; + public final static int FINISHED_EVENT = 1010; @Override public void handleMessage(Message msg) { @@ -1107,6 +1416,9 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun mProfile = false; } break; + case FINISHED_EVENT: + handleFinishedEvent(msg.arg1, msg.arg2 != 0); + break; case DISPATCH_KEY: if (LOCAL_LOGV) Log.v( "ViewRoot", "Dispatching key " @@ -1124,7 +1436,7 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun } didFinish = true; } else { - didFinish = false; + didFinish = event.getAction() == MotionEvent.ACTION_OUTSIDE; } try { @@ -1136,7 +1448,10 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun if (isDown) { ensureTouchMode(true); } - + if(Config.LOGV) { + captureMotionLog("captureDispatchPointer", event); + } + event.offsetLocation(0, mCurScrollY); handled = mView.dispatchTouchEvent(event); if (!handled && isDown) { int edgeSlop = ViewConfiguration.getEdgeSlop(); @@ -1207,7 +1522,11 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun handleGetNewSurface(); break; case RESIZED: - if (mWinFrame.width() == msg.arg1 && mWinFrame.height() == msg.arg2) { + Rect coveredInsets = ((Rect[])msg.obj)[0]; + Rect visibleInsets = ((Rect[])msg.obj)[1]; + if (mWinFrame.width() == msg.arg1 && mWinFrame.height() == msg.arg2 + && mPendingContentInsets.equals(coveredInsets) + && mPendingVisibleInsets.equals(visibleInsets)) { break; } // fall through... @@ -1217,6 +1536,8 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun mWinFrame.right = msg.arg1; mWinFrame.top = 0; mWinFrame.bottom = msg.arg2; + mPendingContentInsets.set(((Rect[])msg.obj)[0]); + mPendingVisibleInsets.set(((Rect[])msg.obj)[1]); if (msg.what == RESIZED_REPORT) { mReportNextDraw = true; } @@ -1245,6 +1566,18 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun if (mView != null) { mView.dispatchWindowFocusChanged(hasWindowFocus); } + + // Note: must be done after the focus change callbacks, + // so all of the view state is set up correctly. + if (hasWindowFocus) { + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + imm.onWindowFocus(mView.findFocus(), + mWindowAttributes.softInputMode, !mHasHadWindowFocus, + mWindowAttributes.flags); + } + mHasHadWindowFocus = true; + } } } break; case DIE: @@ -1387,7 +1720,7 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun didFinish = false; } - //Log.i("foo", "Motion event:" + event); + if (DEBUG_TRACKBALL) Log.v(TAG, "Motion event:" + event); boolean handled = false; try { @@ -1397,7 +1730,7 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun handled = mView.dispatchTrackballEvent(event); if (!handled) { // we could do something here, like changing the focus - // or someting? + // or something? } } } finally { @@ -1456,8 +1789,8 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun + " / Y=" + y.position + " step=" + y.step + " dir=" + y.dir + " acc=" + y.acceleration + " move=" + event.getY()); - final float xOff = x.collect(event.getX(), "X"); - final float yOff = y.collect(event.getY(), "Y"); + final float xOff = x.collect(event.getX(), event.getEventTime(), "X"); + final float yOff = y.collect(event.getY(), event.getEventTime(), "Y"); // Generate DPAD events based on the trackball movement. // We pick the axis that has moved the most as the direction of @@ -1489,9 +1822,9 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun if (keycode != 0) { if (movement < 0) movement = -movement; int accelMovement = (int)(movement * accel); - //Log.i(TAG, "Move: movement=" + movement - // + " accelMovement=" + accelMovement - // + " accel=" + accel); + if (DEBUG_TRACKBALL) Log.v(TAG, "Move: movement=" + movement + + " accelMovement=" + accelMovement + + " accel=" + accel); if (accelMovement > movement) { if (DEBUG_TRACKBALL) Log.v("foo", "Delivering fake DPAD: " + keycode); @@ -1602,8 +1935,116 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun return false; } - + /** + * log motion events + */ + private static void captureMotionLog(String subTag, MotionEvent ev) { + //check dynamic switch + if (ev == null || + SystemProperties.getInt(ViewDebug.SYSTEM_PROPERTY_CAPTURE_EVENT, 0) == 0) { + return; + } + + StringBuilder sb = new StringBuilder(subTag + ": "); + sb.append(ev.getDownTime()).append(','); + sb.append(ev.getEventTime()).append(','); + sb.append(ev.getAction()).append(','); + sb.append(ev.getX()).append(','); + sb.append(ev.getY()).append(','); + sb.append(ev.getPressure()).append(','); + sb.append(ev.getSize()).append(','); + sb.append(ev.getMetaState()).append(','); + sb.append(ev.getXPrecision()).append(','); + sb.append(ev.getYPrecision()).append(','); + sb.append(ev.getDeviceId()).append(','); + sb.append(ev.getEdgeFlags()); + Log.d(TAG, sb.toString()); + } + /** + * log motion events + */ + private static void captureKeyLog(String subTag, KeyEvent ev) { + //check dynamic switch + if (ev == null || + SystemProperties.getInt(ViewDebug.SYSTEM_PROPERTY_CAPTURE_EVENT, 0) == 0) { + return; + } + StringBuilder sb = new StringBuilder(subTag + ": "); + sb.append(ev.getDownTime()).append(','); + sb.append(ev.getEventTime()).append(','); + sb.append(ev.getAction()).append(','); + sb.append(ev.getKeyCode()).append(','); + sb.append(ev.getRepeatCount()).append(','); + sb.append(ev.getMetaState()).append(','); + sb.append(ev.getDeviceId()).append(','); + sb.append(ev.getScanCode()); + Log.d(TAG, sb.toString()); + } + + int enqueuePendingEvent(Object event, boolean sendDone) { + int seq = mPendingEventSeq+1; + if (seq < 0) seq = 0; + mPendingEventSeq = seq; + mPendingEvents.put(seq, event); + return sendDone ? seq : -seq; + } + + Object retrievePendingEvent(int seq) { + if (seq < 0) seq = -seq; + Object event = mPendingEvents.get(seq); + if (event != null) { + mPendingEvents.remove(seq); + } + return event; + } + private void deliverKeyEvent(KeyEvent event, boolean sendDone) { + boolean handled = false; + handled = mView.dispatchKeyEventPreIme(event); + if (handled) { + if (sendDone) { + if (LOCAL_LOGV) Log.v( + "ViewRoot", "Telling window manager key is finished"); + try { + sWindowSession.finishKey(mWindow); + } catch (RemoteException e) { + } + } + return; + } + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null && mView != null && imm.isActive()) { + int seq = enqueuePendingEvent(event, sendDone); + if (DEBUG_IMF) Log.v(TAG, "Sending key event to IME: seq=" + + seq + " event=" + event); + imm.dispatchKeyEvent(mView.getContext(), seq, event, + mInputMethodCallback); + return; + } + deliverKeyEventToViewHierarchy(event, sendDone); + } + + void handleFinishedEvent(int seq, boolean handled) { + final KeyEvent event = (KeyEvent)retrievePendingEvent(seq); + if (DEBUG_IMF) Log.v(TAG, "IME finished event: seq=" + seq + + " handled=" + handled + " event=" + event); + if (event != null) { + final boolean sendDone = seq >= 0; + if (!handled) { + deliverKeyEventToViewHierarchy(event, sendDone); + return; + } else if (sendDone) { + if (LOCAL_LOGV) Log.v( + "ViewRoot", "Telling window manager key is finished"); + try { + sWindowSession.finishKey(mWindow); + } catch (RemoteException e) { + } + } + } + } + + private void deliverKeyEventToViewHierarchy(KeyEvent event, boolean sendDone) { try { if (mView != null && mAdded) { final int action = event.getAction(); @@ -1611,8 +2052,11 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun if (checkForLeavingTouchModeAndConsume(event)) { return; + } + + if (Config.LOGV) { + captureKeyLog("captureDispatchKeyEvent", event); } - boolean keyHandled = mView.dispatchKeyEvent(event); if ((!keyHandled && isDown) || (action == KeyEvent.ACTION_MULTIPLE)) { @@ -1740,10 +2184,11 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun // animation info. try { if ((sWindowSession.relayout( - mWindow, mWindowAttributes, - mView.mMeasuredWidth, mView.mMeasuredHeight, - viewVisibility, mWinFrame, mCoveredInsets, mSurface) - &WindowManagerImpl.RELAYOUT_FIRST_TIME) != 0) { + mWindow, mWindowAttributes, + mView.mMeasuredWidth, mView.mMeasuredHeight, + viewVisibility, false, mWinFrame, mPendingContentInsets, + mPendingVisibleInsets, mSurface) + &WindowManagerImpl.RELAYOUT_FIRST_TIME) != 0) { sWindowSession.finishDrawing(mWindow); } } catch (RemoteException e) { @@ -1767,12 +2212,23 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun } } - public void dispatchResized(int w, int h, boolean reportDraw) { - if (DEBUG_DRAW) Log.v(TAG, "Resized " + this + ": w=" + w - + " h=" + h + " reportDraw=" + reportDraw); - Message msg = obtainMessage(reportDraw ? RESIZED_REPORT : RESIZED); + public void dispatchFinishedEvent(int seq, boolean handled) { + Message msg = obtainMessage(FINISHED_EVENT); + msg.arg1 = seq; + msg.arg2 = handled ? 1 : 0; + sendMessage(msg); + } + + public void dispatchResized(int w, int h, Rect coveredInsets, + Rect visibleInsets, boolean reportDraw) { + if (DEBUG_LAYOUT) Log.v(TAG, "Resizing " + this + ": w=" + w + + " h=" + h + " coveredInsets=" + coveredInsets.toShortString() + + " visibleInsets=" + visibleInsets.toShortString() + + " reportDraw=" + reportDraw); + Message msg = obtainMessage(reportDraw ? RESIZED_REPORT :RESIZED); msg.arg1 = w; msg.arg2 = h; + msg.obj = new Rect[] { new Rect(coveredInsets), new Rect(visibleInsets) }; sendMessage(msg); } @@ -1855,6 +2311,30 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun // ViewRoot never intercepts touch event, so this can be a no-op } + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, + boolean immediate) { + return scrollToRectOrFocus(rectangle, immediate); + } + + static class InputMethodCallback extends IInputMethodCallback.Stub { + private WeakReference<ViewRoot> mViewRoot; + + public InputMethodCallback(ViewRoot viewRoot) { + mViewRoot = new WeakReference<ViewRoot>(viewRoot); + } + + public void finishedEvent(int seq, boolean handled) { + final ViewRoot viewRoot = mViewRoot.get(); + if (viewRoot != null) { + viewRoot.dispatchFinishedEvent(seq, handled); + } + } + + public void sessionCreated(IInputMethodSession session) throws RemoteException { + // Stub -- not for use in the client. + } + } + static class W extends IWindow.Stub { private WeakReference<ViewRoot> mViewRoot; @@ -1862,10 +2342,12 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun mViewRoot = new WeakReference<ViewRoot>(viewRoot); } - public void resized(int w, int h, boolean reportDraw) { + public void resized(int w, int h, Rect coveredInsets, + Rect visibleInsets, boolean reportDraw) { final ViewRoot viewRoot = mViewRoot.get(); if (viewRoot != null) { - viewRoot.dispatchResized(w, h, reportDraw); + viewRoot.dispatchResized(w, h, coveredInsets, + visibleInsets, reportDraw); } } @@ -1961,9 +2443,30 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun * discrete (DPAD) movements based on raw trackball motion. */ static final class TrackballAxis { + /** + * The maximum amount of acceleration we will apply. + */ + static final float MAX_ACCELERATION = 20; + + /** + * The maximum amount of time (in milliseconds) between events in order + * for us to consider the user to be doing fast trackball movements, + * and thus apply an acceleration. + */ + static final long FAST_MOVE_TIME = 100; + + /** + * Scaling factor to the time (in milliseconds) between events to how + * much to multiple/divide the current acceleration. When movement + * is < FAST_MOVE_TIME this multiplies the acceleration; when > + * FAST_MOVE_TIME it divides it. + */ + static final float ACCEL_MOVE_SCALING_FACTOR = (1.0f/50); + float position; float absPosition; float acceleration = 1; + long lastMoveTime = 0; int step; int dir; int nonAccelMovement; @@ -1971,6 +2474,7 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun void reset(int _step) { position = 0; acceleration = 1; + lastMoveTime = 0; step = _step; dir = 0; } @@ -1985,23 +2489,56 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun * @return Returns the absolute value of the amount of movement * collected so far. */ - float collect(float off, String axis) { + float collect(float off, long time, String axis) { + long normTime; if (off > 0) { + normTime = (long)(off * FAST_MOVE_TIME); if (dir < 0) { if (DEBUG_TRACKBALL) Log.v(TAG, axis + " reversed to positive!"); position = 0; step = 0; acceleration = 1; + lastMoveTime = 0; } dir = 1; } else if (off < 0) { + normTime = (long)((-off) * FAST_MOVE_TIME); if (dir > 0) { if (DEBUG_TRACKBALL) Log.v(TAG, axis + " reversed to negative!"); position = 0; step = 0; acceleration = 1; + lastMoveTime = 0; } dir = -1; + } else { + normTime = 0; + } + + // The number of milliseconds between each movement that is + // considered "normal" and will not result in any acceleration + // or deceleration, scaled by the offset we have here. + if (normTime > 0) { + long delta = time - lastMoveTime; + lastMoveTime = time; + float acc = acceleration; + if (delta < normTime) { + // The user is scrolling rapidly, so increase acceleration. + float scale = (normTime-delta) * ACCEL_MOVE_SCALING_FACTOR; + if (scale > 1) acc *= scale; + if (DEBUG_TRACKBALL) Log.v(TAG, axis + " accelerate: off=" + + off + " normTime=" + normTime + " delta=" + delta + + " scale=" + scale + " acc=" + acc); + acceleration = acc < MAX_ACCELERATION ? acc : MAX_ACCELERATION; + } else { + // The user is scrolling slowly, so decrease acceleration. + float scale = (delta-normTime) * ACCEL_MOVE_SCALING_FACTOR; + if (scale > 1) acc /= scale; + if (DEBUG_TRACKBALL) Log.v(TAG, axis + " deccelerate: off=" + + off + " normTime=" + normTime + " delta=" + delta + + " scale=" + scale + " acc=" + acc); + acceleration = acc > 1 ? acc : 1; + } } position += off; return (absPosition = Math.abs(position)); @@ -2050,12 +2587,11 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun break; // After the first two, we generate discrete movements // consistently with the trackball, applying an acceleration - // if the trackball is moving quickly. The acceleration is - // currently very simple, just reducing the amount of - // trackball motion required as more discrete movements are - // generated. This should probably be changed to take time - // more into account, so that quick trackball movements will - // have increased acceleration. + // if the trackball is moving quickly. This is a simple + // acceleration on top of what we already compute based + // on how quickly the wheel is being turned, to apply + // a longer increasing acceleration to continuous movement + // in one direction. default: if (absPosition < 1) { return movement; @@ -2065,7 +2601,7 @@ final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Soun absPosition = Math.abs(position); float acc = acceleration; acc *= 1.1f; - acceleration = acc < 20 ? acc : acceleration; + acceleration = acc < MAX_ACCELERATION ? acc : acceleration; break; } } while (true); diff --git a/core/java/android/view/ViewTreeObserver.java b/core/java/android/view/ViewTreeObserver.java index 2e1e01a..05f5fa2 100644 --- a/core/java/android/view/ViewTreeObserver.java +++ b/core/java/android/view/ViewTreeObserver.java @@ -16,6 +16,8 @@ package android.view; +import android.graphics.Rect; + import java.util.ArrayList; /** @@ -32,6 +34,7 @@ public final class ViewTreeObserver { private ArrayList<OnGlobalLayoutListener> mOnGlobalLayoutListeners; private ArrayList<OnPreDrawListener> mOnPreDrawListeners; private ArrayList<OnTouchModeChangeListener> mOnTouchModeChangeListeners; + private ArrayList<OnComputeInternalInsetsListener> mOnComputeInternalInsetsListeners; private boolean mAlive = true; @@ -96,6 +99,108 @@ public final class ViewTreeObserver { } /** + * Parameters used with OnComputeInternalInsetsListener. + * {@hide pending API Council approval} + */ + public final static class InternalInsetsInfo { + /** + * Offsets from the frame of the window at which the content of + * windows behind it should be placed. + */ + public final Rect contentInsets = new Rect(); + + /** + * Offsets from the fram of the window at which windows behind it + * are visible. + */ + public final Rect visibleInsets = new Rect(); + + /** + * Option for {@link #setTouchableInsets(int)}: the entire window frame + * can be touched. + */ + public static final int TOUCHABLE_INSETS_FRAME = 0; + + /** + * Option for {@link #setTouchableInsets(int)}: the area inside of + * the content insets can be touched. + */ + public static final int TOUCHABLE_INSETS_CONTENT = 1; + + /** + * Option for {@link #setTouchableInsets(int)}: the area inside of + * the visible insets can be touched. + */ + public static final int TOUCHABLE_INSETS_VISIBLE = 2; + + /** + * Set which parts of the window can be touched: either + * {@link #TOUCHABLE_INSETS_FRAME}, {@link #TOUCHABLE_INSETS_CONTENT}, + * or {@link #TOUCHABLE_INSETS_VISIBLE}. + */ + public void setTouchableInsets(int val) { + mTouchableInsets = val; + } + + public int getTouchableInsets() { + return mTouchableInsets; + } + + int mTouchableInsets; + + void reset() { + final Rect givenContent = contentInsets; + final Rect givenVisible = visibleInsets; + givenContent.left = givenContent.top = givenContent.right + = givenContent.bottom = givenVisible.left = givenVisible.top + = givenVisible.right = givenVisible.bottom = 0; + mTouchableInsets = TOUCHABLE_INSETS_FRAME; + } + + @Override public boolean equals(Object o) { + try { + if (o == null) { + return false; + } + InternalInsetsInfo other = (InternalInsetsInfo)o; + if (!contentInsets.equals(other.contentInsets)) { + return false; + } + if (!visibleInsets.equals(other.visibleInsets)) { + return false; + } + return mTouchableInsets == other.mTouchableInsets; + } catch (ClassCastException e) { + return false; + } + } + + void set(InternalInsetsInfo other) { + contentInsets.set(other.contentInsets); + visibleInsets.set(other.visibleInsets); + mTouchableInsets = other.mTouchableInsets; + } + } + + /** + * Interface definition for a callback to be invoked when layout has + * completed and the client can compute its interior insets. + * {@hide pending API Council approval} + */ + public interface OnComputeInternalInsetsListener { + /** + * Callback method to be invoked when layout has completed and the + * client can compute its interior insets. + * + * @param inoutInfo Should be filled in by the implementation with + * the information about the insets of the window. This is called + * with whatever values the previous OnComputeInternalInsetsListener + * returned, if there are multiple such listeners in the window. + */ + public void onComputeInternalInsets(InternalInsetsInfo inoutInfo); + } + + /** * Creates a new ViewTreeObserver. This constructor should not be called */ ViewTreeObserver() { @@ -141,6 +246,14 @@ public final class ViewTreeObserver { } } + if (observer.mOnComputeInternalInsetsListeners != null) { + if (mOnComputeInternalInsetsListeners != null) { + mOnComputeInternalInsetsListeners.addAll(observer.mOnComputeInternalInsetsListeners); + } else { + mOnComputeInternalInsetsListeners = observer.mOnComputeInternalInsetsListeners; + } + } + observer.kill(); } @@ -281,6 +394,43 @@ public final class ViewTreeObserver { mOnTouchModeChangeListeners.remove(victim); } + /** + * Register a callback to be invoked when the invoked when it is time to + * compute the window's internal insets. + * + * @param listener The callback to add + * + * @throws IllegalStateException If {@link #isAlive()} returns false + * {@hide pending API Council approval} + */ + public void addOnComputeInternalInsetsListener(OnComputeInternalInsetsListener listener) { + checkIsAlive(); + + if (mOnComputeInternalInsetsListeners == null) { + mOnComputeInternalInsetsListeners = new ArrayList<OnComputeInternalInsetsListener>(); + } + + mOnComputeInternalInsetsListeners.add(listener); + } + + /** + * Remove a previously installed internal insets computation callback + * + * @param victim The callback to remove + * + * @throws IllegalStateException If {@link #isAlive()} returns false + * + * @see #addOnComputeInternalInsetsListener(OnComputeInternalInsetsListener) + * {@hide pending API Council approval} + */ + public void removeOnComputeInternalInsetsListener(OnComputeInternalInsetsListener victim) { + checkIsAlive(); + if (mOnComputeInternalInsetsListeners == null) { + return; + } + mOnComputeInternalInsetsListeners.remove(victim); + } + private void checkIsAlive() { if (!mAlive) { throw new IllegalStateException("This ViewTreeObserver is not alive, call " @@ -373,4 +523,25 @@ public final class ViewTreeObserver { } } } + + /** + * Returns whether there are listeners for computing internal insets. + */ + final boolean hasComputeInternalInsetsListeners() { + final ArrayList<OnComputeInternalInsetsListener> listeners = mOnComputeInternalInsetsListeners; + return (listeners != null && listeners.size() > 0); + } + + /** + * Calls all listeners to compute the current insets. + */ + final void dispatchOnComputeInternalInsets(InternalInsetsInfo inoutInfo) { + final ArrayList<OnComputeInternalInsetsListener> listeners = mOnComputeInternalInsetsListeners; + if (listeners != null) { + final int count = listeners.size(); + for (int i = count - 1; i >= 0; i--) { + listeners.get(i).onComputeInternalInsets(inoutInfo); + } + } + } } diff --git a/core/java/android/view/VolumePanel.java b/core/java/android/view/VolumePanel.java index 24f4853..f4d0fde 100644 --- a/core/java/android/view/VolumePanel.java +++ b/core/java/android/view/VolumePanel.java @@ -19,10 +19,15 @@ package android.view; import android.media.ToneGenerator; import android.media.AudioManager; import android.media.AudioService; +import android.media.AudioSystem; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; import android.os.Handler; import android.os.Message; import android.os.Vibrator; +import android.text.TextUtils; import android.util.Config; import android.util.Log; import android.widget.ImageView; @@ -69,46 +74,47 @@ public class VolumePanel extends Handler private static final int MSG_STOP_SOUNDS = 3; private static final int MSG_VIBRATE = 4; - private final String RINGTONE_VOLUME_TEXT; - private final String MUSIC_VOLUME_TEXT; - private final String INCALL_VOLUME_TEXT; - private final String ALARM_VOLUME_TEXT; - private final String UNKNOWN_VOLUME_TEXT; + private static final int RINGTONE_VOLUME_TEXT = com.android.internal.R.string.volume_ringtone; + private static final int MUSIC_VOLUME_TEXT = com.android.internal.R.string.volume_music; + private static final int INCALL_VOLUME_TEXT = com.android.internal.R.string.volume_call; + private static final int ALARM_VOLUME_TEXT = com.android.internal.R.string.volume_alarm; + private static final int UNKNOWN_VOLUME_TEXT = com.android.internal.R.string.volume_unknown; + private static final int NOTIFICATION_VOLUME_TEXT = + com.android.internal.R.string.volume_notification; protected Context mContext; + private AudioManager mAudioManager; protected AudioService mAudioService; - private Toast mToast; - private View mView; - private TextView mMessage; - private ImageView mOtherStreamIcon; - private ImageView mRingerStreamIcon; - private ProgressBar mLevel; + private final Toast mToast; + private final View mView; + private final TextView mMessage; + private final TextView mAdditionalMessage; + private final ImageView mSmallStreamIcon; + private final ImageView mLargeStreamIcon; + private final ProgressBar mLevel; // Synchronize when accessing this private ToneGenerator mToneGenerators[]; private Vibrator mVibrator; - + public VolumePanel(Context context, AudioService volumeService) { mContext = context; + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mAudioService = volumeService; mToast = new Toast(context); - - RINGTONE_VOLUME_TEXT = context.getString(com.android.internal.R.string.volume_ringtone); - MUSIC_VOLUME_TEXT = context.getString(com.android.internal.R.string.volume_music); - INCALL_VOLUME_TEXT = context.getString(com.android.internal.R.string.volume_call); - ALARM_VOLUME_TEXT = context.getString(com.android.internal.R.string.volume_alarm); - UNKNOWN_VOLUME_TEXT = context.getString(com.android.internal.R.string.volume_unknown); - + LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = mView = inflater.inflate(com.android.internal.R.layout.volume_adjust, null); mMessage = (TextView) view.findViewById(com.android.internal.R.id.message); - mOtherStreamIcon = (ImageView) view.findViewById(com.android.internal.R.id.other_stream_icon); - mRingerStreamIcon = (ImageView) view.findViewById(com.android.internal.R.id.ringer_stream_icon); + mAdditionalMessage = + (TextView) view.findViewById(com.android.internal.R.id.additional_message); + mSmallStreamIcon = (ImageView) view.findViewById(com.android.internal.R.id.other_stream_icon); + mLargeStreamIcon = (ImageView) view.findViewById(com.android.internal.R.id.ringer_stream_icon); mLevel = (ProgressBar) view.findViewById(com.android.internal.R.id.level); - mToneGenerators = new ToneGenerator[AudioManager.NUM_STREAMS]; + mToneGenerators = new ToneGenerator[AudioSystem.getNumStreamTypes()]; mVibrator = new Vibrator(); } @@ -148,13 +154,17 @@ public class VolumePanel extends Handler protected void onShowVolumeChanged(int streamType, int flags) { int index = mAudioService.getStreamVolume(streamType); - String message = UNKNOWN_VOLUME_TEXT; + int message = UNKNOWN_VOLUME_TEXT; + int additionalMessage = 0; if (LOGD) { Log.d(TAG, "onShowVolumeChanged(streamType: " + streamType + ", flags: " + flags + "), index: " + index); } + // get max volume for progress bar + int max = mAudioService.getStreamMaxVolume(streamType); + switch (streamType) { case AudioManager.STREAM_RING: { @@ -165,32 +175,60 @@ public class VolumePanel extends Handler case AudioManager.STREAM_MUSIC: { message = MUSIC_VOLUME_TEXT; - setOtherIcon(index); + if (mAudioManager.isBluetoothA2dpOn()) { + additionalMessage = + com.android.internal.R.string.volume_music_hint_playing_through_bluetooth; + setLargeIcon(com.android.internal.R.drawable.ic_volume_bluetooth_ad2p); + } else { + setSmallIcon(index); + } break; } case AudioManager.STREAM_VOICE_CALL: { - message = INCALL_VOLUME_TEXT; /* - * For in-call voice call volume, there is no inaudible volume - * level, so never show the mute icon + * For in-call voice call volume, there is no inaudible volume. + * Rescale the UI control so the progress bar doesn't go all + * the way to zero and don't show the mute icon. */ - setOtherIcon(index == 0 ? 1 : index); + index++; + max++; + message = INCALL_VOLUME_TEXT; + if (mAudioManager.isBluetoothScoOn()) { + additionalMessage = + com.android.internal.R.string.volume_call_hint_playing_through_bluetooth; + setLargeIcon(com.android.internal.R.drawable.ic_volume_bluetooth_in_call); + } else { + setSmallIcon(index); + } break; } case AudioManager.STREAM_ALARM: { message = ALARM_VOLUME_TEXT; - setOtherIcon(index); + setSmallIcon(index); + break; + } + + case AudioManager.STREAM_NOTIFICATION: { + message = NOTIFICATION_VOLUME_TEXT; + setSmallIcon(index); break; } } - if (!mMessage.getText().equals(message)) { - mMessage.setText(message); + String messageString = Resources.getSystem().getString(message); + if (!mMessage.getText().equals(messageString)) { + mMessage.setText(messageString); } - int max = mAudioService.getStreamMaxVolume(streamType); + if (additionalMessage == 0) { + mAdditionalMessage.setVisibility(View.GONE); + } else { + mAdditionalMessage.setVisibility(View.VISIBLE); + mAdditionalMessage.setText(Resources.getSystem().getString(additionalMessage)); + } + if (max != mLevel.getMax()) { mLevel.setMax(max); } @@ -230,7 +268,8 @@ public class VolumePanel extends Handler protected void onStopSounds() { synchronized (this) { - for (int i = AudioManager.NUM_STREAMS - 1; i >= 0; i--) { + int numStreamTypes = AudioSystem.getNumStreamTypes(); + for (int i = numStreamTypes - 1; i >= 0; i--) { ToneGenerator toneGen = mToneGenerators[i]; if (toneGen != null) { toneGen.stopTone(); @@ -261,19 +300,41 @@ public class VolumePanel extends Handler } } } - - private void setOtherIcon(int index) { - mRingerStreamIcon.setVisibility(View.GONE); - mOtherStreamIcon.setVisibility(View.VISIBLE); + + /** + * Makes the small icon visible, and hides the large icon. + * + * @param index The volume index, where 0 means muted. + */ + private void setSmallIcon(int index) { + mLargeStreamIcon.setVisibility(View.GONE); + mSmallStreamIcon.setVisibility(View.VISIBLE); - mOtherStreamIcon.setImageResource(index == 0 + mSmallStreamIcon.setImageResource(index == 0 ? com.android.internal.R.drawable.ic_volume_off_small : com.android.internal.R.drawable.ic_volume_small); } + /** + * Makes the large image view visible with the given icon. + * + * @param resId The icon to display. + */ + private void setLargeIcon(int resId) { + mSmallStreamIcon.setVisibility(View.GONE); + mLargeStreamIcon.setVisibility(View.VISIBLE); + mLargeStreamIcon.setImageResource(resId); + } + + /** + * Makes the ringer icon visible with an icon that is chosen + * based on the current ringer mode. + * + * @param index + */ private void setRingerIcon(int index) { - mOtherStreamIcon.setVisibility(View.GONE); - mRingerStreamIcon.setVisibility(View.VISIBLE); + mSmallStreamIcon.setVisibility(View.GONE); + mLargeStreamIcon.setVisibility(View.VISIBLE); int ringerMode = mAudioService.getRingerMode(); int icon; @@ -287,14 +348,14 @@ public class VolumePanel extends Handler } else { icon = com.android.internal.R.drawable.ic_volume; } - mRingerStreamIcon.setImageResource(icon); + mLargeStreamIcon.setImageResource(icon); } protected void onFreeResources() { // We'll keep the views, just ditch the cached drawable and hence // bitmaps - mOtherStreamIcon.setImageDrawable(null); - mRingerStreamIcon.setImageDrawable(null); + mSmallStreamIcon.setImageDrawable(null); + mLargeStreamIcon.setImageDrawable(null); synchronized (this) { for (int i = mToneGenerators.length - 1; i >= 0; i--) { diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 4aeab2d..a68436b 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -24,6 +24,7 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; +import android.util.Log; /** * Abstract base class for a top-level window look and behavior policy. An @@ -106,6 +107,8 @@ public abstract class Window { private boolean mHaveWindowFormat = false; private int mDefaultWindowFormat = PixelFormat.OPAQUE; + private boolean mHasSoftInputMode = false; + // The current window attributes. private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams(); @@ -382,11 +385,7 @@ public abstract class Window { && mAppName != null) { wp.setTitle(mAppName); } - if (wp.windowAnimations == 0) { - wp.windowAnimations = getWindowStyle().getResourceId( - com.android.internal.R.styleable.Window_windowAnimationStyle, 0); - } - } + } if (wp.packageName == null) { wp.packageName = mContext.getPackageName(); } @@ -526,6 +525,26 @@ public abstract class Window { } /** + * Specify an explicit soft input mode to use for the window, as per + * {@link WindowManager.LayoutParams#softInputMode + * WindowManager.LayoutParams.softInputMode}. Providing anything besides + * "unspecified" here will override the input mode the window would + * normally retrieve from its theme. + */ + public void setSoftInputMode(int mode) { + final WindowManager.LayoutParams attrs = getAttributes(); + if (mode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) { + attrs.softInputMode = mode; + mHasSoftInputMode = true; + } else { + mHasSoftInputMode = false; + } + if (mCallback != null) { + mCallback.onWindowAttributesChanged(attrs); + } + } + + /** * Convenience function to set the flag bits as specified in flags, as * per {@link #setFlags}. * @param flags The flag bits to be set. @@ -572,14 +591,17 @@ public abstract class Window { } /** - * Specify custom window attributes. + * Specify custom window attributes. <strong>PLEASE NOTE:</strong> the + * layout params you give here should generally be from values previously + * retrieved with {@link #getAttributes()}; you probably do not want to + * blindly create and apply your own, since this will blow away any values + * set by the framework that you are not interested in. * * @param a The new window attributes, which will completely override any * current values. */ public void setAttributes(WindowManager.LayoutParams a) { mWindowAttributes.copyFrom(a); - mForcedWindowFlags = 0xffffffff; if (mCallback != null) { mCallback.onWindowAttributesChanged(mWindowAttributes); } @@ -604,6 +626,13 @@ public abstract class Window { } /** + * Has the app specified their own soft input mode? + */ + protected final boolean hasSoftInputMode() { + return mHasSoftInputMode; + } + + /** * Enable extended screen features. This must be called before * setContentView(). May be called as many times as desired as long as it * is before setContentView(). If not called, no extended features @@ -926,8 +955,7 @@ public abstract class Window { * @see #setFormat * @see PixelFormat */ - protected void setDefaultWindowFormat(int format) - { + protected void setDefaultWindowFormat(int format) { mDefaultWindowFormat = format; if (!mHaveWindowFormat) { final WindowManager.LayoutParams attrs = getAttributes(); diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 4c1dec5..7d202aa 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -125,6 +125,9 @@ public interface WindowManager extends ViewManager { * @see #TYPE_APPLICATION_PANEL * @see #TYPE_APPLICATION_MEDIA * @see #TYPE_APPLICATION_SUB_PANEL + * @see #TYPE_APPLICATION_ATTACHED_DIALOG + * @see #TYPE_INPUT_METHOD + * @see #TYPE_INPUT_METHOD_DIALOG * @see #TYPE_STATUS_BAR * @see #TYPE_SEARCH_BAR * @see #TYPE_PHONE @@ -133,6 +136,12 @@ public interface WindowManager extends ViewManager { * @see #TYPE_TOAST * @see #TYPE_SYSTEM_OVERLAY * @see #TYPE_PRIORITY_PHONE + * @see #TYPE_STATUS_BAR_PANEL + * @see #TYPE_SYSTEM_DIALOG + * @see #TYPE_KEYGUARD_DIALOG + * @see #TYPE_SYSTEM_ERROR + * @see #TYPE_INPUT_METHOD + * @see #TYPE_INPUT_METHOD_DIALOG */ public int type; @@ -193,12 +202,18 @@ public interface WindowManager extends ViewManager { * {@link #TYPE_APPLICATION_PANEL} panels. */ public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2; - + + /** Window type: like {@link #TYPE_APPLICATION_PANEL}, but layout + * of the window happens as that of a top-level window, <em>not</em> + * as a child of its container. + */ + public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3; + /** * End of types of sub-windows. */ public static final int LAST_SUB_WINDOW = 1999; - + /** * Start of system-specific window types. These are not normally * created by applications. @@ -278,10 +293,23 @@ public interface WindowManager extends ViewManager { public static final int TYPE_SYSTEM_ERROR = FIRST_SYSTEM_WINDOW+10; /** + * Window type: internal input methods windows, which appear above + * the normal UI. Application windows may be resized or panned to keep + * the input focus visible while this window is displayed. + */ + public static final int TYPE_INPUT_METHOD = FIRST_SYSTEM_WINDOW+11; + + /** + * Window type: internal input methods dialog windows, which appear above + * the current input method window. + */ + public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12; + + /** * End of types of system windows. */ public static final int LAST_SYSTEM_WINDOW = 2999; - + /** * Specifies what type of memory buffers should be used by this window. * Default is normal. @@ -330,7 +358,19 @@ public interface WindowManager extends ViewManager { /** Window flag: blur everything behind this window. */ public static final int FLAG_BLUR_BEHIND = 0x00000004; - /** Window flag: this window won't ever get focus. */ + /** Window flag: this window won't ever get key input focus, so the + * user can not send key or other button events to it. Those will + * instead go to whatever focusable window is behind it. This flag + * will also enable {@link #FLAG_NOT_TOUCH_MODAL} whether or not that + * is explicitly set. + * + * <p>Setting this flag also implies that the window will not need to + * interact with + * a soft input method, so it will be Z-ordered and positioned + * independently of any active input method (typically this means it + * gets Z-ordered on top of the input method, so it can use the full + * screen for its content and cover the input method if needed. You + * can use {@link #FLAG_ALT_FOCUSABLE_IM} to modify this behavior. */ public static final int FLAG_NOT_FOCUSABLE = 0x00000008; /** Window flag: this window can never receive touch events. */ @@ -405,6 +445,33 @@ public interface WindowManager extends ViewManager { * set for you by Window as described in {@link Window#setFlags}.*/ public static final int FLAG_LAYOUT_INSET_DECOR = 0x00010000; + /** Window flag: invert the state of {@link #FLAG_NOT_FOCUSABLE} with + * respect to how this window interacts with the current method. That + * is, if FLAG_NOT_FOCUSABLE is set and this flag is set, then the + * window will behave as if it needs to interact with the input method + * and thus be placed behind/away from it; if FLAG_NOT_FOCUSABLE is + * not set and this flag is set, then the window will behave as if it + * doesn't need to interact with the input method and can be placed + * to use more space and cover the input method. + */ + public static final int FLAG_ALT_FOCUSABLE_IM = 0x00020000; + + /** Window flag: if you have set {@link #FLAG_NOT_TOUCH_MODAL}, you + * can set this flag to receive a single special MotionEvent with + * the action + * {@link MotionEvent#ACTION_OUTSIDE MotionEvent.ACTION_OUTSIDE} for + * touches that occur outside of your window. Note that you will not + * receive the full down/move/up gesture, only the location of the + * first down as an ACTION_OUTSIDE. + */ + public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000; + + /** Window flag: set when this window was created from the restored + * state of a previous window, indicating this is not the first time + * the user has navigated to it. + */ + public static final int FLAG_RESTORED_STATE = 0x00080000; + /** Window flag: a special option intended for system dialogs. When * this flag is set, the window will demand focus unconditionally when * it is created. @@ -412,6 +479,90 @@ public interface WindowManager extends ViewManager { public static final int FLAG_SYSTEM_ERROR = 0x40000000; /** + * Mask for {@link #softInputMode} of the bits that determine the + * desired visibility state of the soft input area for this window. + */ + public static final int SOFT_INPUT_MASK_STATE = 0x0f; + + /** + * Visibility state for {@link #softInputMode}: no state has been specified. + */ + public static final int SOFT_INPUT_STATE_UNSPECIFIED = 0; + + /** + * Visibility state for {@link #softInputMode}: please don't change the state of + * the soft input area. + */ + public static final int SOFT_INPUT_STATE_UNCHANGED = 1; + + /** + * Visibility state for {@link #softInputMode}: please hide any soft input + * area. + */ + public static final int SOFT_INPUT_STATE_HIDDEN = 2; + + /** + * Visibility state for {@link #softInputMode}: please show the soft input area + * the first time the window is shown. + */ + public static final int SOFT_INPUT_STATE_FIRST_VISIBLE = 3; + + /** + * Visibility state for {@link #softInputMode}: please always show the soft + * input area. + */ + public static final int SOFT_INPUT_STATE_VISIBLE = 4; + + /** + * Mask for {@link #softInputMode} of the bits that determine the + * way that the window should be adjusted to accomodate the soft + * input window. + */ + public static final int SOFT_INPUT_MASK_ADJUST = 0xf0; + + /** Adjustment option for {@link #softInputMode}: nothing specified. + * The system will try to pick one or + * the other depending on the contents of the window. + */ + public static final int SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00; + + /** Adjustment option for {@link #softInputMode}: set to allow the + * window to be resized when an input + * method is shown, so that its contents are not covered by the input + * method. This can <em>not<em> be combined with + * {@link #SOFT_INPUT_ADJUST_PAN}; if + * neither of these are set, then the system will try to pick one or + * the other depending on the contents of the window. + */ + public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10; + + /** Adjustment option for {@link #softInputMode}: set to have a window + * pan when an input method is + * shown, so it doesn't need to deal with resizing but just panned + * by the framework to ensure the current input focus is visible. This + * can <em>not<em> be combined with {@link #SOFT_INPUT_ADJUST_RESIZE}; if + * neither of these are set, then the system will try to pick one or + * the other depending on the contents of the window. + */ + public static final int SOFT_INPUT_ADJUST_PAN = 0x20; + + /** + * Desired operating mode for any soft input area. May any combination + * of: + * + * <ul> + * <li> One of the visibility states + * {@link #SOFT_INPUT_STATE_UNSPECIFIED}, {@link #SOFT_INPUT_STATE_UNCHANGED}, + * {@link #SOFT_INPUT_STATE_HIDDEN}, {@link #SOFT_INPUT_STATE_FIRST_VISIBLE}, or + * {@link #SOFT_INPUT_STATE_VISIBLE}. + * <li> One of the adjustment options + * {@link #SOFT_INPUT_ADJUST_UNSPECIFIED}, + * {@link #SOFT_INPUT_ADJUST_RESIZE}, or + * {@link #SOFT_INPUT_ADJUST_PAN}. + */ + public int softInputMode; + + /** * Placement of window within the screen as per {@link Gravity} * * @see Gravity @@ -533,6 +684,7 @@ public interface WindowManager extends ViewManager { out.writeInt(type); out.writeInt(memoryType); out.writeInt(flags); + out.writeInt(softInputMode); out.writeInt(gravity); out.writeFloat(horizontalMargin); out.writeFloat(verticalMargin); @@ -565,6 +717,7 @@ public interface WindowManager extends ViewManager { type = in.readInt(); memoryType = in.readInt(); flags = in.readInt(); + softInputMode = in.readInt(); gravity = in.readInt(); horizontalMargin = in.readFloat(); verticalMargin = in.readFloat(); @@ -586,6 +739,7 @@ public interface WindowManager extends ViewManager { public static final int TITLE_CHANGED = 1<<6; public static final int ALPHA_CHANGED = 1<<7; public static final int MEMORY_TYPE_CHANGED = 1<<8; + public static final int SOFT_INPUT_MODE_CHANGED = 1<<9; public final int copyFrom(LayoutParams o) { int changes = 0; @@ -606,6 +760,22 @@ public interface WindowManager extends ViewManager { y = o.y; changes |= LAYOUT_CHANGED; } + if (horizontalWeight != o.horizontalWeight) { + horizontalWeight = o.horizontalWeight; + changes |= LAYOUT_CHANGED; + } + if (verticalWeight != o.verticalWeight) { + verticalWeight = o.verticalWeight; + changes |= LAYOUT_CHANGED; + } + if (horizontalMargin != o.horizontalMargin) { + horizontalMargin = o.horizontalMargin; + changes |= LAYOUT_CHANGED; + } + if (verticalMargin != o.verticalMargin) { + verticalMargin = o.verticalMargin; + changes |= LAYOUT_CHANGED; + } if (type != o.type) { type = o.type; changes |= TYPE_CHANGED; @@ -618,6 +788,10 @@ public interface WindowManager extends ViewManager { flags = o.flags; changes |= FLAGS_CHANGED; } + if (softInputMode != o.softInputMode) { + softInputMode = o.softInputMode; + changes |= SOFT_INPUT_MODE_CHANGED; + } if (gravity != o.gravity) { gravity = o.gravity; changes |= LAYOUT_CHANGED; @@ -677,15 +851,37 @@ public interface WindowManager extends ViewManager { @Override public String toString() { - return "WM.LayoutParams{" - + Integer.toHexString(System.identityHashCode(this)) - + " (" + x + "," + y + ")(" - + (width==FILL_PARENT?"fill_parent":(width==WRAP_CONTENT?"wrap_content":width)) - + "x" - + (height==FILL_PARENT?"fill_parent":(height==WRAP_CONTENT?"wrap_content":height)) - + ") gr=#" + Integer.toHexString(gravity) - + " ty=" + type + " fl=#" + Integer.toHexString(flags) - + " fmt=" + format + "}"; + StringBuilder sb = new StringBuilder(256); + sb.append("WM.LayoutParams{"); + sb.append("("); + sb.append(x); + sb.append(','); + sb.append(y); + sb.append(")("); + sb.append((width==FILL_PARENT?"fill":(width==WRAP_CONTENT?"wrap":width))); + sb.append('x'); + sb.append((height==FILL_PARENT?"fill":(height==WRAP_CONTENT?"wrap":height))); + sb.append(")"); + if (softInputMode != 0) { + sb.append(" sim=#"); + sb.append(Integer.toHexString(softInputMode)); + } + if (gravity != 0) { + sb.append(" gr=#"); + sb.append(Integer.toHexString(gravity)); + } + sb.append(" ty="); + sb.append(type); + sb.append(" fl=#"); + sb.append(Integer.toHexString(flags)); + sb.append(" fmt="); + sb.append(format); + if (windowAnimations != 0) { + sb.append(" wanim=0x"); + sb.append(Integer.toHexString(windowAnimations)); + } + sb.append('}'); + return sb.toString(); } private CharSequence mTitle = ""; diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index fbecf46..755d7b8 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -22,6 +22,7 @@ import android.util.AndroidRuntimeException; import android.util.Config; import android.util.Log; import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; final class WindowLeaked extends AndroidRuntimeException { public WindowLeaked(String msg) { @@ -128,7 +129,7 @@ public class WindowManagerImpl implements WindowManager { root.mAddNesting++; // Update layout parameters. view.setLayoutParams(wparams); - root.setLayoutParams(wparams); + root.setLayoutParams(wparams, true); return; } @@ -144,7 +145,7 @@ public class WindowManagerImpl implements WindowManager { } } - root = new ViewRoot(); + root = new ViewRoot(view.getContext()); root.mAddNesting = 1; view.setLayoutParams(wparams); @@ -191,7 +192,7 @@ public class WindowManagerImpl implements WindowManager { int index = findViewLocked(view, true); ViewRoot root = mRoots[index]; mParams[index] = wparams; - root.setLayoutParams(wparams); + root.setLayoutParams(wparams, false); } } @@ -236,6 +237,10 @@ public class WindowManagerImpl implements WindowManager { return view; } + InputMethodManager imm = InputMethodManager.getInstance(view.getContext()); + if (imm != null) { + imm.windowDismissed(mViews[index].getWindowToken()); + } root.die(false); finishRemoveViewLocked(view, index); return view; @@ -294,6 +299,26 @@ public class WindowManagerImpl implements WindowManager { } } + public WindowManager.LayoutParams getRootViewLayoutParameter(View view) { + ViewParent vp = view.getParent(); + while (vp != null && !(vp instanceof ViewRoot)) { + vp = vp.getParent(); + } + + if (vp == null) return null; + + ViewRoot vr = (ViewRoot)vp; + + int N = mRoots.length; + for (int i = 0; i < N; ++i) { + if (mRoots[i] == vr) { + return mParams[i]; + } + } + + return null; + } + public void closeAll() { closeAll(null, null, null); } diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 93e1a0b..a6def51 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -109,45 +109,91 @@ public interface WindowManagerPolicy { * getFrame() if so desired. Must be called with the window manager * lock held. * - * @param parentLeft The left edge of the parent container this window - * is in, used for computing its position. - * @param parentTop The top edge of the parent container this window - * is in, used for computing its position. - * @param parentRight The right edge of the parent container this window - * is in, used for computing its position. - * @param parentBottom The bottom edge of the parent container this window - * is in, used for computing its position. - * @param displayLeft The left edge of the available display, used - * for constraining the overall dimensions of the window. - * @param displayTop The left edge of the available display, used - * for constraining the overall dimensions of the window. - * @param displayRight The right edge of the available display, used - * for constraining the overall dimensions of the window. - * @param displayBottom The bottom edge of the available display, used - * for constraining the overall dimensions of the window. + * @param parentFrame The frame of the parent container this window + * is in, used for computing its basic position. + * @param displayFrame The frame of the overall display in which this + * window can appear, used for constraining the overall dimensions + * of the window. + * @param contentFrame The frame within the display in which we would + * like active content to appear. This will cause windows behind to + * be resized to match the given content frame. + * @param visibleFrame The frame within the display that the window + * is actually visible, used for computing its visible insets to be + * given to windows behind. + * This can be used as a hint for scrolling (avoiding resizing) + * the window to make certain that parts of its content + * are visible. */ - public void computeFrameLw(int parentLeft, int parentRight, int parentBottom, - int parentHeight, int displayLeft, int displayTop, - int displayRight, int displayBottom); + public void computeFrameLw(Rect parentFrame, Rect displayFrame, + Rect contentFrame, Rect visibleFrame); /** - * Set the window's frame to an exact value. Must be called with the + * Retrieve the current frame of the window. Must be called with the * window manager lock held. * - * @param left Left edge of the window. - * @param top Top edge of the window. - * @param right Right edge (exclusive) of the window. - * @param bottom Bottom edge (exclusive) of the window. + * @return Rect The rectangle holding the window frame. */ - public void setFrameLw(int left, int top, int right, int bottom); + public Rect getFrameLw(); /** - * Retrieve the current frame of the window. Must be called with the + * Retrieve the frame of the display that this window was last + * laid out in. Must be called with the * window manager lock held. * - * @return Rect The rectangle holding the window frame. + * @return Rect The rectangle holding the display frame. */ - public Rect getFrameLw(); + public Rect getDisplayFrameLw(); + + /** + * Retrieve the frame of the content area that this window was last + * laid out in. This is the area in which the content of the window + * should be placed. It will be smaller than the display frame to + * account for screen decorations such as a status bar or soft + * keyboard. Must be called with the + * window manager lock held. + * + * @return Rect The rectangle holding the content frame. + */ + public Rect getContentFrameLw(); + + /** + * Retrieve the frame of the visible area that this window was last + * laid out in. This is the area of the screen in which the window + * will actually be fully visible. It will be smaller than the + * content frame to account for transient UI elements blocking it + * such as an input method's candidates UI. Must be called with the + * window manager lock held. + * + * @return Rect The rectangle holding the visible frame. + */ + public Rect getVisibleFrameLw(); + + /** + * Returns true if this window is waiting to receive its given + * internal insets from the client app, and so should not impact the + * layout of other windows. + */ + public boolean getGivenInsetsPendingLw(); + + /** + * Retrieve the insets given by this window's client for the content + * area of windows behind it. Must be called with the + * window manager lock held. + * + * @return Rect The left, top, right, and bottom insets, relative + * to the window's frame, of the actual contents. + */ + public Rect getGivenContentInsetsLw(); + + /** + * Retrieve the insets given by this window's client for the visible + * area of windows behind it. Must be called with the + * window manager lock held. + * + * @return Rect The left, top, right, and bottom insets, relative + * to the window's frame, of the actual visible area. + */ + public Rect getGivenVisibleInsetsLw(); /** * Retrieve the current LayoutParams of the window. @@ -158,6 +204,11 @@ public interface WindowManagerPolicy { public WindowManager.LayoutParams getAttrs(); /** + * Get the layer at which this window's surface will be Z-ordered. + */ + public int getSurfaceLayer(); + + /** * Return the token for the application (actually activity) that owns * this window. May return null for system windows. * @@ -240,18 +291,6 @@ public interface WindowManagerPolicy { * lock held. */ public void showLw(); - - /** - * Sets insets on the window that represent the area within the window that is covered - * by system windows (e.g. status bar). Must be called with the window - * manager lock held. - * - * @param l - * @param t - * @param r - * @param b - */ - public void setCoveredInsetsLw(int l, int t, int r, int b); } /** No transition happening. */ @@ -506,14 +545,15 @@ public interface WindowManagerPolicy { /** - * Return the insets for the areas covered by system windows. These values are computed on the - * mose recent layout, so they are not guaranteed to be correct. + * Return the insets for the areas covered by system windows. These values + * are computed on the most recent layout, so they are not guaranteed to + * be correct. * * @param attrs The LayoutParams of the window. - * @param coveredInset The areas covered by system windows, expressed as positive insets + * @param contentInset The areas covered by system windows, expressed as positive insets * */ - public void getCoveredInsetHintLw(WindowManager.LayoutParams attrs, Rect coveredInset); + public void getContentInsetHintLw(WindowManager.LayoutParams attrs, Rect contentInset); /** * Called when layout of the windows is finished. After this function has diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java index 39fe561..9f3650b 100644 --- a/core/java/android/view/animation/Animation.java +++ b/core/java/android/view/animation/Animation.java @@ -582,6 +582,16 @@ public abstract class Animation { } /** + * Compute a hint at how long the entire animation may last, in milliseconds. + * Animations can be written to cause themselves to run for a different + * duration than what is computed here, but generally this should be + * accurate. + */ + public long computeDurationHint() { + return (getStartOffset() + getDuration()) * (getRepeatCount() + 1); + } + + /** * Gets the transformation to apply at a specified point in time. Implementations of this * method should always replace the specified Transformation or document they are doing * otherwise. diff --git a/core/java/android/view/animation/AnimationSet.java b/core/java/android/view/animation/AnimationSet.java index 3c5920f..688da70 100644 --- a/core/java/android/view/animation/AnimationSet.java +++ b/core/java/android/view/animation/AnimationSet.java @@ -224,6 +224,23 @@ public class AnimationSet extends Animation { } /** + * The duration hint of an animation set is the maximum of the duration + * hints of all of its component animations. + * + * @see android.view.animation.Animation#computeDurationHint + */ + public long computeDurationHint() { + long duration = 0; + final int count = mAnimations.size(); + final ArrayList<Animation> animations = mAnimations; + for (int i = count - 1; i >= 0; --i) { + final long d = animations.get(i).computeDurationHint(); + if (d > duration) duration = d; + } + return duration; + } + + /** * The transformation of an animation set is the concatenation of all of its * component animations. * diff --git a/core/java/android/view/animation/Transformation.java b/core/java/android/view/animation/Transformation.java index c7a0cc8..f9e85bf 100644 --- a/core/java/android/view/animation/Transformation.java +++ b/core/java/android/view/animation/Transformation.java @@ -134,6 +134,14 @@ public class Transformation { @Override public String toString() { - return "Transformation{alpha=" + mAlpha + " matrix=" + mMatrix + "}"; + return "Transformation{alpha=" + mAlpha + " matrix=" + + mMatrix.toShortString() + "}"; + } + + /** + * Return a string representation of the transformation in a compact form. + */ + public String toShortString() { + return "{alpha=" + mAlpha + " matrix=" + mMatrix.toShortString() + "}"; } } diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java new file mode 100644 index 0000000..4416ee5 --- /dev/null +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.view.inputmethod; + +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewRoot; + +/** + * Base class for implementors of the InputConnection interface, taking care + * of implementing common system-oriented parts of the functionality. + */ +public abstract class BaseInputConnection implements InputConnection { + final InputMethodManager mIMM; + final Handler mH; + final View mTargetView; + + BaseInputConnection(InputMethodManager mgr) { + mIMM = mgr; + mTargetView = null; + mH = null; + } + + public BaseInputConnection(View targetView) { + mIMM = (InputMethodManager)targetView.getContext().getSystemService( + Context.INPUT_METHOD_SERVICE); + mH = targetView.getHandler(); + mTargetView = targetView; + } + + /** + * Provides standard implementation for sending a key event to the window + * attached to the input connection's view. + */ + public boolean sendKeyEvent(KeyEvent event) { + synchronized (mIMM.mH) { + Handler h = mH; + if (h == null) { + if (mIMM.mServedView != null) { + h = mIMM.mServedView.getHandler(); + } + } + if (h != null && mTargetView != null) { + h.post(new DispatchKey(event, mTargetView.getRootView())); + } + } + return false; + } + + /** + * Provides standard implementation for hiding the status icon associated + * with the current input method. + */ + public boolean hideStatusIcon() { + mIMM.updateStatusIcon(0, null); + return true; + } + + /** + * Provides standard implementation for showing the status icon associated + * with the current input method. + */ + public boolean showStatusIcon(String packageName, int resId) { + mIMM.updateStatusIcon(resId, packageName); + return true; + } + + static class DispatchKey implements Runnable { + KeyEvent mEvent; + View mView; + + DispatchKey(KeyEvent event, View v) { + mEvent = event; + mView = v; + } + + public void run() { + mView.dispatchKeyEvent(mEvent); + } + } +}
\ No newline at end of file diff --git a/core/java/android/view/inputmethod/CompletionInfo.aidl b/core/java/android/view/inputmethod/CompletionInfo.aidl new file mode 100644 index 0000000..e601054 --- /dev/null +++ b/core/java/android/view/inputmethod/CompletionInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.inputmethod; + +parcelable CompletionInfo; diff --git a/core/java/android/view/inputmethod/CompletionInfo.java b/core/java/android/view/inputmethod/CompletionInfo.java new file mode 100644 index 0000000..3a8fe72 --- /dev/null +++ b/core/java/android/view/inputmethod/CompletionInfo.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.view.inputmethod; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * Information about a single text completion that an editor has reported to + * an input method. + */ +public final class CompletionInfo implements Parcelable { + static final String TAG = "CompletionInfo"; + + final long mId; + final int mPosition; + final CharSequence mText; + final CharSequence mLabel; + + /** + * Create a simple completion with just text, no label. + */ + public CompletionInfo(long id, int index, CharSequence text) { + mId = id; + mPosition = index; + mText = text; + mLabel = null; + } + + /** + * Create a full completion with both text and label. + */ + public CompletionInfo(long id, int index, CharSequence text, CharSequence label) { + mId = id; + mPosition = index; + mText = text; + mLabel = label; + } + + CompletionInfo(Parcel source) { + mId = source.readLong(); + mPosition = source.readInt(); + mText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + } + + /** + * Return the abstract identifier for this completion, typically + * corresponding to the id associated with it in the original adapter. + */ + public long getId() { + return mId; + } + + /** + * Return the original position of this completion, typically + * corresponding to its position in the original adapter. + */ + public int getPosition() { + return mPosition; + } + + /** + * Return the actual text associated with this completion. This is the + * real text that will be inserted into the editor if the user selects it. + */ + public CharSequence getText() { + return mText; + } + + /** + * Return the user-visible label for the completion, or null if the plain + * text should be shown. If non-null, this will be what the user sees as + * the completion option instead of the actual text. + */ + public CharSequence getLabel() { + return mLabel; + } + + @Override + public String toString() { + return "CompletionInfo{#" + mPosition + " \"" + mText + + "\" id=" + mId + " label=" + mLabel + "}"; + } + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(mId); + dest.writeInt(mPosition); + TextUtils.writeToParcel(mText, dest, flags); + TextUtils.writeToParcel(mLabel, dest, flags); + } + + /** + * Used to make this class parcelable. + */ + public static final Parcelable.Creator<CompletionInfo> CREATOR + = new Parcelable.Creator<CompletionInfo>() { + public CompletionInfo createFromParcel(Parcel source) { + return new CompletionInfo(source); + } + + public CompletionInfo[] newArray(int size) { + return new CompletionInfo[size]; + } + }; + + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/view/inputmethod/DefaultInputMethod.java b/core/java/android/view/inputmethod/DefaultInputMethod.java new file mode 100644 index 0000000..da5cab5 --- /dev/null +++ b/core/java/android/view/inputmethod/DefaultInputMethod.java @@ -0,0 +1,239 @@ +package android.view.inputmethod; + +import android.graphics.Rect; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; + +import com.android.internal.view.IInputContext; +import com.android.internal.view.IInputMethod; +import com.android.internal.view.IInputMethodCallback; +import com.android.internal.view.IInputMethodSession; +import com.android.internal.view.InputConnectionWrapper; + +/** + * This is the default input method that runs in the same context of the + * application that requests text input. It does nothing but returns false for + * any key events, so that all key events will be processed by the key listener + * of the focused text box. + * {@hide} + */ +public class DefaultInputMethod implements InputMethod, InputMethodSession { + private static IInputMethod sInstance = new SimpleInputMethod( + new DefaultInputMethod()); + + private static InputMethodInfo sProperty = new InputMethodInfo( + "android.text.inputmethod", DefaultInputMethod.class.getName(), + "Default", "android.text.inputmethod.defaultImeSettings"); + + private InputConnection mInputConnection; + + public static IInputMethod getInstance() { + return sInstance; + } + + public static InputMethodInfo getMetaInfo() { + return sProperty; + } + + public void bindInput(InputBinding binding) { + mInputConnection = binding.getConnection(); + } + + public void unbindInput() { + } + + public void createSession(SessionCallback callback) { + callback.sessionCreated(this); + } + + public void setSessionEnabled(InputMethodSession session, boolean enabled) { + } + + public void revokeSession(InputMethodSession session) { + } + + public void finishInput() { + mInputConnection.hideStatusIcon(); + } + + public void displayCompletions(CompletionInfo[] completions) { + } + + public void updateExtractedText(int token, ExtractedText text) { + } + + public void updateSelection(int oldSelStart, int oldSelEnd, + int newSelStart, int newSelEnd) { + } + + public void updateCursor(Rect newCursor) { + } + + public void dispatchKeyEvent(int seq, KeyEvent event, EventCallback callback) { + callback.finishedEvent(seq, false); + } + + public void dispatchTrackballEvent(int seq, MotionEvent event, EventCallback callback) { + callback.finishedEvent(seq, false); + } + + public void restartInput(EditorInfo attribute) { + } + + public void attachToken(IBinder token) { + } + + public void startInput(EditorInfo attribute) { + mInputConnection + .showStatusIcon("android", com.android.internal.R.drawable.ime_qwerty); + } + + public void appPrivateCommand(String action, Bundle data) { + } + + public void hideSoftInput() { + } + + public void showSoftInput() { + } +} + +// ---------------------------------------------------------------------- + +class SimpleInputMethod extends IInputMethod.Stub { + final InputMethod mInputMethod; + + static class Session extends IInputMethodSession.Stub { + final InputMethodSession mSession; + + Session(InputMethodSession session) { + mSession = session; + } + + public void finishInput() { + mSession.finishInput(); + } + + public void updateSelection(int oldSelStart, int oldSelEnd, + int newSelStart, int newSelEnd) { + mSession.updateSelection(oldSelStart, oldSelEnd, + newSelStart, newSelEnd); + } + + public void updateCursor(Rect newCursor) { + mSession.updateCursor(newCursor); + } + + static class InputMethodEventCallbackWrapper implements InputMethodSession.EventCallback { + final IInputMethodCallback mCb; + InputMethodEventCallbackWrapper(IInputMethodCallback cb) { + mCb = cb; + } + public void finishedEvent(int seq, boolean handled) { + try { + mCb.finishedEvent(seq, handled); + } catch (RemoteException e) { + } + } + } + + public void dispatchKeyEvent(int seq, KeyEvent event, IInputMethodCallback callback) { + mSession.dispatchKeyEvent(seq, event, + new InputMethodEventCallbackWrapper(callback)); + } + + public void dispatchTrackballEvent(int seq, MotionEvent event, IInputMethodCallback callback) { + mSession.dispatchTrackballEvent(seq, event, + new InputMethodEventCallbackWrapper(callback)); + } + + public void displayCompletions(CompletionInfo[] completions) { + mSession.displayCompletions(completions); + } + + public void updateExtractedText(int token, ExtractedText text) { + mSession.updateExtractedText(token, text); + } + + public void appPrivateCommand(String action, Bundle data) { + mSession.appPrivateCommand(action, data); + } + } + + public SimpleInputMethod(InputMethod inputMethod) { + mInputMethod = inputMethod; + } + + public InputMethod getInternalInputMethod() { + return mInputMethod; + } + + public void attachToken(IBinder token) { + mInputMethod.attachToken(token); + } + + public void bindInput(InputBinding binding) { + InputConnectionWrapper ic = new InputConnectionWrapper( + IInputContext.Stub.asInterface(binding.getConnectionToken())); + InputBinding nu = new InputBinding(ic, binding); + mInputMethod.bindInput(nu); + } + + public void unbindInput() { + mInputMethod.unbindInput(); + } + + public void restartInput(EditorInfo attribute) { + mInputMethod.restartInput(attribute); + } + + public void startInput(EditorInfo attribute) { + mInputMethod.startInput(attribute); + } + + static class InputMethodSessionCallbackWrapper implements InputMethod.SessionCallback { + final IInputMethodCallback mCb; + InputMethodSessionCallbackWrapper(IInputMethodCallback cb) { + mCb = cb; + } + + public void sessionCreated(InputMethodSession session) { + try { + mCb.sessionCreated(new Session(session)); + } catch (RemoteException e) { + } + } + } + + public void createSession(IInputMethodCallback callback) throws RemoteException { + mInputMethod.createSession(new InputMethodSessionCallbackWrapper(callback)); + } + + public void setSessionEnabled(IInputMethodSession session, boolean enabled) throws RemoteException { + try { + InputMethodSession ls = ((Session)session).mSession; + mInputMethod.setSessionEnabled(ls, enabled); + } catch (ClassCastException e) { + Log.w("SimpleInputMethod", "Incoming session not of correct type: " + session, e); + } + } + + public void revokeSession(IInputMethodSession session) throws RemoteException { + try { + InputMethodSession ls = ((Session)session).mSession; + mInputMethod.revokeSession(ls); + } catch (ClassCastException e) { + Log.w("SimpleInputMethod", "Incoming session not of correct type: " + session, e); + } + } + + public void showSoftInput() { + } + + public void hideSoftInput() { + } +} diff --git a/core/java/android/view/inputmethod/EditorInfo.aidl b/core/java/android/view/inputmethod/EditorInfo.aidl new file mode 100644 index 0000000..48068f0 --- /dev/null +++ b/core/java/android/view/inputmethod/EditorInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.inputmethod; + +parcelable EditorInfo; diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java new file mode 100644 index 0000000..c050691 --- /dev/null +++ b/core/java/android/view/inputmethod/EditorInfo.java @@ -0,0 +1,126 @@ +package android.view.inputmethod; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.InputType; +import android.text.TextUtils; + +/** + * An EditorInfo describes several attributes of a text editing object + * that an input method is communicating with (typically an EditText), most + * importantly the type of text content it contains. + */ +public class EditorInfo implements InputType, Parcelable { + /** + * The content type of the text box, whose bits are defined by + * {@link InputType}. + * + * @see InputType + * @see #TYPE_MASK_CLASS + * @see #TYPE_MASK_VARIATION + * @see #TYPE_MASK_FLAGS + */ + public int inputType = TYPE_CLASS_TEXT; + + /** + * A string supplying additional information about the content type that + * is private to a particular IME implementation. The string must be + * scoped to a package owned by the implementation, to ensure there are + * no conflicts between implementations, but other than that you can put + * whatever you want in it to communicate with the IME. For example, + * you could have a string that supplies an argument like + * <code>"com.example.myapp.SpecialMode=3"</code>. This field is can be + * filled in from the {@link android.R.attr#editorPrivateContentType} + * attribute of a TextView. + */ + public String privateContentType = null; + + /** + * The text offset of the start of the selection at the time editing + * began; -1 if not known. + */ + public int initialSelStart = -1; + + /** + * The text offset of the end of the selection at the time editing + * began; -1 if not known. + */ + public int initialSelEnd = -1; + + /** + * The capitalization mode of the first character being edited in the + * text. Values may be any combination of + * {@link TextUtils#CAP_MODE_CHARACTERS TextUtils.CAP_MODE_CHARACTERS}, + * {@link TextUtils#CAP_MODE_WORDS TextUtils.CAP_MODE_WORDS}, and + * {@link TextUtils#CAP_MODE_SENTENCES TextUtils.CAP_MODE_SENTENCES}, though + * you should generally just take a non-zero value to mean start out in + * caps mode. + */ + public int initialCapsMode = 0; + + /** + * The "hint" text of the text view, typically shown in-line when the + * text is empty to tell the user what to enter. + */ + public CharSequence hintText; + + /** + * A label to show to the user describing the text they are writing. + */ + public CharSequence label; + + /** + * Any extra data to supply to the input method. This is for extended + * communication with specific input methods; the name fields in the + * bundle should be scoped (such as "com.mydomain.im.SOME_FIELD") so + * that they don't conflict with others. This field is can be + * filled in from the {@link android.R.attr#editorExtras} + * attribute of a TextView. + */ + public Bundle extras; + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(inputType); + dest.writeString(privateContentType); + dest.writeInt(initialSelStart); + dest.writeInt(initialSelEnd); + dest.writeInt(initialCapsMode); + TextUtils.writeToParcel(hintText, dest, flags); + TextUtils.writeToParcel(label, dest, flags); + dest.writeBundle(extras); + } + + /** + * Used to make this class parcelable. + */ + public static final Parcelable.Creator<EditorInfo> CREATOR = new Parcelable.Creator<EditorInfo>() { + public EditorInfo createFromParcel(Parcel source) { + EditorInfo res = new EditorInfo(); + res.inputType = source.readInt(); + res.privateContentType = source.readString(); + res.initialSelStart = source.readInt(); + res.initialSelEnd = source.readInt(); + res.initialCapsMode = source.readInt(); + res.hintText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + res.label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + res.extras = source.readBundle(); + return res; + } + + public EditorInfo[] newArray(int size) { + return new EditorInfo[size]; + } + }; + + public int describeContents() { + return 0; + } + +} diff --git a/core/java/android/view/inputmethod/ExtractedText.aidl b/core/java/android/view/inputmethod/ExtractedText.aidl new file mode 100644 index 0000000..95e56d7 --- /dev/null +++ b/core/java/android/view/inputmethod/ExtractedText.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.inputmethod; + +parcelable ExtractedText; diff --git a/core/java/android/view/inputmethod/ExtractedText.java b/core/java/android/view/inputmethod/ExtractedText.java new file mode 100644 index 0000000..0ca3c79 --- /dev/null +++ b/core/java/android/view/inputmethod/ExtractedText.java @@ -0,0 +1,82 @@ +package android.view.inputmethod; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * Information about text that has been extracted for use by an input method. + */ +public class ExtractedText implements Parcelable { + /** + * The text that has been extracted. + */ + public CharSequence text; + + /** + * The offset in the overall text at which the extracted text starts. + */ + public int startOffset; + + /** + * The offset where the selection currently starts within the extracted + * text. The real selection start position is at + * <var>startOffset</var>+<var>selectionStart</var>. + */ + public int selectionStart; + + /** + * The offset where the selection currently ends within the extracted + * text. The real selection end position is at + * <var>startOffset</var>+<var>selectionEnd</var>. + */ + public int selectionEnd; + + /** + * Bit for {@link #flags}: set if the text being edited can only be on + * a single line. + */ + public static final int FLAG_SINGLE_LINE = 0x0001; + + /** + * Additional bit flags of information about the edited text. + */ + public int flags; + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + public void writeToParcel(Parcel dest, int flags) { + TextUtils.writeToParcel(text, dest, flags); + dest.writeInt(startOffset); + dest.writeInt(selectionStart); + dest.writeInt(selectionEnd); + dest.writeInt(flags); + } + + /** + * Used to make this class parcelable. + */ + public static final Parcelable.Creator<ExtractedText> CREATOR = new Parcelable.Creator<ExtractedText>() { + public ExtractedText createFromParcel(Parcel source) { + ExtractedText res = new ExtractedText(); + res.text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + res.startOffset = source.readInt(); + res.selectionStart = source.readInt(); + res.selectionEnd = source.readInt(); + res.flags = source.readInt(); + return res; + } + + public ExtractedText[] newArray(int size) { + return new ExtractedText[size]; + } + }; + + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/view/inputmethod/ExtractedTextRequest.aidl b/core/java/android/view/inputmethod/ExtractedTextRequest.aidl new file mode 100644 index 0000000..c69acc7 --- /dev/null +++ b/core/java/android/view/inputmethod/ExtractedTextRequest.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.inputmethod; + +parcelable ExtractedTextRequest; diff --git a/core/java/android/view/inputmethod/ExtractedTextRequest.java b/core/java/android/view/inputmethod/ExtractedTextRequest.java new file mode 100644 index 0000000..d962329 --- /dev/null +++ b/core/java/android/view/inputmethod/ExtractedTextRequest.java @@ -0,0 +1,61 @@ +package android.view.inputmethod; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * Description of what an input method would like from an application when + * extract text from its input editor. + */ +public class ExtractedTextRequest implements Parcelable { + /** + * Arbitrary integer that can be supplied in the request, which will be + * delivered back when reporting updates. + */ + public int token; + + /** + * Hint for the maximum number of lines to return. + */ + public int hintMaxLines; + + /** + * Hint for the maximum number of characters to return. + */ + public int hintMaxChars; + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(token); + dest.writeInt(hintMaxLines); + dest.writeInt(hintMaxChars); + } + + /** + * Used to make this class parcelable. + */ + public static final Parcelable.Creator<ExtractedTextRequest> CREATOR + = new Parcelable.Creator<ExtractedTextRequest>() { + public ExtractedTextRequest createFromParcel(Parcel source) { + ExtractedTextRequest res = new ExtractedTextRequest(); + res.token = source.readInt(); + res.hintMaxLines = source.readInt(); + res.hintMaxChars = source.readInt(); + return res; + } + + public ExtractedTextRequest[] newArray(int size) { + return new ExtractedTextRequest[size]; + } + }; + + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/view/inputmethod/InputBinding.aidl b/core/java/android/view/inputmethod/InputBinding.aidl new file mode 100644 index 0000000..ea09d8b --- /dev/null +++ b/core/java/android/view/inputmethod/InputBinding.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.inputmethod; + +parcelable InputBinding; diff --git a/core/java/android/view/inputmethod/InputBinding.java b/core/java/android/view/inputmethod/InputBinding.java new file mode 100644 index 0000000..f4209ef --- /dev/null +++ b/core/java/android/view/inputmethod/InputBinding.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.view.inputmethod; + +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * Information given to an {@link InputMethod} about a client connecting + * to it. + */ +public final class InputBinding implements Parcelable { + static final String TAG = "InputBinding"; + + /** + * The connection back to the client. + */ + final InputConnection mConnection; + + /** + * A remotable token for the connection back to the client. + */ + final IBinder mConnectionToken; + + /** + * The UID where this binding came from. + */ + final int mUid; + + /** + * The PID where this binding came from. + */ + final int mPid; + + /** + * Constructor. + * + * @param conn The interface for communicating back with the application. + * @param connToken A remoteable token for communicating across processes. + * @param uid The user id of the client of this binding. + * @param pid The process id of where the binding came from. + */ + public InputBinding(InputConnection conn, IBinder connToken, + int uid, int pid) { + mConnection = conn; + mConnectionToken = connToken; + mUid = uid; + mPid = pid; + } + + /** + * Constructor from an existing InputBinding taking a new local input + * connection interface. + * + * @param conn The new connection interface. + * @param binding Existing binding to copy. + */ + public InputBinding(InputConnection conn, InputBinding binding) { + mConnection = conn; + mConnectionToken = binding.getConnectionToken(); + mUid = binding.getUid(); + mPid = binding.getPid(); + } + + InputBinding(Parcel source) { + mConnection = null; + mConnectionToken = source.readStrongBinder(); + mUid = source.readInt(); + mPid = source.readInt(); + } + + /** + * Return the connection for interacting back with the application. + */ + public InputConnection getConnection() { + return mConnection; + } + + /** + * Return the token for the connection back to the application. You can + * not use this directly, it must be converted to a {@link InputConnection} + * for you. + */ + public IBinder getConnectionToken() { + return mConnectionToken; + } + + /** + * Return the user id of the client associated with this binding. + */ + public int getUid() { + return mUid; + } + + /** + * Return the process id where this binding came from. + */ + public int getPid() { + return mPid; + } + + @Override + public String toString() { + return "InputBinding{" + mConnectionToken + + " / uid " + mUid + " / pid " + mPid + "}"; + } + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mConnectionToken); + dest.writeInt(mUid); + dest.writeInt(mPid); + } + + /** + * Used to make this class parcelable. + */ + public static final Parcelable.Creator<InputBinding> CREATOR = new Parcelable.Creator<InputBinding>() { + public InputBinding createFromParcel(Parcel source) { + return new InputBinding(source); + } + + public InputBinding[] newArray(int size) { + return new InputBinding[size]; + } + }; + + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java new file mode 100644 index 0000000..5f8ba1f --- /dev/null +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.view.inputmethod; + +import android.os.Bundle; +import android.text.Spanned; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; + +/** + * The InputConnection interface is the communication channel from an + * {@link InputMethod} back to the application that is receiving its input. It + * is used to perform such things as reading text around the cursor, + * committing text to the text box, and sending raw key events to the application. + */ +public interface InputConnection { + /** + * Get <var>n</var> characters of text before the current cursor position. + * + * <p>This method may fail either if the input connection has become invalid + * (such as its process crashing) or the client is taking too long to + * respond with the text (it is given a couple seconds to return). + * In either case, a null is returned. + * + * @param n The expected length of the text. + * + * @return Returns the text before the cursor position; the length of the + * returned text might be less than <var>n</var>. + */ + public CharSequence getTextBeforeCursor(int n); + + /** + * Get <var>n</var> characters of text after the current cursor position. + * + * <p>This method may fail either if the input connection has become invalid + * (such as its process crashing) or the client is taking too long to + * respond with the text (it is given a couple seconds to return). + * In either case, a null is returned. + * + * @param n The expected length of the text. + * + * @return Returns the text after the cursor position; the length of the + * returned text might be less than <var>n</var>. + */ + public CharSequence getTextAfterCursor(int n); + + /** + * Retrieve the current capitalization mode in effect at the current + * cursor position in the text. See + * {@link android.text.TextUtils#getCapsMode TextUtils.getCapsMode} for + * more information. + * + * <p>This method may fail either if the input connection has become invalid + * (such as its process crashing) or the client is taking too long to + * respond with the text (it is given a couple seconds to return). + * In either case, a 0 is returned. + * + * @param reqModes The desired modes to retrieve, as defined by + * {@link android.text.TextUtils#getCapsMode TextUtils.getCapsMode}. These + * constants are defined so that you can simply pass the current + * {@link EditorInfo#inputType TextBoxAttribute.contentType} value + * directly in to here. + * + * @return Returns the caps mode flags that are in effect. + */ + public int getCursorCapsMode(int reqModes); + + public static final int EXTRACTED_TEXT_MONITOR = 0x0001; + + /** + * Retrieve the current text in the input connection's editor, and monitor + * for any changes to it. This function returns with the current text, + * and optionally the input connection can send updates to the + * input method when its text changes. + * + * <p>This method may fail either if the input connection has become invalid + * (such as its process crashing) or the client is taking too long to + * respond with the text (it is given a couple seconds to return). + * In either case, a null is returned. + * + * @param request Description of how the text should be returned. + * @param flags Additional options to control the client, either 0 or + * {@link #EXTRACTED_TEXT_MONITOR}. + * + * @return Returns an ExtractedText object describing the state of the + * text view and containing the extracted text itself. + */ + public ExtractedText getExtractedText(ExtractedTextRequest request, + int flags); + + /** + * Delete <var>leftLength</var> characters of text before the current cursor + * position, and delete <var>rightLength</var> characters of text after the + * current cursor position, excluding composing text. + * + * @param leftLength The number of characters to be deleted before the + * current cursor position. + * @param rightLength The number of characters to be deleted after the + * current cursor position. + * + * @return Returns true on success, false if the input connection is no longer + * valid. + */ + boolean deleteSurroundingText(int leftLength, int rightLength); + + /** + * Set composing text around the current cursor position with the given text, + * and set the new cursor position. Any composing text set previously will + * be removed automatically. + * + * @param text The composing text with styles if necessary. If no style + * object attached to the text, the default style for composing text + * is used. See {#link android.text.Spanned} for how to attach style + * object to the text. {#link android.text.SpannableString} and + * {#link android.text.SpannableStringBuilder} are two + * implementations of the interface {#link android.text.Spanned}. + * @param newCursorPosition The new cursor position within the + * <var>text</var>. + * + * @return Returns true on success, false if the input connection is no longer + * valid. + */ + public boolean setComposingText(CharSequence text, int newCursorPosition); + + /** + * Commit text to the text box and set the new cursor position. + * Any composing text set previously will be removed + * automatically. + * + * @param text The committed text. + * @param newCursorPosition The new cursor position within the + * <var>text</var>. + * + * + * @return Returns true on success, false if the input connection is no longer + * valid. + */ + public boolean commitText(CharSequence text, int newCursorPosition); + + /** + * Commit a completion the user has selected from the possible ones + * previously reported to {@link InputMethodSession#displayCompletions + * InputMethodSession.displayCompletions()}. This will result in the + * same behavior as if the user had selected the completion from the + * actual UI. + * + * @param text The committed completion. + * + * @return Returns true on success, false if the input connection is no longer + * valid. + */ + public boolean commitCompletion(CompletionInfo text); + + /** + * Send a key event to the process that is currently attached through + * this input connection. The event will be dispatched like a normal + * key event, to the currently focused; this generally is the view that + * is providing this InputConnection, but due to the asynchronous nature + * of this protocol that can not be guaranteed and the focus may have + * changed by the time the event is received. + * + * <p> + * This method can be used to send key events to the application. For + * example, an on-screen keyboard may use this method to simulate a hardware + * keyboard. There are three types of standard keyboards, numeric (12-key), + * predictive (20-key) and ALPHA (QWERTY). You can specify the keyboard type + * by specify the device id of the key event. + * + * <p> + * You will usually want to set the flag + * {@link KeyEvent#FLAG_SOFT_KEYBOARD KeyEvent.FLAG_SOFT_KEYBOARD} on all + * key event objects you give to this API; the flag will not be set + * for you. + * + * @param event The key event. + * + * @return Returns true on success, false if the input connection is no longer + * valid. + * + * @see KeyEvent + * @see KeyCharacterMap#NUMERIC + * @see KeyCharacterMap#PREDICTIVE + * @see KeyCharacterMap#ALPHA + */ + public boolean sendKeyEvent(KeyEvent event); + + /** + * Clear the given meta key pressed states in the given input connection. + * + * @param states The states to be cleared, may be one or more bits as + * per {@link KeyEvent#getMetaState() KeyEvent.getMetaState()}. + * + * @return Returns true on success, false if the input connection is no longer + * valid. + */ + public boolean clearMetaKeyStates(int states); + + /** + * API to send private commands from an input method to its connected + * editor. This can be used to provide domain-specific features that are + * only known between certain input methods and their clients. Note that + * because the InputConnection protocol is asynchronous, you have no way + * to get a result back or know if the client understood the command; you + * can use the information in {@link EditorInfo} to determine if + * a client supports a particular command. + * + * @param action Name of the command to be performed. This <em>must</em> + * be a scoped name, i.e. prefixed with a package name you own, so that + * different developers will not create conflicting commands. + * @param data Any data to include with the command. + * @return Returns true if the command was sent (whether or not the + * associated editor understood it), false if the input connection is no longer + * valid. + */ + public boolean performPrivateCommand(String action, Bundle data); + + /** + * Show an icon in the status bar. + * + * @param packageName The package holding the icon resource to be shown. + * @param resId The resource id of the icon to show. + * + * @return Returns true on success, false if the input connection is no longer + * valid. + */ + public boolean showStatusIcon(String packageName, int resId); + + /** + * Hide the icon shown in the status bar. + * + * @return Returns true on success, false if the input connection is no longer + * valid. + */ + public boolean hideStatusIcon(); +} diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java new file mode 100644 index 0000000..f150ad6 --- /dev/null +++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.view.inputmethod; + +import android.os.Bundle; +import android.view.KeyEvent; + +/** + * Wrapper around InputConnection interface, calling through to another + * implementation of it. + */ +public class InputConnectionWrapper implements InputConnection { + InputConnection mBase; + + /** + * Create a new wrapper around an existing InputConnection implementation. + */ + public InputConnectionWrapper(InputConnection base) { + mBase = base; + } + + /** + * Return the base InputConnection that this class is wrapping. + */ + InputConnection getBase() { + return mBase; + } + + public CharSequence getTextBeforeCursor(int n) { + return mBase.getTextBeforeCursor(n); + } + + public CharSequence getTextAfterCursor(int n) { + return mBase.getTextAfterCursor(n); + } + + public int getCursorCapsMode(int reqModes) { + return mBase.getCursorCapsMode(reqModes); + } + + public ExtractedText getExtractedText(ExtractedTextRequest request, + int flags) { + return mBase.getExtractedText(request, flags); + } + + public boolean deleteSurroundingText(int leftLength, int rightLength) { + return mBase.deleteSurroundingText(leftLength, rightLength); + } + + public boolean setComposingText(CharSequence text, int newCursorPosition) { + return mBase.setComposingText(text, newCursorPosition); + } + + public boolean commitText(CharSequence text, int newCursorPosition) { + return mBase.commitText(text, newCursorPosition); + } + + public boolean commitCompletion(CompletionInfo text) { + return mBase.commitCompletion(text); + } + + public boolean sendKeyEvent(KeyEvent event) { + return mBase.sendKeyEvent(event); + } + + public boolean clearMetaKeyStates(int states) { + return mBase.clearMetaKeyStates(states); + } + + public boolean performPrivateCommand(String action, Bundle data) { + return mBase.performPrivateCommand(action, data); + } + + public boolean showStatusIcon(String packageName, int resId) { + return mBase.showStatusIcon(packageName, resId); + } + + public boolean hideStatusIcon() { + return mBase.hideStatusIcon(); + } +} diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java new file mode 100644 index 0000000..259e759 --- /dev/null +++ b/core/java/android/view/inputmethod/InputMethod.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.view.inputmethod; + +import android.graphics.Rect; +import android.inputmethodservice.InputMethodService; +import android.os.Bundle; +import android.os.IBinder; +import android.view.KeyEvent; +import android.view.MotionEvent; + +/** + * The InputMethod interface represents an input method which can generate key + * events and text, such as digital, email addresses, CJK characters, other + * language characters, and etc., while handling various input events, and send + * the text back to the application that requests text input. + * + * <p>Applications will not normally use this interface themselves, instead + * relying on the standard interaction provided by + * {@link android.widget.TextView} and {@link android.widget.EditText}. + * + * <p>Those implementing input methods should normally do so by deriving from + * {@link InputMethodService} or one of its subclasses. When implementing + * an input method, the service component containing it must also supply + * a {@link #SERVICE_META_DATA} meta-data field, referencing an XML resource + * providing details about the input method. All input methods also must + * require that clients hold the + * {@link android.Manifest.permission#BIND_INPUT_METHOD} in order to interact + * with the service; if this is not required, the system will not use that + * input method, because it can not trust that it is not compromised. + */ +public interface InputMethod { + /** + * This is the interface name that a service implementing an input + * method should say that it supports -- that is, this is the action it + * uses for its intent filter. (Note: this name is used because this + * interface should be moved to the view package.) + */ + public static final String SERVICE_INTERFACE = "android.view.InputMethod"; + + /** + * Name under which an InputMethod service component publishes information + * about itself. This meta-data must reference an XML resource containing + * an + * <code><{@link android.R.styleable#InputMethod input-method}></code> + * tag. + */ + public static final String SERVICE_META_DATA = "android.view.im"; + + public interface SessionCallback { + public void sessionCreated(InputMethodSession session); + } + + /** + * Called first thing after an input method is created, this supplies a + * unique token for the session it has with the system service. It is + * needed to identify itself with the service to validate its operations. + * This token <strong>must not</strong> be passed to applications, since + * it grants special priviledges that should not be given to applications. + * + * <p>Note: to protect yourself from malicious clients, you should only + * accept the first token given to you. Any after that may come from the + * client. + */ + public void attachToken(IBinder token); + + /** + * Bind a new application environment in to the input method, so that it + * can later start and stop input processing. + * Typically this method is called when this input method is enabled in an + * application for the first time. + * + * @param binding Information about the application window that is binding + * to the input method. + * + * @see InputBinding + * @see #unbindInput() + */ + public void bindInput(InputBinding binding); + + /** + * Unbind an application environment, called when the information previously + * set by {@link #bindInput} is no longer valid for this input method. + * + * <p> + * Typically this method is called when the application changes to be + * non-foreground. + */ + public void unbindInput(); + + /** + * This method is called when the application starts to receive text and it + * is ready for this input method to process received events and send result + * text back to the application. + * + * @param attribute The attribute of the text box (typically, a EditText) + * that requests input. + * + * @see EditorInfo + */ + public void startInput(EditorInfo attribute); + + /** + * This method is called when the state of this input method needs to be + * reset. + * + * <p> + * Typically, this method is called when the input focus is moved from one + * text box to another. + * + * @param attribute The attribute of the text box (typically, a EditText) + * that requests input. + * + * @see EditorInfo + */ + public void restartInput(EditorInfo attribute); + + /** + * Create a new {@link InputMethodSession} that can be handed to client + * applications for interacting with the input method. You can later + * use {@link #revokeSession(InputMethodSession)} to destroy the session + * so that it can no longer be used by any clients. + * + * @param callback Interface that is called with the newly created session. + */ + public void createSession(SessionCallback callback); + + /** + * Control whether a particular input method session is active. + * + * @param session The {@link InputMethodSession} previously provided through + * SessionCallback.sessionCreated() that is to be changed. + */ + public void setSessionEnabled(InputMethodSession session, boolean enabled); + + /** + * Disable and destroy a session that was previously created with + * {@link #createSession(android.view.inputmethod.InputMethod.SessionCallback)}. + * After this call, the given session interface is no longer active and + * calls on it will fail. + * + * @param session The {@link InputMethodSession} previously provided through + * SessionCallback.sessionCreated() that is to be revoked. + */ + public void revokeSession(InputMethodSession session); + + /** + * Request that any soft input part of the input method be shown to the user. + */ + public void showSoftInput(); + + /** + * Request that any soft input part of the input method be hidden from the user. + */ + public void hideSoftInput(); +} diff --git a/core/java/android/view/inputmethod/InputMethodInfo.aidl b/core/java/android/view/inputmethod/InputMethodInfo.aidl new file mode 100644 index 0000000..5f4d6b6 --- /dev/null +++ b/core/java/android/view/inputmethod/InputMethodInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.inputmethod; + +parcelable InputMethodInfo; diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java new file mode 100644 index 0000000..e8f4b54 --- /dev/null +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.view.inputmethod; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Printer; +import android.util.Xml; + +import java.io.IOException; + +/** + * This class is used to specify meta information of an input method. + */ +public final class InputMethodInfo implements Parcelable { + static final String TAG = "InputMethodMetaInfo"; + + /** + * The Service that implements this input method component. + */ + final ResolveInfo mService; + + /** + * The unique string Id to identify the input method. This is generated + * from the input method component. + */ + final String mId; + + /** + * The input method setting activity's name, used by the system settings to + * launch the setting activity of this input method. + */ + final String mSettingsActivityName; + + /** + * The resource in the input method's .apk that holds a boolean indicating + * whether it should be considered the default input method for this + * system. This is a resource ID instead of the final value so that it + * can change based on the configuration (in particular locale). + */ + final int mIsDefaultResId; + + /** + * Constructor. + * + * @param context The Context in which we are parsing the input method. + * @param service The ResolveInfo returned from the package manager about + * this input method's component. + */ + public InputMethodInfo(Context context, ResolveInfo service) + throws XmlPullParserException, IOException { + mService = service; + ServiceInfo si = service.serviceInfo; + mId = new ComponentName(si.packageName, si.name).flattenToShortString(); + + PackageManager pm = context.getPackageManager(); + String settingsActivityComponent = null; + int isDefaultResId = 0; + + XmlResourceParser parser = null; + try { + parser = si.loadXmlMetaData(pm, InputMethod.SERVICE_META_DATA); + if (parser == null) { + throw new XmlPullParserException("No " + + InputMethod.SERVICE_META_DATA + " meta-data"); + } + + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + } + + String nodeName = parser.getName(); + if (!"input-method".equals(nodeName)) { + throw new XmlPullParserException( + "Meta-data does not start with input-method tag"); + } + + TypedArray sa = context.getResources().obtainAttributes(attrs, + com.android.internal.R.styleable.InputMethod); + settingsActivityComponent = sa.getString( + com.android.internal.R.styleable.InputMethod_settingsActivity); + isDefaultResId = sa.getResourceId( + com.android.internal.R.styleable.InputMethod_isDefault, 0); + sa.recycle(); + } finally { + if (parser != null) parser.close(); + } + + mSettingsActivityName = settingsActivityComponent; + mIsDefaultResId = isDefaultResId; + } + + InputMethodInfo(Parcel source) { + mId = source.readString(); + mSettingsActivityName = source.readString(); + mIsDefaultResId = source.readInt(); + mService = ResolveInfo.CREATOR.createFromParcel(source); + } + + /** + * Temporary API for creating a built-in input method. + */ + public InputMethodInfo(String packageName, String className, + CharSequence label, String settingsActivity) { + ResolveInfo ri = new ResolveInfo(); + ServiceInfo si = new ServiceInfo(); + ApplicationInfo ai = new ApplicationInfo(); + ai.packageName = packageName; + ai.enabled = true; + si.applicationInfo = ai; + si.enabled = true; + si.packageName = packageName; + si.name = className; + si.exported = true; + si.nonLocalizedLabel = label; + ri.serviceInfo = si; + mService = ri; + mId = new ComponentName(si.packageName, si.name).flattenToShortString(); + mSettingsActivityName = settingsActivity; + mIsDefaultResId = 0; + } + + /** + * Return a unique ID for this input method. The ID is generated from + * the package and class name implementing the method. + */ + public String getId() { + return mId; + } + + /** + * Return the .apk package that implements this input method. + */ + public String getPackageName() { + return mService.serviceInfo.packageName; + } + + /** + * Return the class name of the service component that implements + * this input method. + */ + public String getServiceName() { + return mService.serviceInfo.name; + } + + /** + * Return the component of the service that implements this input + * method. + */ + public ComponentName getComponent() { + return new ComponentName(mService.serviceInfo.packageName, + mService.serviceInfo.name); + } + + /** + * Load the user-displayed label for this input method. + * + * @param pm Supply a PackageManager used to load the input method's + * resources. + */ + public CharSequence loadLabel(PackageManager pm) { + return mService.loadLabel(pm); + } + + /** + * Load the user-displayed icon for this input method. + * + * @param pm Supply a PackageManager used to load the input method's + * resources. + */ + public Drawable loadIcon(PackageManager pm) { + return mService.loadIcon(pm); + } + + /** + * Return the class name of an activity that provides a settings UI for + * the input method. You can launch this activity be starting it with + * an {@link android.content.Intent} whose action is MAIN and with an + * explicit {@link android.content.ComponentName} + * composed of {@link #getPackageName} and the class name returned here. + * + * <p>A null will be returned if there is no settings activity associated + * with the input method. + */ + public String getSettingsActivity() { + return mSettingsActivityName; + } + + /** + * Return the resource identifier of a resource inside of this input + * method's .apk that determines whether it should be considered a + * default input method for the system. + */ + public int getIsDefaultResourceId() { + return mIsDefaultResId; + } + + /** + * Returns true if this input method is one of the components that is + * built in to the system. + */ + public boolean isBuiltin() { + return mService.serviceInfo.packageName.equals( + InputMethodManager.BUILDIN_INPUTMETHOD_PACKAGE); + } + + public void dump(Printer pw, String prefix) { + pw.println(prefix + "mId=" + mId + + " mSettingsActivityName=" + mSettingsActivityName); + pw.println(prefix + "mIsDefaultResId=0x" + + Integer.toHexString(mIsDefaultResId)); + pw.println(prefix + "Service:"); + mService.dump(pw, prefix + " "); + } + + @Override + public String toString() { + return "InputMethodMetaInfo{" + mId + + ", settings: " + + mSettingsActivityName + "}"; + } + + /** + * Used to test whether the given parameter object is an + * {@link InputMethodInfo} and its Id is the same to this one. + * + * @return true if the given parameter object is an + * {@link InputMethodInfo} and its Id is the same to this one. + */ + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (o == null) return false; + + if (!(o instanceof InputMethodInfo)) return false; + + InputMethodInfo obj = (InputMethodInfo) o; + return mId.equals(obj.mId); + } + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mId); + dest.writeString(mSettingsActivityName); + dest.writeInt(mIsDefaultResId); + mService.writeToParcel(dest, flags); + } + + /** + * Used to make this class parcelable. + */ + public static final Parcelable.Creator<InputMethodInfo> CREATOR = new Parcelable.Creator<InputMethodInfo>() { + public InputMethodInfo createFromParcel(Parcel source) { + return new InputMethodInfo(source); + } + + public InputMethodInfo[] newArray(int size) { + return new InputMethodInfo[size]; + } + }; + + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java new file mode 100644 index 0000000..da82593 --- /dev/null +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -0,0 +1,900 @@ +/* + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.view.inputmethod; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; + +import com.android.internal.view.IInputConnectionWrapper; +import com.android.internal.view.IInputContext; +import com.android.internal.view.IInputMethodCallback; +import com.android.internal.view.IInputMethodClient; +import com.android.internal.view.IInputMethodManager; +import com.android.internal.view.IInputMethodSession; +import com.android.internal.view.InputBindResult; + +import java.util.List; + +/** + * Public interface to the global input method manager. You can retrieve + * an instance of this interface with + * {@link Context#getSystemService(String) Context.getSystemService()}. + */ +public final class InputMethodManager { + static final boolean DEBUG = false; + static final String TAG = "InputMethodManager"; + + /** + * The package name of the build-in input method. + * {@hide} + */ + public static final String BUILDIN_INPUTMETHOD_PACKAGE = "android.text.inputmethod"; + + static final Object mInstanceSync = new Object(); + static InputMethodManager mInstance; + + final IInputMethodManager mService; + final Looper mMainLooper; + + // For scheduling work on the main thread. This also serves as our + // global lock. + final H mH; + + // The currently active input connection. + final MutableInputConnectionWrapper mInputConnectionWrapper; + final IInputContext mIInputContext; + + /** + * True if this input method client is active, initially false. + */ + boolean mActive = false; + + /** + * The current base input connection, used when mActive is true. + */ + InputConnection mCurrentInputConnection; + + // ----------------------------------------------------------- + + /** + * This is the view that should currently be served by an input method, + * regardless of the state of setting that up. + */ + View mServedView; + /** + * For evaluating the state after a focus change, this is the view that + * had focus. + */ + View mLastServedView; + /** + * This is set when we are in the process of connecting, to determine + * when we have actually finished. + */ + boolean mServedConnecting; + /** + * This is non-null when we have connected the served view; it holds + * the attributes that were last retrieved from the served view and given + * to the input connection. + */ + EditorInfo mCurrentTextBoxAttribute; + /** + * The InputConnection that was last retrieved from the served view. + */ + InputConnection mServedInputConnection; + /** + * The completions that were last provided by the served view. + */ + CompletionInfo[] mCompletions; + + // Cursor position on the screen. + Rect mTmpCursorRect = new Rect(); + Rect mCursorRect = new Rect(); + int mCursorSelStart; + int mCursorSelEnd; + + // ----------------------------------------------------------- + + /** + * Sequence number of this binding, as returned by the server. + */ + int mBindSequence = -1; + /** + * ID of the method we are bound to. + */ + String mCurId; + /** + * The actual instance of the method to make calls on it. + */ + IInputMethodSession mCurMethod; + + // ----------------------------------------------------------- + + static final int MSG_CHECK_FOCUS = 1; + + class H extends Handler { + H(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_CHECK_FOCUS: + checkFocus(); + return; + } + } + } + + static class NoOpInputConnection implements InputConnection { + + public boolean clearMetaKeyStates(int states) { + return false; + } + + public boolean commitCompletion(CompletionInfo text) { + return false; + } + + public boolean commitText(CharSequence text, int newCursorPosition) { + return false; + } + + public boolean deleteSurroundingText(int leftLength, int rightLength) { + return false; + } + + public int getCursorCapsMode(int reqModes) { + return 0; + } + + public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { + return null; + } + + public CharSequence getTextAfterCursor(int n) { + return null; + } + + public CharSequence getTextBeforeCursor(int n) { + return null; + } + + public boolean hideStatusIcon() { + return false; + } + + public boolean performPrivateCommand(String action, Bundle data) { + return false; + } + + public boolean sendKeyEvent(KeyEvent event) { + return false; + } + + public boolean setComposingText(CharSequence text, int newCursorPosition) { + return false; + } + + public boolean showStatusIcon(String packageName, int resId) { + return false; + } + } + + final NoOpInputConnection mNoOpInputConnection = new NoOpInputConnection(); + + final IInputMethodClient.Stub mClient = new IInputMethodClient.Stub() { + public void setUsingInputMethod(boolean state) { + + } + + public void onBindMethod(InputBindResult res) { + synchronized (mH) { + if (mBindSequence < 0 || mBindSequence != res.sequence) { + Log.w(TAG, "Ignoring onBind: cur seq=" + mBindSequence + + ", given seq=" + res.sequence); + return; + } + + mCurMethod = res.method; + mCurId = res.id; + mBindSequence = res.sequence; + } + startInputInner(); + } + + public void onUnbindMethod(int sequence) { + synchronized (mH) { + if (mBindSequence == sequence) { + if (false) { + // XXX the server has already unbound! + if (mCurMethod != null && mCurrentTextBoxAttribute != null) { + try { + mCurMethod.finishInput(); + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId, e); + } + } + } + clearBindingLocked(); + + // If we were actively using the last input method, then + // we would like to re-connect to the next input method. + if (mServedView != null && mServedView.isFocused()) { + mServedConnecting = true; + } + } + startInputInner(); + } + } + + public void setActive(boolean active) { + mActive = active; + mInputConnectionWrapper.setBaseInputConnection(active + ? mCurrentInputConnection : mNoOpInputConnection); + } + }; + + final InputConnection mDummyInputConnection = new BaseInputConnection(this) { + public boolean commitText(CharSequence text, int newCursorPosition) { + return false; + } + public boolean commitCompletion(CompletionInfo text) { + return false; + } + public boolean deleteSurroundingText(int leftLength, int rightLength) { + return false; + } + public ExtractedText getExtractedText(ExtractedTextRequest request, + int flags) { + return null; + } + public CharSequence getTextAfterCursor(int n) { + return null; + } + public CharSequence getTextBeforeCursor(int n) { + return null; + } + public int getCursorCapsMode(int reqModes) { + return 0; + } + public boolean clearMetaKeyStates(int states) { + return false; + } + public boolean performPrivateCommand(String action, Bundle data) { + return false; + } + public boolean setComposingText(CharSequence text, int newCursorPosition) { + return false; + } + }; + + InputMethodManager(IInputMethodManager service, Looper looper) { + mService = service; + mMainLooper = looper; + mH = new H(looper); + mInputConnectionWrapper = new MutableInputConnectionWrapper(mNoOpInputConnection); + mIInputContext = new IInputConnectionWrapper(looper, + mInputConnectionWrapper); + setCurrentInputConnection(mDummyInputConnection); + + if (mInstance == null) { + mInstance = this; + } + } + + /** + * Retrieve the global InputMethodManager instance, creating it if it + * doesn't already exist. + * @hide + */ + static public InputMethodManager getInstance(Context context) { + synchronized (mInstanceSync) { + if (mInstance != null) { + return mInstance; + } + IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE); + IInputMethodManager service = IInputMethodManager.Stub.asInterface(b); + mInstance = new InputMethodManager(service, context.getMainLooper()); + } + return mInstance; + } + + /** + * Private optimization: retrieve the global InputMethodManager instance, + * if it exists. + * @hide + */ + static public InputMethodManager peekInstance() { + return mInstance; + } + + /** @hide */ + public IInputMethodClient getClient() { + return mClient; + } + + /** @hide */ + public IInputContext getInputContext() { + return mIInputContext; + } + + public List<InputMethodInfo> getInputMethodList() { + try { + return mService.getInputMethodList(); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + public List<InputMethodInfo> getEnabledInputMethodList() { + try { + return mService.getEnabledInputMethodList(); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + public void updateStatusIcon(int iconId, String iconPackage) { + try { + mService.updateStatusIcon(iconId, iconPackage); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Return true if the given view is the currently active view for the + * input method. + */ + public boolean isActive(View view) { + synchronized (mH) { + return mServedView == view && mCurrentTextBoxAttribute != null; + } + } + + /** + * Return true if any view is currently active in the input method. + */ + public boolean isActive() { + synchronized (mH) { + return mServedView != null && mCurrentTextBoxAttribute != null; + } + } + + /** + * Return true if the currently served view is accepting full text edits. + * If false, it has no input connection, so can only handle raw key events. + */ + public boolean isAcceptingText() { + return mServedInputConnection != null; + } + + /** + * Reset all of the state associated with being bound to an input method. + */ + void clearBindingLocked() { + clearConnectionLocked(); + mBindSequence = -1; + mCurId = null; + mCurMethod = null; + } + + /** + * Record the desired input connection, but only set it if mActive is true. + */ + void setCurrentInputConnection(InputConnection connection) { + mCurrentInputConnection = connection; + mInputConnectionWrapper.setBaseInputConnection(mActive + ? connection : mNoOpInputConnection); + } + + /** + * Reset all of the state associated with a served view being connected + * to an input method + */ + void clearConnectionLocked() { + mCurrentTextBoxAttribute = null; + mServedInputConnection = null; + setCurrentInputConnection(mDummyInputConnection); + } + + /** + * Disconnect any existing input connection, clearing the served view. + */ + void finishInputLocked() { + synchronized (mH) { + if (mServedView != null) { + if (DEBUG) Log.v(TAG, "FINISH INPUT: " + mServedView); + updateStatusIcon(0, null); + + if (mCurrentTextBoxAttribute != null) { + try { + mService.finishInput(mClient); + } catch (RemoteException e) { + } + } + + mServedView = null; + mCompletions = null; + mServedConnecting = false; + clearConnectionLocked(); + } + } + } + + public void displayCompletions(View view, CompletionInfo[] completions) { + synchronized (mH) { + if (mServedView != view) { + return; + } + + mCompletions = completions; + if (mCurMethod != null) { + try { + mCurMethod.displayCompletions(mCompletions); + } catch (RemoteException e) { + } + } + } + } + + public void updateExtractedText(View view, int token, ExtractedText text) { + synchronized (mH) { + if (mServedView != view) { + return; + } + + if (mCurMethod != null) { + try { + mCurMethod.updateExtractedText(token, text); + } catch (RemoteException e) { + } + } + } + } + + /** + * Explicitly request that the current input method's soft input area be + * shown to the user, if needed. Call this if the user interacts with + * your view in such a way that they have expressed they would like to + * start performing input into it. + * + * @param view The currently focused view, which would like to receive + * soft keyboard input. + */ + public void showSoftInput(View view) { + synchronized (mH) { + if (mServedView != view) { + return; + } + + try { + mService.showSoftInput(mClient); + } catch (RemoteException e) { + } + } + } + + /** + * Request to hide the soft input window from the context of the window + * that is currently accepting input. This should be called as a result + * of the user doing some actually than fairly explicitly requests to + * have the input window hidden. + * + * @param windowToken The token of the window that is making the request, + * as returned by {@link View#getWindowToken() View.getWindowToken()}. + */ + public void hideSoftInputFromWindow(IBinder windowToken) { + synchronized (mH) { + if (mServedView == null || mServedView.getWindowToken() != windowToken) { + return; + } + + try { + mService.hideSoftInput(mClient); + } catch (RemoteException e) { + } + } + } + + /** + * If the input method is currently connected to the given view, + * restart it with its new contents. You should call this when the text + * within your view changes outside of the normal input method or key + * input flow, such as when an application calls TextView.setText(). + * + * @param view The view whose text has changed. + */ + public void restartInput(View view) { + synchronized (mH) { + if (mServedView != view) { + return; + } + + mServedConnecting = true; + } + + startInputInner(); + } + + void startInputInner() { + final View view; + synchronized (mH) { + view = mServedView; + + // Make sure we have a window token for the served view. + if (DEBUG) Log.v(TAG, "Starting input: view=" + view); + if (view == null) { + if (DEBUG) Log.v(TAG, "ABORT input: no served view!"); + return; + } + } + + // Now we need to get an input connection from the served view. + // This is complicated in a couple ways: we can't be holding our lock + // when calling out to the view, and we need to make sure we call into + // the view on the same thread that is driving its view hierarchy. + Handler vh = view.getHandler(); + if (vh == null) { + // If the view doesn't have a handler, something has changed out + // from under us, so just bail. + if (DEBUG) Log.v(TAG, "ABORT input: no handler for view!"); + return; + } + if (vh.getLooper() != Looper.myLooper()) { + // The view is running on a different thread than our own, so + // we need to reschedule our work for over there. + if (DEBUG) Log.v(TAG, "Starting input: reschedule to view thread"); + vh.post(new Runnable() { + public void run() { + startInputInner(); + } + }); + } + + // Okay we are now ready to call into the served view and have it + // do its stuff. + // Life is good: let's hook everything up! + EditorInfo tba = new EditorInfo(); + InputConnection ic = view.createInputConnection(tba); + if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic); + + synchronized (mH) { + // Now that we are locked again, validate that our state hasn't + // changed. + if (mServedView != view || !mServedConnecting) { + // Something else happened, so abort. + if (DEBUG) Log.v(TAG, + "Starting input: finished by someone else (view=" + + mServedView + " conn=" + mServedConnecting + ")"); + return; + } + + // If we already have a text box, then this view is already + // connected so we want to restart it. + final boolean initial = mCurrentTextBoxAttribute == null; + + // Hook 'em up and let 'er rip. + mCurrentTextBoxAttribute = tba; + mServedConnecting = false; + mServedInputConnection = ic; + if (ic != null) { + mCursorSelStart = tba.initialSelStart; + mCursorSelEnd = tba.initialSelEnd; + mCursorRect.setEmpty(); + setCurrentInputConnection(ic); + } else { + setCurrentInputConnection(mDummyInputConnection); + } + + try { + if (DEBUG) Log.v(TAG, "START INPUT: " + view + " ic=" + + ic + " tba=" + tba); + InputBindResult res = mService.startInput(mClient, tba, initial, + mCurMethod == null); + if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res); + if (res != null) { + if (res.id != null) { + mBindSequence = res.sequence; + mCurMethod = res.method; + } else { + // This means there is no input method available. + if (DEBUG) Log.v(TAG, "ABORT input: no input method!"); + return; + } + } + if (mCurMethod != null && mCompletions != null) { + try { + mCurMethod.displayCompletions(mCompletions); + } catch (RemoteException e) { + } + } + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId, e); + } + } + } + + /** + * When the focused window is dismissed, this method is called to finish the + * input method started before. + * @hide + */ + public void windowDismissed(IBinder appWindowToken) { + synchronized (mH) { + if (mServedView != null && + mServedView.getWindowToken() == appWindowToken) { + finishInputLocked(); + } + } + } + + /** + * Call this when a view receives focus. + * @hide + */ + public void focusIn(View view) { + synchronized (mH) { + if (DEBUG) Log.v(TAG, "focusIn: " + view); + // Okay we have a new view that is being served. + mServedView = view; + mCompletions = null; + mServedConnecting = true; + } + + startInputInner(); + } + + /** + * Call this when a view loses focus. + * @hide + */ + public void focusOut(View view) { + synchronized (mH) { + if (DEBUG) Log.v(TAG, "focusOut: " + view + + " mServedView=" + mServedView + + " winFocus=" + view.hasWindowFocus()); + if (mServedView == view && view.hasWindowFocus()) { + mLastServedView = view; + mH.removeMessages(MSG_CHECK_FOCUS); + mH.sendEmptyMessage(MSG_CHECK_FOCUS); + } + } + } + + void checkFocus() { + synchronized (mH) { + if (DEBUG) Log.v(TAG, "checkFocus: view=" + mServedView + + " last=" + mLastServedView); + if (mServedView == mLastServedView) { + finishInputLocked(); + // In this case, we used to have a focused view on the window, + // but no longer do. We should make sure the input method is + // no longer shown, since it serves no purpose. + closeCurrentInput(); + } + mLastServedView = null; + } + } + + void closeCurrentInput() { + try { + mService.hideSoftInput(mClient); + } catch (RemoteException e) { + } + } + + /** + * Called by ViewRoot the first time it gets window focus. + */ + public void onWindowFocus(View focusedView, int softInputMode, + boolean first, int windowFlags) { + synchronized (mH) { + if (DEBUG) Log.v(TAG, "onWindowFocus: " + focusedView + + " softInputMode=" + softInputMode + + " first=" + first + " flags=#" + + Integer.toHexString(windowFlags)); + try { + mService.windowGainedFocus(mClient, focusedView != null, + softInputMode, first, windowFlags); + } catch (RemoteException e) { + } + } + } + + /** + * Report the current selection range. + */ + public void updateSelection(View view, int selStart, int selEnd) { + synchronized (mH) { + if (mServedView != view || mCurrentTextBoxAttribute == null + || mCurMethod == null) { + return; + } + + if (mCursorSelStart != selStart || mCursorSelEnd != selEnd) { + if (DEBUG) Log.d(TAG, "updateSelection"); + + try { + if (DEBUG) Log.v(TAG, "SELECTION CHANGE: " + mCurMethod); + mCurMethod.updateSelection(mCursorSelStart, mCursorSelEnd, + selStart, selEnd); + mCursorSelStart = selStart; + mCursorSelEnd = selEnd; + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId, e); + } + } + } + } + + /** + * Returns true if the current input method wants to watch the location + * of the input editor's cursor in its window. + */ + public boolean isWatchingCursor(View view) { + return false; + } + + /** + * Report the current cursor location in its window. + */ + public void updateCursor(View view, int left, int top, int right, int bottom) { + synchronized (mH) { + if (mServedView != view || mCurrentTextBoxAttribute == null + || mCurMethod == null) { + return; + } + + mTmpCursorRect.set(left, top, right, bottom); + if (!mCursorRect.equals(mTmpCursorRect)) { + if (DEBUG) Log.d(TAG, "updateCursor"); + + try { + if (DEBUG) Log.v(TAG, "CURSOR CHANGE: " + mCurMethod); + mCurMethod.updateCursor(mTmpCursorRect); + mCursorRect.set(mTmpCursorRect); + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId, e); + } + } + } + } + + /** + * Force switch to a new input method component. This can only be called + * from the currently active input method, as validated by the given token. + * @param token Supplies the identifying token given to an input method + * when it was started, which allows it to perform this operation on + * itself. + * @param id The unique identifier for the new input method to be switched to. + */ + public void setInputMethod(IBinder token, String id) { + try { + mService.setInputMethod(token, id); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Close/hide the input method's soft input area, so the user no longer + * sees it or can interact with it. This can only be called + * from the currently active input method, as validated by the given token. + * @param token Supplies the identifying token given to an input method + * when it was started, which allows it to perform this operation on + * itself. + */ + public void hideSoftInputFromInputMethod(IBinder token) { + try { + mService.hideMySoftInput(token); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * @hide + */ + public void dispatchKeyEvent(Context context, int seq, KeyEvent key, + IInputMethodCallback callback) { + synchronized (mH) { + if (DEBUG) Log.d(TAG, "dispatchKeyEvent"); + + if (mCurMethod == null || mCurrentTextBoxAttribute == null) { + try { + callback.finishedEvent(seq, false); + } catch (RemoteException e) { + } + return; + } + + if (key.getAction() == KeyEvent.ACTION_DOWN + && key.getKeyCode() == KeyEvent.KEYCODE_SYM) { + showInputMethodPicker(); + try { + callback.finishedEvent(seq, true); + } catch (RemoteException e) { + } + return; + } + try { + if (DEBUG) Log.v(TAG, "DISPATCH KEY: " + mCurMethod); + mCurMethod.dispatchKeyEvent(seq, key, callback); + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId + " dropping: " + key, e); + try { + callback.finishedEvent(seq, false); + } catch (RemoteException ex) { + } + } + } + } + + /** + * @hide + */ + void dispatchTrackballEvent(Context context, int seq, MotionEvent motion, + IInputMethodCallback callback) { + synchronized (mH) { + if (DEBUG) Log.d(TAG, "dispatchTrackballEvent"); + + if (mCurMethod == null || mCurrentTextBoxAttribute == null) { + try { + callback.finishedEvent(seq, false); + } catch (RemoteException e) { + } + return; + } + + try { + if (DEBUG) Log.v(TAG, "DISPATCH TRACKBALL: " + mCurMethod); + mCurMethod.dispatchTrackballEvent(seq, motion, callback); + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId + " dropping trackball: " + motion, e); + try { + callback.finishedEvent(seq, false); + } catch (RemoteException ex) { + } + } + } + } + + public void showInputMethodPicker() { + synchronized (mH) { + try { + mService.showInputMethodPickerFromClient(mClient); + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId, e); + } + } + } +} diff --git a/core/java/android/view/inputmethod/InputMethodSession.java b/core/java/android/view/inputmethod/InputMethodSession.java new file mode 100644 index 0000000..603da13 --- /dev/null +++ b/core/java/android/view/inputmethod/InputMethodSession.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.view.inputmethod; + +import android.graphics.Rect; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.MotionEvent; + +/** + * The InputMethodSession interface provides the per-client functionality + * of {@link InputMethod} that is safe to expose to applications. + * + * <p>Applications will not normally use this interface themselves, instead + * relying on the standard interaction provided by + * {@link android.widget.TextView} and {@link android.widget.EditText}. + */ +public interface InputMethodSession { + + public interface EventCallback { + void finishedEvent(int seq, boolean handled); + } + + /** + * This method is called when the application would like to stop + * receiving text input. + */ + public void finishInput(); + + /** + * This method is called when the selection or cursor in the current + * target input field has changed. + * + * @param oldSelStart The previous text offset of the cursor selection + * start position. + * @param oldSelEnd The previous text offset of the cursor selection + * end position. + * @param newSelStart The new text offset of the cursor selection + * start position. + * @param newSelEnd The new text offset of the cursor selection + * end position. + */ + public void updateSelection(int oldSelStart, int oldSelEnd, + int newSelStart, int newSelEnd); + + /** + * This method is called when cursor location of the target input field + * has changed within its window. This is not normally called, but will + * only be reported if requested by the input method. + * + * @param newCursor The rectangle of the cursor currently being shown in + * the input field's window coordinates. + */ + public void updateCursor(Rect newCursor); + + /** + * Called by a text editor that performs auto completion, to tell the + * input method about the completions it has available. This can be used + * by the input method to display them to the user to select the text to + * be inserted. + * + * @param completions Array of text completions that are available, starting with + * the best. If this array is null, any existing completions will be + * removed. + */ + public void displayCompletions(CompletionInfo[] completions); + + /** + * Called by a text editor to report its new extracted text when its + * contents change. This will only be called if the input method + * calls {@link InputConnection#getExtractedText(ExtractedTextRequest, int) + * InputConnection.getExtractedText()} with the option to report updates. + * + * @param token The input method supplied token for identifying its request. + * @param text The new extracted text. + */ + public void updateExtractedText(int token, ExtractedText text); + + /** + * This method is called when a key is pressed. When done with the event, + * the implementation must call back on <var>callback</var> with its + * result. + * + * <p> + * If the input method wants to handle this event, return true, otherwise + * return false and the caller (i.e. the application) will handle the event. + * + * @param event The key event. + * + * @return Whether the input method wants to handle this event. + * + * @see #dispatchKeyUp + * @see android.view.KeyEvent + */ + public void dispatchKeyEvent(int seq, KeyEvent event, EventCallback callback); + + /** + * This method is called when there is a track ball event. + * + * <p> + * If the input method wants to handle this event, return true, otherwise + * return false and the caller (i.e. the application) will handle the event. + * + * @param event The motion event. + * + * @return Whether the input method wants to handle this event. + * + * @see android.view.MotionEvent + */ + public void dispatchTrackballEvent(int seq, MotionEvent event, EventCallback callback); + + /** + * Process a private command sent from the application to the input method. + * This can be used to provide domain-specific features that are + * only known between certain input methods and their clients. + * + * @param action Name of the command to be performed. This <em>must</em> + * be a scoped name, i.e. prefixed with a package name you own, so that + * different developers will not create conflicting commands. + * @param data Any data to include with the command. + */ + public void appPrivateCommand(String action, Bundle data); +} diff --git a/core/java/android/view/inputmethod/MutableInputConnectionWrapper.java b/core/java/android/view/inputmethod/MutableInputConnectionWrapper.java new file mode 100644 index 0000000..025a059 --- /dev/null +++ b/core/java/android/view/inputmethod/MutableInputConnectionWrapper.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.view.inputmethod; + + +/** + * Special version of {@link InputConnectionWrapper} that allows the base + * input connection to be modified after it is initially set. + */ +public class MutableInputConnectionWrapper extends InputConnectionWrapper { + public MutableInputConnectionWrapper(InputConnection base) { + super(base); + } + + /** + * Change the base InputConnection for this wrapper. All calls will then be + * delegated to the base input connection. + * + * @param base The new base InputConnection for this wrapper. + */ + public void setBaseInputConnection(InputConnection base) { + mBase = base; + } +} diff --git a/core/java/android/view/inputmethod/package.html b/core/java/android/view/inputmethod/package.html new file mode 100644 index 0000000..348aba6 --- /dev/null +++ b/core/java/android/view/inputmethod/package.html @@ -0,0 +1,11 @@ +<html> +<body> +Framework classes for interaction between views and input methods (such +as soft keyboards). In most cases the main classes here are not needed for +most applications, since they are dealt with for you by +{@link android.widget.TextView}. When implementing a custom text editor, +however, you will need to implement the +{@link android.view.inputmethod.InputConnection} class to allow the current +input method to interact with your view. +</body> +</html> diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index e99c444..1dd37be 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -26,10 +26,10 @@ import android.os.Handler; import android.os.Message; import android.util.Config; import android.util.Log; +import android.util.TypedValue; import junit.framework.Assert; -import java.net.URLDecoder; import java.net.URLEncoder; import java.util.HashMap; import java.util.Iterator; @@ -52,9 +52,7 @@ class BrowserFrame extends Handler { private final WebViewDatabase mDatabase; private final WebViewCore mWebViewCore; private boolean mLoadInitFromJava; - private String mCurrentUrl; private int mLoadType; - private String mCompletedUrl; private boolean mFirstLayoutDone = true; private boolean mCommitted = true; @@ -114,9 +112,7 @@ class BrowserFrame extends Handler { CookieSyncManager.createInstance(context); } AssetManager am = context.getAssets(); - nativeCreateFrame(am, proxy.getBackForwardList()); - // Create a native FrameView and attach it to the native frame. - nativeCreateView(w); + nativeCreateFrame(w, am, proxy.getBackForwardList()); mSettings = settings; mContext = context; @@ -142,11 +138,7 @@ class BrowserFrame extends Handler { stringByEvaluatingJavaScriptFromString( url.substring("javascript:".length())); } else { - if (!nativeLoadUrl(url)) { - reportError(android.net.http.EventHandler.ERROR_BAD_URL, - mContext.getString(com.android.internal.R.string.httpErrorBadUrl), - url); - } + nativeLoadUrl(url); } mLoadInitFromJava = false; } @@ -186,6 +178,17 @@ class BrowserFrame extends Handler { } /** + * Go back or forward the number of steps given. + * @param steps A negative or positive number indicating the direction + * and number of steps to move. + */ + public void goBackOrForward(int steps) { + mLoadInitFromJava = true; + nativeGoBackOrForward(steps); + mLoadInitFromJava = false; + } + + /** * native callback * Report an error to an activity. * @param errorCode The HTTP error code. @@ -216,34 +219,12 @@ class BrowserFrame extends Handler { return mLoadType; } - /* package */String currentUrl() { - return mCurrentUrl; - } - - /* package */void didFirstLayout(String url) { - // this is common case - if (url.equals(mCurrentUrl)) { - if (!mFirstLayoutDone) { - mFirstLayoutDone = true; - // ensure {@link WebViewCore#webkitDraw} is called as we were - // blocking the update in {@link #loadStarted} - mWebViewCore.contentInvalidate(); - } - } else if (url.equals(mCompletedUrl)) { - /* - * FIXME: when loading http://www.google.com/m, - * mCurrentUrl will be http://www.google.com/m, - * mCompletedUrl will be http://www.google.com/m#search - * and url will be http://www.google.com/m#search. - * This is probably a bug in WebKit. If url matches mCompletedUrl, - * also set mFirstLayoutDone to be true and update. - */ - if (!mFirstLayoutDone) { - mFirstLayoutDone = true; - // ensure {@link WebViewCore#webkitDraw} is called as we were - // blocking the update in {@link #loadStarted} - mWebViewCore.contentInvalidate(); - } + /* package */void didFirstLayout() { + if (!mFirstLayoutDone) { + mFirstLayoutDone = true; + // ensure {@link WebViewCore#webkitDraw} is called as we were + // blocking the update in {@link #loadStarted} + mWebViewCore.contentDraw(); } mWebViewCore.mEndScaleZoom = true; } @@ -258,8 +239,6 @@ class BrowserFrame extends Handler { mIsMainFrame = isMainFrame; if (isMainFrame || loadType == FRAME_LOADTYPE_STANDARD) { - mCurrentUrl = url; - mCompletedUrl = null; mLoadType = loadType; if (isMainFrame) { @@ -310,7 +289,6 @@ class BrowserFrame extends Handler { // mIsMainFrame and isMainFrame are better be equal!!! if (isMainFrame || loadType == FRAME_LOADTYPE_STANDARD) { - mCompletedUrl = url; if (isMainFrame) { mCallbackProxy.switchOutDrawHistory(); mCallbackProxy.onPageFinished(url); @@ -355,8 +333,8 @@ class BrowserFrame extends Handler { WebAddress uri = new WebAddress( mCallbackProxy.getBackForwardList().getCurrentItem() .getUrl()); - String host = uri.mHost; - String[] up = mDatabase.getUsernamePassword(host); + String schemePlusHost = uri.mScheme + uri.mHost; + String[] up = mDatabase.getUsernamePassword(schemePlusHost); if (up != null && up[0] != null) { setUsernamePassword(up[0], up[1]); } @@ -441,7 +419,13 @@ class BrowserFrame extends Handler { if (mLoadInitFromJava == true) { return false; } - return mCallbackProxy.shouldOverrideUrlLoading(url); + if (mCallbackProxy.shouldOverrideUrlLoading(url)) { + // if the url is hijacked, reset the state of the BrowserFrame + didFirstLayout(); + return true; + } else { + return false; + } } public void addJavascriptInterface(Object obj, String interfaceName) { @@ -462,7 +446,7 @@ class BrowserFrame extends Handler { * @param method The http method. * @param headers The http headers. * @param postData If the method is "POST" postData is sent as the request - * body. + * body. Is null when empty. * @param cacheMode The cache mode to use when loading this resource. * @param isHighPriority True if this resource needs to be put at the front * of the network queue. @@ -473,7 +457,7 @@ class BrowserFrame extends Handler { String url, String method, HashMap headers, - String postData, + byte[] postData, int cacheMode, boolean isHighPriority, boolean synchronous) { @@ -497,41 +481,47 @@ class BrowserFrame extends Handler { } WebAddress uri = new WebAddress(mCallbackProxy .getBackForwardList().getCurrentItem().getUrl()); - String host = uri.mHost; + String schemePlusHost = uri.mScheme + uri.mHost; String[] ret = getUsernamePassword(); - if (ret != null && postData != null && ret[0].length() > 0 - && ret[1].length() > 0 - && postData.contains(URLEncoder.encode(ret[0])) - && postData.contains(URLEncoder.encode(ret[1]))) { - String[] saved = mDatabase.getUsernamePassword(host); - if (saved != null) { - // null username implies that user has chosen not to - // save password - if (saved[0] != null) { - // non-null username implies that user has - // chosen to save password, so update the - // recorded password - mDatabase.setUsernamePassword(host, ret[0], - ret[1]); + // Has the user entered a username/password pair and is + // there some POST data + if (ret != null && postData != null && + ret[0].length() > 0 && ret[1].length() > 0) { + // Check to see if the username & password appear in + // the post data (there could be another form on the + // page and that was posted instead. + String postString = new String(postData); + if (postString.contains(URLEncoder.encode(ret[0])) && + postString.contains(URLEncoder.encode(ret[1]))) { + String[] saved = mDatabase.getUsernamePassword( + schemePlusHost); + if (saved != null) { + // null username implies that user has chosen not to + // save password + if (saved[0] != null) { + // non-null username implies that user has + // chosen to save password, so update the + // recorded password + mDatabase.setUsernamePassword( + schemePlusHost, ret[0], ret[1]); + } + } else { + // CallbackProxy will handle creating the resume + // message + mCallbackProxy.onSavePassword(schemePlusHost, ret[0], + ret[1], null); } - } else { - // CallbackProxy will handle creating the resume - // message - mCallbackProxy.onSavePassword(host, ret[0], ret[1], - null); } } } catch (ParseException ex) { // if it is bad uri, don't save its password } - } - if (postData == null) { - postData = ""; + } } // is this resource the main-frame top-level page? - boolean isMainFramePage = mIsMainFrame && url.equals(mCurrentUrl); + boolean isMainFramePage = mIsMainFrame; if (Config.LOGV) { Log.v(LOGTAG, "startLoadingResource: url=" + url + ", method=" @@ -561,8 +551,8 @@ class BrowserFrame extends Handler { CacheManager.endCacheTransaction(); } - FrameLoader loader = new FrameLoader(loadListener, - mSettings.getUserAgentString(), method, isHighPriority); + FrameLoader loader = new FrameLoader(loadListener, mSettings, + method, isHighPriority); loader.setHeaders(headers); loader.setPostData(postData); loader.setCacheMode(cacheMode); // Set the load mode to the mode used @@ -666,36 +656,51 @@ class BrowserFrame extends Handler { return mSettings.getUserAgentString(); } + // these ids need to be in sync with enum RAW_RES_ID in WebFrame + private static final int NODOMAIN = 1; + private static final int LOADERROR = 2; + + String getRawResFilename(int id) { + int resid; + switch (id) { + case NODOMAIN: + resid = com.android.internal.R.raw.nodomain; + break; + + case LOADERROR: + resid = com.android.internal.R.raw.loaderror; + break; + + default: + Log.e(LOGTAG, "getRawResFilename got incompatible resource ID"); + return new String(); + } + TypedValue value = new TypedValue(); + mContext.getResources().getValue(resid, value, true); + return value.string.toString(); + } + //========================================================================== // native functions //========================================================================== /** - * Create a new native frame. + * Create a new native frame for a given WebView + * @param w A WebView that the frame draws into. * @param am AssetManager to use to get assets. * @param list The native side will add and remove items from this list as * the native list changes. */ - private native void nativeCreateFrame(AssetManager am, + private native void nativeCreateFrame(WebViewCore w, AssetManager am, WebBackForwardList list); /** - * Create a native view attached to a WebView. - * @param w A WebView that the frame draws into. - */ - private native void nativeCreateView(WebViewCore w); - - private native void nativeCallPolicyFunction(int policyFunction, - int decision); - /** * Destroy the native frame. */ public native void nativeDestroyFrame(); - /** - * Detach the view from the frame. - */ - private native void nativeDetachView(); + private native void nativeCallPolicyFunction(int policyFunction, + int decision); /** * Reload the current main frame. @@ -707,7 +712,7 @@ class BrowserFrame extends Handler { * @param steps A negative or positive number indicating the direction * and number of steps to move. */ - public native void goBackOrForward(int steps); + private native void nativeGoBackOrForward(int steps); /** * stringByEvaluatingJavaScriptFromString will execute the @@ -738,7 +743,7 @@ class BrowserFrame extends Handler { /** * Returns false if the url is bad. */ - private native boolean nativeLoadUrl(String url); + private native void nativeLoadUrl(String url); private native void nativeLoadData(String baseUrl, String data, String mimeType, String encoding, String failUrl); diff --git a/core/java/android/webkit/CacheManager.java b/core/java/android/webkit/CacheManager.java index f5a09b8..d12940d 100644 --- a/core/java/android/webkit/CacheManager.java +++ b/core/java/android/webkit/CacheManager.java @@ -62,8 +62,18 @@ public final class CacheManager { // Reference count the enable/disable transaction private static int mRefCount; + // trimCacheIfNeeded() is called when a page is fully loaded. But JavaScript + // can load the content, e.g. in a slideshow, continuously, so we need to + // trim the cache on a timer base too. endCacheTransaction() is called on a + // timer base. We share the same timer with less frequent update. + private static int mTrimCacheCount = 0; + private static final int TRIM_CACHE_INTERVAL = 5; + private static WebViewDatabase mDataBase; private static File mBaseDir; + + // Flag to clear the cache when the CacheManager is initialized + private static boolean mClearCacheOnInit = false; public static class CacheResult { // these fields are saved to the database @@ -145,16 +155,37 @@ public final class CacheManager { static void init(Context context) { mDataBase = WebViewDatabase.getInstance(context); mBaseDir = new File(context.getCacheDir(), "webviewCache"); + if (createCacheDirectory() && mClearCacheOnInit) { + removeAllCacheFiles(); + mClearCacheOnInit = false; + } + } + + /** + * Create the cache directory if it does not already exist. + * + * @return true if the cache directory didn't exist and was created. + */ + static private boolean createCacheDirectory() { if (!mBaseDir.exists()) { if(!mBaseDir.mkdirs()) { Log.w(LOGTAG, "Unable to create webviewCache directory"); - return; + return false; } FileUtils.setPermissions( mBaseDir.toString(), FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, -1, -1); + // If we did create the directory, we need to flush + // the cache database. The directory could be recreated + // because the system flushed all the data/cache directories + // to free up disk space. + WebViewCore.endCacheTransaction(); + mDataBase.clearCache(); + WebViewCore.startCacheTransaction(); + return true; } + return false; } /** @@ -224,7 +255,12 @@ public final class CacheManager { // only called from WebCore thread // make sure to call startCacheTransaction/endCacheTransaction in pair public static boolean endCacheTransaction() { - return mDataBase.endCacheTransaction(); + boolean ret = mDataBase.endCacheTransaction(); + if (++mTrimCacheCount >= TRIM_CACHE_INTERVAL) { + mTrimCacheCount = 0; + trimCacheIfNeeded(); + } + return ret; } /** @@ -319,7 +355,20 @@ public final class CacheManager { try { ret.outStream = new FileOutputStream(ret.outFile); } catch (FileNotFoundException e) { - return null; + // This can happen with the system did a purge and our + // subdirectory has gone, so lets try to create it again + if (createCacheDirectory()) { + try { + ret.outStream = new FileOutputStream(ret.outFile); + } catch (FileNotFoundException e2) { + // We failed to create the file again, so there + // is something else wrong. Return null. + return null; + } + } else { + // Failed to create cache directory + return null; + } } ret.mimeType = mimeType; } @@ -371,14 +420,25 @@ public final class CacheManager { */ // only called from WebCore thread static boolean removeAllCacheFiles() { + // Note, this is called before init() when the database is + // created or upgraded. + if (mBaseDir == null) { + // Init() has not been called yet, so just flag that + // we need to clear the cache when init() is called. + mClearCacheOnInit = true; + return true; + } // delete cache in a separate thread to not block UI. final Runnable clearCache = new Runnable() { public void run() { // delete all cache files try { String[] files = mBaseDir.list(); - for (int i = 0; i < files.length; i++) { - new File(mBaseDir, files[i]).delete(); + // if mBaseDir doesn't exist, files can be null. + if (files != null) { + for (int i = 0; i < files.length; i++) { + new File(mBaseDir, files[i]).delete(); + } } } catch (SecurityException e) { // Ignore SecurityExceptions. diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index 7c296cc..cae94c9 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -354,12 +354,12 @@ class CallbackProxy extends Handler { case SAVE_PASSWORD: Bundle bundle = msg.getData(); - String host = bundle.getString("host"); + String schemePlusHost = bundle.getString("host"); String username = bundle.getString("username"); String password = bundle.getString("password"); // If the client returned false it means that the notify message // will not be sent and we should notify WebCore ourselves. - if (!mWebView.onSavePassword(host, username, password, + if (!mWebView.onSavePassword(schemePlusHost, username, password, (Message) msg.obj)) { synchronized (this) { notify(); @@ -700,8 +700,8 @@ class CallbackProxy extends Handler { // functions just need to operate within the UI thread. //-------------------------------------------------------------------------- - public boolean onSavePassword(String host, String username, String password, - Message resumeMsg) { + public boolean onSavePassword(String schemePlusHost, String username, + String password, Message resumeMsg) { // resumeMsg should be null at this point because we want to create it // within the CallbackProxy. if (Config.DEBUG) { @@ -711,7 +711,7 @@ class CallbackProxy extends Handler { Message msg = obtainMessage(SAVE_PASSWORD, resumeMsg); Bundle bundle = msg.getData(); - bundle.putString("host", host); + bundle.putString("host", schemePlusHost); bundle.putString("username", username); bundle.putString("password", password); synchronized (this) { diff --git a/core/java/android/webkit/CookieManager.java b/core/java/android/webkit/CookieManager.java index 176471f..00b17d2 100644 --- a/core/java/android/webkit/CookieManager.java +++ b/core/java/android/webkit/CookieManager.java @@ -151,13 +151,34 @@ public final class CookieManager { } boolean domainMatch(String urlHost) { - return urlHost.equals(domain) || - (domain.startsWith(".") && - urlHost.endsWith(domain.substring(1))); + if (domain.startsWith(".")) { + if (urlHost.endsWith(domain.substring(1))) { + int len = domain.length(); + int urlLen = urlHost.length(); + if (urlLen > len - 1) { + // make sure bar.com doesn't match .ar.com + return urlHost.charAt(urlLen - len) == PERIOD; + } + return true; + } + return false; + } else { + // exact match if domain is not leading w/ dot + return urlHost.equals(domain); + } } boolean pathMatch(String urlPath) { - return urlPath.startsWith (path); + if (urlPath.startsWith(path)) { + int len = path.length(); + int urlLen = urlPath.length(); + if (urlLen > len) { + // make sure /wee doesn't match /we + return urlPath.charAt(len) == PATH_DELIM; + } + return true; + } + return false; } public String toString() { @@ -232,7 +253,7 @@ public final class CookieManager { * a system private class. */ public synchronized void setCookie(WebAddress uri, String value) { - if (value != null && value.length() > 4096) { + if (value != null && value.length() > MAX_COOKIE_LENGTH) { return; } if (!mAcceptCookie || uri == null) { @@ -246,25 +267,19 @@ public final class CookieManager { if (hostAndPath == null) { return; } + + // For default path, when setting a cookie, the spec says: + //Path: Defaults to the path of the request URL that generated the + // Set-Cookie response, up to, but not including, the + // right-most /. + if (hostAndPath[1].length() > 1) { + int index = hostAndPath[1].lastIndexOf(PATH_DELIM); + hostAndPath[1] = hostAndPath[1].substring(0, + index > 0 ? index : index + 1); + } ArrayList<Cookie> cookies = null; try { - /* Google is setting cookies like the following to detect whether - * a browser supports cookie. We need to skip the leading "www" for - * the default host. Otherwise the second cookie will make the first - * cookie expired. - * - * url: https://www.google.com/accounts/ServiceLoginAuth - * value: LSID=xxxxxxxxxxxxx;Path=/accounts; - * Expires=Tue, 13-Mar-2018 01:41:39 GMT - * - * url: https://www.google.com/accounts/ServiceLoginAuth - * value:LSID=EXPIRED;Domain=www.google.com;Path=/accounts; - * Expires=Mon, 01-Jan-1990 00:00:00 GMT - */ - if (hostAndPath[0].startsWith("www.")) { - hostAndPath[0] = hostAndPath[0].substring(3); - } cookies = parseCookie(hostAndPath[0], hostAndPath[1], value); } catch (RuntimeException ex) { Log.e(LOGTAG, "parse cookie failed for: " + value); @@ -622,26 +637,17 @@ public final class CookieManager { /* * find cookie path, e.g. for http://www.google.com, the path is "/" - * for http://www.google.com/lab/, the path is "/lab/" - * for http://www.google.com/lab/foo, the path is "/lab/" - * for http://www.google.com/lab?hl=en, the path is "/lab/" - * for http://www.google.com/lab.asp?hl=en, the path is "/" + * for http://www.google.com/lab/, the path is "/lab" + * for http://www.google.com/lab/foo, the path is "/lab/foo" + * for http://www.google.com/lab?hl=en, the path is "/lab" + * for http://www.google.com/lab.asp?hl=en, the path is "/lab.asp" * Note: the path from URI has at least one "/" + * See: + * http://www.unix.com.ua/rfc/rfc2109.html */ index = ret[1].indexOf(QUESTION_MARK); if (index != -1) { ret[1] = ret[1].substring(0, index); - if (ret[1].charAt(ret[1].length() - 1) != PATH_DELIM) { - index = ret[1].lastIndexOf(PATH_DELIM); - if (ret[1].lastIndexOf('.') > index) { - ret[1] = ret[1].substring(0, index + 1); - } else { - ret[1] += PATH_DELIM; - } - } - } else if (ret[1].charAt(ret[1].length() - 1) != PATH_DELIM) { - ret[1] = ret[1].substring(0, - ret[1].lastIndexOf(PATH_DELIM) + 1); } return ret; } else @@ -687,10 +693,6 @@ public final class CookieManager { String cookieString) { ArrayList<Cookie> ret = new ArrayList<Cookie>(); - // domain needs at least two PERIOD, - if (host.indexOf(PERIOD) == host.lastIndexOf(PERIOD)) { - host = PERIOD + host; - } int index = 0; int length = cookieString.length(); while (true) { @@ -841,15 +843,14 @@ public final class CookieManager { "illegal format for max-age: " + value); } } else if (name.equals(PATH)) { - // make sure path ends with PATH_DELIM - if (value.length() > 1 && - value.charAt(value.length() - 1) != PATH_DELIM) { - cookie.path = value + PATH_DELIM; - } else { - cookie.path = value; - } + cookie.path = value; } else if (name.equals(DOMAIN)) { int lastPeriod = value.lastIndexOf(PERIOD); + if (lastPeriod == 0) { + // disallow cookies set for TLDs like [.com] + cookie.domain = null; + continue; + } try { Integer.parseInt(value.substring(lastPeriod + 1)); // no wildcard for ip address match @@ -862,15 +863,22 @@ public final class CookieManager { // ignore the exception, value is a host name } value = value.toLowerCase(); - if (value.endsWith(host) || host.endsWith(value)) { - // domain needs at least two PERIOD - if (value.indexOf(PERIOD) == lastPeriod) { - value = PERIOD + value; + if (value.charAt(0) != PERIOD) { + // pre-pended dot to make it as a domain cookie + value = PERIOD + value; + lastPeriod++; + } + if (host.endsWith(value.substring(1))) { + int len = value.length(); + int hostLen = host.length(); + if (hostLen > (len - 1) + && host.charAt(hostLen - len) != PERIOD) { + // make sure the bar.com doesn't match .ar.com + cookie.domain = null; + continue; } // disallow cookies set on ccTLDs like [.co.uk] - int len = value.length(); - if ((value.charAt(0) == PERIOD) - && (len == lastPeriod + 3) + if ((len == lastPeriod + 3) && (len >= 6 && len <= 8)) { String s = value.substring(1, lastPeriod); if (Arrays.binarySearch(BAD_COUNTRY_2LDS, s) >= 0) { @@ -880,7 +888,7 @@ public final class CookieManager { } cookie.domain = value; } else { - // no cross-site cookie + // no cross-site or more specific sub-domain cookie cookie.domain = null; } } diff --git a/core/java/android/webkit/DateSorter.java b/core/java/android/webkit/DateSorter.java index 3dc15c1..750403b 100644 --- a/core/java/android/webkit/DateSorter.java +++ b/core/java/android/webkit/DateSorter.java @@ -17,7 +17,7 @@ package android.webkit; import android.content.Context; -import android.util.Log; +import android.content.res.Resources; import java.util.Calendar; import java.util.Date; @@ -40,6 +40,8 @@ public class DateSorter { private long [] mBins = new long[DAY_COUNT]; private String [] mLabels = new String[DAY_COUNT]; + + private static final int NUM_DAYS_AGO = 5; Date mDate = new Date(); Calendar mCal = Calendar.getInstance(); @@ -48,6 +50,7 @@ public class DateSorter { * @param context Application context */ public DateSorter(Context context) { + Resources resources = context.getResources(); Calendar c = Calendar.getInstance(); beginningOfDay(c); @@ -56,9 +59,9 @@ public class DateSorter { mBins[0] = c.getTimeInMillis(); // Today c.roll(Calendar.DAY_OF_YEAR, -1); mBins[1] = c.getTimeInMillis(); // Yesterday - c.roll(Calendar.DAY_OF_YEAR, -4); + c.roll(Calendar.DAY_OF_YEAR, -(NUM_DAYS_AGO - 1)); mBins[2] = c.getTimeInMillis(); // Five days ago - c.roll(Calendar.DAY_OF_YEAR, 5); // move back to today + c.roll(Calendar.DAY_OF_YEAR, NUM_DAYS_AGO); // move back to today c.roll(Calendar.MONTH, -1); mBins[3] = c.getTimeInMillis(); // One month ago c.roll(Calendar.MONTH, -1); @@ -67,14 +70,14 @@ public class DateSorter { // build labels mLabels[0] = context.getText(com.android.internal.R.string.today).toString(); mLabels[1] = context.getText(com.android.internal.R.string.yesterday).toString(); - mLabels[2] = context.getString(com.android.internal.R.string.daysDurationPastPlural, 5); - mLabels[3] = context.getText(com.android.internal.R.string.oneMonthDurationPast).toString(); - StringBuilder sb = new StringBuilder(); - sb.append(context.getText(com.android.internal.R.string.before)).append(" "); - sb.append(context.getText(com.android.internal.R.string.oneMonthDurationPast)); - mLabels[4] = sb.toString(); - + int resId = com.android.internal.R.plurals.num_days_ago; + String format = resources.getQuantityString(resId, NUM_DAYS_AGO); + mLabels[2] = String.format(format, NUM_DAYS_AGO); + + mLabels[3] = context.getText(com.android.internal.R.string.oneMonthDurationPast).toString(); + mLabels[4] = context.getText(com.android.internal.R.string.beforeOneMonthDurationPast) + .toString(); } /** diff --git a/core/java/android/webkit/FileLoader.java b/core/java/android/webkit/FileLoader.java index 6696bae..10343b2 100644 --- a/core/java/android/webkit/FileLoader.java +++ b/core/java/android/webkit/FileLoader.java @@ -16,6 +16,8 @@ package android.webkit; +import com.android.internal.R; + import android.content.Context; import android.content.res.AssetManager; import android.net.http.EventHandler; @@ -35,6 +37,7 @@ class FileLoader extends StreamLoader { private String mPath; // Full path to the file to load private Context mContext; // Application context, used for asset loads private boolean mIsAsset; // Indicates if the load is an asset or not + private boolean mAllowFileAccess; // Allow/block file system access /** * Construct a FileLoader with the file URL specified as the content @@ -44,12 +47,15 @@ class FileLoader extends StreamLoader { * @param loadListener LoadListener to pass the content to * @param context Context to use to access the asset. * @param asset true if url points to an asset. + * @param allowFileAccess true if this WebView is allowed to access files + * on the file system. */ FileLoader(String url, LoadListener loadListener, Context context, - boolean asset) { + boolean asset, boolean allowFileAccess) { super(loadListener); mIsAsset = asset; mContext = context; + mAllowFileAccess = allowFileAccess; // clean the Url int index = url.indexOf('?'); @@ -73,35 +79,27 @@ class FileLoader extends StreamLoader { mDataStream = mContext.getAssets().open(mPath, AssetManager.ACCESS_STREAMING); } else { - mHandler.error(EventHandler.FILE_ERROR, - mContext.getString( - com.android.internal.R.string.httpErrorFileNotFound)); - return false; -/* - if (!mPath.startsWith( - Environment.getExternalStorageDirectory().getPath())) { + if (!mAllowFileAccess) { mHandler.error(EventHandler.FILE_ERROR, - mContext.getString( - com.android.internal.R.string.httpErrorFileNotFound)); + mContext.getString(R.string.httpErrorFileNotFound)); return false; } + mDataStream = new FileInputStream(mPath); mContentLength = (new File(mPath)).length(); -*/ } mHandler.status(1, 1, 0, "OK"); } catch (java.io.FileNotFoundException ex) { mHandler.error( EventHandler.FILE_NOT_FOUND_ERROR, - mContext.getString(com.android.internal.R.string.httpErrorFileNotFound) + + mContext.getString(R.string.httpErrorFileNotFound) + " " + ex.getMessage()); return false; } catch (java.io.IOException ex) { mHandler.error(EventHandler.FILE_ERROR, - mContext.getString( - com.android.internal.R.string.httpErrorFileNotFound) + + mContext.getString(R.string.httpErrorFileNotFound) + " " + ex.getMessage()); return false; } @@ -121,10 +119,13 @@ class FileLoader extends StreamLoader { * @param loadListener LoadListener to pass the content to * @param context Context to use to access the asset. * @param asset true if url points to an asset. + * @param allowFileAccess true if this FileLoader can load files from the + * file system. */ public static void requestUrl(String url, LoadListener loadListener, - Context context, boolean asset) { - FileLoader loader = new FileLoader(url, loadListener, context, asset); + Context context, boolean asset, boolean allowFileAccess) { + FileLoader loader = new FileLoader(url, loadListener, context, asset, + allowFileAccess); loader.load(); } diff --git a/core/java/android/webkit/FrameLoader.java b/core/java/android/webkit/FrameLoader.java index ebfebd0..7a3bbe6 100644 --- a/core/java/android/webkit/FrameLoader.java +++ b/core/java/android/webkit/FrameLoader.java @@ -28,16 +28,16 @@ import java.util.Map; class FrameLoader { - protected LoadListener mListener; - protected Map<String, String> mHeaders; - protected String mMethod; - protected String mPostData; - protected boolean mIsHighPriority; - protected Network mNetwork; - protected int mCacheMode; - protected String mReferrer; - protected String mUserAgent; - protected String mContentType; + private final LoadListener mListener; + private final String mMethod; + private final boolean mIsHighPriority; + private final WebSettings mSettings; + private Map<String, String> mHeaders; + private byte[] mPostData; + private Network mNetwork; + private int mCacheMode; + private String mReferrer; + private String mContentType; private static final int URI_PROTOCOL = 0x100; @@ -53,43 +53,14 @@ class FrameLoader { private static final String LOGTAG = "webkit"; - /* - * Construct the Accept_Language once. If the user changes language, then - * the phone will be rebooted. - */ - private static String ACCEPT_LANGUAGE; - static { - // Set the accept-language to the current locale plus US if we are in a - // different locale than US. - java.util.Locale l = java.util.Locale.getDefault(); - ACCEPT_LANGUAGE = ""; - if (l.getLanguage() != null) { - ACCEPT_LANGUAGE += l.getLanguage(); - if (l.getCountry() != null) { - ACCEPT_LANGUAGE += "-" + l.getCountry(); - } - } - if (!l.equals(java.util.Locale.US)) { - ACCEPT_LANGUAGE += ", "; - java.util.Locale us = java.util.Locale.US; - if (us.getLanguage() != null) { - ACCEPT_LANGUAGE += us.getLanguage(); - if (us.getCountry() != null) { - ACCEPT_LANGUAGE += "-" + us.getCountry(); - } - } - } - } - - - FrameLoader(LoadListener listener, String userAgent, + FrameLoader(LoadListener listener, WebSettings settings, String method, boolean highPriority) { mListener = listener; mHeaders = null; mMethod = method; mIsHighPriority = highPriority; mCacheMode = WebSettings.LOAD_NORMAL; - mUserAgent = userAgent; + mSettings = settings; } public void setReferrer(String ref) { @@ -97,7 +68,7 @@ class FrameLoader { if (URLUtil.isNetworkUrl(ref)) mReferrer = ref; } - public void setPostData(String postData) { + public void setPostData(byte[] postData) { mPostData = postData; } @@ -140,12 +111,15 @@ class FrameLoader { } if (URLUtil.isNetworkUrl(url)){ + if (mSettings.getBlockNetworkLoads()) { + mListener.error(EventHandler.ERROR_BAD_URL, + mListener.getContext().getString( + com.android.internal.R.string.httpErrorBadUrl)); + return false; + } mNetwork = Network.getInstance(mListener.getContext()); - return handleHTTPLoad(false); - } else if (URLUtil.isCookielessProxyUrl(url)) { - mNetwork = Network.getInstance(mListener.getContext()); - return handleHTTPLoad(true); - } else if (handleLocalFile(url, mListener)) { + return handleHTTPLoad(); + } else if (handleLocalFile(url, mListener, mSettings)) { return true; } if (Config.LOGV) { @@ -160,14 +134,15 @@ class FrameLoader { } /* package */ - static boolean handleLocalFile(String url, LoadListener loadListener) { + static boolean handleLocalFile(String url, LoadListener loadListener, + WebSettings settings) { if (URLUtil.isAssetUrl(url)) { FileLoader.requestUrl(url, loadListener, loadListener.getContext(), - true); + true, settings.getAllowFileAccess()); return true; } else if (URLUtil.isFileUrl(url)) { FileLoader.requestUrl(url, loadListener, loadListener.getContext(), - false); + false, settings.getAllowFileAccess()); return true; } else if (URLUtil.isContentUrl(url)) { // Send the raw url to the ContentLoader because it will do a @@ -186,21 +161,12 @@ class FrameLoader { return false; } - protected boolean handleHTTPLoad(boolean proxyUrl) { + private boolean handleHTTPLoad() { if (mHeaders == null) { mHeaders = new HashMap<String, String>(); } populateStaticHeaders(); - - if (!proxyUrl) { - // Don't add private information if this is a proxy load, ie don't - // add cookies and authentication - populateHeaders(); - } else { - // If this is a proxy URL, fix it to be a network load - mListener.setUrl("http://" - + mListener.url().substring(URLUtil.PROXY_BASE.length())); - } + populateHeaders(); // response was handled by UrlIntercept, don't issue HTTP request if (handleUrlIntercept()) return true; @@ -246,7 +212,7 @@ class FrameLoader { * This function is used by handleUrlInterecpt and handleCache to * setup a load from the byte stream in a CacheResult. */ - protected void startCacheLoad(CacheResult result) { + private void startCacheLoad(CacheResult result) { if (Config.LOGV) { Log.v(LOGTAG, "FrameLoader: loading from cache: " + mListener.url()); @@ -264,7 +230,7 @@ class FrameLoader { * * Returns true if the response was handled by UrlIntercept. */ - protected boolean handleUrlIntercept() { + private boolean handleUrlIntercept() { // Check if the URL can be served from UrlIntercept. If // successful, return the data just like a cache hit. CacheResult result = UrlInterceptRegistry.getSurrogate( @@ -284,7 +250,7 @@ class FrameLoader { * correctly. * Returns true if the response was handled from the cache */ - protected boolean handleCache() { + private boolean handleCache() { switch (mCacheMode) { // This mode is normally used for a reload, it instructs the http // loader to not use the cached content. @@ -357,11 +323,12 @@ class FrameLoader { } mHeaders.put("Accept-Charset", "utf-8, iso-8859-1, utf-16, *;q=0.7"); - if (ACCEPT_LANGUAGE.length() > 0) { - mHeaders.put("Accept-Language", ACCEPT_LANGUAGE); + String acceptLanguage = mSettings.getAcceptLanguage(); + if (acceptLanguage.length() > 0) { + mHeaders.put("Accept-Language", acceptLanguage); } - - mHeaders.put("User-Agent", mUserAgent); + + mHeaders.put("User-Agent", mSettings.getUserAgentString()); } /** diff --git a/core/java/android/webkit/HttpDateTime.java b/core/java/android/webkit/HttpDateTime.java index b22f2ba..c6ec2d2 100644 --- a/core/java/android/webkit/HttpDateTime.java +++ b/core/java/android/webkit/HttpDateTime.java @@ -16,7 +16,7 @@ package android.webkit; -import android.pim.Time; +import android.text.format.Time; import java.util.Calendar; import java.util.regex.Matcher; diff --git a/core/java/android/webkit/JWebCoreJavaBridge.java b/core/java/android/webkit/JWebCoreJavaBridge.java index 1cfea99..a0049ac 100644 --- a/core/java/android/webkit/JWebCoreJavaBridge.java +++ b/core/java/android/webkit/JWebCoreJavaBridge.java @@ -191,4 +191,5 @@ final class JWebCoreJavaBridge extends Handler { private native void nativeFinalize(); private native void sharedTimerFired(); private native void setDeferringTimers(boolean defer); + public native void setNetworkOnLine(boolean online); } diff --git a/core/java/android/webkit/LoadListener.java b/core/java/android/webkit/LoadListener.java index 86947a2..c45ab29 100644 --- a/core/java/android/webkit/LoadListener.java +++ b/core/java/android/webkit/LoadListener.java @@ -99,7 +99,7 @@ class LoadListener extends Handler implements EventHandler { // cache. It is needed if the cache returns a redirect private String mMethod; private Map<String, String> mRequestHeaders; - private String mPostData; + private byte[] mPostData; private boolean mIsHighPriority; // Flag to indicate that this load is synchronous. private boolean mSynchronous; @@ -220,7 +220,8 @@ class LoadListener extends Handler implements EventHandler { */ Message contMsg = obtainMessage(MSG_LOCATION_CHANGED); Message stopMsg = obtainMessage(MSG_CONTENT_FINISHED); - //TODO, need to call mCallbackProxy and request UI. + mBrowserFrame.getCallbackProxy().onFormResubmission( + stopMsg, contMsg); break; } @@ -286,15 +287,16 @@ class LoadListener extends Handler implements EventHandler { if (newMimeType != null) { mMimeType = newMimeType; } - } else if (mMimeType.equalsIgnoreCase("text/vnd.wap.wml") || - mMimeType. - equalsIgnoreCase("application/vnd.wap.xhtml+xml")) { + } else if (mMimeType.equalsIgnoreCase("text/vnd.wap.wml")) { // As we don't support wml, render it as plain text mMimeType = "text/plain"; } else { // XXX: Until the servers send us either correct xhtml or // text/html, treat application/xhtml+xml as text/html. - if (mMimeType.equalsIgnoreCase("application/xhtml+xml")) { + // It seems that xhtml+xml and vnd.wap.xhtml+xml mime + // subtypes are used interchangeably. So treat them the same. + if (mMimeType.equalsIgnoreCase("application/xhtml+xml") || + mMimeType.equals("application/vnd.wap.xhtml+xml")) { mMimeType = "text/html"; } } @@ -527,23 +529,21 @@ class LoadListener extends Handler implements EventHandler { } else { sendMessageInternal(obtainMessage(MSG_LOCATION_CHANGED)); } - - break; + return; case HTTP_AUTH: case HTTP_PROXY_AUTH: + // According to rfc2616, the response for HTTP_AUTH must include + // WWW-Authenticate header field and the response for + // HTTP_PROXY_AUTH must include Proxy-Authenticate header field. if (mAuthHeader != null && (Network.getInstance(mContext).isValidProxySet() || !mAuthHeader.isProxy())) { Network.getInstance(mContext).handleAuthRequest(this); - } else { - final int stringId = - com.android.internal.R.string.httpErrorUnsupportedAuthScheme; - error(EventHandler.ERROR_UNSUPPORTED_AUTH_SCHEME, - getContext().getText(stringId).toString()); + return; } - break; - + break; // use default + case HTTP_NOT_MODIFIED: // Server could send back NOT_MODIFIED even if we didn't // ask for it, so make sure we have a valid CacheLoader @@ -554,16 +554,18 @@ class LoadListener extends Handler implements EventHandler { if (Config.LOGV) { Log.v(LOGTAG, "LoadListener cache load url=" + url()); } - break; - } // Fall through to default if there is no CacheLoader + return; + } + break; // use default case HTTP_NOT_FOUND: // Not an error, the server can send back content. default: - sendMessageInternal(obtainMessage(MSG_CONTENT_FINISHED)); - detachRequestHandle(); break; } + + sendMessageInternal(obtainMessage(MSG_CONTENT_FINISHED)); + detachRequestHandle(); } /** @@ -725,7 +727,7 @@ class LoadListener extends Handler implements EventHandler { * @param isHighPriority */ void setRequestData(String method, Map<String, String> headers, - String postData, boolean isHighPriority) { + byte[] postData, boolean isHighPriority) { mMethod = method; mRequestHeaders = headers; mPostData = postData; @@ -878,37 +880,16 @@ class LoadListener extends Handler implements EventHandler { int statusCode = mStatusCode == HTTP_NOT_MODIFIED ? HTTP_OK : mStatusCode; // pass content-type content-length and content-encoding - int nativeResponse = nativeCreateResponse(mUrl, statusCode, mStatusText, + final int nativeResponse = nativeCreateResponse( + mUrl, statusCode, mStatusText, mMimeType, mContentLength, mEncoding, mCacheResult == null ? 0 : mCacheResult.expires / 1000); if (mHeaders != null) { - // "content-disposition", - String value = mHeaders.getContentDisposition(); - if (value != null) { - nativeSetResponseHeader(nativeResponse, - Headers.CONTENT_DISPOSITION, value); - } - - // location - value = mHeaders.getLocation(); - if (value != null) { - nativeSetResponseHeader(nativeResponse, - Headers.LOCATION, value); - } - - // refresh (paypal.com are using this) - value = mHeaders.getRefresh(); - if (value != null) { - nativeSetResponseHeader(nativeResponse, - Headers.REFRESH, value); - } - - // Content-Type - value = mHeaders.getContentType(); - if (value != null) { - nativeSetResponseHeader(nativeResponse, - Headers.CONTENT_TYPE, value); - } + mHeaders.getHeaders(new Headers.HeaderCallback() { + public void header(String name, String value) { + nativeSetResponseHeader(nativeResponse, name, value); + } + }); } return nativeResponse; } @@ -1048,7 +1029,6 @@ class LoadListener extends Handler implements EventHandler { cancel(); return; } else if (!URLUtil.isNetworkUrl(redirectTo)) { - cancel(); final String text = mContext .getString(com.android.internal.R.string.open_permission_deny) + "\n" + redirectTo; @@ -1250,7 +1230,12 @@ class LoadListener extends Handler implements EventHandler { */ void setUrl(String url) { if (url != null) { - mUrl = URLUtil.stripAnchor(url); + if (URLUtil.isDataUrl(url)) { + // Don't strip anchor as that is a valid part of the URL + mUrl = url; + } else { + mUrl = URLUtil.stripAnchor(url); + } mUri = null; if (URLUtil.isNetworkUrl(mUrl)) { try { diff --git a/core/java/android/webkit/MimeTypeMap.java b/core/java/android/webkit/MimeTypeMap.java index 2700aa5..85cb8c0 100644 --- a/core/java/android/webkit/MimeTypeMap.java +++ b/core/java/android/webkit/MimeTypeMap.java @@ -65,7 +65,7 @@ public /* package */ class MimeTypeMap { // if the filename contains special characters, we don't // consider it valid for our matching purposes: if (filename.length() > 0 && - Pattern.matches("[a-zA-Z_0-9\\.\\-]+", filename)) { + Pattern.matches("[a-zA-Z_0-9\\.\\-\\(\\)]+", filename)) { int dotPos = filename.lastIndexOf('.'); if (0 <= dotPos) { return filename.substring(dotPos + 1); diff --git a/core/java/android/webkit/Network.java b/core/java/android/webkit/Network.java index ea42e58..74622b3 100644 --- a/core/java/android/webkit/Network.java +++ b/core/java/android/webkit/Network.java @@ -155,7 +155,7 @@ class Network { */ public boolean requestURL(String method, Map<String, String> headers, - String postData, + byte [] postData, LoadListener loader, boolean isHighPriority) { @@ -178,9 +178,8 @@ class Network { InputStream bodyProvider = null; int bodyLength = 0; if (postData != null) { - byte[] data = postData.getBytes(); - bodyLength = data.length; - bodyProvider = new ByteArrayInputStream(data); + bodyLength = postData.length; + bodyProvider = new ByteArrayInputStream(postData); } RequestQueue q = mRequestQueue; diff --git a/core/java/android/webkit/TextDialog.java b/core/java/android/webkit/TextDialog.java index 95209c7..4e9370c 100644 --- a/core/java/android/webkit/TextDialog.java +++ b/core/java/android/webkit/TextDialog.java @@ -124,6 +124,10 @@ import android.widget.AutoCompleteTextView; int flags = paint.getFlags() | Paint.SUBPIXEL_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG & ~Paint.DEV_KERN_TEXT_FLAG; paint.setFlags(flags); + // Set the text color to black, regardless of the theme. This ensures + // that other applications that use embedded WebViews will properly + // display the text in textfields. + setTextColor(Color.BLACK); } @Override @@ -395,6 +399,7 @@ import android.widget.AutoCompleteTextView; * focus to the host. */ /* package */ void remove() { + mHandler.removeMessages(LONGPRESS); mWebView.removeView(this); mWebView.requestFocus(); } @@ -532,6 +537,7 @@ import android.widget.AutoCompleteTextView; mPreChange = text.toString(); Editable edit = (Editable) getText(); edit.replace(0, edit.length(), text); + updateCachedTextfield(); } /** diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java index 43666c1..0e8144e 100644 --- a/core/java/android/webkit/URLUtil.java +++ b/core/java/android/webkit/URLUtil.java @@ -145,6 +145,7 @@ public final class URLUtil { /** * @return True iff the url is an proxy url to allow cookieless network * requests from a file url. + * @deprecated Cookieless proxy is no longer supported. */ public static boolean isCookielessProxyUrl(String url) { return (null != url) && url.startsWith(PROXY_BASE); diff --git a/core/java/android/webkit/WebBackForwardList.java b/core/java/android/webkit/WebBackForwardList.java index c86b21d..9dea5ec 100644 --- a/core/java/android/webkit/WebBackForwardList.java +++ b/core/java/android/webkit/WebBackForwardList.java @@ -133,7 +133,7 @@ public class WebBackForwardList implements Cloneable, Serializable { } /* Remove the item at the given index. Called by JNI only. */ - private void removeHistoryItem(int index) { + private synchronized void removeHistoryItem(int index) { // XXX: This is a special case. Since the callback is only triggered // when removing the first item, we can assert that the index is 0. // This lets us change the current index without having to query the diff --git a/core/java/android/webkit/WebHistoryItem.java b/core/java/android/webkit/WebHistoryItem.java index 5570af8..a408e06 100644 --- a/core/java/android/webkit/WebHistoryItem.java +++ b/core/java/android/webkit/WebHistoryItem.java @@ -33,6 +33,8 @@ public class WebHistoryItem implements Cloneable { private String mTitle; // The base url of this item. private String mUrl; + // The original requested url of this item. + private String mOriginalUrl; // The favicon for this item. private Bitmap mFavicon; // The pre-flattened data used for saving the state. @@ -95,6 +97,18 @@ public class WebHistoryItem implements Cloneable { } /** + * Return the original url of this history item. This was the requested + * url, the final url may be different as there might have been + * redirects while loading the site. + * @return The original url of this history item. + * + * @hide pending API Council approval + */ + public String getOriginalUrl() { + return mOriginalUrl; + } + + /** * Return the document title of this history item. * @return The document title of this history item. * Note: The VM ensures 32-bit atomic read/write operations so we don't have @@ -154,8 +168,10 @@ public class WebHistoryItem implements Cloneable { private native void inflate(int nativeFrame, byte[] data); /* Called by jni when the item is updated */ - private void update(String url, String title, Bitmap favicon, byte[] data) { + private void update(String url, String originalUrl, String title, + Bitmap favicon, byte[] data) { mUrl = url; + mOriginalUrl = originalUrl; mTitle = title; mFavicon = favicon; mFlattenedData = data; diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index de64b30..1a7c4ff 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -20,6 +20,8 @@ import android.content.Context; import android.os.Build; import android.os.Handler; import android.os.Message; +import java.lang.SecurityException; +import android.content.pm.PackageManager; import java.util.Locale; @@ -111,6 +113,7 @@ public class WebSettings { // retrieve the values. After setXXX, postSync() needs to be called. // XXX: The default values need to match those in WebSettings.cpp private LayoutAlgorithm mLayoutAlgorithm = LayoutAlgorithm.NARROW_COLUMNS; + private Context mContext; private TextSize mTextSize = TextSize.NORMAL; private String mStandardFontFamily = "sans-serif"; private String mFixedFontFamily = "monospace"; @@ -119,7 +122,9 @@ public class WebSettings { private String mCursiveFontFamily = "cursive"; private String mFantasyFontFamily = "fantasy"; private String mDefaultTextEncoding = "Latin-1"; - private String mUserAgent = ANDROID_USERAGENT; + private String mUserAgent; + private boolean mUseDefaultUserAgent; + private String mAcceptLanguage; private String mPluginsPath = ""; private int mMinimumFontSize = 8; private int mMinimumLogicalFontSize = 8; @@ -127,12 +132,14 @@ public class WebSettings { private int mDefaultFixedFontSize = 13; private boolean mLoadsImagesAutomatically = true; private boolean mBlockNetworkImage = false; + private boolean mBlockNetworkLoads = false; private boolean mJavaScriptEnabled = false; private boolean mPluginsEnabled = false; private boolean mJavaScriptCanOpenWindowsAutomatically = false; private boolean mUseDoubleTree = false; private boolean mUseWideViewport = false; private boolean mSupportMultipleWindows = false; + private boolean mShrinksStandaloneImagesToFit = false; // Don't need to synchronize the get/set methods as they // are basic types, also none of these values are used in // native WebCore code. @@ -144,6 +151,7 @@ public class WebSettings { private boolean mNeedInitialFocus = true; private boolean mNavDump = false; private boolean mSupportZoom = true; + private boolean mAllowFileAccess = true; // Class to handle messages before WebCore is ready. private class EventHandler { @@ -212,57 +220,112 @@ public class WebSettings { // User agent strings. private static final String DESKTOP_USERAGENT = - "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/522+ " + - "(KHTML, like Gecko) Safari/419.3"; - private static final String IPHONE_USERAGENT = "Mozilla/5.0 (iPhone; U; " + - "CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) " + - "Version/3.0 Mobile/1A543 Safari/419.3"; - private static String ANDROID_USERAGENT; - + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en)" + + " AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2" + + " Safari/525.20.1"; + private static final String IPHONE_USERAGENT = + "Mozilla/5.0 (iPhone; U; CPU iPhone 2_1 like Mac OS X; en)" + + " AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2" + + " Mobile/5F136 Safari/525.20.1"; + private static Locale sLocale; + private static Object sLockForLocaleSettings; + /** * Package constructor to prevent clients from creating a new settings * instance. */ - WebSettings(Context context) { - if (ANDROID_USERAGENT == null) { - StringBuffer arg = new StringBuffer(); - // Add version - final String version = Build.VERSION.RELEASE; - if (version.length() > 0) { - arg.append(version); - } else { - // default to "1.0" - arg.append("1.0"); + WebSettings(Context context) { + mEventHandler = new EventHandler(); + mContext = context; + + if (sLockForLocaleSettings == null) { + sLockForLocaleSettings = new Object(); + sLocale = Locale.getDefault(); + } + mAcceptLanguage = getCurrentAcceptLanguage(); + mUserAgent = getCurrentUserAgent(); + mUseDefaultUserAgent = true; + + verifyNetworkAccess(); + } + + /** + * Looks at sLocale and returns current AcceptLanguage String. + * @return Current AcceptLanguage String. + */ + private String getCurrentAcceptLanguage() { + Locale locale; + 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); } - arg.append("; "); - // Initialize the mobile user agent with the default locale. - final Locale l = Locale.getDefault(); - final String language = l.getLanguage(); - if (language != null) { - arg.append(language.toLowerCase()); - final String country = l.getCountry(); + } + 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) { - arg.append("-"); - arg.append(country.toLowerCase()); + buffer.append("-"); + buffer.append(country); } - } else { - // default to "en" - arg.append("en"); } - // Add device name - final String device = Build.DEVICE; - if (device.length() > 0) { - arg.append("; "); - arg.append(device); + } + + return buffer.toString(); + } + + /** + * Looks at sLocale and mContext and returns current UserAgent String. + * @return Current UserAgent String. + */ + private synchronized String getCurrentUserAgent() { + Locale locale; + synchronized(sLockForLocaleSettings) { + locale = sLocale; + } + StringBuffer buffer = new StringBuffer(); + // Add version + final String version = Build.VERSION.RELEASE; + if (version.length() > 0) { + buffer.append(version); + } else { + // default to "1.0" + buffer.append("1.0"); + } + buffer.append("; "); + final String language = locale.getLanguage(); + if (language != null) { + buffer.append(language.toLowerCase()); + final String country = locale.getCountry(); + if (country != null) { + buffer.append("-"); + buffer.append(country.toLowerCase()); } - final String base = context.getResources().getText( - com.android.internal.R.string.web_user_agent).toString(); - ANDROID_USERAGENT = String.format(base, arg); - mUserAgent = ANDROID_USERAGENT; + } else { + // default to "en" + buffer.append("en"); } - mEventHandler = new EventHandler(); + + final String device = Build.DEVICE; + if (device.length() > 0) { + buffer.append("; "); + buffer.append(device); + } + final String base = mContext.getResources().getText( + com.android.internal.R.string.web_user_agent).toString(); + return String.format(base, buffer); } - + /** * Enables dumping the pages navigation cache to a text file. */ @@ -292,6 +355,21 @@ public class WebSettings { } /** + * Enable or disable file access within WebView. File access is enabled by + * default. + */ + public void setAllowFileAccess(boolean allow) { + mAllowFileAccess = allow; + } + + /** + * Returns true if this WebView supports file access. + */ + public boolean getAllowFileAccess() { + return mAllowFileAccess; + } + + /** * Store whether the WebView is saving form data. */ public void setSaveFormData(boolean save) { @@ -377,34 +455,48 @@ public class WebSettings { * Tell the WebView about user-agent string. * @param ua 0 if the WebView should use an Android user-agent string, * 1 if the WebView should use a desktop user-agent string. - * 2 if the WebView should use an iPhone user-agent string. + * + * @deprecated Please use setUserAgentString instead. */ + @Deprecated public synchronized void setUserAgent(int ua) { - if (ua == 0 && !ANDROID_USERAGENT.equals(mUserAgent)) { - mUserAgent = ANDROID_USERAGENT; - postSync(); - } else if (ua == 1 && !DESKTOP_USERAGENT.equals(mUserAgent)) { - mUserAgent = DESKTOP_USERAGENT; - postSync(); - } else if (ua == 2 && !IPHONE_USERAGENT.equals(mUserAgent)) { - mUserAgent = IPHONE_USERAGENT; - postSync(); + String uaString = null; + if (ua == 1) { + if (DESKTOP_USERAGENT.equals(mUserAgent)) { + return; // do nothing + } else { + uaString = DESKTOP_USERAGENT; + } + } else if (ua == 2) { + if (IPHONE_USERAGENT.equals(mUserAgent)) { + return; // do nothing + } else { + uaString = IPHONE_USERAGENT; + } + } else if (ua != 0) { + return; // do nothing } + setUserAgentString(uaString); } /** * Return user-agent as int * @return int 0 if the WebView is using an Android user-agent string. * 1 if the WebView is using a desktop user-agent string. - * 2 if the WebView is using an iPhone user-agent string. + * -1 if the WebView is using user defined user-agent string. + * + * @deprecated Please use getUserAgentString instead. */ + @Deprecated public synchronized int getUserAgent() { if (DESKTOP_USERAGENT.equals(mUserAgent)) { return 1; } else if (IPHONE_USERAGENT.equals(mUserAgent)) { return 2; + } else if (mUseDefaultUserAgent) { + return 0; } - return 0; + return -1; } /** @@ -706,6 +798,40 @@ public class WebSettings { public synchronized boolean getBlockNetworkImage() { return mBlockNetworkImage; } + + /** + * @hide + * Tell the WebView to block all network load requests. + * @param flag True if the WebView should block all network loads + */ + public synchronized void setBlockNetworkLoads(boolean flag) { + if (mBlockNetworkLoads != flag) { + mBlockNetworkLoads = flag; + verifyNetworkAccess(); + } + } + + /** + * @hide + * Return true if the WebView will block all network loads. + * @return True if the WebView blocks all network loads. + */ + public synchronized boolean getBlockNetworkLoads() { + return mBlockNetworkLoads; + } + + + private void verifyNetworkAccess() { + if (!mBlockNetworkLoads) { + if (mContext.checkPermission("android.permission.INTERNET", + android.os.Process.myPid(), 0) != + PackageManager.PERMISSION_GRANTED) { + throw new SecurityException + ("Permission denied - " + + "application missing INTERNET permission"); + } + } + } /** * Tell the WebView to enable javascript execution. @@ -806,11 +932,69 @@ public class WebSettings { return mDefaultTextEncoding; } - /* Package api to grab the user agent string. */ - /*package*/ synchronized String getUserAgentString() { + /** + * Set the WebView's user-agent string. If the string "ua" is null or empty, + * it will use the system default user-agent string. + */ + public synchronized void setUserAgentString(String ua) { + if (ua == null || ua.length() == 0) { + synchronized(sLockForLocaleSettings) { + Locale currentLocale = Locale.getDefault(); + if (!sLocale.equals(currentLocale)) { + sLocale = currentLocale; + mAcceptLanguage = getCurrentAcceptLanguage(); + } + } + ua = getCurrentUserAgent(); + mUseDefaultUserAgent = true; + } else { + mUseDefaultUserAgent = false; + } + + if (!ua.equals(mUserAgent)) { + mUserAgent = ua; + postSync(); + } + } + + /** + * Return the WebView's user-agent string. + */ + public synchronized String getUserAgentString() { + if (DESKTOP_USERAGENT.equals(mUserAgent) || + IPHONE_USERAGENT.equals(mUserAgent) || + !mUseDefaultUserAgent) { + return mUserAgent; + } + + boolean doPostSync = false; + synchronized(sLockForLocaleSettings) { + Locale currentLocale = Locale.getDefault(); + if (!sLocale.equals(currentLocale)) { + sLocale = currentLocale; + mUserAgent = getCurrentUserAgent(); + mAcceptLanguage = getCurrentAcceptLanguage(); + doPostSync = true; + } + } + if (doPostSync) { + postSync(); + } return mUserAgent; } + /* package api to grab the Accept Language string. */ + /*package*/ synchronized String getAcceptLanguage() { + synchronized(sLockForLocaleSettings) { + Locale currentLocale = Locale.getDefault(); + if (!sLocale.equals(currentLocale)) { + sLocale = currentLocale; + mAcceptLanguage = getCurrentAcceptLanguage(); + } + } + return mAcceptLanguage; + } + /** * Tell the WebView whether it needs to set a node to have focus when * {@link WebView#requestFocus(int, android.graphics.Rect)} is called. @@ -863,6 +1047,20 @@ public class WebSettings { public int getCacheMode() { return mOverrideCacheMode; } + + /** + * If set, webkit alternately shrinks and expands images viewed outside + * of an HTML page to fit the screen. This conflicts with attempts by + * the UI to zoom in and out of an image, so it is set false by default. + * @param shrink Set true to let webkit shrink the standalone image to fit. + * {@hide} + */ + public void setShrinksStandaloneImagesToFit(boolean shrink) { + if (mShrinksStandaloneImagesToFit != shrink) { + mShrinksStandaloneImagesToFit = shrink; + postSync(); + } + } /** * Transfer messages from the queue to the new WebCoreThread. Called from diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 6623257..7467b83 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -56,6 +56,7 @@ import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import android.view.ViewTreeObserver; +import android.view.inputmethod.InputMethodManager; import android.webkit.WebViewCore.EventHub; import android.widget.AbsoluteLayout; import android.widget.AdapterView; @@ -94,6 +95,12 @@ public class WebView extends AbsoluteLayout implements ViewTreeObserver.OnGlobalFocusChangeListener, ViewGroup.OnHierarchyChangeListener { + // if AUTO_REDRAW_HACK is true, then the CALL key will toggle redrawing + // the screen all-the-time. Good for profiling our drawing code + static private final boolean AUTO_REDRAW_HACK = false; + // true means redraw the screen all-the-time. Only with AUTO_REDRAW_HACK + private boolean mAutoRedraw; + // keep debugging parameters near the top of the file static final String LOGTAG = "webview"; static final boolean DEBUG = false; @@ -112,7 +119,8 @@ public class WebView extends AbsoluteLayout (com.android.internal.R.id.zoomMagnify); } - public void show(boolean canZoomOut) { + public void show(boolean showZoom, boolean canZoomOut) { + mZoomControls.setVisibility(showZoom ? View.VISIBLE : View.GONE); mZoomMagnify.setVisibility(canZoomOut ? View.VISIBLE : View.GONE); fade(View.VISIBLE, 0.0f, 1.0f); } @@ -211,6 +219,17 @@ public class WebView extends AbsoluteLayout private long mLastTouchTime; /** + * Time of the last time sending touch event to WebViewCore + */ + private long mLastSentTouchTime; + + /** + * The minimum elapsed time before sending another ACTION_MOVE event to + * WebViewCore + */ + private static final int TOUCH_SENT_INTERVAL = 100; + + /** * Helper class to get velocity for fling */ VelocityTracker mVelocityTracker; @@ -226,6 +245,7 @@ public class WebView extends AbsoluteLayout private static final int TOUCH_SHORTPRESS_MODE = 5; private static final int TOUCH_DOUBLECLICK_MODE = 6; private static final int TOUCH_DONE_MODE = 7; + private static final int TOUCH_SELECT_MODE = 8; // touch mode values specific to scale+scroll private static final int FIRST_SCROLL_ZOOM = 9; private static final int SCROLL_ZOOM_ANIMATION_IN = 9; @@ -234,6 +254,9 @@ public class WebView extends AbsoluteLayout private static final int LAST_SCROLL_ZOOM = 11; // end of touch mode values specific to scale+scroll + // Whether to forward the touch events to WebCore + private boolean mForwardTouchEvents = false; + // If updateTextEntry gets called while we are out of focus, use this // variable to remember to do it next time we gain focus. private boolean mNeedsUpdateTextEntry = false; @@ -337,7 +360,9 @@ public class WebView extends AbsoluteLayout static final int NOTIFY_FOCUS_SET_MSG_ID = 20; static final int MARK_NODE_INVALID_ID = 21; static final int UPDATE_CLIPBOARD = 22; - static final int LONG_PRESS_TRACKBALL = 24; + static final int LONG_PRESS_ENTER = 23; + static final int PREVENT_TOUCH_ID = 24; + static final int WEBCORE_NEED_TOUCH_EVENTS = 25; // width which view is considered to be fully zoomed out static final int ZOOM_OUT_WIDTH = 1024; @@ -524,23 +549,23 @@ public class WebView extends AbsoluteLayout setVerticalScrollBarEnabled(true); } - /* package */ boolean onSavePassword(String host, String username, + /* package */ boolean onSavePassword(String schemePlusHost, String username, String password, final Message resumeMsg) { boolean rVal = false; if (resumeMsg == null) { // null resumeMsg implies saving password silently - mDatabase.setUsernamePassword(host, username, password); + mDatabase.setUsernamePassword(schemePlusHost, username, password); } else { final Message remember = mPrivateHandler.obtainMessage( REMEMBER_PASSWORD); - remember.getData().putString("host", host); + remember.getData().putString("host", schemePlusHost); remember.getData().putString("username", username); remember.getData().putString("password", password); remember.obj = resumeMsg; final Message neverRemember = mPrivateHandler.obtainMessage( NEVER_REMEMBER_PASSWORD); - neverRemember.getData().putString("host", host); + neverRemember.getData().putString("host", schemePlusHost); neverRemember.getData().putString("username", username); neverRemember.getData().putString("password", password); neverRemember.obj = resumeMsg; @@ -749,14 +774,36 @@ public class WebView extends AbsoluteLayout public static void disablePlatformNotifications() { Network.disablePlatformNotifications(); } + + /** + * Inform WebView of the network state. This is used to set + * the javascript property window.navigator.isOnline and + * generates the online/offline event as specified in HTML5, sec. 5.7.7 + * @param networkUp boolean indicating if network is available + * + * @hide pending API Council approval + */ + public void setNetworkAvailable(boolean networkUp) { + BrowserFrame.sJavaBridge.setNetworkOnLine(networkUp); + } /** - * Save the state of this WebView used in Activity.onSaveInstanceState. + * Save the state of this WebView used in + * {@link android.app.Activity#onSaveInstanceState}. Please note that this + * method no longer stores the display data for this WebView. The previous + * behavior could potentially leak files if {@link #restoreState} was never + * called. See {@link #savePicture} and {@link #restorePicture} for saving + * and restoring the display data. * @param outState The Bundle to store the WebView state. * @return The same copy of the back/forward list used to save the state. If * saveState fails, the returned list will be null. + * @see #savePicture + * @see #restorePicture */ public WebBackForwardList saveState(Bundle outState) { + if (outState == null) { + return null; + } // We grab a copy of the back/forward list because a client of WebView // may have invalidated the history list by calling clearHistory. WebBackForwardList list = copyBackForwardList(); @@ -782,29 +829,6 @@ public class WebView extends AbsoluteLayout return null; } history.add(data); - if (i == currentIndex) { - Picture p = capturePicture(); - String path = mContext.getDir("thumbnails", 0).getPath() - + File.separatorChar + hashCode() + "_pic.save"; - File f = new File(path); - try { - final FileOutputStream out = new FileOutputStream(f); - p.writeToStream(out); - out.close(); - } catch (FileNotFoundException e){ - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } catch (RuntimeException e) { - e.printStackTrace(); - } - if (f.length() > 0) { - outState.putString("picture", path); - outState.putInt("scrollX", mScrollX); - outState.putInt("scrollY", mScrollY); - outState.putFloat("scale", mActualScale); - } - } } outState.putSerializable("history", history); if (mCertificate != null) { @@ -815,16 +839,103 @@ public class WebView extends AbsoluteLayout } /** + * Save the current display data to the Bundle given. Used in conjunction + * with {@link #saveState}. + * @param b A Bundle to store the display data. + * @param dest The file to store the serialized picture data. Will be + * overwritten with this WebView's picture data. + * @return True if the picture was successfully saved. + */ + public boolean savePicture(Bundle b, File dest) { + if (dest == null || b == null) { + return false; + } + final Picture p = capturePicture(); + try { + final FileOutputStream out = new FileOutputStream(dest); + p.writeToStream(out); + out.close(); + } catch (FileNotFoundException e){ + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (RuntimeException e) { + e.printStackTrace(); + } + if (dest.length() > 0) { + b.putInt("scrollX", mScrollX); + b.putInt("scrollY", mScrollY); + b.putFloat("scale", mActualScale); + return true; + } + return false; + } + + /** + * Restore the display data that was save in {@link #savePicture}. Used in + * conjunction with {@link #restoreState}. + * @param b A Bundle containing the saved display data. + * @param src The file where the picture data was stored. + * @return True if the picture was successfully restored. + */ + public boolean restorePicture(Bundle b, File src) { + if (src == null || b == null) { + return false; + } + if (src.exists()) { + Picture p = null; + try { + final FileInputStream in = new FileInputStream(src); + p = Picture.createFromStream(in); + in.close(); + } catch (FileNotFoundException e){ + e.printStackTrace(); + } catch (RuntimeException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + if (p != null) { + int sx = b.getInt("scrollX", 0); + int sy = b.getInt("scrollY", 0); + float scale = b.getFloat("scale", 1.0f); + mDrawHistory = true; + mHistoryPicture = p; + mScrollX = sx; + mScrollY = sy; + mHistoryWidth = Math.round(p.getWidth() * scale); + mHistoryHeight = Math.round(p.getHeight() * scale); + // as getWidth() / getHeight() of the view are not + // available yet, set up mActualScale, so that when + // onSizeChanged() is called, the rest will be set + // correctly + mActualScale = scale; + invalidate(); + return true; + } + } + return false; + } + + /** * Restore the state of this WebView from the given map used in - * Activity.onThaw. This method should be called to restore the state of - * the WebView before using the object. If it is called after the WebView - * has had a chance to build state (load pages, create a back/forward list, - * etc.) there may be undesirable side-effects. + * {@link android.app.Activity#onRestoreInstanceState}. This method should + * be called to restore the state of the WebView before using the object. If + * it is called after the WebView has had a chance to build state (load + * pages, create a back/forward list, etc.) there may be undesirable + * side-effects. Please note that this method no longer restores the + * display data for this WebView. See {@link #savePicture} and {@link + * #restorePicture} for saving and restoring the display data. * @param inState The incoming Bundle of state. * @return The restored back/forward list or null if restoreState failed. + * @see #savePicture + * @see #restorePicture */ public WebBackForwardList restoreState(Bundle inState) { WebBackForwardList returnList = null; + if (inState == null) { + return returnList; + } if (inState.containsKey("index") && inState.containsKey("history")) { mCertificate = SslCertificate.restoreState( inState.getBundle("certificate")); @@ -853,42 +964,6 @@ public class WebView extends AbsoluteLayout WebHistoryItem item = new WebHistoryItem(data); list.addHistoryItem(item); } - if (inState.containsKey("picture")) { - String path = inState.getString("picture"); - File f = new File(path); - if (f.exists()) { - Picture p = null; - try { - final FileInputStream in = new FileInputStream(f); - p = Picture.createFromStream(in); - in.close(); - f.delete(); - } catch (FileNotFoundException e){ - e.printStackTrace(); - } catch (RuntimeException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - if (p != null) { - int sx = inState.getInt("scrollX", 0); - int sy = inState.getInt("scrollY", 0); - float scale = inState.getFloat("scale", 1.0f); - mDrawHistory = true; - mHistoryPicture = p; - mScrollX = sx; - mScrollY = sy; - mHistoryWidth = Math.round(p.getWidth() * scale); - mHistoryHeight = Math.round(p.getHeight() * scale); - // as getWidth() / getHeight() of the view are not - // available yet, set up mActualScale, so that when - // onSizeChanged() is called, the rest will be set - // correctly - mActualScale = scale; - invalidate(); - } - } - } // Grab the most recent copy to return to the caller. returnList = copyBackForwardList(); // Update the copy to have the correct index. @@ -929,14 +1004,22 @@ public class WebView extends AbsoluteLayout * Load the given data into the WebView, use the provided URL as the base * URL for the content. The base URL is the URL that represents the page * that is loaded through this interface. As such, it is used for the - * history entry and to resolve any relative URLs. - * The failUrl is used if browser fails to load the data provided. If it - * is empty or null, and the load fails, then no history entry is created. + * history entry and to resolve any relative URLs. The failUrl is used if + * browser fails to load the data provided. If it is empty or null, and the + * load fails, then no history entry is created. + * <p> + * Note for post 1.0. Due to the change in the WebKit, the access to asset + * files through "file:///android_asset/" for the sub resources is more + * restricted. If you provide null or empty string as baseUrl, you won't be + * able to access asset files. If the baseUrl is anything other than + * http(s)/ftp(s)/about/javascript as scheme, you can access asset files for + * sub resources. + * * @param baseUrl Url to resolve relative paths with, if null defaults to - * "about:blank" + * "about:blank" * @param data A String of data in the given encoding. * @param mimeType The MIMEType of the data. i.e. text/html. If null, - * defaults to "text/html" + * defaults to "text/html" * @param encoding The encoding of the data. i.e. utf-8, us-ascii * @param failUrl URL to use if the content fails to load or null. */ @@ -1133,7 +1216,7 @@ public class WebView extends AbsoluteLayout public void clearView() { mContentWidth = 0; mContentHeight = 0; - mWebViewCore.clearContentPicture(); + mWebViewCore.sendMessage(EventHub.CLEAR_CONTENT); } /** @@ -1197,7 +1280,7 @@ public class WebView extends AbsoluteLayout clearTextEntry(); ExtendedZoomControls zoomControls = (ExtendedZoomControls) getZoomControls(); - zoomControls.show(canZoomScrollOut()); + zoomControls.show(true, canZoomScrollOut()); zoomControls.requestFocus(); mPrivateHandler.removeCallbacks(mZoomControlRunnable); mPrivateHandler.postDelayed(mZoomControlRunnable, @@ -1206,11 +1289,15 @@ public class WebView extends AbsoluteLayout /** * Return a HitTestResult based on the current focus node. If a HTML::a tag - * is found, the HitTestResult type is set to ANCHOR_TYPE and the url has to - * be retrieved through {@link #requestFocusNodeHref} asynchronously. If a - * HTML::img tag is found, the HitTestResult type is set to IMAGE_TYPE and - * the url has to be retrieved through {@link #requestFocusNodeHref} - * asynchronously. If a phone number is found, the HitTestResult type is set + * is found and the anchor has a non-javascript url, the HitTestResult type + * is set to SRC_ANCHOR_TYPE and the url is set in the "extra" field. If the + * anchor does not have a url or if it is a javascript url, the type will + * be UNKNOWN_TYPE and the url has to be retrieved through + * {@link #requestFocusNodeHref} asynchronously. If a HTML::img tag is + * found, the HitTestResult type is set to IMAGE_TYPE and the url is set in + * the "extra" field. A type of + * SRC_IMAGE_ANCHOR_TYPE indicates an anchor with a url that has an image as + * a child node. If a phone number is found, the HitTestResult type is set * to PHONE_TYPE and the phone number is set in the "extra" field of * HitTestResult. If a map address is found, the HitTestResult type is set * to GEO_TYPE and the address is set in the "extra" field of HitTestResult. @@ -1227,42 +1314,38 @@ public class WebView extends AbsoluteLayout if (nativeUpdateFocusNode()) { FocusNode node = mFocusNode; - if (node.mIsAnchor && node.mText == null) { - result.setType(HitTestResult.ANCHOR_TYPE); - } else if (node.mIsTextField || node.mIsTextArea) { + if (node.mIsTextField || node.mIsTextArea) { result.setType(HitTestResult.EDIT_TEXT_TYPE); - } else { + } else if (node.mText != null) { String text = node.mText; - if (text != null) { - if (text.startsWith(SCHEME_TEL)) { - result.setType(HitTestResult.PHONE_TYPE); - result.setExtra(text.substring(SCHEME_TEL.length())); - } else if (text.startsWith(SCHEME_MAILTO)) { - result.setType(HitTestResult.EMAIL_TYPE); - result.setExtra(text.substring(SCHEME_MAILTO.length())); - } else if (text.startsWith(SCHEME_GEO)) { - result.setType(HitTestResult.GEO_TYPE); - result.setExtra(URLDecoder.decode(text - .substring(SCHEME_GEO.length()))); - } + if (text.startsWith(SCHEME_TEL)) { + result.setType(HitTestResult.PHONE_TYPE); + result.setExtra(text.substring(SCHEME_TEL.length())); + } else if (text.startsWith(SCHEME_MAILTO)) { + result.setType(HitTestResult.EMAIL_TYPE); + result.setExtra(text.substring(SCHEME_MAILTO.length())); + } else if (text.startsWith(SCHEME_GEO)) { + result.setType(HitTestResult.GEO_TYPE); + result.setExtra(URLDecoder.decode(text + .substring(SCHEME_GEO.length()))); + } else if (node.mIsAnchor) { + result.setType(HitTestResult.SRC_ANCHOR_TYPE); + result.setExtra(text); } } } int type = result.getType(); if (type == HitTestResult.UNKNOWN_TYPE - || type == HitTestResult.ANCHOR_TYPE) { + || type == HitTestResult.SRC_ANCHOR_TYPE) { // Now check to see if it is an image. int contentX = viewToContent((int) mLastTouchX + mScrollX); int contentY = viewToContent((int) mLastTouchY + mScrollY); - if (nativeIsImage(contentX, contentY)) { + String text = nativeImageURI(contentX, contentY); + if (text != null) { result.setType(type == HitTestResult.UNKNOWN_TYPE ? HitTestResult.IMAGE_TYPE : - HitTestResult.IMAGE_ANCHOR_TYPE); - } - if (nativeHasSrcUrl()) { - result.setType(result.getType() == HitTestResult.ANCHOR_TYPE ? - HitTestResult.SRC_ANCHOR_TYPE : HitTestResult.SRC_IMAGE_ANCHOR_TYPE); + result.setExtra(text); } } return result; @@ -1284,12 +1367,15 @@ public class WebView extends AbsoluteLayout if (nativeUpdateFocusNode()) { FocusNode node = mFocusNode; if (node.mIsAnchor) { + // NOTE: We may already have the url of the anchor stored in + // node.mText but it may be out of date or the caller may want + // to know about javascript urls. mWebViewCore.sendMessage(EventHub.REQUEST_FOCUS_HREF, node.mFramePointer, node.mNodePointer, hrefMsg); } } } - + /** * Request the url of the image last touched by the user. msg will be sent * to its target with a String representing the url as its object. @@ -1298,13 +1384,13 @@ public class WebView extends AbsoluteLayout * as the data member with "url" as key. The result can be null. */ public void requestImageRef(Message msg) { - if (msg == null || mNativeClass == 0) { - return; - } int contentX = viewToContent((int) mLastTouchX + mScrollX); int contentY = viewToContent((int) mLastTouchY + mScrollY); - mWebViewCore.sendMessage(EventHub.REQUEST_IMAGE_HREF, contentX, - contentY, msg); + String ref = nativeImageURI(contentX, contentY); + Bundle data = msg.getData(); + data.putString("url", ref); + msg.setData(data); + msg.sendToTarget(); } private static int pinLoc(int x, int viewMax, int docMax) { @@ -1340,6 +1426,25 @@ public class WebView extends AbsoluteLayout return Math.round(x * mActualScale); } + // Called by JNI to invalidate the View, given rectangle coordinates in + // content space + private void viewInvalidate(int l, int t, int r, int b) { + invalidate(contentToView(l), contentToView(t), contentToView(r), + contentToView(b)); + } + + // Called by JNI to invalidate the View after a delay, given rectangle + // coordinates in content space + private void viewInvalidateDelayed(long delay, int l, int t, int r, int b) { + postInvalidateDelayed(delay, contentToView(l), contentToView(t), + contentToView(r), contentToView(b)); + } + + private Rect contentToView(Rect x) { + return new Rect(contentToView(x.left), contentToView(x.top) + , contentToView(x.right), contentToView(x.bottom)); + } + /* call from webcoreview.draw(), so we're still executing in the UI thread */ private void recordNewContentSize(int w, int h, boolean updateLayout) { @@ -1420,15 +1525,29 @@ public class WebView extends AbsoluteLayout // Used to avoid sending many visible rect messages. private Rect mLastVisibleRectSent; + private Rect mLastGlobalRect; private Rect sendOurVisibleRect() { Rect rect = new Rect(); calcOurContentVisibleRect(rect); + if (mFindIsUp) { + rect.bottom -= viewToContent(FIND_HEIGHT); + } // Rect.equals() checks for null input. if (!rect.equals(mLastVisibleRectSent)) { - mWebViewCore.sendMessage(EventHub.SET_VISIBLE_RECT, rect); + mWebViewCore.sendMessage(EventHub.SET_SCROLL_OFFSET, + rect.left, rect.top); mLastVisibleRectSent = rect; } + Rect globalRect = new Rect(); + if (getGlobalVisibleRect(globalRect) + && !globalRect.equals(mLastGlobalRect)) { + // TODO: the global offset is only used by windowRect() + // in ChromeClientAndroid ; other clients such as touch + // and mouse events could return view + screen relative points. + mWebViewCore.sendMessage(EventHub.SET_GLOBAL_BOUNDS, globalRect); + mLastGlobalRect = globalRect; + } return rect; } @@ -1488,12 +1607,19 @@ public class WebView extends AbsoluteLayout } } + // Make sure this stays in sync with the actual height of the FindDialog. + private static final int FIND_HEIGHT = 79; + @Override protected int computeVerticalScrollRange() { if (mDrawHistory) { return mHistoryHeight; } else { - return contentToView(mContentHeight); + int height = contentToView(mContentHeight); + if (mFindIsUp) { + height += FIND_HEIGHT; + } + return height; } } @@ -1507,6 +1633,21 @@ public class WebView extends AbsoluteLayout WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getUrl() : null; } + + /** + * Get the original url for the current page. This is not always the same + * as the url passed to WebViewClient.onPageStarted because although the + * load for that url has begun, the current page may not have changed. + * Also, there may have been redirects resulting in a different url to that + * originally requested. + * @return The url that was originally requested for the current page. + * + * @hide pending API Council approval + */ + public String getOriginalUrl() { + WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); + return h != null ? h.getOriginalUrl() : null; + } /** * Get the title for the current page. This is the title of the current page @@ -1612,82 +1753,35 @@ public class WebView extends AbsoluteLayout } /* - * Making the find methods private since we are disabling for 1.0 - * - * Find and highlight the next occurance of the String find, beginning with - * the current selection. Wraps the page infinitely, and scrolls. When - * WebCore determines whether it has found it, the response message is sent - * to its target with true(1) or false(0) as arg1 depending on whether the - * text was found. - * @param find String to find. - * @param response A Message object that will be dispatched with the result - * as the arg1 member. A result of 1 means the search - * succeeded. - */ - private void findNext(String find, Message response) { - if (response == null) { - return; - } - Message m = Message.obtain(null, EventHub.FIND, 1, 0, response); - m.getData().putString("find", find); - mWebViewCore.sendMessage(m); - } - - /* - * Making the find methods private since we are disabling for 1.0 - * - * Find and highlight the previous occurance of the String find, beginning - * with the current selection. - * @param find String to find. - * @param response A Message object that will be dispatched with the result - * as the arg1 member. A result of 1 means the search - * succeeded. - */ - private void findPrevious(String find, Message response) { - if (response == null) { - return; - } - Message m = Message.obtain(null, EventHub.FIND, -1, 0, response); - m.getData().putString("find", find); - mWebViewCore.sendMessage(m); - } - - /* - * Making the find methods private since we are disabling for 1.0 + * Highlight and scroll to the next occurance of String in findAll. + * Wraps the page infinitely, and scrolls. Must be called after + * calling findAll. * - * Find and highlight the first occurance of find, beginning with the start - * of the page. - * @param find String to find. - * @param response A Message object that will be dispatched with the result - * as the arg1 member. A result of 1 means the search - * succeeded. + * @param forward Direction to search. */ - private void findFirst(String find, Message response) { - if (response == null) { - return; - } - Message m = Message.obtain(null, EventHub.FIND, 0, 0, response); - m.getData().putString("find", find); - mWebViewCore.sendMessage(m); + public void findNext(boolean forward) { + nativeFindNext(forward); } /* - * Making the find methods private since we are disabling for 1.0 - * * Find all instances of find on the page and highlight them. * @param find String to find. - * @param response A Message object that will be dispatched with the result - * as the arg1 member. The result will be the number of - * matches to the String find. + * @return int The number of occurances of the String "find" + * that were found. */ - private void findAll(String find, Message response) { - if (response == null) { - return; - } - Message m = Message.obtain(null, EventHub.FIND_ALL, 0, 0, response); - m.getData().putString("find", find); - mWebViewCore.sendMessage(m); + public int findAll(String find) { + mFindIsUp = true; + int result = nativeFindAll(find.toLowerCase(), find.toUpperCase()); + invalidate(); + return result; } + + // Used to know whether the find dialog is open. Affects whether + // or not we draw the highlights for matches. + private boolean mFindIsUp; + + private native int nativeFindAll(String findLower, String findUpper); + private native void nativeFindNext(boolean forward); /** * Return the first substring consisting of the address of a physical @@ -1714,12 +1808,15 @@ public class WebView extends AbsoluteLayout } /* - * Making the find methods private since we are disabling for 1.0 - * * Clear the highlighting surrounding text matches created by findAll. */ - private void clearMatches() { - mWebViewCore.sendMessage(EventHub.CLEAR_MATCHES); + public void clearMatches() { + mFindIsUp = false; + nativeSetFindIsDown(); + // 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); + invalidate(); } /** @@ -2023,10 +2120,21 @@ public class WebView extends AbsoluteLayout nativeRecomputeFocus(); // Update the buttons in the picture, so when we draw the picture // to the screen, they are in the correct state. - nativeRecordButtons(); + // Tell the native side if user is a) touching the screen, + // b) pressing the trackball down, or c) pressing the enter key + // If the focus is a button, we need to draw it in the pressed + // state. + // If mNativeClass is 0, we should not reach here, so we do not + // need to check it again. + nativeRecordButtons(mTouchMode == TOUCH_SHORTPRESS_START_MODE + || mTrackballDown || mGotEnterDown, false); drawCoreAndFocusRing(canvas, mBackgroundColor, mDrawFocusRing); } canvas.restoreToCount(sc); + + if (AUTO_REDRAW_HACK && mAutoRedraw) { + invalidate(); + } } @Override @@ -2097,7 +2205,12 @@ public class WebView extends AbsoluteLayout if (mNativeClass == 0) return; if (mShiftIsPressed) { - nativeDrawSelection(canvas, mSelectX, mSelectY, mExtendSelection); + if (mTouchSelection) { + nativeDrawSelectionRegion(canvas); + } else { + nativeDrawSelection(canvas, mSelectX, mSelectY, + mExtendSelection); + } } else if (drawFocus) { if (mTouchMode == TOUCH_SHORTPRESS_START_MODE) { mTouchMode = TOUCH_SHORTPRESS_MODE; @@ -2111,7 +2224,14 @@ public class WebView extends AbsoluteLayout } nativeDrawFocusRing(canvas); } + // When the FindDialog is up, only draw the matches if we are not in + // the process of scrolling them into view. + if (mFindIsUp && !animateScroll) { + nativeDrawMatches(canvas); + } } + + private native void nativeDrawMatches(Canvas canvas); private float scrollZoomGridScale(float invScale) { float griddedInvScale = (int) (invScale * SCROLL_ZOOM_GRID) @@ -2163,19 +2283,40 @@ public class WebView extends AbsoluteLayout Rect scrollFrame = new Rect(); scrollFrame.set(mZoomScrollX, mZoomScrollY, mZoomScrollX + width, mZoomScrollY + height); - if (mContentWidth == width) { - float offsetX = (width * halfScale - width) / 2; + if (mContentWidth * mZoomScrollLimit < width) { + float scale = zoomFrameScaleX(width, halfScale, 1.0f); + float offsetX = (width * scale - width) * 0.5f; scrollFrame.left -= offsetX; scrollFrame.right += offsetX; } - if (mContentHeight == height) { - float offsetY = (height * halfScale - height) / 2; + if (mContentHeight * mZoomScrollLimit < height) { + float scale = zoomFrameScaleY(height, halfScale, 1.0f); + float offsetY = (height * scale - height) * 0.5f; scrollFrame.top -= offsetY; scrollFrame.bottom += offsetY; } return scrollFrame; } + private float zoomFrameScaleX(int width, float halfScale, float noScale) { + // mContentWidth > width > mContentWidth * mZoomScrollLimit + if (mContentWidth <= width) { + return halfScale; + } + float part = (width - mContentWidth * mZoomScrollLimit) + / (width * (1 - mZoomScrollLimit)); + return halfScale * part + noScale * (1.0f - part); + } + + private float zoomFrameScaleY(int height, float halfScale, float noScale) { + if (mContentHeight <= height) { + return halfScale; + } + float part = (height - mContentHeight * mZoomScrollLimit) + / (height * (1 - mZoomScrollLimit)); + return halfScale * part + noScale * (1.0f - part); + } + private float scrollZoomMagScale(float invScale) { return (invScale * 2 + mInvActualScale) / 3; } @@ -2252,8 +2393,14 @@ public class WebView extends AbsoluteLayout } int sc = canvas.save(); canvas.clipRect(scrollFrame); - float halfX = maxX > 0 ? (float) mZoomScrollX / maxX : 0.5f; - float halfY = maxY > 0 ? (float) mZoomScrollY / maxY : 0.5f; + float halfX = (float) mZoomScrollX / maxX; + if (mContentWidth * mZoomScrollLimit < width) { + halfX = zoomFrameScaleX(width, 0.5f, halfX); + } + float halfY = (float) mZoomScrollY / maxY; + if (mContentHeight * mZoomScrollLimit < height) { + halfY = zoomFrameScaleY(height, 0.5f, halfY); + } canvas.scale(halfScale, halfScale, mZoomScrollX + width * halfX , mZoomScrollY + height * halfY); if (LOGV_ENABLED) { @@ -2350,7 +2497,10 @@ public class WebView extends AbsoluteLayout } private void zoomScrollOut() { - if (canZoomScrollOut() == false) return; + if (canZoomScrollOut() == false) { + mTouchMode = TOUCH_DONE_MODE; + return; + } startZoomScrollOut(); mTouchMode = SCROLL_ZOOM_ANIMATION_OUT; invalidate(); @@ -2377,8 +2527,8 @@ public class WebView extends AbsoluteLayout + " mZoomScrollLimit=" + mZoomScrollLimit + " x=" + x); } x += maxScreenX * mLastScrollX / maxZoomX - mLastTouchX; - x = Math.max(0, Math.min(maxScreenX, x)); - mZoomScrollX = (int) (x * maxZoomX / maxScreenX); + x *= Math.max(maxZoomX / maxScreenX, mZoomScrollInvLimit); + mZoomScrollX = Math.max(0, Math.min(maxZoomX, (int) x)); } int maxZoomY = mContentHeight - height; if (maxZoomY > 0) { @@ -2390,8 +2540,8 @@ public class WebView extends AbsoluteLayout + " mZoomScrollLimit=" + mZoomScrollLimit + " y=" + y); } y += maxScreenY * mLastScrollY / maxZoomY - mLastTouchY; - y = Math.max(0, Math.min(maxScreenY, y)); - mZoomScrollY = (int) (y * maxZoomY / maxScreenY); + y *= Math.max(maxZoomY / maxScreenY, mZoomScrollInvLimit); + mZoomScrollY = Math.max(0, Math.min(maxZoomY, (int) y)); } if (oldX != mZoomScrollX || oldY != mZoomScrollY) { invalidate(); @@ -2540,6 +2690,13 @@ public class WebView extends AbsoluteLayout new WebViewCore.FocusData(mFocusData)); } + // Called by JNI when a touch event puts a textfield into focus. + private void displaySoftKeyboard() { + InputMethodManager imm = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mTextEntry); + } + // Used to register the global focus change listener one time to avoid // multiple references to WebView private boolean mGlobalFocusChangeListenerAdded; @@ -2663,7 +2820,8 @@ public class WebView extends AbsoluteLayout ArrayList<String> pastEntries = mDatabase.getFormData(mUrl, mName); if (pastEntries.size() > 0) { ArrayAdapter<String> adapter = new ArrayAdapter<String>( - mContext, com.android.internal.R.layout.simple_list_item_1, + mContext, com.android.internal.R.layout + .search_dropdown_item_1line, pastEntries); ((HashMap) mUpdateMessage.obj).put("adapter", adapter); mUpdateMessage.sendToTarget(); @@ -2679,152 +2837,166 @@ public class WebView extends AbsoluteLayout mTextEntry.setRect(x, y, width, height); } - // These variables are used to determine long press with the enter key, or + // This is used to determine long press with the enter key, or // a center key. Does not affect long press with the trackball/touch. - private long mDownTime = 0; - private boolean mGotDown = false; + private boolean mGotEnterDown = false; // Enable copy/paste with trackball here. // This should be left disabled until the framework can guarantee // delivering matching key-up and key-down events for the shift key - private static final boolean ENABLE_COPY_PASTE = false; + private static final boolean ENABLE_COPY_PASTE = true; @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM) { + if (LOGV_ENABLED) { + Log.v(LOGTAG, "keyDown at " + System.currentTimeMillis() + + ", " + event); + } + + if (mNativeClass == 0) { return false; } - if (keyCode != KeyEvent.KEYCODE_DPAD_CENTER && - keyCode != KeyEvent.KEYCODE_ENTER) { - mGotDown = false; + + // do this hack up front, so it always works, regardless of touch-mode + if (AUTO_REDRAW_HACK && (keyCode == KeyEvent.KEYCODE_CALL)) { + mAutoRedraw = !mAutoRedraw; + if (mAutoRedraw) { + invalidate(); + } + return true; } - if (mAltIsPressed == false && (keyCode == KeyEvent.KEYCODE_ALT_LEFT - || keyCode == KeyEvent.KEYCODE_ALT_RIGHT)) { - mAltIsPressed = true; + + // Bubble up the key event if + // 1. it is a system key; or + // 2. the host application wants to handle it; or + // 3. webview is in scroll-zoom state; + if (event.isSystem() + || mCallbackProxy.uiOverrideKeyEvent(event) + || (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM)) { + return false; } + if (ENABLE_COPY_PASTE && mShiftIsPressed == false && (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT)) { mExtendSelection = false; mShiftIsPressed = true; - if (mNativeClass != 0 && nativeUpdateFocusNode()) { + if (nativeUpdateFocusNode()) { FocusNode node = mFocusNode; - mSelectX = node.mBounds.left; - mSelectY = node.mBounds.top; + mSelectX = contentToView(node.mBounds.left); + mSelectY = contentToView(node.mBounds.top); } else { - mSelectX = mScrollX + SELECT_CURSOR_OFFSET; - mSelectY = mScrollY + SELECT_CURSOR_OFFSET; + mSelectX = mScrollX + (int) mLastTouchX; + mSelectY = mScrollY + (int) mLastTouchY; } } - if (keyCode == KeyEvent.KEYCODE_CALL) { - if (mNativeClass != 0 && nativeUpdateFocusNode()) { - FocusNode node = mFocusNode; - String text = node.mText; - if (!node.mIsTextField && !node.mIsTextArea && text != null && - text.startsWith(SCHEME_TEL)) { - Intent intent = new Intent(Intent.ACTION_DIAL, - Uri.parse(text)); - getContext().startActivity(intent); - return true; - } + + if (keyCode >= KeyEvent.KEYCODE_DPAD_UP + && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { + // always handle the navigation keys in the UI thread + switchOutDrawHistory(); + if (navHandledKey(keyCode, 1, false, event.getEventTime())) { + playSoundEffect(keyCodeToSoundsEffect(keyCode)); + return true; } + // Bubble up the key event as WebView doesn't handle it return false; } - if (event.isSystem() || mCallbackProxy.uiOverrideKeyEvent(event)) { - return false; - } - if (LOGV_ENABLED) { - Log.v(LOGTAG, "keyDown at " + System.currentTimeMillis() - + ", " + event); - } - boolean isArrowKey = keyCode == KeyEvent.KEYCODE_DPAD_UP || - keyCode == KeyEvent.KEYCODE_DPAD_DOWN || - keyCode == KeyEvent.KEYCODE_DPAD_LEFT || - keyCode == KeyEvent.KEYCODE_DPAD_RIGHT; - if (isArrowKey && event.getEventTime() - mTrackballLastTime - <= TRACKBALL_KEY_TIMEOUT) { - if (LOGV_ENABLED) Log.v(LOGTAG, "ignore arrow"); + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER + || keyCode == KeyEvent.KEYCODE_ENTER) { + switchOutDrawHistory(); + if (event.getRepeatCount() == 0) { + mGotEnterDown = true; + mPrivateHandler.sendMessageDelayed(mPrivateHandler + .obtainMessage(LONG_PRESS_ENTER), LONG_PRESS_TIMEOUT); + nativeRecordButtons(true, true); + // FIXME, currently in webcore keydown it doesn't do anything. + // In keyup, it calls both keydown and keyup, we should fix it. + mWebViewCore.sendMessage(EventHub.KEY_DOWN, keyCode, + EventHub.KEYEVENT_UNHANDLED_TYPE, event); + return true; + } + // Bubble up the key event as WebView doesn't handle it return false; } - boolean weHandledTheKey = false; - if (event.getMetaState() == 0) { + if (getSettings().getNavDump()) { switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_RIGHT: - case KeyEvent.KEYCODE_DPAD_LEFT: - // TODO: alternatively we can do panning as touch does - switchOutDrawHistory(); - weHandledTheKey = navHandledKey(keyCode, 1, false - , event.getEventTime()); - if (weHandledTheKey) { - playSoundEffect(keyCodeToSoundsEffect(keyCode)); - } + case KeyEvent.KEYCODE_4: + // "/data/data/com.android.browser/displayTree.txt" + nativeDumpDisplayTree(getUrl()); break; - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_ENTER: - if (event.getRepeatCount() == 0) { - switchOutDrawHistory(); - mDownTime = event.getEventTime(); - mGotDown = true; - return true; - } - if (mGotDown && event.getEventTime() - mDownTime > - ViewConfiguration.getLongPressTimeout()) { - performLongClick(); - mGotDown = false; - return true; - } + case KeyEvent.KEYCODE_5: + case KeyEvent.KEYCODE_6: + // 5: dump the dom tree to the file + // "/data/data/com.android.browser/domTree.txt" + // 6: dump the dom tree to the adb log + mWebViewCore.sendMessage(EventHub.DUMP_DOMTREE, + (keyCode == KeyEvent.KEYCODE_5) ? 1 : 0, 0); break; - case KeyEvent.KEYCODE_9: - if (mNativeClass != 0 && getSettings().getNavDump()) { - debugDump(); - } + case KeyEvent.KEYCODE_7: + case KeyEvent.KEYCODE_8: + // 7: dump the render tree to the file + // "/data/data/com.android.browser/renderTree.txt" + // 8: dump the render tree to the adb log + mWebViewCore.sendMessage(EventHub.DUMP_RENDERTREE, + (keyCode == KeyEvent.KEYCODE_7) ? 1 : 0, 0); break; - default: + case KeyEvent.KEYCODE_9: + debugDump(); break; } } - // suppress sending arrow keys to webkit - if (!weHandledTheKey && !isArrowKey) { - mWebViewCore.sendMessage(EventHub.KEY_DOWN, keyCode, 0, event); + if (nativeFocusNodeWantsKeyEvents()) { + mWebViewCore.sendMessage(EventHub.KEY_DOWN, keyCode, + EventHub.KEYEVENT_FOCUS_NODE_TYPE, event); + // return true as DOM handles the key + return true; + } else if (false) { // reserved to check the meta tag + // pass the key to DOM + mWebViewCore.sendMessage(EventHub.KEY_DOWN, keyCode, + EventHub.KEYEVENT_UNHANDLED_TYPE, event); + // return true as DOM handles the key + return true; } - return weHandledTheKey; + // Bubble up the key event as WebView doesn't handle it + return false; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT - || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { - if (mExtendSelection) { - // copy region so core operates on copy without touching orig. - Region selection = new Region(nativeGetSelection()); - if (selection.isEmpty() == false) { - Toast.makeText(mContext - , com.android.internal.R.string.text_copied - , Toast.LENGTH_SHORT).show(); - mWebViewCore.sendMessage(EventHub.GET_SELECTION, selection); - } - } - mShiftIsPressed = false; - return true; - } if (LOGV_ENABLED) { - Log.v(LOGTAG, "MT keyUp at" + System.currentTimeMillis() + Log.v(LOGTAG, "keyUp at " + System.currentTimeMillis() + ", " + event); } - if (keyCode == KeyEvent.KEYCODE_ALT_LEFT - || keyCode == KeyEvent.KEYCODE_ALT_RIGHT) { - mAltIsPressed = false; + + if (mNativeClass == 0) { + return false; } + + // special CALL handling when focus node's href is "tel:XXX" + if (keyCode == KeyEvent.KEYCODE_CALL && nativeUpdateFocusNode()) { + FocusNode node = mFocusNode; + String text = node.mText; + if (!node.mIsTextField && !node.mIsTextArea && text != null + && text.startsWith(SCHEME_TEL)) { + Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(text)); + getContext().startActivity(intent); + return true; + } + } + + // 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)) { return false; } + // special handling in scroll_zoom state if (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM) { if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode && mTouchMode != SCROLL_ZOOM_ANIMATION_IN) { @@ -2835,64 +3007,112 @@ public class WebView extends AbsoluteLayout } return false; } - Rect visibleRect = sendOurVisibleRect(); - // Note that sendOurVisibleRect calls viewToContent, so the coordinates - // should be in content coordinates. - boolean nodeOnScreen = false; - boolean isTextField = false; - boolean isTextArea = false; - FocusNode node = null; - if (mNativeClass != 0 && nativeUpdateFocusNode()) { - node = mFocusNode; - isTextField = node.mIsTextField; - isTextArea = node.mIsTextArea; - nodeOnScreen = Rect.intersects(node.mBounds, visibleRect); - } - - if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode) { - if (mShiftIsPressed) { - return false; - } - if (getSettings().supportZoom()) { - if (mTouchMode == TOUCH_DOUBLECLICK_MODE) { - zoomScrollOut(); - } else { - if (LOGV_ENABLED) Log.v(LOGTAG, "TOUCH_DOUBLECLICK_MODE"); - mPrivateHandler.sendMessageDelayed(mPrivateHandler - .obtainMessage(SWITCH_TO_ENTER), TAP_TIMEOUT); - mTouchMode = TOUCH_DOUBLECLICK_MODE; - } + + if (ENABLE_COPY_PASTE && (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT + || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT)) { + if (commitCopy()) { return true; - } else { - keyCode = KeyEvent.KEYCODE_ENTER; } } - if (KeyEvent.KEYCODE_ENTER == keyCode) { - if (LOGV_ENABLED) Log.v(LOGTAG, "KEYCODE_ENTER == keyCode"); - if (!nodeOnScreen) { - return false; + + if (keyCode >= KeyEvent.KEYCODE_DPAD_UP + && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { + // always handle the navigation keys in the UI thread + // Bubble up the key event as WebView doesn't handle it + return false; + } + + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER + || keyCode == KeyEvent.KEYCODE_ENTER) { + // remove the long press message first + mPrivateHandler.removeMessages(LONG_PRESS_ENTER); + mGotEnterDown = false; + + if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode) { + if (mShiftIsPressed) { + return false; + } + if (getSettings().supportZoom()) { + if (mTouchMode == TOUCH_DOUBLECLICK_MODE) { + zoomScrollOut(); + } else { + if (LOGV_ENABLED) { + Log.v(LOGTAG, "TOUCH_DOUBLECLICK_MODE"); + } + mPrivateHandler.sendMessageDelayed(mPrivateHandler + .obtainMessage(SWITCH_TO_ENTER), TAP_TIMEOUT); + mTouchMode = TOUCH_DOUBLECLICK_MODE; + } + return true; + } + } + + Rect visibleRect = sendOurVisibleRect(); + // Note that sendOurVisibleRect calls viewToContent, so the + // coordinates should be in content coordinates. + boolean nodeOnScreen = false; + boolean isTextField = false; + boolean isTextArea = false; + FocusNode node = null; + if (nativeUpdateFocusNode()) { + node = mFocusNode; + isTextField = node.mIsTextField; + isTextArea = node.mIsTextArea; + nodeOnScreen = Rect.intersects(node.mBounds, visibleRect); } - if (node != null && !isTextField && !isTextArea) { + if (nodeOnScreen && !isTextField && !isTextArea) { nativeSetFollowedLink(true); - mWebViewCore.sendMessage(EventHub.SET_FINAL_FOCUS, + mWebViewCore.sendMessage(EventHub.SET_FINAL_FOCUS, EventHub.BLOCK_FOCUS_CHANGE_UNTIL_KEY_UP, 0, new WebViewCore.FocusData(mFocusData)); - if (mCallbackProxy.uiOverrideUrlLoading(node.mText)) { - return true; - } playSoundEffect(SoundEffectConstants.CLICK); + if (!mCallbackProxy.uiOverrideUrlLoading(node.mText)) { + mWebViewCore.sendMessage(EventHub.KEY_UP, keyCode, + EventHub.KEYEVENT_UNHANDLED_TYPE, event); + } + return true; } - } - if (!nodeOnScreen) { - // FIXME: Want to give Callback a chance to handle it, and - // possibly pass down to javascript. + // Bubble up the key event as WebView doesn't handle it return false; } - if (LOGV_ENABLED) Log.v(LOGTAG, "onKeyUp send EventHub.KEY_UP"); - mWebViewCore.sendMessage(EventHub.KEY_UP, keyCode, 0, event); - return true; + + if (nativeFocusNodeWantsKeyEvents()) { + mWebViewCore.sendMessage(EventHub.KEY_UP, keyCode, + EventHub.KEYEVENT_FOCUS_NODE_TYPE, event); + // return true as DOM handles the key + return true; + } else if (false) { // reserved to check the meta tag + // pass the key to DOM + mWebViewCore.sendMessage(EventHub.KEY_UP, keyCode, + EventHub.KEYEVENT_UNHANDLED_TYPE, event); + // return true as DOM handles the key + return true; + } + + // Bubble up the key event as WebView doesn't handle it + return false; } - + + private boolean commitCopy() { + boolean copiedSomething = false; + if (mExtendSelection) { + // copy region so core operates on copy without touching orig. + Region selection = new Region(nativeGetSelection()); + if (selection.isEmpty() == false) { + Toast.makeText(mContext + , com.android.internal.R.string.text_copied + , Toast.LENGTH_SHORT).show(); + mWebViewCore.sendMessage(EventHub.GET_SELECTION, selection); + copiedSomething = true; + } + } + mShiftIsPressed = false; + if (mTouchMode == TOUCH_SELECT_MODE) { + mTouchMode = TOUCH_INIT_MODE; + } + return copiedSomething; + } + // Set this as a hierarchy change listener so we can know when this view // is removed and still have access to our parent. @Override @@ -2962,6 +3182,7 @@ public class WebView extends AbsoluteLayout // we regain focus. mDrawFocusRing = false; mGotKeyDown = false; + mShiftIsPressed = false; if (inEditingMode()) { clearTextEntry(); mNeedsUpdateTextEntry = true; @@ -3055,8 +3276,7 @@ public class WebView extends AbsoluteLayout @Override public boolean onTouchEvent(MotionEvent ev) { - if (mNativeClass == 0 || !isClickable() || !isLongClickable() || - !hasFocus()) { + if (mNativeClass == 0 || !isClickable() || !isLongClickable()) { return false; } @@ -3069,6 +3289,32 @@ public class WebView extends AbsoluteLayout float x = ev.getX(); float y = ev.getY(); long eventTime = ev.getEventTime(); + + // Due to the touch screen edge effect, a touch closer to the edge + // always snapped to the edge. As getViewWidth() can be different from + // getWidth() due to the scrollbar, adjusting the point to match + // getViewWidth(). Same applied to the height. + if (x > getViewWidth() - 1) { + x = getViewWidth() - 1; + } + if (y > getViewHeight() - 1) { + y = getViewHeight() - 1; + } + + // pass the touch events from UI thread to WebCore thread + if (mForwardTouchEvents && mTouchMode != SCROLL_ZOOM_OUT + && mTouchMode != SCROLL_ZOOM_ANIMATION_IN + && mTouchMode != SCROLL_ZOOM_ANIMATION_OUT + && (action != MotionEvent.ACTION_MOVE || + eventTime - mLastSentTouchTime > TOUCH_SENT_INTERVAL)) { + WebViewCore.TouchEventData ted = new WebViewCore.TouchEventData(); + ted.mAction = action; + ted.mX = viewToContent((int) x + mScrollX); + ted.mY = viewToContent((int) y + mScrollY);; + mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); + mLastSentTouchTime = eventTime; + } + switch (action) { case MotionEvent.ACTION_DOWN: { if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN @@ -3083,6 +3329,16 @@ public class WebView extends AbsoluteLayout mScroller.abortAnimation(); mTouchMode = TOUCH_DRAG_START_MODE; mPrivateHandler.removeMessages(RESUME_WEBCORE_UPDATE); + } else if (mShiftIsPressed) { + mSelectX = mScrollX + (int) x; + mSelectY = mScrollY + (int) y; + mTouchMode = TOUCH_SELECT_MODE; + if (LOGV_ENABLED) { + Log.v(LOGTAG, "select=" + mSelectX + "," + mSelectY); + } + nativeMoveSelection(viewToContent(mSelectX) + , viewToContent(mSelectY), false); + mTouchSelection = mExtendSelection = true; } else { mTouchMode = TOUCH_INIT_MODE; } @@ -3116,6 +3372,17 @@ public class WebView extends AbsoluteLayout int deltaY = (int) (mLastTouchY - y); if (mTouchMode != TOUCH_DRAG_MODE) { + if (mTouchMode == TOUCH_SELECT_MODE) { + mSelectX = mScrollX + (int) x; + mSelectY = mScrollY + (int) y; + if (LOGV_ENABLED) { + Log.v(LOGTAG, "xtend=" + mSelectX + "," + mSelectY); + } + nativeMoveSelection(viewToContent(mSelectX) + , viewToContent(mSelectY), true); + invalidate(); + break; + } if ((deltaX * deltaX + deltaY * deltaY) < TOUCH_SLOP_SQUARE) { break; @@ -3214,11 +3481,13 @@ public class WebView extends AbsoluteLayout mUserScroll = true; } - if (mZoomControls != null && mMinZoomScale < mMaxZoomScale) { + boolean showPlusMinus = mMinZoomScale < mMaxZoomScale; + boolean showMagnify = canZoomScrollOut(); + if (mZoomControls != null && (showPlusMinus || showMagnify)) { if (mZoomControls.getVisibility() == View.VISIBLE) { mPrivateHandler.removeCallbacks(mZoomControlRunnable); } else { - mZoomControls.show(canZoomScrollOut()); + mZoomControls.show(showPlusMinus, showMagnify); } mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT); @@ -3244,6 +3513,10 @@ public class WebView extends AbsoluteLayout doShortPress(); } break; + case TOUCH_SELECT_MODE: + commitCopy(); + mTouchSelection = mExtendSelection = false; + break; case SCROLL_ZOOM_ANIMATION_IN: case SCROLL_ZOOM_ANIMATION_OUT: // no action during scroll animation @@ -3349,6 +3622,7 @@ public class WebView extends AbsoluteLayout private int mTrackballXMove = 0; private int mTrackballYMove = 0; private boolean mExtendSelection = false; + private boolean mTouchSelection = false; private static final int TRACKBALL_KEY_TIMEOUT = 1000; private static final int TRACKBALL_TIMEOUT = 200; private static final int TRACKBALL_WAIT = 100; @@ -3359,7 +3633,6 @@ public class WebView extends AbsoluteLayout private int mSelectX = 0; private int mSelectY = 0; private boolean mShiftIsPressed = false; - private boolean mAltIsPressed = false; private boolean mTrackballDown = false; private long mTrackballUpTime = 0; private long mLastFocusTime = 0; @@ -3386,17 +3659,18 @@ public class WebView extends AbsoluteLayout @Override public boolean onTrackballEvent(MotionEvent ev) { long time = ev.getEventTime(); - if (mAltIsPressed) { + if ((ev.getMetaState() & KeyEvent.META_ALT_ON) != 0) { if (ev.getY() > 0) pageDown(true); if (ev.getY() < 0) pageUp(true); return true; } if (ev.getAction() == MotionEvent.ACTION_DOWN) { mPrivateHandler.removeMessages(SWITCH_TO_ENTER); - mPrivateHandler.sendMessageDelayed( - mPrivateHandler.obtainMessage(LONG_PRESS_TRACKBALL), 1000); mTrackTrackball = true; mTrackballDown = true; + if (mNativeClass != 0) { + nativeRecordButtons(true, true); + } if (time - mLastFocusTime <= TRACKBALL_TIMEOUT && !mLastFocusBounds.equals(nativeGetFocusRingBounds())) { nativeSelectBestAt(mLastFocusBounds); @@ -3408,7 +3682,8 @@ public class WebView extends AbsoluteLayout } return false; // let common code in onKeyDown at it } else if (mTrackTrackball) { - mPrivateHandler.removeMessages(LONG_PRESS_TRACKBALL); + // LONG_PRESS_ENTER is set in common onKeyDown + mPrivateHandler.removeMessages(LONG_PRESS_ENTER); mTrackTrackball = false; } if (ev.getAction() == MotionEvent.ACTION_UP) { @@ -3823,13 +4098,7 @@ public class WebView extends AbsoluteLayout return; } switchOutDrawHistory(); - // mLastTouchX and mLastTouchY are the point in the current viewport - int contentX = viewToContent((int) mLastTouchX + mScrollX); - int contentY = viewToContent((int) mLastTouchY + mScrollY); - int contentSize = ViewConfiguration.getTouchSlop(); - nativeMotionUp(contentX, contentY, contentSize, true); - - // call uiOverride next to check whether it is a special node, + // call uiOverride to check whether it is a special node, // phone/email/address, which are not handled by WebKit if (nativeUpdateFocusNode()) { FocusNode node = mFocusNode; @@ -3840,6 +4109,11 @@ public class WebView extends AbsoluteLayout } playSoundEffect(SoundEffectConstants.CLICK); } + // mLastTouchX and mLastTouchY are the point in the current viewport + int contentX = viewToContent((int) mLastTouchX + mScrollX); + int contentY = viewToContent((int) mLastTouchY + mScrollY); + int contentSize = ViewConfiguration.getTouchSlop(); + nativeMotionUp(contentX, contentY, contentSize, true); } @Override @@ -3907,6 +4181,8 @@ public class WebView extends AbsoluteLayout mHeightCanMeasure = false; } } + } else { + mHeightCanMeasure = false; } if (mNativeClass != 0) { nativeSetHeightCanMeasure(mHeightCanMeasure); @@ -3915,6 +4191,8 @@ public class WebView extends AbsoluteLayout if (widthMode == MeasureSpec.UNSPECIFIED) { mWidthCanMeasure = true; measuredWidth = contentWidth; + } else { + mWidthCanMeasure = false; } synchronized (this) { @@ -4076,10 +4354,14 @@ public class WebView extends AbsoluteLayout break; case NEW_PICTURE_MSG_ID: // called for new content - final Point viewSize = (Point) msg.obj; + final WebViewCore.DrawData draw = + (WebViewCore.DrawData) msg.obj; + final Point viewSize = draw.mViewPoint; if (mZoomScale > 0) { - if (Math.abs(mZoomScale * viewSize.x - - getViewWidth()) < 1) { + // use the same logic in sendViewSizeZoom() to make sure + // the mZoomScale has matched the viewSize so that we + // can clear mZoomScale + if (Math.round(getViewWidth() / mZoomScale) == viewSize.x) { mZoomScale = 0; mWebViewCore.sendMessage(EventHub.SET_SNAP_ANCHOR, 0, 0); @@ -4091,8 +4373,14 @@ public class WebView extends AbsoluteLayout // received in the fixed dimension. final boolean updateLayout = viewSize.x == mLastWidthSent && viewSize.y == mLastHeightSent; - recordNewContentSize(msg.arg1, msg.arg2, updateLayout); - invalidate(); + recordNewContentSize(draw.mWidthHeight.x, + draw.mWidthHeight.y, updateLayout); + if (LOGV_ENABLED) { + Rect b = draw.mInvalRegion.getBounds(); + Log.v(LOGTAG, "NEW_PICTURE_MSG_ID {" + + b.left+","+b.top+","+b.right+","+b.bottom+"}"); + } + invalidate(contentToView(draw.mInvalRegion.getBounds())); if (mPictureListener != null) { mPictureListener.onNewPicture(WebView.this, capturePicture()); } @@ -4156,6 +4444,7 @@ public class WebView extends AbsoluteLayout } int initialScale = msg.arg1; int viewportWidth = msg.arg2; + // by default starting a new page with 100% zoom scale. float scale = 1.0f; if (mInitialScale > 0) { scale = mInitialScale / 100.0f; @@ -4165,10 +4454,15 @@ public class WebView extends AbsoluteLayout // to 0 mLastWidthSent = 0; } - // by default starting a new page with 100% zoom scale. - scale = initialScale == 0 ? (viewportWidth > 0 ? - ((float) width / viewportWidth) : 1.0f) - : initialScale / 100.0f; + if (initialScale == 0) { + // if viewportWidth is defined and it is smaller + // than the view width, zoom in to fill the view + if (viewportWidth > 0 && viewportWidth < width) { + scale = (float) width / viewportWidth; + } + } else { + scale = initialScale / 100.0f; + } } setNewZoomScale(scale, false); break; @@ -4213,10 +4507,32 @@ public class WebView extends AbsoluteLayout WebViewCore.resumeUpdate(mWebViewCore); break; - case LONG_PRESS_TRACKBALL: + case LONG_PRESS_ENTER: + // as this is shared by keydown and trackballdown, reset all + // the states + mGotEnterDown = false; mTrackTrackball = false; mTrackballDown = false; - performLongClick(); + // LONG_PRESS_ENTER is sent as a delayed message. If we + // switch to windows overview, the WebView will be + // temporarily removed from the view system. In that case, + // do nothing. + if (getParent() != null) { + performLongClick(); + } + break; + + case WEBCORE_NEED_TOUCH_EVENTS: + mForwardTouchEvents = (msg.arg1 != 0); + break; + + case PREVENT_TOUCH_ID: + // update may have already been paused by touch; restore since + // this effectively aborts touch and skips logic in touch up + if (mTouchMode == TOUCH_DRAG_MODE) { + WebViewCore.resumeUpdate(mWebViewCore); + } + mTouchMode = TOUCH_DONE_MODE; break; default: @@ -4514,10 +4830,7 @@ public class WebView extends AbsoluteLayout } Rect contentFocus = nativeGetFocusRingBounds(); if (contentFocus.isEmpty()) return keyHandled; - Rect viewFocus = new Rect(contentToView(contentFocus.left) - , contentToView(contentFocus.top) - , contentToView(contentFocus.right) - , contentToView(contentFocus.bottom)); + Rect viewFocus = contentToView(contentFocus); Rect visRect = new Rect(); calcOurVisibleRect(visRect); Rect outset = new Rect(visRect); @@ -4559,7 +4872,7 @@ public class WebView extends AbsoluteLayout public void debugDump() { nativeDebugDump(); - mWebViewCore.sendMessage(EventHub.DUMP_WEBKIT); + mWebViewCore.sendMessage(EventHub.DUMP_NAVTREE); } /** @@ -4583,6 +4896,7 @@ public class WebView extends AbsoluteLayout private native void nativeDrawFocusRing(Canvas content); private native void nativeDrawSelection(Canvas content , int x, int y, boolean extendSelection); + private native void nativeDrawSelectionRegion(Canvas content); private native boolean nativeUpdateFocusNode(); private native Rect nativeGetFocusRingBounds(); private native Rect nativeGetNavBounds(); @@ -4593,17 +4907,27 @@ public class WebView extends AbsoluteLayout boolean noScroll); private native void nativeNotifyFocusSet(boolean inEditingMode); private native void nativeRecomputeFocus(); - private native void nativeRecordButtons(); + // 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 pressed, + boolean invalidate); private native void nativeResetFocus(); private native void nativeResetNavClipBounds(); private native void nativeSelectBestAt(Rect rect); + private native void nativeSetFindIsDown(); private native void nativeSetFollowedLink(boolean followed); private native void nativeSetHeightCanMeasure(boolean measure); private native void nativeSetNavBounds(Rect rect); private native void nativeSetNavClipBounds(Rect rect); - private native boolean nativeHasSrcUrl(); - private native boolean nativeIsImage(int x, int y); + private native String nativeImageURI(int x, int y); + /** + * Returns true if the native focus nodes says it wants to handle key events + * (ala plugins). This can only be called if mNativeClass is non-zero! + */ + private native boolean nativeFocusNodeWantsKeyEvents(); private native void nativeMoveSelection(int x, int y , boolean extendSelection); private native Region nativeGetSelection(); + + private native void nativeDumpDisplayTree(String urlOrNull); } diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index 263346e..9e413f9 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -70,13 +70,6 @@ final class WebViewCore { // The BrowserFrame is an interface to the native Frame component. private BrowserFrame mBrowserFrame; - - /* This is a ring of pictures for content. After B is built, it is swapped - with A. - */ - private Picture mContentPictureA = new Picture(); // draw() - private Picture mContentPictureB = new Picture(); // nativeDraw() - /* * range is from 200 to 10,000. 0 is a special value means device-width. -1 * means undefined. @@ -196,7 +189,8 @@ final class WebViewCore { sWebCoreHandler.removeMessages(WebCoreThread.INITIALIZE, this); } - /* Get the BrowserFrame component. This is used for subwindow creation. */ + /* Get the BrowserFrame component. This is used for subwindow creation and + * is called only from BrowserFrame in the WebCore thread. */ /* package */ BrowserFrame getBrowserFrame() { return mBrowserFrame; } @@ -278,30 +272,40 @@ final class WebViewCore { static native String nativeFindAddress(String addr); /** - * Find and highlight an occurance of text matching find - * @param find The text to find. - * @param forward If true, search forward. Else, search backwards. - * @param fromSelection Whether to start from the current selection or from - * the beginning of the viewable page. - * @return boolean Whether the text was found. + * Empty the picture set. */ - private native boolean nativeFind(String find, - boolean forward, - boolean fromSelection); - + private native void nativeClearContent(); + + /** + * Create a flat picture from the set of pictures. + */ + private native void nativeCopyContentToPicture(Picture picture); + + /** + * Draw the picture set with a background color. Returns true + * if some individual picture took too long to draw and can be + * split into parts. Called from the UI thread. + */ + private native boolean nativeDrawContent(Canvas canvas, int color); + /** - * Find all occurances of text matching find and highlight them. - * @param find The text to find. - * @return int The number of occurances of find found. + * Redraw a portion of the picture set. The Point wh returns the + * width and height of the overall picture. */ - private native int nativeFindAll(String find); + private native boolean nativeRecordContent(Region invalRegion, Point wh); /** - * Clear highlights on text created by nativeFindAll. + * Splits slow parts of the picture set. Called from the webkit + * thread after nativeDrawContent returns true. */ - private native void nativeClearMatches(); + private native void nativeSplitContent(); + + // these must be kept lock-step with the KeyState enum in WebViewCore.h + static private final int KEY_ACTION_DOWN = 0; + static private final int KEY_ACTION_UP = 1; - private native void nativeDraw(Picture content); + private native boolean nativeSendKeyToFocusNode(int keyCode, int unichar, + int repeatCount, boolean isShift, boolean isAlt, int keyAction); private native boolean nativeKeyUp(int keycode, int keyvalue); @@ -343,21 +347,12 @@ final class WebViewCore { private native String nativeRetrieveHref(int framePtr, int nodePtr); - /** - * Return the url of the image located at (x,y) in content coordinates, or - * null if there is no image at that point. - * - * @param x x content ordinate - * @param y y content ordinate - * @return String url of the image located at (x,y), or null if there is - * no image there. - */ - private native String nativeRetrieveImageRef(int x, int y); - private native void nativeTouchUp(int touchGeneration, int buildGeneration, int framePtr, int nodePtr, int x, int y, int size, boolean isClick, boolean retry); + private native boolean nativeHandleTouchEvent(int action, int x, int y); + private native void nativeUnblockFocus(); private native void nativeUpdateFrameCache(); @@ -368,7 +363,11 @@ final class WebViewCore { private native void nativeSetBackgroundColor(int color); - private native void nativeDump(); + private native void nativeDumpDomTree(boolean useFile); + + private native void nativeDumpRenderTree(boolean useFile); + + private native void nativeDumpNavTree(); private native void nativeRefreshPlugins(boolean reloadOpenPages); @@ -393,6 +392,10 @@ final class WebViewCore { private native String nativeGetSelection(Region sel); + // Register a scheme to be treated as local scheme so that it can access + // local asset files for resources + private native void nativeRegisterURLSchemeAsLocal(String scheme); + // EventHub for processing messages private final EventHub mEventHub; // WebCore thread handler @@ -421,10 +424,7 @@ final class WebViewCore { switch (msg.what) { case INITIALIZE: WebViewCore core = (WebViewCore) msg.obj; - synchronized (core) { - core.initialize(); - core.notify(); - } + core.initialize(); break; case REDUCE_PRIORITY: @@ -501,6 +501,12 @@ final class WebViewCore { boolean mRetry; } + static class TouchEventData { + int mAction; // MotionEvent.getAction() + int mX; + int mY; + } + class EventHub { // Message Ids static final int LOAD_URL = 100; @@ -510,7 +516,7 @@ final class WebViewCore { static final int KEY_UP = 104; static final int VIEW_SIZE_CHANGED = 105; static final int GO_BACK_FORWARD = 106; - static final int SET_VISIBLE_RECT = 107; + static final int SET_SCROLL_OFFSET = 107; static final int RESTORE_STATE = 108; static final int PAUSE_TIMERS = 109; static final int RESUME_TIMERS = 110; @@ -519,16 +525,13 @@ final class WebViewCore { static final int SET_SELECTION = 113; static final int REPLACE_TEXT = 114; static final int PASS_TO_JS = 115; - static final int FIND = 116; + static final int SET_GLOBAL_BOUNDS = 116; static final int UPDATE_CACHE_AND_TEXT_ENTRY = 117; - static final int FIND_ALL = 118; - static final int CLEAR_MATCHES = 119; static final int DOC_HAS_IMAGES = 120; static final int SET_SNAP_ANCHOR = 121; static final int DELETE_SELECTION = 122; static final int LISTBOX_CHOICES = 123; static final int SINGLE_LISTBOX_CHOICE = 124; - static final int DUMP_WEBKIT = 125; static final int SET_BACKGROUND_COLOR = 126; static final int UNBLOCK_FOCUS = 127; static final int SAVE_DOCUMENT_STATE = 128; @@ -536,9 +539,10 @@ final class WebViewCore { static final int WEBKIT_DRAW = 130; static final int SYNC_SCROLL = 131; static final int REFRESH_PLUGINS = 132; - + static final int SPLIT_PICTURE_SET = 133; + static final int CLEAR_CONTENT = 134; + // UI nav messages - static final int REQUEST_IMAGE_HREF = 134; static final int SET_FINAL_FOCUS = 135; static final int SET_KIT_FOCUS = 136; static final int REQUEST_FOCUS_HREF = 137; @@ -547,6 +551,8 @@ final class WebViewCore { // motion static final int TOUCH_UP = 140; + // message used to pass UI touch events to WebCore + static final int TOUCH_EVENT = 141; // Network-based messaging static final int CLEAR_SSL_PREF_TABLE = 150; @@ -554,6 +560,12 @@ final class WebViewCore { // Test harness messages static final int REQUEST_EXT_REPRESENTATION = 160; static final int REQUEST_DOC_AS_TEXT = 161; + + // debugging + static final int DUMP_DOMTREE = 170; + static final int DUMP_RENDERTREE = 171; + static final int DUMP_NAVTREE = 172; + // private message ids private static final int DESTROY = 200; @@ -561,6 +573,19 @@ final class WebViewCore { static final int NO_FOCUS_CHANGE_BLOCK = 0; static final int BLOCK_FOCUS_CHANGE_UNTIL_KEY_UP = 1; + /* The KEY_DOWN and KEY_UP messages pass the keyCode in arg1, and a + "type" in arg2. These are the types, and they describe what the + circumstances were that prompted the UI thread to send the keyevent + to webkit. + + FOCUS_NODE - the currently focused node says it wants key events + (e.g. plugins) + UNHANDLED - the UI side did not handle the key, so we give webkit + a shot at it. + */ + static final int KEYEVENT_FOCUS_NODE_TYPE = 0; + static final int KEYEVENT_UNHANDLED_TYPE = 1; + // Private handler for WebCore messages. private Handler mHandler; // Message queue for containing messages before the WebCore thread is @@ -607,8 +632,30 @@ final class WebViewCore { case LOAD_DATA: HashMap loadParams = (HashMap) msg.obj; - mBrowserFrame.loadData( - (String) loadParams.get("baseUrl"), + String baseUrl = (String) loadParams.get("baseUrl"); + if (baseUrl != null) { + int i = baseUrl.indexOf(':'); + if (i > 0) { + /* + * In 1.0, {@link + * WebView#loadDataWithBaseURL} can access + * local asset files as long as the data is + * valid. In the new WebKit, the restriction + * is tightened. To be compatible with 1.0, + * we automatically add the scheme of the + * baseUrl for local access as long as it is + * not http(s)/ftp(s)/about/javascript + */ + String scheme = baseUrl.substring(0, i); + if (!scheme.startsWith("http") && + !scheme.startsWith("ftp") && + !scheme.startsWith("about") && + !scheme.startsWith("javascript")) { + nativeRegisterURLSchemeAsLocal(scheme); + } + } + } + mBrowserFrame.loadData(baseUrl, (String) loadParams.get("data"), (String) loadParams.get("mimeType"), (String) loadParams.get("encoding"), @@ -622,8 +669,7 @@ final class WebViewCore { // up with native side if (mBrowserFrame.committed() && !mBrowserFrame.firstLayoutDone()) { - mBrowserFrame.didFirstLayout(mBrowserFrame - .currentUrl()); + mBrowserFrame.didFirstLayout(); } // Do this after syncing up the layout state. stopLoading(); @@ -634,11 +680,11 @@ final class WebViewCore { break; case KEY_DOWN: - keyDown(msg.arg1, (KeyEvent) msg.obj); + keyDown(msg.arg1, msg.arg2, (KeyEvent) msg.obj); break; case KEY_UP: - keyUp(msg.arg1, (KeyEvent) msg.obj); + keyUp(msg.arg1, msg.arg2, (KeyEvent) msg.obj); break; case VIEW_SIZE_CHANGED: @@ -646,12 +692,16 @@ final class WebViewCore { ((Float) msg.obj).floatValue()); break; - case SET_VISIBLE_RECT: - Rect r = (Rect) msg.obj; + case SET_SCROLL_OFFSET: // note: these are in document coordinates // (inv-zoom) - nativeSetVisibleRect(r.left, r.top, r.width(), - r.height()); + nativeSetScrollOffset(msg.arg1, msg.arg2); + break; + + case SET_GLOBAL_BOUNDS: + Rect r = (Rect) msg.obj; + nativeSetGlobalBounds(r.left, r.top, r.width(), + r.height()); break; case GO_BACK_FORWARD: @@ -745,30 +795,6 @@ final class WebViewCore { break; } - case FIND: - /* arg1: - * 1 - Find next - * -1 - Find previous - * 0 - Find first - */ - Message response = (Message) msg.obj; - boolean find = nativeFind(msg.getData().getString("find"), - msg.arg1 != -1, msg.arg1 != 0); - response.arg1 = find ? 1 : 0; - response.sendToTarget(); - break; - - case FIND_ALL: - int found = nativeFindAll(msg.getData().getString("find")); - Message resAll = (Message) msg.obj; - resAll.arg1 = found; - resAll.sendToTarget(); - break; - - case CLEAR_MATCHES: - nativeClearMatches(); - break; - case CLEAR_SSL_PREF_TABLE: Network.getInstance(mContext) .clearUserSslPrefTable(); @@ -784,6 +810,17 @@ final class WebViewCore { touchUpData.mRetry); break; + case TOUCH_EVENT: { + TouchEventData ted = (TouchEventData) msg.obj; + if (nativeHandleTouchEvent(ted.mAction, ted.mX, + ted.mY)) { + Message.obtain(mWebView.mPrivateHandler, + WebView.PREVENT_TOUCH_ID) + .sendToTarget(); + } + break; + } + case ADD_JS_INTERFACE: HashMap map = (HashMap) msg.obj; Object obj = map.get("object"); @@ -826,27 +863,17 @@ final class WebViewCore { case REQUEST_FOCUS_HREF: { Message hrefMsg = (Message) msg.obj; String res = nativeRetrieveHref(msg.arg1, msg.arg2); - Bundle data = hrefMsg.getData(); - data.putString("url", res); - hrefMsg.setData(data); + hrefMsg.getData().putString("url", res); hrefMsg.sendToTarget(); break; } - case REQUEST_IMAGE_HREF: { - Message refMsg = (Message) msg.obj; - String ref = - nativeRetrieveImageRef(msg.arg1, msg.arg2); - Bundle data = refMsg.getData(); - data.putString("url", ref); - refMsg.setData(data); - refMsg.sendToTarget(); - break; - } - case UPDATE_CACHE_AND_TEXT_ENTRY: nativeUpdateFrameCache(); - sendViewInvalidate(); + // FIXME: this should provide a minimal rectangle + if (mWebView != null) { + mWebView.postInvalidate(); + } sendUpdateTextEntry(); break; @@ -901,9 +928,17 @@ final class WebViewCore { , WebView.UPDATE_CLIPBOARD, str) .sendToTarget(); break; - - case DUMP_WEBKIT: - nativeDump(); + + case DUMP_DOMTREE: + nativeDumpDomTree(msg.arg1 == 1); + break; + + case DUMP_RENDERTREE: + nativeDumpRenderTree(msg.arg1 == 1); + break; + + case DUMP_NAVTREE: + nativeDumpNavTree(); break; case SYNC_SCROLL: @@ -914,6 +949,18 @@ final class WebViewCore { case REFRESH_PLUGINS: nativeRefreshPlugins(msg.arg1 != 0); break; + + case SPLIT_PICTURE_SET: + nativeSplitContent(); + mSplitPictureIsScheduled = false; + break; + + case CLEAR_CONTENT: + // Clear the view so that onDraw() will draw nothing + // but white background + // (See public method WebView.clearView) + nativeClearContent(); + break; } } }; @@ -982,6 +1029,7 @@ final class WebViewCore { private synchronized void removeMessages() { // reset mDrawIsScheduled flag as WEBKIT_DRAW may be removed mDrawIsScheduled = false; + mSplitPictureIsScheduled = false; if (mMessages != null) { mMessages.clear(); } else { @@ -1001,7 +1049,7 @@ final class WebViewCore { // Methods called by host activity (in the same thread) //------------------------------------------------------------------------- - public void stopLoading() { + void stopLoading() { if (LOGV_ENABLED) Log.v(LOGTAG, "CORE stopLoading"); if (mBrowserFrame != null) { mBrowserFrame.stopLoading(); @@ -1082,23 +1130,48 @@ final class WebViewCore { mBrowserFrame.loadUrl(url); } - private void keyDown(int code, KeyEvent event) { + private void keyDown(int code, int target, KeyEvent event) { if (LOGV_ENABLED) { Log.v(LOGTAG, "CORE keyDown at " + System.currentTimeMillis() + ", " + event); } + switch (target) { + case EventHub.KEYEVENT_UNHANDLED_TYPE: + break; + case EventHub.KEYEVENT_FOCUS_NODE_TYPE: + if (nativeSendKeyToFocusNode(code, event.getUnicodeChar(), + event.getRepeatCount(), + event.isShiftPressed(), + event.isAltPressed(), + KEY_ACTION_DOWN)) { + return; + } + break; + } + // If we get here, no one handled it, so call our proxy mCallbackProxy.onUnhandledKeyEvent(event); } - private void keyUp(int code, KeyEvent event) { + private void keyUp(int code, int target, KeyEvent event) { if (LOGV_ENABLED) { Log.v(LOGTAG, "CORE keyUp at " + System.currentTimeMillis() + ", " + event); } - if (!nativeKeyUp(code, event.getUnicodeChar())) { - mCallbackProxy.onUnhandledKeyEvent(event); + switch (target) { + case EventHub.KEYEVENT_UNHANDLED_TYPE: + if (!nativeKeyUp(code, event.getUnicodeChar())) { + mCallbackProxy.onUnhandledKeyEvent(event); + } + break; + case EventHub.KEYEVENT_FOCUS_NODE_TYPE: + nativeSendKeyToFocusNode(code, event.getUnicodeChar(), + event.getRepeatCount(), + event.isShiftPressed(), + event.isAltPressed(), + KEY_ACTION_UP); + break; + } } - } // These values are used to avoid requesting a layout based on old values private int mCurrentViewWidth = 0; @@ -1140,8 +1213,9 @@ final class WebViewCore { mCurrentViewHeight = h; if (needInvalidate) { // ensure {@link #webkitDraw} is called as we were blocking in - // {@link #contentInvalidate} when mCurrentViewWidth is 0 - contentInvalidate(); + // {@link #contentDraw} when mCurrentViewWidth is 0 + if (LOGV_ENABLED) Log.v(LOGTAG, "viewSizeChanged"); + contentDraw(); } mEventHub.sendMessage(Message.obtain(null, EventHub.UPDATE_CACHE_AND_TEXT_ENTRY)); @@ -1156,30 +1230,42 @@ final class WebViewCore { // Used to avoid posting more than one draw message. private boolean mDrawIsScheduled; + + // Used to avoid posting more than one split picture message. + private boolean mSplitPictureIsScheduled; + + // Used to suspend drawing. + private boolean mDrawIsPaused; // Used to end scale+scroll mode, accessed by both threads boolean mEndScaleZoom = false; + public class DrawData { + public DrawData() { + mInvalRegion = new Region(); + mWidthHeight = new Point(); + } + public Region mInvalRegion; + public Point mViewPoint; + public Point mWidthHeight; + } + private void webkitDraw() { mDrawIsScheduled = false; - nativeDraw(mContentPictureB); - int w; - int h; - synchronized (this) { - Picture temp = mContentPictureB; - mContentPictureB = mContentPictureA; - mContentPictureA = temp; - w = mContentPictureA.getWidth(); - h = mContentPictureA.getHeight(); + DrawData draw = new DrawData(); + if (LOGV_ENABLED) Log.v(LOGTAG, "webkitDraw start"); + if (nativeRecordContent(draw.mInvalRegion, draw.mWidthHeight) + == false) { + if (LOGV_ENABLED) Log.v(LOGTAG, "webkitDraw abort"); + return; } - if (mWebView != null) { // Send the native view size that was used during the most recent // layout. + draw.mViewPoint = new Point(mCurrentViewWidth, mCurrentViewHeight); + if (LOGV_ENABLED) Log.v(LOGTAG, "webkitDraw NEW_PICTURE_MSG_ID"); Message.obtain(mWebView.mPrivateHandler, - WebView.NEW_PICTURE_MSG_ID, w, h, - new Point(mCurrentViewWidth, mCurrentViewHeight)) - .sendToTarget(); + WebView.NEW_PICTURE_MSG_ID, draw).sendToTarget(); if (mWebkitScrollX != 0 || mWebkitScrollY != 0) { // as we have the new picture, try to sync the scroll position Message.obtain(mWebView.mPrivateHandler, @@ -1217,37 +1303,18 @@ final class WebViewCore { df = mScrollFilter; } canvas.setDrawFilter(df); - synchronized (this) { - Picture picture = mContentPictureA; - int sc = canvas.save(Canvas.CLIP_SAVE_FLAG); - Rect clip = new Rect(0, 0, picture.getWidth(), picture.getHeight()); - canvas.clipRect(clip, Region.Op.DIFFERENCE); - canvas.drawColor(color); - canvas.restoreToCount(sc); - // experiment commented out - // if (TEST_BUCKET) { - // nativeDrawContentPicture(canvas); - // } else { - canvas.drawPicture(picture); - // } - } + boolean tookTooLong = nativeDrawContent(canvas, color); canvas.setDrawFilter(null); - } - - /* package */ void clearContentPicture() { - // experiment commented out - // if (TEST_BUCKET) { - // nativeClearContentPicture(); - // } - synchronized (this) { - mContentPictureA = new Picture(); + if (tookTooLong && mSplitPictureIsScheduled == false) { + mSplitPictureIsScheduled = true; + sendMessage(EventHub.SPLIT_PICTURE_SET); } } /*package*/ Picture copyContentPicture() { - synchronized (this) { - return new Picture(mContentPictureA); - } + Picture result = new Picture(); + nativeCopyContentToPicture(result); + return result; } static void pauseUpdate(WebViewCore core) { @@ -1263,7 +1330,7 @@ final class WebViewCore { // webcore thread priority is still lowered. if (core != null) { synchronized (core) { - core.mDrawIsScheduled = true; + core.mDrawIsPaused = true; core.mEventHub.removeMessages(EventHub.WEBKIT_DRAW); } } @@ -1278,7 +1345,9 @@ final class WebViewCore { if (core != null) { synchronized (core) { core.mDrawIsScheduled = false; - core.contentInvalidate(); + core.mDrawIsPaused = false; + if (LOGV_ENABLED) Log.v(LOGTAG, "resumeUpdate"); + core.contentDraw(); } } } @@ -1309,7 +1378,7 @@ final class WebViewCore { //------------------------------------------------------------------------- // called from JNI or WebView thread - /* package */ void contentInvalidate() { + /* package */ void contentDraw() { // don't update the Picture until we have an initial width and finish // the first layout if (mCurrentViewWidth == 0 || !mBrowserFrame.firstLayoutDone()) { @@ -1317,14 +1386,14 @@ final class WebViewCore { } // only fire an event if this is our first request synchronized (this) { - if (mDrawIsScheduled) { + if (mDrawIsPaused || mDrawIsScheduled) { return; } mDrawIsScheduled = true; mEventHub.sendMessage(Message.obtain(null, EventHub.WEBKIT_DRAW)); } } - + // called by JNI private void contentScrollBy(int dx, int dy) { if (!mBrowserFrame.firstLayoutDone()) { @@ -1397,6 +1466,7 @@ final class WebViewCore { sWebCoreHandler.removeMessages(WebCoreThread.CACHE_TICKER); sWebCoreHandler.sendMessage(sWebCoreHandler .obtainMessage(WebCoreThread.CACHE_TICKER)); + contentDraw(); } // called by JNI @@ -1408,9 +1478,9 @@ final class WebViewCore { } // called by JNI - private void sendViewInvalidate() { + private void sendViewInvalidate(int left, int top, int right, int bottom) { if (mWebView != null) { - mWebView.postInvalidate(); + mWebView.postInvalidate(left, top, right, bottom); } } @@ -1421,7 +1491,7 @@ final class WebViewCore { private native void setViewportSettingsFromNative(); // called by JNI - private void didFirstLayout(String url) { + private void didFirstLayout() { // Trick to ensure that the Picture has the exact height for the content // by forcing to layout with 0 height after the page is ready, which is // indicated by didFirstLayout. This is essential to get rid of the @@ -1435,7 +1505,7 @@ final class WebViewCore { mWebView.mLastHeightSent, -1.0f)); } - mBrowserFrame.didFirstLayout(url); + mBrowserFrame.didFirstLayout(); // reset the scroll position as it is a new page now mWebkitScrollX = mWebkitScrollY = 0; @@ -1517,7 +1587,16 @@ final class WebViewCore { mRestoredScale = scale; } } - + + // called by JNI + private void needTouchEvents(boolean need) { + if (mWebView != null) { + Message.obtain(mWebView.mPrivateHandler, + WebView.WEBCORE_NEED_TOUCH_EVENTS, need ? 1 : 0, 0) + .sendToTarget(); + } + } + // called by JNI private void updateTextfield(int ptr, boolean changeToPassword, String text, int textGeneration) { @@ -1530,9 +1609,10 @@ final class WebViewCore { } } - // these must be in document space (i.e. not scaled/zoomed. - private native void nativeSetVisibleRect(int x, int y, int width, - int height); + // these must be in document space (i.e. not scaled/zoomed). + private native void nativeSetScrollOffset(int dx, int dy); + + private native void nativeSetGlobalBounds(int x, int y, int w, int h); // called by JNI private void requestListBox(String[] array, boolean[] enabledArray, diff --git a/core/java/android/webkit/WebViewDatabase.java b/core/java/android/webkit/WebViewDatabase.java index b367e27..96f3698 100644 --- a/core/java/android/webkit/WebViewDatabase.java +++ b/core/java/android/webkit/WebViewDatabase.java @@ -39,7 +39,7 @@ public class WebViewDatabase { // log tag protected static final String LOGTAG = "webviewdatabase"; - private static final int DATABASE_VERSION = 8; + private static final int DATABASE_VERSION = 9; // 2 -> 3 Modified Cache table to allow cache of redirects // 3 -> 4 Added Oma-Downloads table // 4 -> 5 Modified Cache table to support persistent contentLength @@ -47,6 +47,7 @@ public class WebViewDatabase { // 5 -> 6 Add INDEX for cache table // 6 -> 7 Change cache localPath from int to String // 7 -> 8 Move cache to its own db + // 8 -> 9 Store both scheme and host when storing passwords private static final int CACHE_DATABASE_VERSION = 1; private static WebViewDatabase mInstance = null; @@ -172,7 +173,6 @@ public class WebViewDatabase { mDatabase.beginTransaction(); try { upgradeDatabase(); - bootstrapDatabase(); mDatabase.setTransactionSuccessful(); } finally { mDatabase.endTransaction(); @@ -200,6 +200,10 @@ public class WebViewDatabase { } finally { mCacheDatabase.endTransaction(); } + // Erase the files from the file system in the + // case that the database was updated and the + // there were existing cache content + CacheManager.removeAllCacheFiles(); } if (mCacheDatabase != null) { @@ -237,24 +241,26 @@ public class WebViewDatabase { if (oldVersion != 0) { Log.i(LOGTAG, "Upgrading database from version " + oldVersion + " to " - + DATABASE_VERSION + ", which will destroy all old data"); + + DATABASE_VERSION + ", which will destroy old data"); + } + boolean justPasswords = 8 == oldVersion && 9 == DATABASE_VERSION; + if (!justPasswords) { + mDatabase.execSQL("DROP TABLE IF EXISTS " + + mTableNames[TABLE_COOKIES_ID]); + mDatabase.execSQL("DROP TABLE IF EXISTS cache"); + mDatabase.execSQL("DROP TABLE IF EXISTS " + + mTableNames[TABLE_FORMURL_ID]); + mDatabase.execSQL("DROP TABLE IF EXISTS " + + mTableNames[TABLE_FORMDATA_ID]); + mDatabase.execSQL("DROP TABLE IF EXISTS " + + mTableNames[TABLE_HTTPAUTH_ID]); } mDatabase.execSQL("DROP TABLE IF EXISTS " - + mTableNames[TABLE_COOKIES_ID]); - mDatabase.execSQL("DROP TABLE IF EXISTS cache"); - mDatabase.execSQL("DROP TABLE IF EXISTS " + mTableNames[TABLE_PASSWORD_ID]); - mDatabase.execSQL("DROP TABLE IF EXISTS " - + mTableNames[TABLE_FORMURL_ID]); - mDatabase.execSQL("DROP TABLE IF EXISTS " - + mTableNames[TABLE_FORMDATA_ID]); - mDatabase.execSQL("DROP TABLE IF EXISTS " - + mTableNames[TABLE_HTTPAUTH_ID]); + mDatabase.setVersion(DATABASE_VERSION); - } - private static void bootstrapDatabase() { - if (mDatabase != null) { + if (!justPasswords) { // cookies mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_COOKIES_ID] + " (" + ID_COL + " INTEGER PRIMARY KEY, " @@ -265,14 +271,6 @@ public class WebViewDatabase { mDatabase.execSQL("CREATE INDEX cookiesIndex ON " + mTableNames[TABLE_COOKIES_ID] + " (path)"); - // password - mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_PASSWORD_ID] - + " (" + ID_COL + " INTEGER PRIMARY KEY, " - + PASSWORD_HOST_COL + " TEXT, " + PASSWORD_USERNAME_COL - + " TEXT, " + PASSWORD_PASSWORD_COL + " TEXT," + " UNIQUE (" - + PASSWORD_HOST_COL + ", " + PASSWORD_USERNAME_COL - + ") ON CONFLICT REPLACE);"); - // formurl mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_FORMURL_ID] + " (" + ID_COL + " INTEGER PRIMARY KEY, " + FORMURL_URL_COL @@ -295,6 +293,13 @@ public class WebViewDatabase { + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL + ", " + HTTPAUTH_USERNAME_COL + ") ON CONFLICT REPLACE);"); } + // passwords + mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_PASSWORD_ID] + + " (" + ID_COL + " INTEGER PRIMARY KEY, " + + PASSWORD_HOST_COL + " TEXT, " + PASSWORD_USERNAME_COL + + " TEXT, " + PASSWORD_PASSWORD_COL + " TEXT," + " UNIQUE (" + + PASSWORD_HOST_COL + ", " + PASSWORD_USERNAME_COL + + ") ON CONFLICT REPLACE);"); } private static void upgradeCacheDatabase() { @@ -639,10 +644,11 @@ public class WebViewDatabase { if (cursor.moveToFirst()) { int batchSize = 100; StringBuilder pathStr = new StringBuilder(20 + 16 * batchSize); - pathStr.append("DELETE FROM cache WHERE filepath = ?"); + pathStr.append("DELETE FROM cache WHERE filepath IN (?"); for (int i = 1; i < batchSize; i++) { - pathStr.append(" OR filepath = ?"); + pathStr.append(", ?"); } + pathStr.append(")"); SQLiteStatement statement = mCacheDatabase.compileStatement(pathStr .toString()); // as bindString() uses 1-based index, initialize index to 1 @@ -658,6 +664,7 @@ public class WebViewDatabase { pathList.add(filePath); if (index++ == batchSize) { statement.execute(); + statement.clearBindings(); index = 1; } } while (cursor.moveToNext() && amount > 0); @@ -679,19 +686,20 @@ public class WebViewDatabase { /** * Set password. Tuple (PASSWORD_HOST_COL, PASSWORD_USERNAME_COL) is unique. * - * @param host The host for the password + * @param schemePlusHost The scheme and host for the password * @param username The username for the password. If it is null, it means * password can't be saved. * @param password The password */ - void setUsernamePassword(String host, String username, String password) { - if (host == null || mDatabase == null) { + void setUsernamePassword(String schemePlusHost, String username, + String password) { + if (schemePlusHost == null || mDatabase == null) { return; } synchronized (mPasswordLock) { final ContentValues c = new ContentValues(); - c.put(PASSWORD_HOST_COL, host); + c.put(PASSWORD_HOST_COL, schemePlusHost); c.put(PASSWORD_USERNAME_COL, username); c.put(PASSWORD_PASSWORD_COL, password); mDatabase.insert(mTableNames[TABLE_PASSWORD_ID], PASSWORD_HOST_COL, @@ -702,12 +710,12 @@ public class WebViewDatabase { /** * Retrieve the username and password for a given host * - * @param host The host which passwords applies to + * @param schemePlusHost The scheme and host which passwords applies to * @return String[] if found, String[0] is username, which can be null and * String[1] is password. Return null if it can't find anything. */ - String[] getUsernamePassword(String host) { - if (host == null || mDatabase == null) { + String[] getUsernamePassword(String schemePlusHost) { + if (schemePlusHost == null || mDatabase == null) { return null; } @@ -718,8 +726,8 @@ public class WebViewDatabase { synchronized (mPasswordLock) { String[] ret = null; Cursor cursor = mDatabase.query(mTableNames[TABLE_PASSWORD_ID], - columns, selection, new String[] { host }, null, null, - null); + columns, selection, new String[] { schemePlusHost }, null, + null, null); if (cursor.moveToFirst()) { ret = new String[2]; ret[0] = cursor.getString( diff --git a/core/java/android/webkit/gears/AndroidWifiDataProvider.java b/core/java/android/webkit/gears/AndroidWifiDataProvider.java new file mode 100644 index 0000000..7379f59 --- /dev/null +++ b/core/java/android/webkit/gears/AndroidWifiDataProvider.java @@ -0,0 +1,136 @@ +// Copyright 2008, Google Inc. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of Google Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package android.webkit.gears; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.os.Looper; +import android.util.Config; +import android.util.Log; +import android.webkit.WebView; +import java.util.List; + +/** + * WiFi data provider implementation for Android. + * {@hide} + */ +public final class AndroidWifiDataProvider extends BroadcastReceiver { + /** + * Logging tag + */ + private static final String TAG = "Gears-J-WifiProvider"; + /** + * Our Wifi manager instance. + */ + private WifiManager mWifiManager; + /** + * The native object ID. + */ + private long mNativeObject; + /** + * The Context instance. + */ + private Context mContext; + + /** + * Constructs a instance of this class and registers for wifi scan + * updates. Note that this constructor must be called on a Looper + * thread. Suitable threads can be created on the native side using + * the AndroidLooperThread C++ class. + */ + public AndroidWifiDataProvider(WebView webview, long object) { + mNativeObject = object; + mContext = webview.getContext(); + mWifiManager = + (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + if (mWifiManager == null) { + Log.e(TAG, + "AndroidWifiDataProvider: could not get location manager."); + throw new NullPointerException( + "AndroidWifiDataProvider: locationManager is null."); + } + + // Create a Handler that identifies the message loop associated + // with the current thread. Note that it is not necessary to + // override handleMessage() at all since the Intent + // ReceiverDispatcher (see the ActivityThread class) only uses + // this handler to post a Runnable to this thread's loop. + Handler handler = new Handler(Looper.myLooper()); + + IntentFilter filter = new IntentFilter(); + filter.addAction(mWifiManager.SCAN_RESULTS_AVAILABLE_ACTION); + mContext.registerReceiver(this, filter, null, handler); + + // Get the last scan results and pass them to the native side. + // We can't just invoke the callback here, so we queue a message + // to this thread's loop. + handler.post(new Runnable() { + public void run() { + onUpdateAvailable(mWifiManager.getScanResults(), mNativeObject); + } + }); + } + + /** + * Called when the provider is no longer needed. + */ + public void shutdown() { + mContext.unregisterReceiver(this); + if (Config.LOGV) { + Log.v(TAG, "Wifi provider closed."); + } + } + + /** + * This method is called when the AndroidWifiDataProvider is receiving an + * Intent broadcast. + * @param context The Context in which the receiver is running. + * @param intent The Intent being received. + */ + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals( + mWifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { + if (Config.LOGV) { + Log.v(TAG, "Wifi scan resulst available"); + } + onUpdateAvailable(mWifiManager.getScanResults(), mNativeObject); + } + } + + /** + * The native method called when new wifi data is available. + * @param scanResults is a list of ScanResults to pass to the native side. + * @param nativeObject is a pointer to the corresponding + * AndroidWifiDataProvider C++ instance. + */ + private static native void onUpdateAvailable( + List<ScanResult> scanResults, long nativeObject); +} diff --git a/core/java/android/webkit/gears/DesktopAndroid.java b/core/java/android/webkit/gears/DesktopAndroid.java index 00a9a47..ee8ca49 100644 --- a/core/java/android/webkit/gears/DesktopAndroid.java +++ b/core/java/android/webkit/gears/DesktopAndroid.java @@ -40,8 +40,6 @@ import android.webkit.WebView; public class DesktopAndroid { private static final String TAG = "Gears-J-Desktop"; - private static final String BROWSER = "com.android.browser"; - private static final String BROWSER_ACTIVITY = BROWSER + ".BrowserActivity"; private static final String EXTRA_SHORTCUT_DUPLICATE = "duplicate"; private static final String ACTION_INSTALL_SHORTCUT = "com.android.launcher.action.INSTALL_SHORTCUT"; @@ -78,11 +76,9 @@ public class DesktopAndroid { String url, String imagePath) { Context context = webview.getContext(); - ComponentName browser = new ComponentName(BROWSER, BROWSER_ACTIVITY); - Intent viewWebPage = new Intent(Intent.ACTION_VIEW); - viewWebPage.setComponent(browser); viewWebPage.setData(Uri.parse(url)); + viewWebPage.addCategory(Intent.CATEGORY_BROWSABLE); Intent intent = new Intent(ACTION_INSTALL_SHORTCUT); intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, viewWebPage); diff --git a/core/java/android/webkit/gears/HttpRequestAndroid.java b/core/java/android/webkit/gears/HttpRequestAndroid.java index 8668c54..30f855f 100644 --- a/core/java/android/webkit/gears/HttpRequestAndroid.java +++ b/core/java/android/webkit/gears/HttpRequestAndroid.java @@ -163,7 +163,20 @@ public final class HttpRequestAndroid { // Setup the connection. This doesn't go to the wire yet - it // doesn't block. try { - connection = (HttpURLConnection) new URL(url).openConnection(); + URL url_object = new URL(url); + // Check that the protocol is indeed HTTP(S). + String protocol = url_object.getProtocol(); + if (protocol == null) { + log("null protocol for URL " + url); + return false; + } + protocol = protocol.toLowerCase(); + if (!"http".equals(protocol) && !"https".equals(protocol)) { + log("Url has wrong protocol: " + url); + return false; + } + + connection = (HttpURLConnection) url_object.openConnection(); connection.setRequestMethod(method); // Manually follow redirects. connection.setInstanceFollowRedirects(false); @@ -197,11 +210,13 @@ public final class HttpRequestAndroid { log("interrupt() called but no child thread"); return; } - if (inBlockingOperation) { - log("Interrupting blocking operation"); - childThread.interrupt(); - } else { - log("Nothing to interrupt"); + synchronized (this) { + if (inBlockingOperation) { + log("Interrupting blocking operation"); + childThread.interrupt(); + } else { + log("Nothing to interrupt"); + } } } @@ -472,7 +487,7 @@ public final class HttpRequestAndroid { String encoding = cacheResult.getEncoding(); // Encoding may not be specified. No default. String contentType = mimeType; - if (encoding != null) { + if (encoding != null && encoding.length() > 0) { contentType += "; charset=" + encoding; } setResponseHeader(KEY_CONTENT_TYPE, contentType); diff --git a/core/java/android/webkit/gears/NativeDialog.java b/core/java/android/webkit/gears/NativeDialog.java new file mode 100644 index 0000000..9e2b375 --- /dev/null +++ b/core/java/android/webkit/gears/NativeDialog.java @@ -0,0 +1,142 @@ +// Copyright 2008 The Android Open Source Project +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of Google Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package android.webkit.gears; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import java.io.File; +import java.lang.InterruptedException; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Utility class to call a modal native dialog on Android + * The dialog itself is an Activity defined in the Browser. + * @hide + */ +public class NativeDialog { + + private static final String TAG = "Gears-J-NativeDialog"; + + private final String DIALOG_PACKAGE = "com.android.browser"; + private final String DIALOG_CLASS = DIALOG_PACKAGE + ".GearsNativeDialog"; + + private static Lock mLock = new ReentrantLock(); + private static Condition mDialogFinished = mLock.newCondition(); + private static String mResults = null; + + private static boolean mAsynchronousDialog; + + /** + * Utility function to build the intent calling the + * dialog activity + */ + private Intent createIntent(String type, String arguments) { + Intent intent = new Intent(); + intent.setClassName(DIALOG_PACKAGE, DIALOG_CLASS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra("dialogArguments", arguments); + intent.putExtra("dialogType", type); + return intent; + } + + /** + * Opens a native dialog synchronously and waits for its completion. + * + * The dialog is an activity (GearsNativeDialog) provided by the Browser + * that we call via startActivity(). Contrary to a normal activity though, + * we need to block until it returns. To do so, we define a static lock + * object in this class, which GearsNativeDialog can unlock once done + */ + public String showDialog(Context context, String file, + String arguments) { + + try { + mAsynchronousDialog = false; + mLock.lock(); + File path = new File(file); + String fileName = path.getName(); + String type = fileName.substring(0, fileName.indexOf(".html")); + Intent intent = createIntent(type, arguments); + + mResults = null; + context.startActivity(intent); + mDialogFinished.await(); + } catch (InterruptedException e) { + Log.e(TAG, "exception e: " + e); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "exception e: " + e); + } finally { + mLock.unlock(); + } + + return mResults; + } + + /** + * Opens a native dialog asynchronously + * + * The dialog is an activity (GearsNativeDialog) provided by the + * Browser. + */ + public void showAsyncDialog(Context context, String type, + String arguments) { + mAsynchronousDialog = true; + Intent intent = createIntent(type, arguments); + context.startActivity(intent); + } + + /** + * Static method that GearsNativeDialog calls to unlock us + */ + public static void signalFinishedDialog() { + if (!mAsynchronousDialog) { + mLock.lock(); + mDialogFinished.signal(); + mLock.unlock(); + } else { + // we call the native callback + closeAsynchronousDialog(mResults); + } + } + + /** + * Static method that GearsNativeDialog calls to set the + * dialog's result + */ + public static void closeDialog(String res) { + mResults = res; + } + + /** + * Native callback method + */ + private native static void closeAsynchronousDialog(String res); +} diff --git a/core/java/android/webkit/gears/PluginSettings.java b/core/java/android/webkit/gears/PluginSettings.java new file mode 100644 index 0000000..2d0cc13 --- /dev/null +++ b/core/java/android/webkit/gears/PluginSettings.java @@ -0,0 +1,79 @@ +// Copyright 2008 The Android Open Source Project +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of Google Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package android.webkit.gears; + +import android.content.Context; +import android.util.Log; +import android.webkit.Plugin; +import android.webkit.Plugin.PreferencesClickHandler; + +/** + * Simple bridge class intercepting the click in the + * browser plugin list and calling the Gears settings + * dialog. + */ +public class PluginSettings { + + private static final String TAG = "Gears-J-PluginSettings"; + private Context mContext; + + public PluginSettings(Plugin plugin) { + plugin.setClickHandler(new ClickHandler()); + } + + /** + * We do not call the dialog synchronously here as the main + * message loop would be blocked, so we call it via a secondary thread. + */ + private class ClickHandler implements PreferencesClickHandler { + public void handleClickEvent(Context context) { + mContext = context.getApplicationContext(); + Thread startDialog = new Thread(new StartDialog(context)); + startDialog.start(); + } + } + + /** + * Simple wrapper class to call the gears native method in + * a separate thread (the native code will then instanciate a NativeDialog + * object which will start the GearsNativeDialog activity defined in + * the Browser). + */ + private class StartDialog implements Runnable { + Context mContext; + + public StartDialog(Context context) { + mContext = context; + } + + public void run() { + runSettingsDialog(mContext); + } + } + + private static native void runSettingsDialog(Context c); + +} diff --git a/core/java/android/webkit/gears/UrlInterceptHandlerGears.java b/core/java/android/webkit/gears/UrlInterceptHandlerGears.java index 95fc30f..288240e 100644 --- a/core/java/android/webkit/gears/UrlInterceptHandlerGears.java +++ b/core/java/android/webkit/gears/UrlInterceptHandlerGears.java @@ -407,6 +407,8 @@ public class UrlInterceptHandlerGears implements UrlInterceptHandler { true); // forceCache if (cacheResult == null) { + // With the no-cache policy we could end up + // with a null result return null; } @@ -444,8 +446,7 @@ public class UrlInterceptHandlerGears implements UrlInterceptHandler { // be used for input. cacheResult = CacheManager.getCacheFile(gearsUrl, null); if (cacheResult != null) { - if (logEnabled) - log("Returning surrogate result"); + log("Returning surrogate result"); return cacheResult; } else { // Not an expected condition, but handle gracefully. Perhaps out @@ -476,7 +477,10 @@ public class UrlInterceptHandlerGears implements UrlInterceptHandler { } /** - * Convenience debug function. Calls Android logging mechanism. + * Convenience debug function. Calls the Android logging + * mechanism. logEnabled is not a constant, so if the string + * evaluation is potentially expensive, the caller also needs to + * check it. * @param str String to log to the Android console. */ private void log(String str) { diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 19b1ce0..1440522 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -58,6 +58,8 @@ import java.util.List; * @attr ref android.R.styleable#AbsListView_textFilterEnabled * @attr ref android.R.styleable#AbsListView_transcriptMode * @attr ref android.R.styleable#AbsListView_cacheColorHint + * @attr ref android.R.styleable#AbsListView_fastScrollEnabled + * @attr ref android.R.styleable#AbsListView_smoothScrollbar */ public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher, ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener, @@ -116,6 +118,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * Indicates the view is in the process of being flung */ static final int TOUCH_MODE_FLING = 4; + + /** + * Indicates that the user is currently dragging the fast scroll thumb + */ + static final int TOUCH_MODE_FAST_SCROLL = 5; /** * Regular layout - usually an unsolicited layout from the view system @@ -304,6 +311,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * bitmap cache after scrolling. */ boolean mScrollingCacheEnabled; + + /** + * Whether or not to enable the fast scroll feature on this list + */ + boolean mFastScrollEnabled; /** * Optional callback to notify client when scroll position has changed @@ -321,6 +333,12 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te EditText mTextFilter; /** + * Indicates whether to use pixels-based or position-based scrollbar + * properties. + */ + private boolean mSmoothScrollbarEnabled = true; + + /** * Indicates that this view supports filtering */ private boolean mTextFilterEnabled; @@ -401,6 +419,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE; /** + * Helper object that renders and controls the fast scroll thumb. + */ + private FastScroller mFastScroller; + + /** * Interface definition for a callback to be invoked when the list or grid * has been scrolled. */ @@ -493,11 +516,84 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te int color = a.getColor(R.styleable.AbsListView_cacheColorHint, 0); setCacheColorHint(color); + + boolean enableFastScroll = a.getBoolean(R.styleable.AbsListView_fastScrollEnabled, false); + setFastScrollEnabled(enableFastScroll); + boolean smoothScrollbar = a.getBoolean(R.styleable.AbsListView_smoothScrollbar, true); + setSmoothScrollbarEnabled(smoothScrollbar); + a.recycle(); } /** + * Enables fast scrolling by letting the user quickly scroll through lists by + * dragging the fast scroll thumb. The adapter attached to the list may want + * to implement {@link SectionIndexer} if it wishes to display alphabet preview and + * jump between sections of the list. + * @see SectionIndexer + * @see #isFastScrollEnabled() + * @param enabled whether or not to enable fast scrolling + */ + public void setFastScrollEnabled(boolean enabled) { + mFastScrollEnabled = enabled; + if (enabled) { + if (mFastScroller == null) { + mFastScroller = new FastScroller(getContext(), this); + } + } else { + if (mFastScroller != null) { + mFastScroller.stop(); + mFastScroller = null; + } + } + } + + /** + * Returns the current state of the fast scroll feature. + * @see #setFastScrollEnabled(boolean) + * @return true if fast scroll is enabled, false otherwise + */ + @ViewDebug.ExportedProperty + public boolean isFastScrollEnabled() { + return mFastScrollEnabled; + } + + /** + * When smooth scrollbar is enabled, the position and size of the scrollbar thumb + * is computed based on the number of visible pixels in the visible items. This + * however assumes that all list items have the same height. If you use a list in + * which items have different heights, the scrollbar will change appearance as the + * user scrolls through the list. To avoid this issue, you need to disable this + * property. + * + * When smooth scrollbar is disabled, the position and size of the scrollbar thumb + * is based solely on the number of items in the adapter and the position of the + * visible items inside the adapter. This provides a stable scrollbar as the user + * navigates through a list of items with varying heights. + * + * @param enabled Whether or not to enable smooth scrollbar. + * + * @see #setSmoothScrollbarEnabled(boolean) + * @attr ref android.R.styleable#AbsListView_smoothScrollbar + */ + public void setSmoothScrollbarEnabled(boolean enabled) { + mSmoothScrollbarEnabled = enabled; + } + + /** + * Returns the current state of the fast scroll feature. + * + * @return True if smooth scrollbar is enabled is enabled, false otherwise. + * + * @see #setSmoothScrollbarEnabled(boolean) + */ + @ViewDebug.ExportedProperty + public boolean isSmoothScrollbarEnabled() { + return mSmoothScrollbarEnabled; + } + + /** * Set the listener that will receive notifications every time the list scrolls. * * @param l the scroll listener @@ -511,6 +607,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * Notify our scroll listener (if there is one) of a change in scroll state */ void invokeOnItemScrollListener() { + if (mFastScroller != null) { + mFastScroller.onScroll(this, mFirstPosition, getChildCount(), mItemCount); + } if (mOnScrollListener != null) { mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount); } @@ -525,6 +624,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * @see #setScrollingCacheEnabled(boolean) * @see View#setDrawingCacheEnabled(boolean) */ + @ViewDebug.ExportedProperty public boolean isScrollingCacheEnabled() { return mScrollingCacheEnabled; } @@ -571,6 +671,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * @see #setTextFilterEnabled(boolean) * @see Filterable */ + @ViewDebug.ExportedProperty public boolean isTextFilterEnabled() { return mTextFilterEnabled; } @@ -595,10 +696,12 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te setWillNotDraw(false); setAlwaysDrawnWithCacheEnabled(false); setScrollingCacheEnabled(true); + setScrollContainer(true); } private void useDefaultSelector() { - setSelector(getResources().getDrawable(com.android.internal.R.drawable.list_selector_background)); + setSelector(getResources().getDrawable( + com.android.internal.R.drawable.list_selector_background)); } /** @@ -607,6 +710,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * * @return true if the content is stacked from the bottom edge, false otherwise */ + @ViewDebug.ExportedProperty public boolean isStackFromBottom() { return mStackFromBottom; } @@ -843,35 +947,54 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te protected int computeVerticalScrollExtent() { final int count = getChildCount(); if (count > 0) { - int extent = count * 100; + if (mSmoothScrollbarEnabled) { + int extent = count * 100; + + View view = getChildAt(0); + final int top = view.getTop(); + int height = view.getHeight(); + if (height > 0) { + extent += (top * 100) / height; + } - View view = getChildAt(0); - final int top = view.getTop(); - int height = view.getHeight(); - if (height > 0) { - extent += (top * 100) / height; - } + view = getChildAt(count - 1); + final int bottom = view.getBottom(); + height = view.getHeight(); + if (height > 0) { + extent -= ((bottom - getHeight()) * 100) / height; + } - view = getChildAt(count - 1); - final int bottom = view.getBottom(); - height = view.getHeight(); - if (height > 0) { - extent -= ((bottom - getHeight()) * 100) / height; + return extent; + } else { + return 1; } - - return extent; } return 0; } @Override protected int computeVerticalScrollOffset() { - if (mFirstPosition >= 0 && getChildCount() > 0) { - final View view = getChildAt(0); - final int top = view.getTop(); - int height = view.getHeight(); - if (height > 0) { - return Math.max(mFirstPosition * 100 - (top * 100) / height, 0); + final int firstPosition = mFirstPosition; + final int childCount = getChildCount(); + if (firstPosition >= 0 && childCount > 0) { + if (mSmoothScrollbarEnabled) { + final View view = getChildAt(0); + final int top = view.getTop(); + int height = view.getHeight(); + if (height > 0) { + return Math.max(firstPosition * 100 - (top * 100) / height, 0); + } + } else { + int index; + final int count = mItemCount; + if (firstPosition == 0) { + index = 0; + } else if (firstPosition + childCount == count) { + index = count; + } else { + index = firstPosition + childCount / 2; + } + return (int) (firstPosition + childCount * (index / (float) count)); } } return 0; @@ -879,7 +1002,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @Override protected int computeVerticalScrollRange() { - return Math.max(mItemCount * 100, 0); + return mSmoothScrollbarEnabled ? Math.max(mItemCount * 100, 0) : mItemCount; } @Override @@ -1140,6 +1263,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mDataChanged = true; rememberSyncState(); } + if (mFastScroller != null) { + mFastScroller.onSizeChanged(w, h, oldw, oldh); + } } /** @@ -1669,6 +1795,13 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @Override public boolean onTouchEvent(MotionEvent ev) { + + if (mFastScroller != null) { + boolean intercepted = mFastScroller.onTouchEvent(ev); + if (intercepted) { + return true; + } + } final int action = ev.getAction(); final int x = (int) ev.getX(); final int y = (int) ev.getY(); @@ -1684,28 +1817,30 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te switch (action) { case MotionEvent.ACTION_DOWN: { int motionPosition = pointToPosition(x, y); - if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0) - && (getAdapter().isEnabled(motionPosition))) { - // User clicked on an actual view (and was not stopping a fling). It might be a - // click or a scroll. Assume it is a click until proven otherwise - mTouchMode = TOUCH_MODE_DOWN; - // FIXME Debounce - if (mPendingCheckForTap == null) { - mPendingCheckForTap = new CheckForTap(); - } - postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); - } else { - if (ev.getEdgeFlags() != 0 && motionPosition < 0) { - // If we couldn't find a view to click on, but the down event was touching - // the edge, we will bail out and try again. This allows the edge correcting - // code in ViewRoot to try to find a nearby view to select - return false; + if (!mDataChanged) { + if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0) + && (getAdapter().isEnabled(motionPosition))) { + // User clicked on an actual view (and was not stopping a fling). It might be a + // click or a scroll. Assume it is a click until proven otherwise + mTouchMode = TOUCH_MODE_DOWN; + // FIXME Debounce + if (mPendingCheckForTap == null) { + mPendingCheckForTap = new CheckForTap(); + } + postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); + } else { + if (ev.getEdgeFlags() != 0 && motionPosition < 0) { + // If we couldn't find a view to click on, but the down event was touching + // the edge, we will bail out and try again. This allows the edge correcting + // code in ViewRoot to try to find a nearby view to select + return false; + } + // User clicked on whitespace, or stopped a fling. It is a scroll. + createScrollingCache(); + mTouchMode = TOUCH_MODE_SCROLL; + motionPosition = findMotionRow(y); + reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } - // User clicked on whitespace, or stopped a fling. It is a scroll. - createScrollingCache(); - mTouchMode = TOUCH_MODE_SCROLL; - motionPosition = findMotionRow(y); - reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } if (motionPosition >= 0) { @@ -1897,6 +2032,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te return true; } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mFastScroller != null) { + mFastScroller.draw(canvas); + } + } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { @@ -1904,6 +2047,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te int x = (int) ev.getX(); int y = (int) ev.getY(); View v; + + if (mFastScroller != null) { + boolean intercepted = mFastScroller.onInterceptTouchEvent(ev); + if (intercepted) { + return true; + } + } + switch (action) { case MotionEvent.ACTION_DOWN: { int motionPosition = findMotionRow(y); @@ -1965,7 +2116,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } - private void reportScrollStateChange(int newState) { + /** + * Fires an "on scroll state changed" event to the registered + * {@link android.widget.AbsListView.OnScrollListener}, if any. The state change + * is fired only if the specified state is different from the previously known state. + * + * @param newState The new scroll state. + */ + void reportScrollStateChange(int newState) { if (newState != mLastScrollState) { if (mOnScrollListener != null) { mOnScrollListener.onScrollStateChanged(this, newState); @@ -2013,10 +2171,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private void endFling() { mTouchMode = TOUCH_MODE_REST; - if (mOnScrollListener != null) { - mOnScrollListener.onScrollStateChanged(AbsListView.this, - OnScrollListener.SCROLL_STATE_IDLE); - } + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); clearScrollingCache(); } @@ -2411,7 +2566,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (selectedPos >= 0) { mLayoutMode = LAYOUT_SPECIFIC; setSelectionInt(selectedPos); + invokeOnItemScrollListener(); } + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); return selectedPos >= 0; } @@ -2547,7 +2704,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // Make sure we have a window before showing the popup if (getWindowVisibility() == View.VISIBLE) { int screenHeight = WindowManagerImpl.getDefault().getDefaultDisplay().getHeight(); - final int[] xy = mLocation; + final int[] xy = new int[2]; getLocationOnScreen(xy); int bottomGap = screenHeight - xy[1] - getHeight() + 20; mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, @@ -2689,6 +2846,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te com.android.internal.R.layout.typing_filter, null); mTextFilter.addTextChangedListener(this); p.setFocusable(false); + p.setTouchable(false); p.setContentView(mTextFilter); p.setWidth(LayoutParams.WRAP_CONTENT); p.setHeight(LayoutParams.WRAP_CONTENT); diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index 1fa7318..65ca885 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -242,6 +242,7 @@ public abstract class AbsSeekBar extends ProgressBar { case MotionEvent.ACTION_MOVE: trackTouchEvent(event); + attemptClaimDrag(); break; case MotionEvent.ACTION_UP: @@ -281,6 +282,16 @@ public abstract class AbsSeekBar extends ProgressBar { setProgress((int) progress, true); } + + /** + * Tries to claim the user's drag motion, and requests disallowing any + * ancestors from stealing events in the drag. + */ + private void attemptClaimDrag() { + if (mParent != null) { + mParent.requestDisallowInterceptTouchEvent(true); + } + } /** * This is called when the user has started touching this widget. diff --git a/core/java/android/widget/AbsoluteLayout.java b/core/java/android/widget/AbsoluteLayout.java index 36a3b10..c77f7ae 100644 --- a/core/java/android/widget/AbsoluteLayout.java +++ b/core/java/android/widget/AbsoluteLayout.java @@ -32,7 +32,11 @@ import android.widget.RemoteViews.RemoteView; * <p><strong>XML attributes</strong></p> <p> See {@link * android.R.styleable#ViewGroup ViewGroup Attributes}, {@link * android.R.styleable#View View Attributes}</p> + * + * @deprecated Use {@link android.widget.FrameLayout}, {@link android.widget.RelativeLayout} + * or a custom layout instead. */ +@Deprecated @RemoteView public class AbsoluteLayout extends ViewGroup { public AbsoluteLayout(Context context) { diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java index e096612..173e80f 100644 --- a/core/java/android/widget/AdapterView.java +++ b/core/java/android/widget/AdapterView.java @@ -140,7 +140,7 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { * The position within the adapter's data set of the item to select * during the next layout. */ - @ViewDebug.ExportedProperty + @ViewDebug.ExportedProperty int mNextSelectedPosition = INVALID_POSITION; /** @@ -151,7 +151,7 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { /** * The position within the adapter's data set of the currently selected item. */ - @ViewDebug.ExportedProperty + @ViewDebug.ExportedProperty int mSelectedPosition = INVALID_POSITION; /** @@ -520,6 +520,7 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { * * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected. */ + @ViewDebug.CapturedViewProperty public int getSelectedItemPosition() { return mNextSelectedPosition; } @@ -528,6 +529,7 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID} * if nothing is selected. */ + @ViewDebug.CapturedViewProperty public long getSelectedItemId() { return mNextSelectedRowId; } @@ -557,6 +559,7 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { * AdapterView. (This is the number of data items, which may be * larger than the number of visible view.) */ + @ViewDebug.CapturedViewProperty public int getCount() { return mItemCount; } diff --git a/core/java/android/widget/AlphabetIndexer.java b/core/java/android/widget/AlphabetIndexer.java new file mode 100644 index 0000000..bbabaaa --- /dev/null +++ b/core/java/android/widget/AlphabetIndexer.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.widget; + +import android.database.Cursor; +import android.database.DataSetObserver; +import android.util.SparseIntArray; + +/** + * A helper class for adapters that implement the SectionIndexer interface. + * If the items in the adapter are sorted by simple alphabet-based sorting, then + * this class provides a way to do fast indexing of large lists using binary search. + * It caches the indices that have been determined through the binary search and also + * invalidates the cache if changes occur in the cursor. + * <p/> + * Your adapter is responsible for updating the cursor by calling {@link #setCursor} if the + * cursor changes. {@link #getPositionForSection} method does the binary search for the starting + * index of a given section (alphabet). + * @hide pending API council approval + */ +public class AlphabetIndexer extends DataSetObserver implements SectionIndexer { + + /** + * Cursor that is used by the adapter of the list view. + */ + protected Cursor mDataCursor; + + /** + * The index of the cursor column that this list is sorted on. + */ + protected int mColumnIndex; + + /** + * The string of characters that make up the indexing sections. + */ + protected CharSequence mAlphabet; + + /** + * Cached length of the alphabet array. + */ + private int mAlphabetLength; + + /** + * This contains a cache of the computed indices so far. It will get reset whenever + * the dataset changes or the cursor changes. + */ + private SparseIntArray mAlphaMap; + + /** + * Use a collator to compare strings in a localized manner. + */ + private java.text.Collator mCollator; + + /** + * The section array converted from the alphabet string. + */ + private String[] mAlphabetArray; + + /** + * Constructs the indexer. + * @param cursor the cursor containing the data set + * @param sortedColumnIndex the column number in the cursor that is sorted + * alphabetically + * @param alphabet string containing the alphabet, with space as the first character. + * For example, use the string " ABCDEFGHIJKLMNOPQRSTUVWXYZ" for English indexing. + * The characters must be uppercase and be sorted in ascii/unicode order. Basically + * characters in the alphabet will show up as preview letters. + */ + public AlphabetIndexer(Cursor cursor, int sortedColumnIndex, CharSequence alphabet) { + mDataCursor = cursor; + mColumnIndex = sortedColumnIndex; + mAlphabet = alphabet; + mAlphabetLength = alphabet.length(); + mAlphabetArray = new String[mAlphabetLength]; + for (int i = 0; i < mAlphabetLength; i++) { + mAlphabetArray[i] = Character.toString(mAlphabet.charAt(i)); + } + mAlphaMap = new SparseIntArray(mAlphabetLength); + if (cursor != null) { + cursor.registerDataSetObserver(this); + } + // Get a Collator for the current locale for string comparisons. + mCollator = java.text.Collator.getInstance(); + mCollator.setStrength(java.text.Collator.PRIMARY); + } + + /** + * Returns the section array constructed from the alphabet provided in the constructor. + * @return the section array + */ + public Object[] getSections() { + return mAlphabetArray; + } + + /** + * Sets a new cursor as the data set and resets the cache of indices. + * @param cursor the new cursor to use as the data set + */ + public void setCursor(Cursor cursor) { + if (mDataCursor != null) { + mDataCursor.unregisterDataSetObserver(this); + } + mDataCursor = cursor; + if (cursor != null) { + mDataCursor.registerDataSetObserver(this); + } + mAlphaMap.clear(); + } + + /** + * Default implementation compares the first character of word with letter. + */ + protected int compare(String word, String letter) { + return mCollator.compare(word.substring(0, 1), letter); + } + + /** + * Performs a binary search or cache lookup to find the first row that + * matches a given section's starting letter. + * @param sectionIndex the section to search for + * @return the row index of the first occurrence, or the nearest next letter. + * For instance, if searching for "T" and no "T" is found, then the first + * row starting with "U" or any higher letter is returned. If there is no + * data following "T" at all, then the list size is returned. + */ + public int getPositionForSection(int sectionIndex) { + final SparseIntArray alphaMap = mAlphaMap; + final Cursor cursor = mDataCursor; + + if (cursor == null || mAlphabet == null) { + return 0; + } + + // Check bounds + if (sectionIndex <= 0) { + return 0; + } + if (sectionIndex >= mAlphabetLength) { + sectionIndex = mAlphabetLength - 1; + } + + int savedCursorPos = cursor.getPosition(); + + int count = cursor.getCount(); + int start = 0; + int end = count; + int pos; + + char letter = mAlphabet.charAt(sectionIndex); + String targetLetter = Character.toString(letter); + int key = letter; + // Check map + if (Integer.MIN_VALUE != (pos = alphaMap.get(key, Integer.MIN_VALUE))) { + // Is it approximate? Using negative value to indicate that it's + // an approximation and positive value when it is the accurate + // position. + if (pos < 0) { + pos = -pos; + end = pos; + } else { + // Not approximate, this is the confirmed start of section, return it + return pos; + } + } + + // Do we have the position of the previous section? + if (sectionIndex > 0) { + int prevLetter = + mAlphabet.charAt(sectionIndex - 1); + int prevLetterPos = alphaMap.get(prevLetter, Integer.MIN_VALUE); + if (prevLetterPos != Integer.MIN_VALUE) { + start = Math.abs(prevLetterPos); + } + } + + // Now that we have a possibly optimized start and end, let's binary search + + pos = (end + start) / 2; + + while (pos < end) { + // Get letter at pos + cursor.moveToPosition(pos); + String curName = cursor.getString(mColumnIndex); + if (curName == null) { + if (pos == 0) { + break; + } else { + pos--; + continue; + } + } + int diff = compare(curName, targetLetter); + if (diff != 0) { + // Commenting out approximation code because it doesn't work for certain + // lists with custom comparators + // Enter approximation in hash if a better solution doesn't exist + // String startingLetter = Character.toString(getFirstLetter(curName)); + // int startingLetterKey = startingLetter.charAt(0); + // int curPos = alphaMap.get(startingLetterKey, Integer.MIN_VALUE); + // if (curPos == Integer.MIN_VALUE || Math.abs(curPos) > pos) { + // Negative pos indicates that it is an approximation + // alphaMap.put(startingLetterKey, -pos); + // } + // if (mCollator.compare(startingLetter, targetLetter) < 0) { + if (diff < 0) { + start = pos + 1; + if (start >= count) { + pos = count; + break; + } + } else { + end = pos; + } + } else { + // They're the same, but that doesn't mean it's the start + if (start == pos) { + // This is it + break; + } else { + // Need to go further lower to find the starting row + end = pos; + } + } + pos = (start + end) / 2; + } + alphaMap.put(key, pos); + cursor.moveToPosition(savedCursorPos); + return pos; + } + + /** + * Returns the section index for a given position in the list by querying the item + * and comparing it with all items in the section array. + */ + public int getSectionForPosition(int position) { + int savedCursorPos = mDataCursor.getPosition(); + mDataCursor.moveToPosition(position); + mDataCursor.moveToPosition(savedCursorPos); + String curName = mDataCursor.getString(mColumnIndex); + // Linear search, as there are only a few items in the section index + // Could speed this up later if it actually gets used. + for (int i = 0; i < mAlphabetLength; i++) { + char letter = mAlphabet.charAt(i); + String targetLetter = Character.toString(letter); + if (compare(curName, targetLetter) == 0) { + return i; + } + } + return 0; // Don't recognize the letter - falls under zero'th section + } + + /* + * @hide + */ + @Override + public void onChanged() { + super.onChanged(); + mAlphaMap.clear(); + } + + /* + * @hide + */ + @Override + public void onInvalidated() { + super.onInvalidated(); + mAlphaMap.clear(); + } +} diff --git a/core/java/android/widget/AnalogClock.java b/core/java/android/widget/AnalogClock.java index 808104e..fbb0105 100644 --- a/core/java/android/widget/AnalogClock.java +++ b/core/java/android/widget/AnalogClock.java @@ -25,9 +25,9 @@ import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.os.Handler; +import android.text.format.Time; import android.util.AttributeSet; import android.view.View; -import android.pim.Time; import java.util.TimeZone; diff --git a/core/java/android/widget/AppSecurityPermissions.java b/core/java/android/widget/AppSecurityPermissions.java index 582117f..5fa00e7 100755 --- a/core/java/android/widget/AppSecurityPermissions.java +++ b/core/java/android/widget/AppSecurityPermissions.java @@ -18,7 +18,6 @@ package android.widget; import com.android.internal.R; import android.content.Context; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageParser; @@ -31,9 +30,17 @@ import android.util.DisplayMetrics; import android.util.Log; import android.view.LayoutInflater; import android.view.View; + import java.io.File; +import java.text.Collator; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -56,15 +63,15 @@ public class AppSecurityPermissions implements View.OnClickListener { BOTH } - private final String TAG = "AppSecurityPermissions"; + private final static String TAG = "AppSecurityPermissions"; private boolean localLOGV = false; private Context mContext; private LayoutInflater mInflater; private PackageManager mPm; private LinearLayout mPermsView; - private HashMap<String, String> mDangerousMap; - private HashMap<String, String> mNormalMap; - private ArrayList<PermissionInfo> mPermsList; + private Map<String, String> mDangerousMap; + private Map<String, String> mNormalMap; + private List<PermissionInfo> mPermsList; private String mDefaultGrpLabel; private String mDefaultGrpName="DefaultGrp"; private String mPermFormat; @@ -79,18 +86,129 @@ public class AppSecurityPermissions implements View.OnClickListener { private State mCurrentState; private LinearLayout mNonDangerousList; private LinearLayout mDangerousList; - private HashMap<String, String> mGroupLabelCache; + private HashMap<String, CharSequence> mGroupLabelCache; private View mNoPermsView; - - public AppSecurityPermissions(Context context) { - this(context, null); - } - - public AppSecurityPermissions(Context context, ArrayList<PermissionInfo> permList) { + + public AppSecurityPermissions(Context context, List<PermissionInfo> permList) { mContext = context; - mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mPm = context.getPackageManager(); + mPm = mContext.getPackageManager(); mPermsList = permList; + } + + public AppSecurityPermissions(Context context, String packageName) { + mContext = context; + mPm = mContext.getPackageManager(); + mPermsList = new ArrayList<PermissionInfo>(); + Set<PermissionInfo> permSet = new HashSet<PermissionInfo>(); + PackageInfo pkgInfo; + try { + pkgInfo = mPm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS); + } catch (NameNotFoundException e) { + Log.w(TAG, "Could'nt retrieve permissions for package:"+packageName); + return; + } + // Extract all user permissions + if((pkgInfo.applicationInfo != null) && (pkgInfo.applicationInfo.uid != -1)) { + getAllUsedPermissions(pkgInfo.applicationInfo.uid, permSet); + } + for(PermissionInfo tmpInfo : permSet) { + mPermsList.add(tmpInfo); + } + } + + public AppSecurityPermissions(Context context, PackageParser.Package pkg) { + mContext = context; + mPm = mContext.getPackageManager(); + mPermsList = new ArrayList<PermissionInfo>(); + Set<PermissionInfo> permSet = new HashSet<PermissionInfo>(); + if(pkg == null) { + return; + } + // Extract shared user permissions if any + if(pkg.mSharedUserId != null) { + int sharedUid; + try { + sharedUid = mPm.getUidForSharedUser(pkg.mSharedUserId); + } catch (NameNotFoundException e) { + Log.w(TAG, "Could'nt retrieve shared user id for:"+pkg.packageName); + return; + } + getAllUsedPermissions(sharedUid, permSet); + } else { + ArrayList<String> strList = pkg.requestedPermissions; + int size; + if((strList == null) || ((size = strList.size()) == 0)) { + return; + } + // Extract permissions defined in current package + extractPerms(strList.toArray(new String[size]), permSet); + } + for(PermissionInfo tmpInfo : permSet) { + mPermsList.add(tmpInfo); + } + } + + public PackageParser.Package getPackageInfo(Uri packageURI) { + final String archiveFilePath = packageURI.getPath(); + PackageParser packageParser = new PackageParser(archiveFilePath); + File sourceFile = new File(archiveFilePath); + DisplayMetrics metrics = new DisplayMetrics(); + metrics.setToDefaults(); + return packageParser.parsePackage(sourceFile, archiveFilePath, metrics, 0); + } + + private void getAllUsedPermissions(int sharedUid, Set<PermissionInfo> permSet) { + String sharedPkgList[] = mPm.getPackagesForUid(sharedUid); + if(sharedPkgList == null || (sharedPkgList.length == 0)) { + return; + } + for(String sharedPkg : sharedPkgList) { + getPermissionsForPackage(sharedPkg, permSet); + } + } + + private void getPermissionsForPackage(String packageName, + Set<PermissionInfo> permSet) { + PackageInfo pkgInfo; + try { + pkgInfo = mPm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS); + } catch (NameNotFoundException e) { + Log.w(TAG, "Could'nt retrieve permissions for package:"+packageName); + return; + } + if(pkgInfo == null) { + return; + } + String strList[] = pkgInfo.requestedPermissions; + if(strList == null) { + return; + } + extractPerms(strList, permSet); + } + + private void extractPerms(String strList[], Set<PermissionInfo> permSet) { + if((strList == null) || (strList.length == 0)) { + return; + } + for(String permName:strList) { + try { + PermissionInfo tmpPermInfo = mPm.getPermissionInfo(permName, 0); + if(tmpPermInfo != null) { + permSet.add(tmpPermInfo); + } + } catch (NameNotFoundException e) { + Log.i(TAG, "Ignoring unknown permission:"+permName); + } + } + } + + public int getPermissionCount() { + return mPermsList.size(); + } + + public View getPermissionsView() { + + mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mPermsView = (LinearLayout) mInflater.inflate(R.layout.app_perms_summary, null); mShowMore = mPermsView.findViewById(R.id.show_more); mShowMoreIcon = (ImageView) mShowMore.findViewById(R.id.show_more_icon); @@ -112,32 +230,9 @@ public class AppSecurityPermissions implements View.OnClickListener { mDangerousIcon = mContext.getResources().getDrawable(R.drawable.ic_bullet_key_permission); mShowMaxIcon = mContext.getResources().getDrawable(R.drawable.expander_ic_maximized); mShowMinIcon = mContext.getResources().getDrawable(R.drawable.expander_ic_minimized); - } - - public void setSecurityPermissionsView() { - setPermissions(mPermsList); - } - - public void setSecurityPermissionsView(Uri pkgURI) { - final String archiveFilePath = pkgURI.getPath(); - PackageParser packageParser = new PackageParser(archiveFilePath); - File sourceFile = new File(archiveFilePath); - DisplayMetrics metrics = new DisplayMetrics(); - metrics.setToDefaults(); - PackageParser.Package pkgInfo = packageParser.parsePackage(sourceFile, - archiveFilePath, metrics, 0); - mPermsList = generatePermissionsInfo(pkgInfo.requestedPermissions); - //For packages that havent been installed we need the application info object - //to load the labels and other resources. - setPermissions(mPermsList, pkgInfo.applicationInfo); - } - - public void setSecurityPermissionsView(PackageInfo pInfo) { - mPermsList = generatePermissionsInfo(pInfo.requestedPermissions); + + // Set permissions view setPermissions(mPermsList); - } - - public View getPermissionsView() { return mPermsView; } @@ -164,74 +259,30 @@ public class AppSecurityPermissions implements View.OnClickListener { * is null the other non null value is returned without formatting * this is to placate initial error checks */ - private String formatPermissions(String groupDesc, String permDesc) { + private String formatPermissions(String groupDesc, CharSequence permDesc) { if(groupDesc == null) { - return permDesc; - } - groupDesc = canonicalizeGroupDesc(groupDesc); - if(permDesc == null) { - return groupDesc; + if(permDesc == null) { + return null; + } + return permDesc.toString(); } - return String.format(mPermFormat, groupDesc, permDesc); - } - - /** - * Utility method that concatenates two strings defined by mPermFormat. - */ - private String formatPermissions(String groupDesc, CharSequence permDesc) { groupDesc = canonicalizeGroupDesc(groupDesc); if(permDesc == null) { return groupDesc; } - // Format only if str1 and str2 are not null. - return formatPermissions(groupDesc, permDesc.toString()); + // groupDesc and permDesc are non null + return String.format(mPermFormat, groupDesc, permDesc.toString()); } - private ArrayList<PermissionInfo> generatePermissionsInfo(String[] strList) { - ArrayList<PermissionInfo> permInfoList = new ArrayList<PermissionInfo>(); - if(strList == null) { - return permInfoList; - } - PermissionInfo tmpPermInfo = null; - for(int i = 0; i < strList.length; i++) { - try { - tmpPermInfo = mPm.getPermissionInfo(strList[i], 0); - permInfoList.add(tmpPermInfo); - } catch (NameNotFoundException e) { - Log.i(TAG, "Ignoring unknown permisison:"+strList[i]); - continue; - } - } - return permInfoList; - } - - private ArrayList<PermissionInfo> generatePermissionsInfo(ArrayList<String> strList) { - ArrayList<PermissionInfo> permInfoList = new ArrayList<PermissionInfo>(); - if(strList != null) { - PermissionInfo tmpPermInfo = null; - for(String permName:strList) { - try { - tmpPermInfo = mPm.getPermissionInfo(permName, 0); - permInfoList.add(tmpPermInfo); - } catch (NameNotFoundException e) { - Log.i(TAG, "Ignoring unknown permisison:"+permName); - continue; - } - } - } - return permInfoList; - } - - private String getGroupLabel(String grpName) { + private CharSequence getGroupLabel(String grpName) { if (grpName == null) { //return default label return mDefaultGrpLabel; } - String cachedLabel = mGroupLabelCache.get(grpName); + CharSequence cachedLabel = mGroupLabelCache.get(grpName); if (cachedLabel != null) { return cachedLabel; } - PermissionGroupInfo pgi; try { pgi = mPm.getPermissionGroupInfo(grpName, 0); @@ -239,7 +290,7 @@ public class AppSecurityPermissions implements View.OnClickListener { Log.i(TAG, "Invalid group name:" + grpName); return null; } - String label = pgi.loadLabel(mPm).toString(); + CharSequence label = pgi.loadLabel(mPm).toString(); mGroupLabelCache.put(grpName, label); return label; } @@ -249,13 +300,13 @@ public class AppSecurityPermissions implements View.OnClickListener { * list of permission descriptions. */ private void displayPermissions(boolean dangerous) { - HashMap<String, String> permInfoMap = dangerous ? mDangerousMap : mNormalMap; + Map<String, String> permInfoMap = dangerous ? mDangerousMap : mNormalMap; LinearLayout permListView = dangerous ? mDangerousList : mNonDangerousList; permListView.removeAllViews(); Set<String> permInfoStrSet = permInfoMap.keySet(); for (String loopPermGrpInfoStr : permInfoStrSet) { - String grpLabel = getGroupLabel(loopPermGrpInfoStr); + CharSequence grpLabel = getGroupLabel(loopPermGrpInfoStr); //guaranteed that grpLabel wont be null since permissions without groups //will belong to the default group if(localLOGV) Log.i(TAG, "Adding view group:" + grpLabel + ", desc:" @@ -269,7 +320,7 @@ public class AppSecurityPermissions implements View.OnClickListener { mNoPermsView.setVisibility(View.VISIBLE); } - private View getPermissionItemView(String grpName, String permList, + private View getPermissionItemView(CharSequence grpName, String permList, boolean dangerous) { View permView = mInflater.inflate(R.layout.app_permission_item, null); Drawable icon = dangerous ? mDangerousIcon : mNormalIcon; @@ -334,35 +385,105 @@ public class AppSecurityPermissions implements View.OnClickListener { } return false; } - - private void setPermissions(ArrayList<PermissionInfo> permList) { - setPermissions(permList, null); + + /* + * Utility method that aggregates all permission descriptions categorized by group + * Say group1 has perm11, perm12, perm13, the group description will be + * perm11_Desc, perm12_Desc, perm13_Desc + */ + private void aggregateGroupDescs( + Map<String, List<PermissionInfo> > map, Map<String, String> retMap) { + if(map == null) { + return; + } + if(retMap == null) { + return; + } + Set<String> grpNames = map.keySet(); + Iterator<String> grpNamesIter = grpNames.iterator(); + while(grpNamesIter.hasNext()) { + String grpDesc = null; + String grpNameKey = grpNamesIter.next(); + List<PermissionInfo> grpPermsList = map.get(grpNameKey); + if(grpPermsList == null) { + continue; + } + for(PermissionInfo permInfo: grpPermsList) { + CharSequence permDesc = permInfo.loadLabel(mPm); + grpDesc = formatPermissions(grpDesc, permDesc); + } + // Insert grpDesc into map + if(grpDesc != null) { + if(localLOGV) Log.i(TAG, "Group:"+grpNameKey+" description:"+grpDesc.toString()); + retMap.put(grpNameKey, grpDesc.toString()); + } + } } - private void setPermissions(ArrayList<PermissionInfo> permList, ApplicationInfo appInfo) { - mDangerousMap = new HashMap<String, String>(); - mNormalMap = new HashMap<String, String>(); - mGroupLabelCache = new HashMap<String, String>(); + private static class PermissionInfoComparator implements Comparator<PermissionInfo> { + private PackageManager mPm; + private final Collator sCollator = Collator.getInstance(); + PermissionInfoComparator(PackageManager pm) { + mPm = pm; + } + public final int compare(PermissionInfo a, PermissionInfo b) { + CharSequence sa = a.loadLabel(mPm); + CharSequence sb = b.loadLabel(mPm); + return sCollator.compare(sa, sb); + } + } + + private void setPermissions(List<PermissionInfo> permList) { + mGroupLabelCache = new HashMap<String, CharSequence>(); //add the default label so that uncategorized permissions can go here mGroupLabelCache.put(mDefaultGrpName, mDefaultGrpLabel); + + // Map containing group names and a list of permissions under that group + // categorized as dangerous + mDangerousMap = new HashMap<String, String>(); + // Map containing group names and a list of permissions under that group + // categorized as normal + mNormalMap = new HashMap<String, String>(); + + // Additional structures needed to ensure that permissions are unique under + // each group + Map<String, List<PermissionInfo>> dangerousMap = + new HashMap<String, List<PermissionInfo>>(); + Map<String, List<PermissionInfo> > normalMap = + new HashMap<String, List<PermissionInfo>>(); + PermissionInfoComparator permComparator = new PermissionInfoComparator(mPm); + if (permList != null) { + // First pass to group permissions for (PermissionInfo pInfo : permList) { + if(localLOGV) Log.i(TAG, "Processing permission:"+pInfo.name); if(!isDisplayablePermission(pInfo)) { + if(localLOGV) Log.i(TAG, "Permission:"+pInfo.name+" is not displayable"); continue; } - String grpName = (pInfo.group == null) ? mDefaultGrpName : pInfo.group; - HashMap<String, String> permInfoMap = + Map<String, List<PermissionInfo> > permInfoMap = (pInfo.protectionLevel == PermissionInfo.PROTECTION_DANGEROUS) ? - mDangerousMap : mNormalMap; - // Check to make sure we have a label for the group - if (getGroupLabel(grpName) == null) { - continue; + dangerousMap : normalMap; + String grpName = (pInfo.group == null) ? mDefaultGrpName : pInfo.group; + if(localLOGV) Log.i(TAG, "Permission:"+pInfo.name+" belongs to group:"+grpName); + List<PermissionInfo> grpPermsList = permInfoMap.get(grpName); + if(grpPermsList == null) { + grpPermsList = new ArrayList<PermissionInfo>(); + permInfoMap.put(grpName, grpPermsList); + grpPermsList.add(pInfo); + } else { + int idx = Collections.binarySearch(grpPermsList, pInfo, permComparator); + if(localLOGV) Log.i(TAG, "idx="+idx+", list.size="+grpPermsList.size()); + if (idx < 0) { + idx = -idx-1; + grpPermsList.add(idx, pInfo); + } } - CharSequence permDesc = pInfo.loadLabel(mPm); - String grpDesc = permInfoMap.get(grpName); - permInfoMap.put(grpName, formatPermissions(grpDesc, permDesc)); - if(localLOGV) Log.i(TAG, pInfo.name + " : " + permDesc+" : " + grpName); } + // Second pass to actually form the descriptions + // Look at dangerous permissions first + aggregateGroupDescs(dangerousMap, mDangerousMap); + aggregateGroupDescs(normalMap, mNormalMap); } mCurrentState = State.NO_PERMS; diff --git a/core/java/android/widget/ArrayAdapter.java b/core/java/android/widget/ArrayAdapter.java index fe50a01..c65a3ce 100644 --- a/core/java/android/widget/ArrayAdapter.java +++ b/core/java/android/widget/ArrayAdapter.java @@ -28,7 +28,7 @@ import java.util.List; /** * A ListAdapter that manages a ListView backed by an array of arbitrary - * objects. By default this class expects that the provided resource id referecnes + * objects. By default this class expects that the provided resource id references * a single TextView. If you want to use a more complex layout, use the constructors that * also takes a field id. That field id should reference a TextView in the larger layout * resource. @@ -179,7 +179,7 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { } /** - * Inserts the spcified object at the specified index in the array. + * Inserts the specified object at the specified index in the array. * * @param object The object to insert into the array. * @param index The index at which the object must be inserted. @@ -385,7 +385,7 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { } /** - * <p>An array filters constrains the content of the array adapter with + * <p>An array filter constrains the content of the array adapter with * a prefix. Each item that does not start with the supplied prefix * is removed from the list.</p> */ diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java index e1f6fa8..d8fa603 100644 --- a/core/java/android/widget/AutoCompleteTextView.java +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -23,11 +23,17 @@ import android.graphics.drawable.Drawable; import android.text.Editable; import android.text.Selection; import android.text.TextUtils; +import android.text.TextWatcher; import android.util.AttributeSet; +import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.EditorInfo; import com.android.internal.R; @@ -55,7 +61,7 @@ import com.android.internal.R; * super.onCreate(icicle); * setContentView(R.layout.countries); * - * ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, + * ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, * android.R.layout.simple_dropdown_item_1line, COUNTRIES); * AutoCompleteTextView textView = (AutoCompleteTextView) * findViewById(R.id.countries_list); @@ -74,6 +80,9 @@ import com.android.internal.R; * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector */ public class AutoCompleteTextView extends EditText implements Filter.FilterListener { + static final boolean DEBUG = false; + static final String TAG = "AutoCompleteTextView"; + private static final int HINT_VIEW_ID = 0x17; private CharSequence mHintText; @@ -85,6 +94,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe private PopupWindow mPopup; private DropDownListView mDropDownList; + private int mDropDownVerticalOffset; + private int mDropDownHorizontalOffset; private Drawable mDropDownListHighlight; @@ -94,7 +105,12 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe private final DropDownItemClickListener mDropDownItemClickListener = new DropDownItemClickListener(); - private boolean mTextChanged; + private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; + private boolean mOpenBefore; + + private Validator mValidator = null; + + private AutoCompleteTextView.ListSelectorHider mHideSelector; public AutoCompleteTextView(Context context) { this(context, null); @@ -107,7 +123,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mPopup = new PopupWindow(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle); + mPopup = new PopupWindow(context, attrs, + com.android.internal.R.attr.autoCompleteTextViewStyle); TypedArray a = context.obtainStyledAttributes( @@ -120,13 +137,34 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe mDropDownListHighlight = a.getDrawable( R.styleable.AutoCompleteTextView_dropDownSelector); + mDropDownVerticalOffset = (int) + a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f); + mDropDownHorizontalOffset = (int) + a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f); mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView, R.layout.simple_dropdown_hint); + // A little trickiness for backwards compatibility: if the app + // didn't specify an explicit content type, then we will fill in the + // auto complete flag for them. + int contentType = a.getInt( + R.styleable.AutoCompleteTextView_inputType, + EditorInfo.TYPE_NULL); + if (contentType == EditorInfo.TYPE_NULL) { + contentType = getInputType(); + if ((contentType&EditorInfo.TYPE_MASK_CLASS) + == EditorInfo.TYPE_CLASS_TEXT) { + contentType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE; + setRawInputType(contentType); + } + } + a.recycle(); setFocusable(true); + + addTextChangedListener(new MyWatcher()); } /** @@ -209,7 +247,10 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * in the drop down list.</p> * * @return the item click listener + * + * @deprecated Use {@link #getOnItemClickListener()} intead */ + @Deprecated public AdapterView.OnItemClickListener getItemClickListener() { return mItemClickListener; } @@ -219,12 +260,35 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * item in the drop down list.</p> * * @return the item selected listener + * + * @deprecated Use {@link #getOnItemSelectedListener()} intead */ + @Deprecated public AdapterView.OnItemSelectedListener getItemSelectedListener() { return mItemSelectedListener; } /** + * <p>Returns the listener that is notified whenever the user clicks an item + * in the drop down list.</p> + * + * @return the item click listener + */ + public AdapterView.OnItemClickListener getOnItemClickListener() { + return mItemClickListener; + } + + /** + * <p>Returns the listener that is notified whenever the user selects an + * item in the drop down list.</p> + * + * @return the item selected listener + */ + public AdapterView.OnItemSelectedListener getOnItemSelectedListener() { + return mItemSelectedListener; + } + + /** * <p>Returns a filterable list adapter used for auto completion.</p> * * @return a data adapter used for auto completion @@ -258,6 +322,19 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe } @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (isPopupShowing()) { + // special case for the back key, we do not even try to send it + // to the drop down list but instead, consume it immediately + if (keyCode == KeyEvent.KEYCODE_BACK) { + dismissDropDown(); + return true; + } + } + return super.onKeyPreIme(keyCode, event); + } + + @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (isPopupShowing()) { boolean consumed = mDropDownList.onKeyUp(keyCode, event); @@ -280,18 +357,41 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe public boolean onKeyDown(int keyCode, KeyEvent event) { // when the drop down is shown, we drive it directly if (isPopupShowing()) { - // special case for the back key, we do not even try to send it - // to the drop down list but instead, consume it immediately - if (keyCode == KeyEvent.KEYCODE_BACK) { - dismissDropDown(); - return true; - // the key events are forwarded to the list in the drop down view // note that ListView handles space but we don't want that to happen - } else if (keyCode != KeyEvent.KEYCODE_SPACE) { - boolean consumed = mDropDownList.onKeyDown(keyCode, event); - + if (keyCode != KeyEvent.KEYCODE_SPACE) { + int curIndex = mDropDownList.getSelectedItemPosition(); + boolean consumed; + if (keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= 0) { + // When the selection is at the top, we block the key + // event to prevent focus from moving. + mDropDownList.hideSelector(); + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); + mPopup.update(); + return true; + } + consumed = mDropDownList.onKeyDown(keyCode, event); + if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + + consumed); if (consumed) { + // If it handled the key event, then the user is + // navigating in the list, so we should put it in front. + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + // Here's a little trick we need to do to make sure that + // the list view is actually showing its focus indicator, + // by ensuring it has focus and getting its window out + // of touch mode. + mDropDownList.requestFocusFromTouch(); + if (false) { + // Update whether the pop-up is in front of or behind + // the input method, depending on whether the user has + // moved down in it. + mPopup.setInputMethodMode(curIndex > 0 + ? PopupWindow.INPUT_METHOD_NOT_NEEDED + : PopupWindow.INPUT_METHOD_NEEDED); + } + mPopup.update(); + switch (keyCode) { // avoid passing the focus from the text view to the // next component @@ -301,22 +401,14 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe case KeyEvent.KEYCODE_DPAD_UP: return true; } - } else{ - int index = mDropDownList.getSelectedItemPosition(); - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_UP: - if (index == 0) { - return true; - } - break; + } else { + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { // when the selection is at the bottom, we block the // event to avoid going to the next focusable widget - case KeyEvent.KEYCODE_DPAD_DOWN: - Adapter adapter = mDropDownList.getAdapter(); - if (index == adapter.getCount() - 1) { - return true; - } - break; + Adapter adapter = mDropDownList.getAdapter(); + if (curIndex == adapter.getCount() - 1) { + return true; + } } } } @@ -327,37 +419,9 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe } } - // when text is changed, inserted or deleted, we attempt to show - // the drop down - boolean openBefore = isPopupShowing(); - mTextChanged = false; - + mLastKeyCode = keyCode; boolean handled = super.onKeyDown(keyCode, event); - - // if the list was open before the keystroke, but closed afterwards, - // then something in the keystroke processing (an input filter perhaps) - // called performCompletion() and we shouldn't do any more processing. - if (openBefore && !isPopupShowing()) { - return handled; - } - - if (mTextChanged) { // would have been set in onTextChanged() - // the drop down is shown only when a minimum number of characters - // was typed in the text view - if (enoughToFilter()) { - if (mFilter != null) { - performFiltering(getText(), keyCode); - } - } else { - // drop down is automatically dismissed when enough characters - // are deleted from the text view - dismissDropDown(); - if (mFilter != null) { - mFilter.filter(null); - } - } - return true; - } + mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; return handled; } @@ -369,14 +433,58 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * triggered. */ public boolean enoughToFilter() { + if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getText().length() + + " threshold=" + mThreshold); return getText().length() >= mThreshold; } - @Override - protected void onTextChanged(CharSequence text, int start, int before, - int after) { - super.onTextChanged(text, start, before, after); - mTextChanged = true; + /** + * This is used to watch for edits to the text view. Note that we call + * to methods on the auto complete text view class so that we can access + * private vars without going through thunks. + */ + private class MyWatcher implements TextWatcher { + public void afterTextChanged(Editable s) { + doAfterTextChanged(); + } + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + doBeforeTextChanged(); + } + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + } + + void doBeforeTextChanged() { + // when text is changed, inserted or deleted, we attempt to show + // the drop down + mOpenBefore = isPopupShowing(); + if (DEBUG) Log.v(TAG, "before text changed: open=" + mOpenBefore); + } + + void doAfterTextChanged() { + // if the list was open before the keystroke, but closed afterwards, + // then something in the keystroke processing (an input filter perhaps) + // called performCompletion() and we shouldn't do any more processing. + if (DEBUG) Log.v(TAG, "after text changed: openBefore=" + mOpenBefore + + " open=" + isPopupShowing()); + if (mOpenBefore && !isPopupShowing()) { + return; + } + + // the drop down is shown only when a minimum number of characters + // was typed in the text view + if (enoughToFilter()) { + if (mFilter != null) { + performFiltering(getText(), mLastKeyCode); + } + } else { + // drop down is automatically dismissed when enough characters + // are deleted from the text view + dismissDropDown(); + if (mFilter != null) { + mFilter.filter(null); + } + } } /** @@ -407,7 +515,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * <code>text</code>.</p> * * @param text the filtering pattern - * @param keyCode the last character inserted in the edit box + * @param keyCode the last character inserted in the edit box; beware that + * this will be null when text is being added through a soft input method. */ @SuppressWarnings({ "UnusedDeclaration" }) protected void performFiltering(CharSequence text, int keyCode) { @@ -423,6 +532,19 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe performCompletion(null, -1, -1); } + @Override public void onCommitCompletion(CompletionInfo completion) { + if (isPopupShowing()) { + replaceText(completion.getText()); + 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()); + } + } + } + private void performCompletion(View selectedView, int position, long id) { if (isPopupShowing()) { Object selectedItem; @@ -464,7 +586,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe public void onFilterComplete(int count) { /* - * This checks enoughToFilter() again because filtering requests + * This checks enoughToFilter() again because filtering requests * are asynchronous, so the result may come back after enough text * has since been deleted to make it no longer appropriate * to filter. @@ -497,22 +619,32 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe } } + @Override + protected void onDetachedFromWindow() { + dismissDropDown(); + super.onDetachedFromWindow(); + } + /** * <p>Closes the drop down if present on screen.</p> */ public void dismissDropDown() { - mPopup.dismiss(); - if (mDropDownList != null) { - // start next time with no selection - mDropDownList.hideSelector(); + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + imm.displayCompletions(this, null); } + mPopup.dismiss(); + mPopup.setContentView(null); + mDropDownList = null; } @Override protected boolean setFrame(int l, int t, int r, int b) { boolean result = super.setFrame(l, t, r, b); - mPopup.update(this, getMeasuredWidth(), -1); + if (mPopup.isShowing()) { + mPopup.update(this, getMeasuredWidth() - mPaddingLeft - mPaddingRight, -1); + } return result; } @@ -523,11 +655,20 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe public void showDropDown() { int height = buildDropDown(); if (mPopup.isShowing()) { - mPopup.update(this, getMeasuredWidth() - mPaddingLeft - mPaddingRight, height); + mPopup.update(this, mDropDownHorizontalOffset, mDropDownVerticalOffset, + getMeasuredWidth() - mPaddingLeft - mPaddingRight, height); } else { - mPopup.setHeight(height); + mPopup.setWindowLayoutMode(0, ViewGroup.LayoutParams.WRAP_CONTENT); mPopup.setWidth(getMeasuredWidth() - mPaddingLeft - mPaddingRight); - mPopup.showAsDropDown(this); + mPopup.setHeight(height); + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); + mPopup.setOutsideTouchable(true); + mPopup.setTouchInterceptor(new PopupTouchIntercepter()); + mPopup.showAsDropDown(this, mDropDownHorizontalOffset, mDropDownVerticalOffset); + mDropDownList.hideSelector(); + mDropDownList.setSelection(0); + mDropDownList.requestFocus(); + post(mHideSelector); } } @@ -541,14 +682,34 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe ViewGroup dropDownView; int otherHeights = 0; + if (mAdapter != null) { + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + int N = mAdapter.getCount(); + if (N > 20) N = 20; + CompletionInfo[] completions = new CompletionInfo[N]; + for (int i=0; i<N; i++) { + Object item = mAdapter.getItem(i); + long id = mAdapter.getItemId(i); + completions[i] = new CompletionInfo(id, i, + convertSelectionToString(item)); + } + imm.displayCompletions(this, completions); + } + } + if (mDropDownList == null) { Context context = getContext(); + mHideSelector = new ListSelectorHider(); + mDropDownList = new DropDownListView(context); mDropDownList.setSelector(mDropDownListHighlight); mDropDownList.setAdapter(mAdapter); mDropDownList.setVerticalFadingEdgeEnabled(true); mDropDownList.setOnItemClickListener(mDropDownItemClickListener); + mDropDownList.setFocusable(true); + mDropDownList.setFocusableInTouchMode(true); if (mItemSelectedListener != null) { mDropDownList.setOnItemSelectedListener(mItemSelectedListener); @@ -614,6 +775,73 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe } } + /** + * Sets the validator used to perform text validation. + * + * @param validator The validator used to validate the text entered in this widget. + * + * @see #getValidator() + * @see #performValidation() + */ + public void setValidator(Validator validator) { + mValidator = validator; + } + + /** + * Returns the Validator set with {@link #setValidator}, + * or <code>null</code> if it was not set. + * + * @see #setValidator(android.widget.AutoCompleteTextView.Validator) + * @see #performValidation() + */ + public Validator getValidator() { + return mValidator; + } + + /** + * If a validator was set on this view and the current string is not valid, + * ask the validator to fix it. + * + * @see #getValidator() + * @see #setValidator(android.widget.AutoCompleteTextView.Validator) + */ + public void performValidation() { + if (mValidator == null) return; + + CharSequence text = getText(); + + if (!TextUtils.isEmpty(text) && !mValidator.isValid(text)) { + setText(mValidator.fixText(text)); + } + } + + /** + * Returns the Filter obtained from {@link Filterable#getFilter}, + * or <code>null</code> if {@link #setAdapter} was not called with + * a Filterable. + */ + protected Filter getFilter() { + return mFilter; + } + + private class ListSelectorHider implements Runnable { + public void run() { + if (mDropDownList != null) { + mDropDownList.hideSelector(); + } + } + } + + private class PopupTouchIntercepter implements OnTouchListener { + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + mPopup.update(); + } + return false; + } + } + private class DropDownItemClickListener implements AdapterView.OnItemClickListener { public void onItemClick(AdapterView parent, View v, int position, long id) { performCompletion(v, position, id); @@ -701,6 +929,21 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe public boolean hasFocus() { return true; } + + protected int[] onCreateDrawableState(int extraSpace) { + int[] res = super.onCreateDrawableState(extraSpace); + if (false) { + StringBuilder sb = new StringBuilder("Created drawable state: ["); + for (int i=0; i<res.length; i++) { + if (i > 0) sb.append(", "); + sb.append("0x"); + sb.append(Integer.toHexString(res[i])); + } + sb.append("]"); + Log.i(TAG, sb.toString()); + } + return res; + } } /** @@ -709,54 +952,26 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * this View with an incorrect value in it, all we can do is try to fix it ourselves * when this happens. */ - static public interface Validator { + public interface Validator { /** - * @return true if the text currently in the text editor is valid. + * Validates the specified text. + * + * @return true If the text currently in the text editor is valid. + * + * @see #fixText(CharSequence) */ boolean isValid(CharSequence text); /** - * @param invalidText a string that doesn't pass validation: - * isValid(invalidText) returns false - * @return a string based on invalidText such as invoking isValid() on it returns true. + * Corrects the specified text to make it valid. + * + * @param invalidText A string that doesn't pass validation: isValid(invalidText) + * returns false + * + * @return A string based on invalidText such as invoking isValid() on it returns true. + * + * @see #isValid(CharSequence) */ CharSequence fixText(CharSequence invalidText); } - - private Validator mValidator = null; - - public void setValidator(Validator validator) { - mValidator = validator; - } - - /** - * Returns the Validator set with {@link #setValidator}, - * or <code>null</code> if it was not set. - */ - public Validator getValidator() { - return mValidator; - } - - /** - * If a validator was set on this view and the current string is not valid, - * ask the validator to fix it. - */ - public void performValidation() { - if (mValidator == null) return; - - CharSequence text = getText(); - - if (!TextUtils.isEmpty(text) && !mValidator.isValid(text)) { - setText(mValidator.fixText(text)); - } - } - - /** - * Returns the Filter obtained from {@link Filterable#getFilter}, - * or <code>null</code> if {@link #setAdapter} was not called with - * a Filterable. - */ - protected Filter getFilter() { - return mFilter; - } } diff --git a/core/java/android/widget/BaseExpandableListAdapter.java b/core/java/android/widget/BaseExpandableListAdapter.java index 3a8bb2a..1bba7f0 100644 --- a/core/java/android/widget/BaseExpandableListAdapter.java +++ b/core/java/android/widget/BaseExpandableListAdapter.java @@ -71,7 +71,7 @@ public abstract class BaseExpandableListAdapter implements ExpandableListAdapter * <p> * Base implementation returns a long: * <li> bit 0: Whether this ID points to a child (unset) or group (set), so for this method - * this bit will be 0. + * this bit will be 1. * <li> bit 1-31: Lower 31 bits of the groupId * <li> bit 32-63: Lower 32 bits of the childId. * <p> @@ -86,7 +86,7 @@ public abstract class BaseExpandableListAdapter implements ExpandableListAdapter * <p> * Base implementation returns a long: * <li> bit 0: Whether this ID points to a child (unset) or group (set), so for this method - * this bit will be 1. + * this bit will be 0. * <li> bit 1-31: Lower 31 bits of the groupId * <li> bit 32-63: Lower 32 bits of the childId. * <p> diff --git a/core/java/android/widget/Chronometer.java b/core/java/android/widget/Chronometer.java index 31d2063..7086ae2 100644 --- a/core/java/android/widget/Chronometer.java +++ b/core/java/android/widget/Chronometer.java @@ -22,7 +22,7 @@ import android.graphics.Canvas; import android.os.Handler; import android.os.Message; import android.os.SystemClock; -import android.pim.DateUtils; +import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; import android.widget.RemoteViews.RemoteView; diff --git a/core/java/android/widget/CursorAdapter.java b/core/java/android/widget/CursorAdapter.java index cacaeab..555f216 100644 --- a/core/java/android/widget/CursorAdapter.java +++ b/core/java/android/widget/CursorAdapter.java @@ -357,9 +357,8 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, @Override public void onChange(boolean selfChange) { - if (mAutoRequery && mCursor != null) { - if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + - " due to update"); + if (mAutoRequery && mCursor != null && !mCursor.isClosed()) { + if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); mDataValid = mCursor.requery(); } } diff --git a/core/java/android/widget/CursorTreeAdapter.java b/core/java/android/widget/CursorTreeAdapter.java index fa8fd4b..7b9b7bd 100644 --- a/core/java/android/widget/CursorTreeAdapter.java +++ b/core/java/android/widget/CursorTreeAdapter.java @@ -450,10 +450,9 @@ public abstract class CursorTreeAdapter extends BaseExpandableListAdapter implem } void changeCursor(Cursor cursor, boolean releaseCursors) { - if (mCursor != null) { - mCursor.unregisterContentObserver(mContentObserver); - mCursor.unregisterDataSetObserver(mDataSetObserver); - } + if (cursor == mCursor) return; + + deactivate(); mCursor = cursor; if (cursor != null) { cursor.registerContentObserver(mContentObserver); diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java index c03bd32..67010b2 100644 --- a/core/java/android/widget/DatePicker.java +++ b/core/java/android/widget/DatePicker.java @@ -21,7 +21,7 @@ import android.content.Context; import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; -import android.pim.DateFormat; +import android.text.format.DateFormat; import android.util.AttributeSet; import android.util.SparseArray; import android.view.LayoutInflater; diff --git a/core/java/android/widget/DigitalClock.java b/core/java/android/widget/DigitalClock.java index 3ca2c81..379883a 100644 --- a/core/java/android/widget/DigitalClock.java +++ b/core/java/android/widget/DigitalClock.java @@ -21,8 +21,8 @@ import android.content.res.Resources; import android.database.ContentObserver; import android.os.Handler; import android.os.SystemClock; -import android.pim.DateFormat; import android.provider.Settings; +import android.text.format.DateFormat; import android.util.AttributeSet; import java.util.Calendar; @@ -105,13 +105,7 @@ public class DigitalClock extends TextView { * Pulls 12/24 mode from system settings */ private boolean get24HourMode() { - String value = Settings.System.getString( - getContext().getContentResolver(), - Settings.System.TIME_12_24); - - if (value == null || value.equals("12")) - return false; - return true; + return android.text.format.DateFormat.is24HourFormat(getContext()); } private void setFormat() { diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java index e89a2bd..57aca24 100644 --- a/core/java/android/widget/EditText.java +++ b/core/java/android/widget/EditText.java @@ -19,7 +19,6 @@ package android.widget; import android.text.*; import android.text.method.*; import android.content.Context; -import android.content.res.Resources; import android.util.AttributeSet; @@ -99,4 +98,13 @@ public class EditText extends TextView { public void extendSelection(int index) { Selection.extendSelection(getText(), index); } + + @Override + public void setEllipsize(TextUtils.TruncateAt ellipsis) { + if (ellipsis == TextUtils.TruncateAt.MARQUEE) { + throw new IllegalArgumentException("EditText cannot use the ellipsize mode " + + "TextUtils.TruncateAt.MARQUEE"); + } + super.setEllipsize(ellipsis); + } } diff --git a/core/java/android/widget/ExpandableListConnector.java b/core/java/android/widget/ExpandableListConnector.java index ddedea3..ccce7c1 100644 --- a/core/java/android/widget/ExpandableListConnector.java +++ b/core/java/android/widget/ExpandableListConnector.java @@ -19,11 +19,13 @@ package android.widget; import android.database.DataSetObserver; import android.os.Parcel; import android.os.Parcelable; -import android.view.KeyEvent; +import android.os.SystemClock; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /* * Implementation notes: @@ -120,7 +122,7 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { * either), so flPos must be a group and its group pos will be the * same as its flPos */ - return new PositionMetadata(flPos, ExpandableListPosition.GROUP, flPos, + return PositionMetadata.obtain(flPos, ExpandableListPosition.GROUP, flPos, -1, null, 0); } @@ -159,7 +161,7 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { * The flat list position is this middle group's flat list * position, so we've found an exact hit */ - return new PositionMetadata(flPos, ExpandableListPosition.GROUP, + return PositionMetadata.obtain(flPos, ExpandableListPosition.GROUP, midExpGm.gPos, -1, midExpGm, midExpGroupIndex); } else if (flPos <= midExpGm.lastChildFlPos /* && flPos > midGm.flPos as deduced from previous @@ -172,7 +174,7 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { * the group */ final int childPos = flPos - (midExpGm.flPos + 1); - return new PositionMetadata(flPos, ExpandableListPosition.CHILD, + return PositionMetadata.obtain(flPos, ExpandableListPosition.CHILD, midExpGm.gPos, childPos, midExpGm, midExpGroupIndex); } } @@ -184,11 +186,13 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { */ - /* If we are to expand this group later, where would it go in the - * mExpGroupMetadataList ? */ + /** + * If we are to expand this group later, where would it go in the + * mExpGroupMetadataList ? + */ int insertPosition = 0; - /* What is its group position from the list of all groups? */ + /** What is its group position in the list of all groups? */ int groupPos = 0; /* @@ -237,7 +241,7 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { throw new RuntimeException("Unknown state"); } - return new PositionMetadata(flPos, ExpandableListPosition.GROUP, groupPos, -1, + return PositionMetadata.obtain(flPos, ExpandableListPosition.GROUP, groupPos, -1, null, insertPosition); } @@ -250,7 +254,7 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { * @param pos a {@link ExpandableListPosition} representing either a group position * or child position * @return the flat list position encompassed in a {@link PositionMetadata} - * object that contains additional useful info for insertion, etc. + * object that contains additional useful info for insertion, etc., or null. */ PositionMetadata getFlattenedPos(final ExpandableListPosition pos) { final ArrayList<GroupMetadata> egml = mExpGroupMetadataList; @@ -268,7 +272,7 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { * its flPos will be the same as its group pos. The * insert position is 0 (since the list is empty). */ - return new PositionMetadata(pos.groupPos, pos.type, + return PositionMetadata.obtain(pos.groupPos, pos.type, pos.groupPos, pos.childPos, null, 0); } @@ -298,11 +302,11 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { if (pos.type == ExpandableListPosition.GROUP) { /* If it's a group, give them this matched group's flPos */ - return new PositionMetadata(midExpGm.flPos, pos.type, + return PositionMetadata.obtain(midExpGm.flPos, pos.type, pos.groupPos, pos.childPos, midExpGm, midExpGroupIndex); } else if (pos.type == ExpandableListPosition.CHILD) { /* If it's a child, calculate the flat list pos */ - return new PositionMetadata(midExpGm.flPos + pos.childPos + return PositionMetadata.obtain(midExpGm.flPos + pos.childPos + 1, pos.type, pos.groupPos, pos.childPos, midExpGm, midExpGroupIndex); } else { @@ -341,7 +345,7 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { leftExpGm.lastChildFlPos + (pos.groupPos - leftExpGm.gPos); - return new PositionMetadata(flPos, pos.type, pos.groupPos, + return PositionMetadata.obtain(flPos, pos.type, pos.groupPos, pos.childPos, null, leftExpGroupIndex); } else if (rightExpGroupIndex < midExpGroupIndex) { @@ -355,7 +359,7 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { final int flPos = rightExpGm.flPos - (rightExpGm.gPos - pos.groupPos); - return new PositionMetadata(flPos, pos.type, pos.groupPos, + return PositionMetadata.obtain(flPos, pos.type, pos.groupPos, pos.childPos, null, rightExpGroupIndex); } else { return null; @@ -370,13 +374,18 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { @Override public boolean isEnabled(int flatListPos) { final ExpandableListPosition pos = getUnflattenedPos(flatListPos).position; - + + boolean retValue; if (pos.type == ExpandableListPosition.CHILD) { - return mExpandableListAdapter.isChildSelectable(pos.groupPos, pos.childPos); + retValue = mExpandableListAdapter.isChildSelectable(pos.groupPos, pos.childPos); } else { // Groups are always selectable - return true; + retValue = true; } + + pos.recycle(); + + return retValue; } public int getCount() { @@ -391,62 +400,80 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { public Object getItem(int flatListPos) { final PositionMetadata posMetadata = getUnflattenedPos(flatListPos); + Object retValue; if (posMetadata.position.type == ExpandableListPosition.GROUP) { - return mExpandableListAdapter + retValue = mExpandableListAdapter .getGroup(posMetadata.position.groupPos); } else if (posMetadata.position.type == ExpandableListPosition.CHILD) { - return mExpandableListAdapter.getChild(posMetadata.position.groupPos, + retValue = mExpandableListAdapter.getChild(posMetadata.position.groupPos, posMetadata.position.childPos); } else { // TODO: clean exit throw new RuntimeException("Flat list position is of unknown type"); } + + posMetadata.recycle(); + + return retValue; } public long getItemId(int flatListPos) { final PositionMetadata posMetadata = getUnflattenedPos(flatListPos); final long groupId = mExpandableListAdapter.getGroupId(posMetadata.position.groupPos); + long retValue; if (posMetadata.position.type == ExpandableListPosition.GROUP) { - return mExpandableListAdapter.getCombinedGroupId(groupId); + retValue = mExpandableListAdapter.getCombinedGroupId(groupId); } else if (posMetadata.position.type == ExpandableListPosition.CHILD) { final long childId = mExpandableListAdapter.getChildId(posMetadata.position.groupPos, posMetadata.position.childPos); - return mExpandableListAdapter.getCombinedChildId(groupId, childId); + retValue = mExpandableListAdapter.getCombinedChildId(groupId, childId); } else { // TODO: clean exit throw new RuntimeException("Flat list position is of unknown type"); } + + posMetadata.recycle(); + + return retValue; } public View getView(int flatListPos, View convertView, ViewGroup parent) { final PositionMetadata posMetadata = getUnflattenedPos(flatListPos); + View retValue; if (posMetadata.position.type == ExpandableListPosition.GROUP) { - return mExpandableListAdapter.getGroupView(posMetadata.position.groupPos, posMetadata + retValue = mExpandableListAdapter.getGroupView(posMetadata.position.groupPos, posMetadata .isExpanded(), convertView, parent); } else if (posMetadata.position.type == ExpandableListPosition.CHILD) { final boolean isLastChild = posMetadata.groupMetadata.lastChildFlPos == flatListPos; - final View view = mExpandableListAdapter.getChildView(posMetadata.position.groupPos, + retValue = mExpandableListAdapter.getChildView(posMetadata.position.groupPos, posMetadata.position.childPos, isLastChild, convertView, parent); - - return view; } else { // TODO: clean exit throw new RuntimeException("Flat list position is of unknown type"); } + + posMetadata.recycle(); + + return retValue; } @Override public int getItemViewType(int flatListPos) { final ExpandableListPosition pos = getUnflattenedPos(flatListPos).position; + int retValue; if (pos.type == ExpandableListPosition.GROUP) { - return 0; + retValue = 0; } else { - return 1; + retValue = 1; } + + pos.recycle(); + + return retValue; } @Override @@ -464,22 +491,51 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { * positions. * * @param forceChildrenCountRefresh Forces refreshing of the children count - * for all expanded groups. + * for all expanded groups. + * @param syncGroupPositions Whether to search for the group positions + * based on the group IDs. This should only be needed when calling + * this from an onChanged callback. */ - private void refreshExpGroupMetadataList(boolean forceChildrenCountRefresh) { + @SuppressWarnings("unchecked") + private void refreshExpGroupMetadataList(boolean forceChildrenCountRefresh, + boolean syncGroupPositions) { final ArrayList<GroupMetadata> egml = mExpGroupMetadataList; - final int egmlSize = egml.size(); + int egmlSize = egml.size(); int curFlPos = 0; /* Update child count as we go through */ mTotalExpChildrenCount = 0; - GroupMetadata curGm; + if (syncGroupPositions) { + // We need to check whether any groups have moved positions + boolean positionsChanged = false; + + for (int i = egmlSize - 1; i >= 0; i--) { + GroupMetadata curGm = egml.get(i); + int newGPos = findGroupPosition(curGm.gId, curGm.gPos); + if (newGPos != curGm.gPos) { + if (newGPos == AdapterView.INVALID_POSITION) { + // Doh, just remove it from the list of expanded groups + egml.remove(i); + egmlSize--; + } + + curGm.gPos = newGPos; + if (!positionsChanged) positionsChanged = true; + } + } + + if (positionsChanged) { + // At least one group changed positions, so re-sort + Collections.sort(egml); + } + } + int gChildrenCount; int lastGPos = 0; for (int i = 0; i < egmlSize; i++) { /* Store in local variable since we'll access freq */ - curGm = egml.get(i); + GroupMetadata curGm = egml.get(i); /* * Get the number of children, try to refrain from calling @@ -518,8 +574,13 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { * @param groupPos position of the group to collapse */ boolean collapseGroup(int groupPos) { - return collapseGroup(getFlattenedPos(new ExpandableListPosition(ExpandableListPosition.GROUP, - groupPos, -1, -1))); + PositionMetadata pm = getFlattenedPos(ExpandableListPosition.obtain( + ExpandableListPosition.GROUP, groupPos, -1, -1)); + if (pm == null) return false; + + boolean retValue = collapseGroup(pm); + pm.recycle(); + return retValue; } boolean collapseGroup(PositionMetadata posMetadata) { @@ -538,7 +599,7 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { mExpGroupMetadataList.remove(posMetadata.groupMetadata); // Refresh the metadata - refreshExpGroupMetadataList(false); + refreshExpGroupMetadataList(false, false); // Notify of change notifyDataSetChanged(); @@ -554,8 +615,11 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { * @param groupPos the group to be expanded */ boolean expandGroup(int groupPos) { - return expandGroup(getFlattenedPos(new ExpandableListPosition(ExpandableListPosition.GROUP, - groupPos, -1, -1))); + PositionMetadata pm = getFlattenedPos(ExpandableListPosition.obtain( + ExpandableListPosition.GROUP, groupPos, -1, -1)); + boolean retValue = expandGroup(pm); + pm.recycle(); + return retValue; } boolean expandGroup(PositionMetadata posMetadata) { @@ -590,16 +654,16 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { } } - GroupMetadata expandedGm = new GroupMetadata(); - - expandedGm.gPos = posMetadata.position.groupPos; - expandedGm.flPos = GroupMetadata.REFRESH; - expandedGm.lastChildFlPos = GroupMetadata.REFRESH; + GroupMetadata expandedGm = GroupMetadata.obtain( + GroupMetadata.REFRESH, + GroupMetadata.REFRESH, + posMetadata.position.groupPos, + mExpandableListAdapter.getGroupId(posMetadata.position.groupPos)); mExpGroupMetadataList.add(posMetadata.groupInsertIndex, expandedGm); // Refresh the metadata - refreshExpGroupMetadataList(false); + refreshExpGroupMetadataList(false, false); // Notify of change notifyDataSetChanged(); @@ -669,7 +733,7 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { } mExpGroupMetadataList = expandedGroupMetadataList; - refreshExpGroupMetadataList(true); + refreshExpGroupMetadataList(true, false); } @Override @@ -678,17 +742,104 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { return adapter != null ? adapter.isEmpty() : true; } + /** + * Searches the expandable list adapter for a group position matching the + * given group ID. The search starts at the given seed position and then + * alternates between moving up and moving down until 1) we find the right + * position, or 2) we run out of time, or 3) we have looked at every + * position + * + * @return Position of the row that matches the given row ID, or + * {@link AdapterView#INVALID_POSITION} if it can't be found + * @see AdapterView#findSyncPosition() + */ + int findGroupPosition(long groupIdToMatch, int seedGroupPosition) { + int count = mExpandableListAdapter.getGroupCount(); + + if (count == 0) { + return AdapterView.INVALID_POSITION; + } + + // If there isn't a selection don't hunt for it + if (groupIdToMatch == AdapterView.INVALID_ROW_ID) { + return AdapterView.INVALID_POSITION; + } + + // Pin seed to reasonable values + seedGroupPosition = Math.max(0, seedGroupPosition); + seedGroupPosition = Math.min(count - 1, seedGroupPosition); + + long endTime = SystemClock.uptimeMillis() + AdapterView.SYNC_MAX_DURATION_MILLIS; + + long rowId; + + // first position scanned so far + int first = seedGroupPosition; + + // last position scanned so far + int last = seedGroupPosition; + + // True if we should move down on the next iteration + boolean next = false; + + // True when we have looked at the first item in the data + boolean hitFirst; + + // True when we have looked at the last item in the data + boolean hitLast; + + // Get the item ID locally (instead of getItemIdAtPosition), so + // we need the adapter + ExpandableListAdapter adapter = getAdapter(); + if (adapter == null) { + return AdapterView.INVALID_POSITION; + } + + while (SystemClock.uptimeMillis() <= endTime) { + rowId = adapter.getGroupId(seedGroupPosition); + if (rowId == groupIdToMatch) { + // Found it! + return seedGroupPosition; + } + + hitLast = last == count - 1; + hitFirst = first == 0; + + if (hitLast && hitFirst) { + // Looked at everything + break; + } + + if (hitFirst || (next && !hitLast)) { + // Either we hit the top, or we are trying to move down + last++; + seedGroupPosition = last; + // Try going up next time + next = false; + } else if (hitLast || (!next && !hitFirst)) { + // Either we hit the bottom, or we are trying to move up + first--; + seedGroupPosition = first; + // Try going down next time + next = true; + } + + } + + return AdapterView.INVALID_POSITION; + } + protected class MyDataSetObserver extends DataSetObserver { @Override public void onChanged() { - refreshExpGroupMetadataList(true); + refreshExpGroupMetadataList(true, true); notifyDataSetChanged(); } @Override public void onInvalidated() { - refreshExpGroupMetadataList(true); + refreshExpGroupMetadataList(true, true); notifyDataSetInvalidated(); } @@ -699,7 +850,7 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { * position to either a) group position for groups, or b) child position for * children */ - static class GroupMetadata implements Parcelable { + static class GroupMetadata implements Parcelable, Comparable { final static int REFRESH = -1; /** This group's flat list position */ @@ -717,6 +868,31 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { * This group's group position */ int gPos; + + /** + * This group's id + */ + long gId; + + private GroupMetadata() { + } + + static GroupMetadata obtain(int flPos, int lastChildFlPos, int gPos, long gId) { + GroupMetadata gm = new GroupMetadata(); + gm.flPos = flPos; + gm.lastChildFlPos = lastChildFlPos; + gm.gPos = gPos; + gm.gId = gId; + return gm; + } + + public int compareTo(Object another) { + if (another == null || !(another instanceof GroupMetadata)) { + throw new ClassCastException(); + } + + return gPos - ((GroupMetadata) another).gPos; + } public int describeContents() { return 0; @@ -726,16 +902,18 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { dest.writeInt(flPos); dest.writeInt(lastChildFlPos); dest.writeInt(gPos); + dest.writeLong(gId); } public static final Parcelable.Creator<GroupMetadata> CREATOR = new Parcelable.Creator<GroupMetadata>() { public GroupMetadata createFromParcel(Parcel in) { - GroupMetadata gm = new GroupMetadata(); - gm.flPos = in.readInt(); - gm.lastChildFlPos = in.readInt(); - gm.gPos = in.readInt(); + GroupMetadata gm = GroupMetadata.obtain( + in.readInt(), + in.readInt(), + in.readInt(), + in.readLong()); return gm; } @@ -752,6 +930,11 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { * where to insert into the flat list, etc.) */ static public class PositionMetadata { + + private static final int MAX_POOL_SIZE = 5; + private static ArrayList<PositionMetadata> sPool = + new ArrayList<PositionMetadata>(MAX_POOL_SIZE); + /** Data type to hold the position and its type (child/group) */ public ExpandableListPosition position; @@ -771,17 +954,46 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { */ public int groupInsertIndex; - public PositionMetadata(int flatListPos, int type, int groupPos, - int childPos) { - position = new ExpandableListPosition(type, groupPos, childPos, flatListPos); + private void resetState() { + position = null; + groupMetadata = null; + groupInsertIndex = 0; + } + + /** + * Use {@link #obtain(int, int, int, int, GroupMetadata, int)} + */ + private PositionMetadata() { } - protected PositionMetadata(int flatListPos, int type, int groupPos, + static PositionMetadata obtain(int flatListPos, int type, int groupPos, int childPos, GroupMetadata groupMetadata, int groupInsertIndex) { - position = new ExpandableListPosition(type, groupPos, childPos, flatListPos); - - this.groupMetadata = groupMetadata; - this.groupInsertIndex = groupInsertIndex; + PositionMetadata pm = getRecycledOrCreate(); + pm.position = ExpandableListPosition.obtain(type, groupPos, childPos, flatListPos); + pm.groupMetadata = groupMetadata; + pm.groupInsertIndex = groupInsertIndex; + return pm; + } + + private static PositionMetadata getRecycledOrCreate() { + PositionMetadata pm; + synchronized (sPool) { + if (sPool.size() > 0) { + pm = sPool.remove(0); + } else { + return new PositionMetadata(); + } + } + pm.resetState(); + return pm; + } + + public void recycle() { + synchronized (sPool) { + if (sPool.size() < MAX_POOL_SIZE) { + sPool.add(this); + } + } } /** diff --git a/core/java/android/widget/ExpandableListPosition.java b/core/java/android/widget/ExpandableListPosition.java index 71e970c..e8d6113 100644 --- a/core/java/android/widget/ExpandableListPosition.java +++ b/core/java/android/widget/ExpandableListPosition.java @@ -16,6 +16,8 @@ package android.widget; +import java.util.ArrayList; + /** * ExpandableListPosition can refer to either a group's position or a child's * position. Referring to a child's position requires both a group position (the @@ -24,6 +26,11 @@ package android.widget; * {@link #obtainGroupPosition(int)}. */ class ExpandableListPosition { + + private static final int MAX_POOL_SIZE = 5; + private static ArrayList<ExpandableListPosition> sPool = + new ArrayList<ExpandableListPosition>(MAX_POOL_SIZE); + /** * This data type represents a child position */ @@ -56,21 +63,14 @@ class ExpandableListPosition { */ public int type; - ExpandableListPosition(int type, int groupPos, int childPos, int flatListPos) { - this.type = type; - this.flatListPos = flatListPos; - this.groupPos = groupPos; - this.childPos = childPos; + private void resetState() { + groupPos = 0; + childPos = 0; + flatListPos = 0; + type = 0; } - - /** - * Used internally by the {@link #obtainChildPosition} and - * {@link #obtainGroupPosition} methods to construct a new object. - */ - private ExpandableListPosition(int type, int groupPos, int childPos) { - this.type = type; - this.groupPos = groupPos; - this.childPos = childPos; + + private ExpandableListPosition() { } long getPackedPosition() { @@ -79,11 +79,11 @@ class ExpandableListPosition { } static ExpandableListPosition obtainGroupPosition(int groupPosition) { - return new ExpandableListPosition(GROUP, groupPosition, 0); + return obtain(GROUP, groupPosition, 0, 0); } static ExpandableListPosition obtainChildPosition(int groupPosition, int childPosition) { - return new ExpandableListPosition(CHILD, groupPosition, childPosition); + return obtain(CHILD, groupPosition, childPosition, 0); } static ExpandableListPosition obtainPosition(long packedPosition) { @@ -91,12 +91,45 @@ class ExpandableListPosition { return null; } - final int type = ExpandableListView.getPackedPositionType(packedPosition) == - ExpandableListView.PACKED_POSITION_TYPE_CHILD ? CHILD : GROUP; - - return new ExpandableListPosition(type, ExpandableListView - .getPackedPositionGroup(packedPosition), ExpandableListView - .getPackedPositionChild(packedPosition)); + ExpandableListPosition elp = getRecycledOrCreate(); + elp.groupPos = ExpandableListView.getPackedPositionGroup(packedPosition); + if (ExpandableListView.getPackedPositionType(packedPosition) == + ExpandableListView.PACKED_POSITION_TYPE_CHILD) { + elp.type = CHILD; + elp.childPos = ExpandableListView.getPackedPositionChild(packedPosition); + } else { + elp.type = GROUP; + } + return elp; + } + + static ExpandableListPosition obtain(int type, int groupPos, int childPos, int flatListPos) { + ExpandableListPosition elp = getRecycledOrCreate(); + elp.type = type; + elp.groupPos = groupPos; + elp.childPos = childPos; + elp.flatListPos = flatListPos; + return elp; + } + + private static ExpandableListPosition getRecycledOrCreate() { + ExpandableListPosition elp; + synchronized (sPool) { + if (sPool.size() > 0) { + elp = sPool.remove(0); + } else { + return new ExpandableListPosition(); + } + } + elp.resetState(); + return elp; } + public void recycle() { + synchronized (sPool) { + if (sPool.size() < MAX_POOL_SIZE) { + sPool.add(this); + } + } + } } diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java index 138cace..3de561a 100644 --- a/core/java/android/widget/ExpandableListView.java +++ b/core/java/android/widget/ExpandableListView.java @@ -25,6 +25,7 @@ import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.graphics.drawable.ColorDrawable; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; @@ -184,7 +185,8 @@ public class ExpandableListView extends ListView { /** Drawable to be used as a divider when it is adjacent to any children */ private Drawable mChildDivider; - + private boolean mClipChildDivider; + public ExpandableListView(Context context) { this(context, null); } @@ -245,7 +247,7 @@ public class ExpandableListView extends ListView { final int myB = mBottom; - PositionMetadata pos; + PositionMetadata pos = null; View item; Drawable indicator; int t, b; @@ -297,28 +299,27 @@ public class ExpandableListView extends ListView { lastItemType = pos.position.type; } - if (indicatorRect.left == indicatorRect.right) { - // The left and right bounds are the same, so nothing will be drawn - continue; - } - - // Use item's full height + the divider height - if (mStackFromBottom) { - // See ListView#dispatchDraw - indicatorRect.top = t - mDividerHeight; - indicatorRect.bottom = b; - } else { - indicatorRect.top = t; - indicatorRect.bottom = b + mDividerHeight; + if (indicatorRect.left != indicatorRect.right) { + // Use item's full height + the divider height + if (mStackFromBottom) { + // See ListView#dispatchDraw + indicatorRect.top = t - mDividerHeight; + indicatorRect.bottom = b; + } else { + indicatorRect.top = t; + indicatorRect.bottom = b + mDividerHeight; + } + + // Get the indicator (with its state set to the item's state) + indicator = getIndicator(pos); + if (indicator != null) { + // Draw the indicator + indicator.setBounds(indicatorRect); + indicator.draw(canvas); + } } - // Get the indicator (with its state set to the item's state) - indicator = getIndicator(pos); - if (indicator == null) continue; - - // Draw the indicator - indicator.setBounds(indicatorRect); - indicator.draw(canvas); + pos.recycle(); } if (clipToPadding) { @@ -376,6 +377,7 @@ public class ExpandableListView extends ListView { */ public void setChildDivider(Drawable childDivider) { mChildDivider = childDivider; + mClipChildDivider = childDivider != null && childDivider instanceof ColorDrawable; } @Override @@ -387,14 +389,25 @@ public class ExpandableListView extends ListView { if (flatListPosition >= 0) { PositionMetadata pos = mConnector.getUnflattenedPos(flatListPosition); // If this item is a child, or it is a non-empty group that is expanded - if ((pos.position.type == ExpandableListPosition.CHILD) - || (pos.isExpanded() && - pos.groupMetadata.lastChildFlPos != pos.groupMetadata.flPos)) { + if ((pos.position.type == ExpandableListPosition.CHILD) || (pos.isExpanded() && + pos.groupMetadata.lastChildFlPos != pos.groupMetadata.flPos)) { // These are the cases where we draw the child divider - mChildDivider.setBounds(bounds); - mChildDivider.draw(canvas); + final Drawable divider = mChildDivider; + final boolean clip = mClipChildDivider; + if (!clip) { + divider.setBounds(bounds); + } else { + canvas.save(); + canvas.clipRect(bounds); + } + divider.draw(canvas); + if (clip) { + canvas.restore(); + } + pos.recycle(); return; } + pos.recycle(); } // Otherwise draw the default divider @@ -495,6 +508,7 @@ public class ExpandableListView extends ListView { id = getChildOrGroupId(posMetadata.position); + boolean returnValue; if (posMetadata.position.type == ExpandableListPosition.GROUP) { /* It's a group, so handle collapsing/expanding */ @@ -513,6 +527,7 @@ public class ExpandableListView extends ListView { if (mOnGroupClickListener != null) { if (mOnGroupClickListener.onGroupClick(this, v, posMetadata.position.groupPos, id)) { + posMetadata.recycle(); return true; } } @@ -527,7 +542,7 @@ public class ExpandableListView extends ListView { } } - return true; + returnValue = true; } else { /* It's a child, so pass on event */ if (mOnChildClickListener != null) { @@ -536,8 +551,12 @@ public class ExpandableListView extends ListView { posMetadata.position.childPos, id); } - return false; + returnValue = false; } + + posMetadata.recycle(); + + return returnValue; } /** @@ -676,7 +695,10 @@ public class ExpandableListView extends ListView { * in packed position representation. */ public long getExpandableListPosition(int flatListPosition) { - return mConnector.getUnflattenedPos(flatListPosition).position.getPackedPosition(); + PositionMetadata pm = mConnector.getUnflattenedPos(flatListPosition); + long packedPos = pm.position.getPackedPosition(); + pm.recycle(); + return packedPos; } /** @@ -691,8 +713,11 @@ public class ExpandableListView extends ListView { * @return The flat list position for the given child or group. */ public int getFlatListPosition(long packedPosition) { - return mConnector.getFlattenedPos(ExpandableListPosition.obtainPosition(packedPosition)). - position.flatListPos; + PositionMetadata pm = mConnector.getFlattenedPos(ExpandableListPosition + .obtainPosition(packedPosition)); + int retValue = pm.position.flatListPos; + pm.recycle(); + return retValue; } /** @@ -737,8 +762,11 @@ public class ExpandableListView extends ListView { */ public void setSelectedGroup(int groupPosition) { ExpandableListPosition elGroupPos = ExpandableListPosition - .obtainGroupPosition(groupPosition); - super.setSelection(mConnector.getFlattenedPos(elGroupPos).position.flatListPos); + .obtainGroupPosition(groupPosition); + PositionMetadata pm = mConnector.getFlattenedPos(elGroupPos); + elGroupPos.recycle(); + super.setSelection(pm.position.flatListPos); + pm.recycle(); } /** @@ -775,6 +803,9 @@ public class ExpandableListView extends ListView { super.setSelection(flatChildPos.position.flatListPos); + elChildPos.recycle(); + flatChildPos.recycle(); + return true; } @@ -883,11 +914,15 @@ public class ExpandableListView extends ListView { @Override ContextMenuInfo createContextMenuInfo(View view, int flatListPosition, long id) { - ExpandableListPosition pos = mConnector.getUnflattenedPos(flatListPosition).position; + PositionMetadata pm = mConnector.getUnflattenedPos(flatListPosition); + ExpandableListPosition pos = pm.position; + pm.recycle(); id = getChildOrGroupId(pos); + long packedPosition = pos.getPackedPosition(); + pos.recycle(); - return new ExpandableListContextMenuInfo(view, pos.getPackedPosition(), id); + return new ExpandableListContextMenuInfo(view, packedPosition, id); } /** diff --git a/core/java/android/widget/FastScroller.java b/core/java/android/widget/FastScroller.java new file mode 100644 index 0000000..bdcfeef --- /dev/null +++ b/core/java/android/widget/FastScroller.java @@ -0,0 +1,471 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.widget; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.SystemClock; +import android.util.TypedValue; +import android.view.MotionEvent; + +/** + * Helper class for AbsListView to draw and control the Fast Scroll thumb + */ +class FastScroller { + + + // Scroll thumb not showing + private static final int STATE_NONE = 0; + // Not implemented yet - fade-in transition + private static final int STATE_ENTER = 1; + // Scroll thumb visible and moving along with the scrollbar + private static final int STATE_VISIBLE = 2; + // Scroll thumb being dragged by user + private static final int STATE_DRAGGING = 3; + // Scroll thumb fading out due to inactivity timeout + private static final int STATE_EXIT = 4; + + private Drawable mThumbDrawable; + private Drawable mOverlayDrawable; + + private int mThumbH; + private int mThumbW; + private int mThumbY; + + private RectF mOverlayPos; + private int mOverlaySize = 104; + + private AbsListView mList; + private boolean mScrollCompleted; + private int mVisibleItem; + private Paint mPaint; + private int mListOffset; + + private Object [] mSections; + private String mSectionText; + private boolean mDrawOverlay; + private ScrollFade mScrollFade; + + private int mState; + + private Handler mHandler = new Handler(); + + private BaseAdapter mListAdapter; + private SectionIndexer mSectionIndexer; + + private boolean mChangedBounds; + + public FastScroller(Context context, AbsListView listView) { + mList = listView; + init(context); + } + + public void setState(int state) { + switch (state) { + case STATE_NONE: + mHandler.removeCallbacks(mScrollFade); + mList.invalidate(); + break; + case STATE_VISIBLE: + if (mState != STATE_VISIBLE) { // Optimization + resetThumbPos(); + } + // Fall through + case STATE_DRAGGING: + mHandler.removeCallbacks(mScrollFade); + break; + case STATE_EXIT: + int viewWidth = mList.getWidth(); + mList.invalidate(viewWidth - mThumbW, mThumbY, viewWidth, mThumbY + mThumbH); + break; + } + mState = state; + } + + public int getState() { + return mState; + } + + private void resetThumbPos() { + final int viewWidth = mList.getWidth(); + // Bounds are always top right. Y coordinate get's translated during draw + mThumbDrawable.setBounds(viewWidth - mThumbW, 0, viewWidth, mThumbH); + mThumbDrawable.setAlpha(ScrollFade.ALPHA_MAX); + } + + private void useThumbDrawable(Drawable drawable) { + mThumbDrawable = drawable; + mThumbW = 64; //mCurrentThumb.getIntrinsicWidth(); + mThumbH = 52; //mCurrentThumb.getIntrinsicHeight(); + mChangedBounds = true; + } + + private void init(Context context) { + // Get both the scrollbar states drawables + final Resources res = context.getResources(); + useThumbDrawable(res.getDrawable( + com.android.internal.R.drawable.scrollbar_handle_accelerated_anim2)); + + mOverlayDrawable = res.getDrawable( + com.android.internal.R.drawable.menu_submenu_background); + + mScrollCompleted = true; + + getSections(); + + mOverlayPos = new RectF(); + mScrollFade = new ScrollFade(); + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setTextAlign(Paint.Align.CENTER); + mPaint.setTextSize(mOverlaySize / 2); + TypedArray ta = context.getTheme().obtainStyledAttributes(new int[] { + android.R.attr.textColorPrimary }); + ColorStateList textColor = ta.getColorStateList(ta.getIndex(0)); + int textColorNormal = textColor.getDefaultColor(); + mPaint.setColor(textColorNormal); + mPaint.setStyle(Paint.Style.FILL_AND_STROKE); + + mState = STATE_NONE; + } + + void stop() { + setState(STATE_NONE); + } + + public void draw(Canvas canvas) { + + if (mState == STATE_NONE) { + // No need to draw anything + return; + } + + final int y = mThumbY; + final int viewWidth = mList.getWidth(); + final FastScroller.ScrollFade scrollFade = mScrollFade; + + int alpha = -1; + if (mState == STATE_EXIT) { + alpha = scrollFade.getAlpha(); + if (alpha < ScrollFade.ALPHA_MAX / 2) { + mThumbDrawable.setAlpha(alpha * 2); + } + int left = viewWidth - (mThumbW * alpha) / ScrollFade.ALPHA_MAX; + mThumbDrawable.setBounds(left, 0, viewWidth, mThumbH); + mChangedBounds = true; + } + + canvas.translate(0, y); + mThumbDrawable.draw(canvas); + canvas.translate(0, -y); + + // If user is dragging the scroll bar, draw the alphabet overlay + if (mState == STATE_DRAGGING && mDrawOverlay) { + mOverlayDrawable.draw(canvas); + final Paint paint = mPaint; + float descent = paint.descent(); + final RectF rectF = mOverlayPos; + canvas.drawText(mSectionText, (int) (rectF.left + rectF.right) / 2, + (int) (rectF.bottom + rectF.top) / 2 + mOverlaySize / 4 - descent, paint); + } else if (mState == STATE_EXIT) { + if (alpha == 0) { // Done with exit + setState(STATE_NONE); + } else { + mList.invalidate(viewWidth - mThumbW, y, viewWidth, y + mThumbH); + } + } + } + + void onSizeChanged(int w, int h, int oldw, int oldh) { + if (mThumbDrawable != null) { + mThumbDrawable.setBounds(w - mThumbW, 0, w, mThumbH); + } + final RectF pos = mOverlayPos; + pos.left = (w - mOverlaySize) / 2; + pos.right = pos.left + mOverlaySize; + pos.top = h / 10; // 10% from top + pos.bottom = pos.top + mOverlaySize; + if (mOverlayDrawable != null) { + mOverlayDrawable.setBounds((int) pos.left, (int) pos.top, + (int) pos.right, (int) pos.bottom); + } + } + + void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + + if (totalItemCount - visibleItemCount > 0 && mState != STATE_DRAGGING ) { + mThumbY = ((mList.getHeight() - mThumbH) * firstVisibleItem) + / (totalItemCount - visibleItemCount); + if (mChangedBounds) { + resetThumbPos(); + mChangedBounds = false; + } + } + mScrollCompleted = true; + if (firstVisibleItem == mVisibleItem) { + return; + } + mVisibleItem = firstVisibleItem; + if (mState != STATE_DRAGGING) { + setState(STATE_VISIBLE); + mHandler.postDelayed(mScrollFade, 1500); + } + } + + private void getSections() { + Adapter adapter = mList.getAdapter(); + mSectionIndexer = null; + if (adapter instanceof HeaderViewListAdapter) { + mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount(); + adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter(); + } + if (adapter instanceof ExpandableListConnector) { + ExpandableListAdapter expAdapter = ((ExpandableListConnector)adapter).getAdapter(); + if (expAdapter instanceof SectionIndexer) { + mSectionIndexer = (SectionIndexer) expAdapter; + mListAdapter = (BaseAdapter) adapter; + mSections = mSectionIndexer.getSections(); + } + } else { + if (adapter instanceof SectionIndexer) { + mListAdapter = (BaseAdapter) adapter; + mSectionIndexer = (SectionIndexer) adapter; + mSections = mSectionIndexer.getSections(); + + } else { + mListAdapter = (BaseAdapter) adapter; + mSections = new String[] { " " }; + } + } + } + + private void scrollTo(float position) { + int count = mList.getCount(); + mScrollCompleted = false; + float fThreshold = (1.0f / count) / 8; + final Object[] sections = mSections; + int sectionIndex; + if (sections != null && sections.length > 1) { + final int nSections = sections.length; + int section = (int) (position * nSections); + if (section >= nSections) { + section = nSections - 1; + } + int exactSection = section; + sectionIndex = section; + int index = mSectionIndexer.getPositionForSection(section); + // Given the expected section and index, the following code will + // try to account for missing sections (no names starting with..) + // It will compute the scroll space of surrounding empty sections + // and interpolate the currently visible letter's range across the + // available space, so that there is always some list movement while + // the user moves the thumb. + int nextIndex = count; + int prevIndex = index; + int prevSection = section; + int nextSection = section + 1; + // Assume the next section is unique + if (section < nSections - 1) { + nextIndex = mSectionIndexer.getPositionForSection(section + 1); + } + + // Find the previous index if we're slicing the previous section + if (nextIndex == index) { + // Non-existent letter + while (section > 0) { + section--; + prevIndex = mSectionIndexer.getPositionForSection(section); + if (prevIndex != index) { + prevSection = section; + sectionIndex = section; + break; + } + } + } + // Find the next index, in case the assumed next index is not + // unique. For instance, if there is no P, then request for P's + // position actually returns Q's. So we need to look ahead to make + // sure that there is really a Q at Q's position. If not, move + // further down... + int nextNextSection = nextSection + 1; + while (nextNextSection < nSections && + mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) { + nextNextSection++; + nextSection++; + } + // Compute the beginning and ending scroll range percentage of the + // currently visible letter. This could be equal to or greater than + // (1 / nSections). + float fPrev = (float) prevSection / nSections; + float fNext = (float) nextSection / nSections; + if (prevSection == exactSection && position - fPrev < fThreshold) { + index = prevIndex; + } else { + index = prevIndex + (int) ((nextIndex - prevIndex) * (position - fPrev) + / (fNext - fPrev)); + } + // Don't overflow + if (index > count - 1) index = count - 1; + + if (mList instanceof ExpandableListView) { + ExpandableListView expList = (ExpandableListView) mList; + expList.setSelectionFromTop(expList.getFlatListPosition( + ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0); + } else if (mList instanceof ListView) { + ((ListView)mList).setSelectionFromTop(index + mListOffset, 0); + } else { + mList.setSelection(index + mListOffset); + } + } else { + int index = (int) (position * count); + if (mList instanceof ExpandableListView) { + ExpandableListView expList = (ExpandableListView) mList; + expList.setSelectionFromTop(expList.getFlatListPosition( + ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0); + } else if (mList instanceof ListView) { + ((ListView)mList).setSelectionFromTop(index + mListOffset, 0); + } else { + mList.setSelection(index + mListOffset); + } + sectionIndex = -1; + } + + if (sectionIndex >= 0) { + String text = mSectionText = sections[sectionIndex].toString(); + mDrawOverlay = (text.length() != 1 || text.charAt(0) != ' ') && + sectionIndex < sections.length; + } else { + mDrawOverlay = false; + } + } + + private void cancelFling() { + // Cancel the list fling + MotionEvent cancelFling = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mList.onTouchEvent(cancelFling); + cancelFling.recycle(); + } + + boolean onInterceptTouchEvent(MotionEvent ev) { + if (mState > STATE_NONE && ev.getAction() == MotionEvent.ACTION_DOWN) { + if (ev.getX() > mList.getWidth() - mThumbW && ev.getY() >= mThumbY && + ev.getY() <= mThumbY + mThumbH) { + setState(STATE_DRAGGING); + return true; + } + } + return false; + } + + boolean onTouchEvent(MotionEvent me) { + if (mState == STATE_NONE) { + return false; + } + if (me.getAction() == MotionEvent.ACTION_DOWN) { + if (me.getX() > mList.getWidth() - mThumbW + && me.getY() >= mThumbY + && me.getY() <= mThumbY + mThumbH) { + + setState(STATE_DRAGGING); + if (mListAdapter == null && mList != null) { + getSections(); + } + + cancelFling(); + return true; + } + } else if (me.getAction() == MotionEvent.ACTION_UP) { + if (mState == STATE_DRAGGING) { + setState(STATE_VISIBLE); + final Handler handler = mHandler; + handler.removeCallbacks(mScrollFade); + handler.postDelayed(mScrollFade, 1000); + return true; + } + } else if (me.getAction() == MotionEvent.ACTION_MOVE) { + if (mState == STATE_DRAGGING) { + final int viewHeight = mList.getHeight(); + // Jitter + int newThumbY = (int) me.getY() - mThumbH + 10; + if (newThumbY < 0) { + newThumbY = 0; + } else if (newThumbY + mThumbH > viewHeight) { + newThumbY = viewHeight - mThumbH; + } + if (Math.abs(mThumbY - newThumbY) < 2) { + return true; + } + mThumbY = newThumbY; + // If the previous scrollTo is still pending + if (mScrollCompleted) { + scrollTo((float) mThumbY / (viewHeight - mThumbH)); + } + return true; + } + } + return false; + } + + public class ScrollFade implements Runnable { + + long mStartTime; + long mFadeDuration; + static final int ALPHA_MAX = 208; + static final long FADE_DURATION = 200; + + void startFade() { + mFadeDuration = FADE_DURATION; + mStartTime = SystemClock.uptimeMillis(); + setState(STATE_EXIT); + } + + int getAlpha() { + if (getState() != STATE_EXIT) { + return ALPHA_MAX; + } + int alpha; + long now = SystemClock.uptimeMillis(); + if (now > mStartTime + mFadeDuration) { + alpha = 0; + } else { + alpha = (int) (ALPHA_MAX - ((now - mStartTime) * ALPHA_MAX) / mFadeDuration); + } + return alpha; + } + + public void run() { + if (getState() != STATE_EXIT) { + startFade(); + return; + } + + if (getAlpha() > 0) { + mList.invalidate(); + } else { + setState(STATE_NONE); + } + } + } +} diff --git a/core/java/android/widget/Gallery.java b/core/java/android/widget/Gallery.java index acf9400..d886155 100644 --- a/core/java/android/widget/Gallery.java +++ b/core/java/android/widget/Gallery.java @@ -122,7 +122,7 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList * Whether to continuously callback on the item selected listener during a * fling. */ - private boolean mShouldCallbackDuringFling; + private boolean mShouldCallbackDuringFling = true; /** * Whether to callback when an item that is not selected is clicked. @@ -133,6 +133,13 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList * If true, do not callback to item selected listener. */ private boolean mSuppressSelectionChanged; + + /** + * If true, we have received the "invoke" (center or enter buttons) key + * down. This is checked before we action on the "invoke" key up, and is + * subsequently cleared. + */ + private boolean mReceivedInvokeKeyDown; private AdapterContextMenuInfo mContextMenuInfo; @@ -882,8 +889,8 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList */ mParent.requestDisallowInterceptTouchEvent(true); - // As the user scrolls, we want to callback selection changes so related - // into on the screen is up-to-date with the user's selection + // As the user scrolls, we want to callback selection changes so related- + // info on the screen is up-to-date with the gallery's selection if (mSuppressSelectionChanged) { mSuppressSelectionChanged = false; } @@ -1062,6 +1069,11 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); } return true; + + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + mReceivedInvokeKeyDown = true; + // fallthrough to default handling } return super.onKeyDown(keyCode, event); @@ -1072,19 +1084,26 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: { - if (mItemCount > 0) { - - dispatchPress(mSelectedChild); - postDelayed(new Runnable() { - public void run() { - dispatchUnpress(); - } - }, ViewConfiguration.getPressedStateDuration()); - - int selectedIndex = mSelectedPosition - mFirstPosition; - performItemClick(getChildAt(selectedIndex), mSelectedPosition, mAdapter - .getItemId(mSelectedPosition)); + + if (mReceivedInvokeKeyDown) { + if (mItemCount > 0) { + + dispatchPress(mSelectedChild); + postDelayed(new Runnable() { + public void run() { + dispatchUnpress(); + } + }, ViewConfiguration.getPressedStateDuration()); + + int selectedIndex = mSelectedPosition - mFirstPosition; + performItemClick(getChildAt(selectedIndex), mSelectedPosition, mAdapter + .getItemId(mSelectedPosition)); + } } + + // Clear the flag + mReceivedInvokeKeyDown = false; + return true; } } diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index 268bf84..38bfc7c 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -36,7 +36,8 @@ public class GridView extends AbsListView { public static final int NO_STRETCH = 0; public static final int STRETCH_SPACING = 1; public static final int STRETCH_COLUMN_WIDTH = 2; - + public static final int STRETCH_SPACING_UNIFORM = 3; + public static final int AUTO_FIT = -1; private int mNumColumns = AUTO_FIT; @@ -228,12 +229,12 @@ public class GridView extends AbsListView { } private View makeRow(int startPos, int y, boolean flow) { - int last; - int nextLeft = mListPadding.left; - final int columnWidth = mColumnWidth; final int horizontalSpacing = mHorizontalSpacing; + int last; + int nextLeft = mListPadding.left + ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0); + if (!mStackFromBottom) { last = Math.min(startPos + mNumColumns, mItemCount); } else { @@ -878,6 +879,17 @@ public class GridView extends AbsListView { mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver; } break; + + case STRETCH_SPACING_UNIFORM: + // Stretch the spacing between columns + mColumnWidth = requestedColumnWidth; + if (mNumColumns > 1) { + mHorizontalSpacing = requestedHorizontalSpacing + + spaceLeftOver / (mNumColumns + 1); + } else { + mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver; + } + break; } break; @@ -1379,7 +1391,6 @@ public class GridView extends AbsListView { handled = arrowScroll(FOCUS_LEFT); break; - case KeyEvent.KEYCODE_DPAD_RIGHT: handled = arrowScroll(FOCUS_RIGHT); break; @@ -1420,7 +1431,6 @@ public class GridView extends AbsListView { } break; } - } if (!handled) { @@ -1460,6 +1470,7 @@ public class GridView extends AbsListView { if (nextPage >= 0) { setSelectionInt(nextPage); + invokeOnItemScrollListener(); return true; } @@ -1478,10 +1489,12 @@ public class GridView extends AbsListView { if (direction == FOCUS_UP) { mLayoutMode = LAYOUT_SET_SELECTION; setSelectionInt(0); + invokeOnItemScrollListener(); moved = true; } else if (direction == FOCUS_DOWN) { mLayoutMode = LAYOUT_SET_SELECTION; setSelectionInt(mItemCount - 1); + invokeOnItemScrollListener(); moved = true; } @@ -1547,6 +1560,7 @@ public class GridView extends AbsListView { if (moved) { playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); + invokeOnItemScrollListener(); } return moved; @@ -1684,7 +1698,7 @@ public class GridView extends AbsListView { * Control how items are stretched to fill their space. * * @param stretchMode Either {@link #NO_STRETCH}, - * {@link #STRETCH_SPACING}, or {@link #STRETCH_COLUMN_WIDTH}. + * {@link #STRETCH_SPACING}, {@link #STRETCH_SPACING_UNIFORM}, or {@link #STRETCH_COLUMN_WIDTH}. * * @attr ref android.R.styleable#GridView_stretchMode */ diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java index de74fa4..36ed8bd 100644 --- a/core/java/android/widget/LinearLayout.java +++ b/core/java/android/widget/LinearLayout.java @@ -952,12 +952,13 @@ public class LinearLayout extends ViewGroup { if (majorGravity != Gravity.TOP) { switch (majorGravity) { case Gravity.BOTTOM: - childTop = mBottom - mTop - mPaddingBottom - mTotalLength; + // mTotalLength contains the padding already, we add the top + // padding to compensate + childTop = mBottom - mTop + mPaddingTop - mTotalLength; break; case Gravity.CENTER_VERTICAL: - childTop += ((mBottom - mTop - mPaddingTop - mPaddingBottom) - - mTotalLength) / 2; + childTop += ((mBottom - mTop) - mTotalLength) / 2; break; } @@ -1039,12 +1040,13 @@ public class LinearLayout extends ViewGroup { if (majorGravity != Gravity.LEFT) { switch (majorGravity) { case Gravity.RIGHT: - childLeft = mRight - mLeft - mPaddingRight - mTotalLength; + // mTotalLength contains the padding already, we add the left + // padding to compensate + childLeft = mRight - mLeft + mPaddingLeft - mTotalLength; break; case Gravity.CENTER_HORIZONTAL: - childLeft += ((mRight - mLeft - mPaddingLeft - mPaddingRight) - - mTotalLength) / 2; + childLeft += ((mRight - mLeft) - mTotalLength) / 2; break; } } diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index d52e51f..dfc7bc3 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -21,6 +21,7 @@ import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.graphics.drawable.ColorDrawable; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; @@ -36,6 +37,7 @@ import android.view.ViewParent; import android.view.SoundEffectConstants; import com.google.android.collect.Lists; +import com.android.internal.R; import java.util.ArrayList; @@ -57,6 +59,8 @@ import java.util.ArrayList; * @attr ref android.R.styleable#ListView_divider * @attr ref android.R.styleable#ListView_dividerHeight * @attr ref android.R.styleable#ListView_choiceMode + * @attr ref android.R.styleable#ListView_headerDividersEnabled + * @attr ref android.R.styleable#ListView_footerDividersEnabled */ public class ListView extends AbsListView { /** @@ -92,10 +96,16 @@ public class ListView extends AbsListView { */ private static final int MIN_SCROLL_PREVIEW_PIXELS = 2; - // TODO: document - class FixedViewInfo { + /** + * A class that represents a fixed view in a list, for example a header at the top + * or a footer at the bottom. + */ + public class FixedViewInfo { + /** The view to add to the list */ public View view; + /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */ public Object data; + /** <code>true</code> if the fixed view should be selectable in the list */ public boolean isSelectable; } @@ -104,6 +114,9 @@ public class ListView extends AbsListView { Drawable mDivider; int mDividerHeight; + private boolean mClipDivider; + private boolean mHeaderDividersEnabled; + private boolean mFooterDividersEnabled; private boolean mAreAllItemsSelectable = true; @@ -137,8 +150,8 @@ public class ListView extends AbsListView { public ListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - TypedArray a = - context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ListView, defStyle, 0); + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.ListView, defStyle, 0); CharSequence[] entries = a.getTextArray( com.android.internal.R.styleable.ListView_entries); @@ -149,19 +162,20 @@ public class ListView extends AbsListView { final Drawable d = a.getDrawable(com.android.internal.R.styleable.ListView_divider); if (d != null) { - // If a divider is specified use its intrinsic height for divider height setDivider(d); - } else { + } - // Else use the height specified, zero being the default - final int dividerHeight = a.getDimensionPixelSize( - com.android.internal.R.styleable.ListView_dividerHeight, 0); - if (dividerHeight != 0) { - setDividerHeight(dividerHeight); - } + // Use the height specified, zero being the default + final int dividerHeight = a.getDimensionPixelSize( + com.android.internal.R.styleable.ListView_dividerHeight, 0); + if (dividerHeight != 0) { + setDividerHeight(dividerHeight); } + mHeaderDividersEnabled = a.getBoolean(R.styleable.ListView_headerDividersEnabled, true); + mFooterDividersEnabled = a.getBoolean(R.styleable.ListView_footerDividersEnabled, true); + a.recycle(); } @@ -198,8 +212,7 @@ public class ListView extends AbsListView { // We only are looking to see if we are too low, not too high delta = 0; } - } - else { + } else { // we are too high, slide all views down to align with bottom child = getChildAt(childCount - 1); delta = child.getBottom() - (getHeight() - mListPadding.bottom); @@ -1989,6 +2002,7 @@ public class ListView extends AbsListView { } setSelectionInt(position); + invokeOnItemScrollListener(); invalidate(); return true; @@ -2014,6 +2028,7 @@ public class ListView extends AbsListView { if (position >= 0) { mLayoutMode = LAYOUT_FORCE_TOP; setSelectionInt(position); + invokeOnItemScrollListener(); } moved = true; } @@ -2023,6 +2038,7 @@ public class ListView extends AbsListView { if (position >= 0) { mLayoutMode = LAYOUT_FORCE_BOTTOM; setSelectionInt(position); + invokeOnItemScrollListener(); } moved = true; } @@ -2739,36 +2755,46 @@ public class ListView extends AbsListView { bounds.right = mRight - mLeft - mPaddingRight; final int count = getChildCount(); - int i; + final int headerCount = mHeaderViewInfos.size(); + final int footerLimit = mItemCount - mFooterViewInfos.size() - 1; + final boolean headerDividers = mHeaderDividersEnabled; + final boolean footerDividers = mFooterDividersEnabled; + final int first = mFirstPosition; - if (mStackFromBottom) { - int top; - int listTop = mListPadding.top; - - for (i = 0; i < count; ++i) { - View child = getChildAt(i); - top = child.getTop(); - if (top > listTop) { - bounds.top = top - dividerHeight; - bounds.bottom = top; - // Give the method the child ABOVE the divider, so we - // subtract one from our child - // position. Give -1 when there is no child above the - // divider. - drawDivider(canvas, bounds, i - 1); + if (!mStackFromBottom) { + int bottom; + int listBottom = mBottom - mTop - mListPadding.bottom; + + for (int i = 0; i < count; i++) { + if ((headerDividers || first + i >= headerCount) && + (footerDividers || first + i < footerLimit)) { + View child = getChildAt(i); + bottom = child.getBottom(); + if (bottom < listBottom) { + bounds.top = bottom; + bounds.bottom = bottom + dividerHeight; + drawDivider(canvas, bounds, i); + } } } } else { - int bottom; - int listBottom = getHeight() - mListPadding.bottom; - - for (i = 0; i < count; ++i) { - View child = getChildAt(i); - bottom = child.getBottom(); - if (bottom < listBottom) { - bounds.top = bottom; - bounds.bottom = bottom + dividerHeight; - drawDivider(canvas, bounds, i); + int top; + int listTop = mListPadding.top; + + for (int i = 0; i < count; i++) { + if ((headerDividers || first + i >= headerCount) && + (footerDividers || first + i < footerLimit)) { + View child = getChildAt(i); + top = child.getTop(); + if (top > listTop) { + bounds.top = top - dividerHeight; + bounds.bottom = top; + // Give the method the child ABOVE the divider, so we + // subtract one from our child + // position. Give -1 when there is no child above the + // divider. + drawDivider(canvas, bounds, i - 1); + } } } } @@ -2789,8 +2815,21 @@ public class ListView extends AbsListView { */ void drawDivider(Canvas canvas, Rect bounds, int childIndex) { // This widget draws the same divider for all children - mDivider.setBounds(bounds); - mDivider.draw(canvas); + final Drawable divider = mDivider; + final boolean clipDivider = mClipDivider; + + if (!clipDivider) { + divider.setBounds(bounds); + } else { + canvas.save(); + canvas.clipRect(bounds); + } + + divider.draw(canvas); + + if (clipDivider) { + canvas.restore(); + } } /** @@ -2811,8 +2850,10 @@ public class ListView extends AbsListView { public void setDivider(Drawable divider) { if (divider != null) { mDividerHeight = divider.getIntrinsicHeight(); + mClipDivider = divider instanceof ColorDrawable; } else { mDividerHeight = 0; + mClipDivider = false; } mDivider = divider; requestLayoutIfNecessary(); @@ -2836,6 +2877,32 @@ public class ListView extends AbsListView { requestLayoutIfNecessary(); } + /** + * Enables or disables the drawing of the divider for header views. + * + * @param headerDividersEnabled True to draw the headers, false otherwise. + * + * @see #setFooterDividersEnabled(boolean) + * @see #addHeaderView(android.view.View) + */ + public void setHeaderDividersEnabled(boolean headerDividersEnabled) { + mHeaderDividersEnabled = headerDividersEnabled; + invalidate(); + } + + /** + * Enables or disables the drawing of the divider for footer views. + * + * @param footerDividersEnabled True to draw the footers, false otherwise. + * + * @see #setHeaderDividersEnabled(boolean) + * @see #addFooterView(android.view.View) + */ + public void setFooterDividersEnabled(boolean footerDividersEnabled) { + mFooterDividersEnabled = footerDividersEnabled; + invalidate(); + } + @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); diff --git a/core/java/android/widget/MediaController.java b/core/java/android/widget/MediaController.java index ad8433f..6c0c164 100644 --- a/core/java/android/widget/MediaController.java +++ b/core/java/android/widget/MediaController.java @@ -271,6 +271,7 @@ public class MediaController extends FrameLayout { p.y = anchorpos[1] + mAnchor.getHeight() - p.height; p.format = PixelFormat.TRANSLUCENT; p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; + p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; p.token = null; p.windowAnimations = 0; // android.R.style.DropDownAnimationDown; mWindowManager.addView(mDecor, p); @@ -387,6 +388,7 @@ public class MediaController extends FrameLayout { int keyCode = event.getKeyCode(); if (event.getRepeatCount() == 0 && event.isDown() && ( keyCode == KeyEvent.KEYCODE_HEADSETHOOK || + keyCode == KeyEvent.KEYCODE_PLAYPAUSE || keyCode == KeyEvent.KEYCODE_SPACE)) { doPauseResume(); show(sDefaultTimeout); diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index 6a7b1fb..b5c4384 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -22,9 +22,9 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; -import android.view.WindowManagerImpl; import android.view.Gravity; import android.view.ViewGroup; +import android.view.View.OnTouchListener; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -43,31 +43,58 @@ import android.util.AttributeSet; */ public class PopupWindow { /** - * The height of the status bar so we know how much of the screen we can - * actually be displayed in. - * <p> - * TODO: This IS NOT the right way to do this. - * Instead of knowing how much of the screen is available, a popup that - * wants anchor and maximize space shouldn't be setting a height, instead - * the PopupViewContainer should have its layout height as fill_parent and - * properly position the popup. + * Mode for {@link #setInputMethodMode(int): the requirements for the + * input method should be based on the focusability of the popup. That is + * if it is focusable than it needs to work with the input method, else + * it doesn't. + */ + public static final int INPUT_METHOD_FROM_FOCUSABLE = 0; + + /** + * Mode for {@link #setInputMethodMode(int): this popup always needs to + * work with an input method, regardless of whether it is focusable. This + * means that it will always be displayed so that the user can also operate + * the input method while it is shown. */ - private static final int STATUS_BAR_HEIGHT = 30; + + public static final int INPUT_METHOD_NEEDED = 1; + + /** + * Mode for {@link #setInputMethodMode(int): this popup never needs to + * work with an input method, regardless of whether it is focusable. This + * means that it will always be displayed to use as much space on the + * screen as needed, regardless of whether this covers the input method. + */ + public static final int INPUT_METHOD_NOT_NEEDED = 2; + + private final Context mContext; + private final WindowManager mWindowManager; private boolean mIsShowing; + private boolean mIsDropdown; private View mContentView; private View mPopupView; private boolean mFocusable; + private int mInputMethodMode = INPUT_METHOD_FROM_FOCUSABLE; + private boolean mTouchable = true; + private boolean mOutsideTouchable = false; + private boolean mClippingEnabled = true; + private OnTouchListener mTouchInterceptor; + + private int mWidthMode; private int mWidth; + private int mHeightMode; private int mHeight; + private int mPopupWidth; + private int mPopupHeight; + private int[] mDrawingLocation = new int[2]; - private int[] mRootLocation = new int[2]; + private int[] mScreenLocation = new int[2]; private Rect mTempRect = new Rect(); - private Context mContext; private Drawable mBackground; private boolean mAboveAnchor; @@ -106,6 +133,8 @@ public class PopupWindow { */ public PopupWindow(Context context, AttributeSet attrs, int defStyle) { mContext = context; + mWindowManager = (WindowManager)context.getSystemService( + Context.WINDOW_SERVICE); TypedArray a = context.obtainStyledAttributes( @@ -183,6 +212,9 @@ public class PopupWindow { */ public PopupWindow(View contentView, int width, int height, boolean focusable) { + mContext = contentView.getContext(); + mWindowManager = (WindowManager)mContext.getSystemService( + Context.WINDOW_SERVICE); setContentView(contentView); setWidth(width); setHeight(height); @@ -218,11 +250,15 @@ public class PopupWindow { } /** - * set the flag on popup to ignore cheek press events - * This method has to be invoked before displaying the content view - * of the popup for the window flags to take effect and will be ignored - * if the pop up is already displayed. By default this flag is set to false + * Set the flag on popup to ignore cheek press eventt; by default this flag + * is set to false * which means the pop wont ignore cheek press dispatch events. + * + * <p>If the popup is showing, calling this method will take effect only + * the next time the popup is shown or through a manual call to one of + * the {@link #update()} methods.</p> + * + * @see #update() */ public void setIgnoreCheekPress() { mIgnoreCheekPress = true; @@ -230,9 +266,17 @@ public class PopupWindow { /** - * <p>Change the animation style for this popup.</p> + * <p>Change the animation style resource for this popup.</p> + * + * <p>If the popup is showing, calling this method will take effect only + * the next time the popup is shown or through a manual call to one of + * the {@link #update()} methods.</p> * - * @param animationStyle animation style to use when the popup appears and disappears + * @param animationStyle animation style to use when the popup appears + * and disappears. Set to -1 for the default animation, 0 for no + * animation, or a resource identifier for an explicit animation. + * + * @see #update() */ public void setAnimationStyle(int animationStyle) { mAnimationStyle = animationStyle; @@ -253,7 +297,8 @@ public class PopupWindow { * <p>Change the popup's content. The content is represented by an instance * of {@link android.view.View}.</p> * - * <p>This method has no effect if called when the popup is showing.</p> + * <p>This method has no effect if called when the popup is showing. To + * apply it while a popup is showing, call </p> * * @param contentView the new content for the popup * @@ -269,6 +314,14 @@ public class PopupWindow { } /** + * Set a callback for all touch events being dispatched to the popup + * window. + */ + public void setTouchInterceptor(OnTouchListener l) { + mTouchInterceptor = l; + } + + /** * <p>Indicate whether the popup window can grab the focus.</p> * * @return true if the popup is focusable, false otherwise @@ -282,21 +335,169 @@ public class PopupWindow { /** * <p>Changes the focusability of the popup window. When focusable, the * window will grab the focus from the current focused widget if the popup - * contains a focusable {@link android.view.View}.</p> + * contains a focusable {@link android.view.View}. By default a popup + * window is not focusable.</p> * * <p>If the popup is showing, calling this method will take effect only - * the next time the popup is shown.</p> + * the next time the popup is shown or through a manual call to one of + * the {@link #update()} methods.</p> * - * @param focusable true if the popup should grab focus, false otherwise + * @param focusable true if the popup should grab focus, false otherwise. * * @see #isFocusable() * @see #isShowing() + * @see #update() */ public void setFocusable(boolean focusable) { mFocusable = focusable; } /** + * Return the current value in {@link #setInputMethodMode(int)}. + * + * @see #setInputMethodMode(int) + */ + public int getInputMethodMode() { + return mInputMethodMode; + + } + + /** + * Control how the popup operates with an input method: one of + * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED}, + * or {@link #INPUT_METHOD_NOT_NEEDED}. + * + * <p>If the popup is showing, calling this method will take effect only + * the next time the popup is shown or through a manual call to one of + * the {@link #update()} methods.</p> + * + * @see #getInputMethodMode() + * @see #update() + */ + public void setInputMethodMode(int mode) { + mInputMethodMode = mode; + } + + /** + * <p>Indicates whether the popup window receives touch events.</p> + * + * @return true if the popup is touchable, false otherwise + * + * @see #setTouchable(boolean) + */ + public boolean isTouchable() { + return mTouchable; + } + + /** + * <p>Changes the touchability of the popup window. When touchable, the + * window will receive touch events, otherwise touch events will go to the + * window below it. By default the window is touchable.</p> + * + * <p>If the popup is showing, calling this method will take effect only + * the next time the popup is shown or through a manual call to one of + * the {@link #update()} methods.</p> + * + * @param touchable true if the popup should receive touch events, false otherwise + * + * @see #isTouchable() + * @see #isShowing() + * @see #update() + */ + public void setTouchable(boolean touchable) { + mTouchable = touchable; + } + + /** + * <p>Indicates whether the popup window will be informed of touch events + * outside of its window.</p> + * + * @return true if the popup is outside touchable, false otherwise + * + * @see #setOutsideTouchable(boolean) + */ + public boolean isOutsideTouchable() { + return mOutsideTouchable; + } + + /** + * <p>Controls whether the pop-up will be informed of touch events outside + * of its window. This only makes sense for pop-ups that are touchable + * but not focusable, which means touches outside of the window will + * be delivered to the window behind. The default is false.</p> + * + * <p>If the popup is showing, calling this method will take effect only + * the next time the popup is shown or through a manual call to one of + * the {@link #update()} methods.</p> + * + * @param touchable true if the popup should receive outside + * touch events, false otherwise + * + * @see #isOutsideTouchable() + * @see #isShowing() + * @see #update() + */ + public void setOutsideTouchable(boolean touchable) { + mOutsideTouchable = touchable; + } + + /** + * <p>Indicates whether clipping of the popup window is enabled.</p> + * + * @return true if the clipping is enabled, false otherwise + * + * @see #setClippingEnabled(boolean) + */ + public boolean isClippingEnabled() { + return mClippingEnabled; + } + + /** + * <p>Allows the popup window to extend beyond the bounds of the screen. By default the + * window is clipped to the screen boundaries. Setting this to false will allow windows to be + * accurately positioned.</p> + * + * <p>If the popup is showing, calling this method will take effect only + * the next time the popup is shown or through a manual call to one of + * the {@link #update()} methods.</p> + * + * @param enabled false if the window should be allowed to extend outside of the screen + * @see #isShowing() + * @see #isClippingEnabled() + * @see #update() + */ + public void setClippingEnabled(boolean enabled) { + mClippingEnabled = enabled; + } + + /** + * <p>Change the width and height measure specs that are given to the + * window manager by the popup. By default these are 0, meaning that + * the current width or height is requested as an explicit size from + * the window manager. You can supply + * {@link ViewGroup.LayoutParams#WRAP_CONTENT} or + * {@link ViewGroup.LayoutParams#FILL_PARENT} to have that measure + * spec supplied instead, replacing the absolute width and height that + * has been set in the popup.</p> + * + * <p>If the popup is showing, calling this method will take effect only + * the next time the popup is shown.</p> + * + * @param widthSpec an explicit width measure spec mode, either + * {@link ViewGroup.LayoutParams#WRAP_CONTENT}, + * {@link ViewGroup.LayoutParams#FILL_PARENT}, or 0 to use the absolute + * width. + * @param heightSpec an explicit height measure spec mode, either + * {@link ViewGroup.LayoutParams#WRAP_CONTENT}, + * {@link ViewGroup.LayoutParams#FILL_PARENT}, or 0 to use the absolute + * height. + */ + public void setWindowLayoutMode(int widthSpec, int heightSpec) { + mWidthMode = widthSpec; + mHeightMode = heightSpec; + } + + /** * <p>Return this popup's height MeasureSpec</p> * * @return the height MeasureSpec of the popup @@ -377,11 +578,10 @@ public class PopupWindow { } mIsShowing = true; + mIsDropdown = false; WindowManager.LayoutParams p = createPopupLayout(parent.getWindowToken()); - if (mAnimationStyle != -1) { - p.windowAnimations = mAnimationStyle; - } + p.windowAnimations = computeAnimationResource(); preparePopup(p); if (gravity == Gravity.NO_GRAVITY) { @@ -426,6 +626,7 @@ public class PopupWindow { } mIsShowing = true; + mIsDropdown = true; WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken()); preparePopup(p); @@ -433,13 +634,9 @@ public class PopupWindow { mPopupView.refreshDrawableState(); } mAboveAnchor = findDropDownPosition(anchor, p, xoff, yoff); - if (mAnimationStyle == -1) { - p.windowAnimations = mAboveAnchor - ? com.android.internal.R.style.Animation_DropDownUp - : com.android.internal.R.style.Animation_DropDownDown; - } else { - p.windowAnimations = mAnimationStyle; - } + if (mHeightMode < 0) p.height = mHeightMode; + if (mWidthMode < 0) p.width = mWidthMode; + p.windowAnimations = computeAnimationResource(); invokePopup(p); } @@ -479,7 +676,8 @@ public class PopupWindow { } else { mPopupView = mContentView; } - + mPopupWidth = p.width; + mPopupHeight = p.height; } /** @@ -491,8 +689,7 @@ public class PopupWindow { * @param p the layout parameters of the popup's content view */ private void invokePopup(WindowManager.LayoutParams p) { - WindowManagerImpl wm = WindowManagerImpl.getDefault(); - wm.addView(mPopupView, p); + mWindowManager.addView(mPopupView, p); } /** @@ -518,18 +715,56 @@ public class PopupWindow { } else { p.format = PixelFormat.TRANSLUCENT; } - if(mIgnoreCheekPress) { - p.flags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; - } - if (!mFocusable) { - p.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - } + p.flags = computeFlags(p.flags); p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; p.token = token; return p; } + private int computeFlags(int curFlags) { + curFlags &= ~( + WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES | + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | + WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | + WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + if(mIgnoreCheekPress) { + curFlags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; + } + if (!mFocusable) { + curFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + if (mInputMethodMode == INPUT_METHOD_NEEDED) { + curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + } + } else if (mInputMethodMode == INPUT_METHOD_NOT_NEEDED) { + curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + } + if (!mTouchable) { + curFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + } + if (mTouchable) { + curFlags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; + } + if (!mClippingEnabled) { + curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; + } + return curFlags; + } + + private int computeAnimationResource() { + if (mAnimationStyle == -1) { + if (mIsDropdown) { + return mAboveAnchor + ? com.android.internal.R.style.Animation_DropDownUp + : com.android.internal.R.style.Animation_DropDownDown; + } + return 0; + } + return mAnimationStyle; + } + /** * <p>Positions the popup window on screen. When the popup window is too * tall to fit under the anchor, a parent scroll view is seeked and scrolled @@ -548,33 +783,48 @@ public class PopupWindow { anchor.getLocationInWindow(mDrawingLocation); p.x = mDrawingLocation[0] + xoff; p.y = mDrawingLocation[1] + anchor.getMeasuredHeight() + yoff; - + boolean onTop = false; - if (p.y + p.height > WindowManagerImpl.getDefault().getDefaultDisplay().getHeight()) { + p.gravity = Gravity.LEFT | Gravity.TOP; + + anchor.getLocationOnScreen(mScreenLocation); + final Rect displayFrame = new Rect(); + anchor.getWindowVisibleDisplayFrame(displayFrame); + + final View root = anchor.getRootView(); + if (mScreenLocation[1] + anchor.getMeasuredHeight() + yoff + mPopupHeight > displayFrame.bottom + || p.x + mPopupWidth - root.getWidth() > 0) { // if the drop down disappears at the bottom of the screen. we try to // scroll a parent scrollview or move the drop down back up on top of // the edit box - View root = anchor.getRootView(); - root.getLocationInWindow(mRootLocation); - int delta = p.y + p.height - mRootLocation[1] - root.getHeight(); - - if (delta > 0 || p.x + p.width - mRootLocation[0] - root.getWidth() > 0) { - Rect r = new Rect(anchor.getScrollX(), anchor.getScrollY(), - p.width, p.height + anchor.getMeasuredHeight()); - - onTop = !anchor.requestRectangleOnScreen(r, true); - - if (onTop) { - p.y -= anchor.getMeasuredHeight() + p.height; - } else { - anchor.getLocationOnScreen(mDrawingLocation); - p.x = mDrawingLocation[0] + xoff; - p.y = mDrawingLocation[1] + anchor.getMeasuredHeight() + yoff; - } + int scrollX = anchor.getScrollX(); + int scrollY = anchor.getScrollY(); + Rect r = new Rect(scrollX, scrollY, scrollX + mPopupWidth, + scrollY + mPopupHeight + anchor.getMeasuredHeight()); + anchor.requestRectangleOnScreen(r, true); + + // now we re-evaluate the space available, and decide from that + // whether the pop-up will go above or below the anchor. + anchor.getLocationInWindow(mDrawingLocation); + p.x = mDrawingLocation[0] + xoff; + p.y = mDrawingLocation[1] + anchor.getMeasuredHeight() + yoff; + + // determine whether there is more space above or below the anchor + anchor.getLocationOnScreen(mScreenLocation); + + onTop = (displayFrame.bottom - mScreenLocation[1] - anchor.getMeasuredHeight() - yoff) + < (mScreenLocation[1] - yoff - displayFrame.top); + if (onTop) { + p.gravity = Gravity.LEFT | Gravity.BOTTOM; + p.y = root.getHeight() - mDrawingLocation[1] - yoff; + } else { + p.y = mDrawingLocation[1] + anchor.getMeasuredHeight() + yoff; } } + p.gravity |= Gravity.DISPLAY_CLIP_VERTICAL; + return onTop; } @@ -589,18 +839,18 @@ public class PopupWindow { * shown. */ public int getMaxAvailableHeight(View anchor) { - // TODO: read comment on STATUS_BAR_HEIGHT - final int screenHeight = WindowManagerImpl.getDefault().getDefaultDisplay().getHeight() - - STATUS_BAR_HEIGHT; + final Rect displayFrame = new Rect(); + anchor.getWindowVisibleDisplayFrame(displayFrame); final int[] anchorPos = mDrawingLocation; anchor.getLocationOnScreen(anchorPos); - anchorPos[1] -= STATUS_BAR_HEIGHT; - - final int distanceFromAnchorToBottom = screenHeight - (anchorPos[1] + anchor.getHeight()); + final int distanceToBottom = displayFrame.bottom + - (anchorPos[1] + anchor.getHeight()); + final int distanceToTop = anchorPos[1] - displayFrame.top; + // anchorPos[1] is distance from anchor to top of screen - int returnedHeight = Math.max(anchorPos[1], distanceFromAnchorToBottom); + int returnedHeight = Math.max(distanceToBottom, distanceToTop); if (mBackground != null) { mBackground.getPadding(mTempRect); returnedHeight -= mTempRect.top + mTempRect.bottom; @@ -618,11 +868,11 @@ public class PopupWindow { */ public void dismiss() { if (isShowing() && mPopupView != null) { - WindowManagerImpl wm = WindowManagerImpl.getDefault(); - wm.removeView(mPopupView); + mWindowManager.removeView(mPopupView); if (mPopupView != mContentView && mPopupView instanceof ViewGroup) { ((ViewGroup) mPopupView).removeView(mContentView); } + mPopupView = null; mIsShowing = false; if (mOnDismissListener != null) { @@ -641,8 +891,44 @@ public class PopupWindow { } /** + * Updates the state of the popup window, if it is currently being displayed, + * from the currently set state. This include: + * {@link #setClippingEnabled(boolean)}, {@link #setFocusable(boolean)}, + * {@link #setIgnoreCheekPress()}, {@link #setInputMethodMode(int)}, + * {@link #setTouchable(boolean)}, and {@link #setAnimationStyle(int)}. + */ + public void update() { + if (!isShowing() || mContentView == null) { + return; + } + + WindowManager.LayoutParams p = (WindowManager.LayoutParams) + mPopupView.getLayoutParams(); + + boolean update = false; + + final int newAnim = computeAnimationResource(); + if (newAnim != p.windowAnimations) { + p.windowAnimations = newAnim; + update = true; + } + + final int newFlags = computeFlags(p.flags); + if (newFlags != p.flags) { + p.flags = newFlags; + update = true; + } + + if (update) { + mWindowManager.updateViewLayout(mPopupView, p); + } + } + + /** * <p>Updates the position and the dimension of the popup window. Width and - * height can be set to -1 to update location only.</p> + * height can be set to -1 to update location only. Calling this function + * also updates the window with the current popup state as + * described for {@link #update()}.</p> * * @param x the new x location * @param y the new y location @@ -667,13 +953,15 @@ public class PopupWindow { boolean update = false; - if (width != -1 && p.width != width) { - p.width = width; + final int finalWidth = mWidthMode < 0 ? mWidthMode : p.width; + if (width != -1 && p.width != finalWidth) { + p.width = finalWidth; update = true; } - if (height != -1 && p.height != height) { - p.height = height; + final int finalHeight = mHeightMode < 0 ? mHeightMode : p.height; + if (height != -1 && p.height != finalHeight) { + p.height = finalHeight; update = true; } @@ -687,6 +975,18 @@ public class PopupWindow { update = true; } + final int newAnim = computeAnimationResource(); + if (newAnim != p.windowAnimations) { + p.windowAnimations = newAnim; + update = true; + } + + final int newFlags = computeFlags(p.flags); + if (newFlags != p.flags) { + p.flags = newFlags; + update = true; + } + if (update) { if (mPopupView != mContentView) { final View popupViewContainer = mPopupView; @@ -704,14 +1004,15 @@ public class PopupWindow { } } - WindowManagerImpl wm = WindowManagerImpl.getDefault(); - wm.updateViewLayout(mPopupView, p); + mWindowManager.updateViewLayout(mPopupView, p); } } /** * <p>Updates the position and the dimension of the popup window. Width and - * height can be set to -1 to update location only.</p> + * height can be set to -1 to update location only. Calling this function + * also updates the window with the current popup state as + * described for {@link #update()}.</p> * * @param anchor the popup's anchor view * @param width the new width, can be -1 to ignore @@ -723,7 +1024,9 @@ public class PopupWindow { /** * <p>Updates the position and the dimension of the popup window. Width and - * height can be set to -1 to update location only.</p> + * height can be set to -1 to update location only. Calling this function + * also updates the window with the current popup state as + * described for {@link #update()}.</p> * * @param anchor the popup's anchor view * @param xoff x offset from the view's left edge @@ -739,17 +1042,25 @@ public class PopupWindow { WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams(); - int x = p.x; - int y = p.y; + if (width == -1) { + width = mPopupWidth; + } else { + mPopupWidth = width; + } + if (height == -1) { + height = mPopupHeight; + } else { + mPopupHeight = height; + } + findDropDownPosition(anchor, p, xoff, yoff); - - update(x, y, width, height); + update(p.x, p.y, width, height); } /** * Listener that is called when this popup window is dismissed. */ - interface OnDismissListener { + public interface OnDismissListener { /** * Called when this popup window is dismissed. */ @@ -785,6 +1096,14 @@ public class PopupWindow { } @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) { + return true; + } + return super.dispatchTouchEvent(ev); + } + + @Override public boolean onTouchEvent(MotionEvent event) { final int x = (int) event.getX(); final int y = (int) event.getY(); @@ -793,6 +1112,9 @@ public class PopupWindow { && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) { dismiss(); return true; + } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { + dismiss(); + return true; } else { return super.onTouchEvent(event); } diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index c1de010..abba6d0 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -22,6 +22,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Shader; +import android.graphics.Rect; import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ClipDrawable; @@ -40,6 +41,8 @@ import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.Transformation; import android.widget.RemoteViews.RemoteView; +import android.os.Parcel; +import android.os.Parcelable; import android.os.SystemClock; import com.android.internal.R; @@ -427,7 +430,7 @@ public class ProgressBar extends View { Drawable getCurrentDrawable() { return mCurrentDrawable; } - + @Override protected boolean verifyDrawable(Drawable who) { return who == mProgressDrawable || who == mIndeterminateDrawable @@ -700,7 +703,7 @@ public class ProgressBar extends View { mAnimation = null; mTransformation = null; if (mIndeterminateDrawable instanceof AnimationDrawable) { - ((AnimationDrawable)mIndeterminateDrawable).stop(); + ((AnimationDrawable) mIndeterminateDrawable).stop(); mShouldStartAnimationDrawable = false; } } @@ -754,17 +757,31 @@ public class ProgressBar extends View { @Override public void invalidateDrawable(Drawable dr) { if (!mInDrawing) { - super.invalidateDrawable(dr); + if (dr == mProgressDrawable || dr == mIndeterminateDrawable) { + final Rect dirty = dr.getBounds(); + final int scrollX = mScrollX + mPaddingLeft; + final int scrollY = mScrollY + mPaddingRight; + + invalidate(dirty.left + scrollX, dirty.top + scrollY, + dirty.right + scrollX, dirty.bottom + scrollY); + } else { + super.invalidateDrawable(dr); + } } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { - Drawable d = mCurrentDrawable; - if (d != null) { - // onDraw will translate the canvas so we draw starting at 0,0 - d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, - h - mPaddingBottom - mPaddingTop); + // onDraw will translate the canvas so we draw starting at 0,0 + int right = w - mPaddingRight - mPaddingLeft; + int bottom = h - mPaddingBottom - mPaddingTop; + + if (mIndeterminateDrawable != null) { + mIndeterminateDrawable.setBounds(0, 0, right, bottom); + } + + if (mProgressDrawable != null) { + mProgressDrawable.setBounds(0, 0, right, bottom); } } @@ -795,8 +812,9 @@ public class ProgressBar extends View { } d.draw(canvas); canvas.restore(); - if (mShouldStartAnimationDrawable && mCurrentDrawable instanceof AnimationDrawable) { - ((AnimationDrawable)mCurrentDrawable).start(); + if (mShouldStartAnimationDrawable && d instanceof AnimationDrawable) { + ((AnimationDrawable) d).start(); + mShouldStartAnimationDrawable = false; } } } @@ -817,4 +835,64 @@ public class ProgressBar extends View { setMeasuredDimension(resolveSize(dw, widthMeasureSpec), resolveSize(dh, heightMeasureSpec)); } + + static class SavedState extends BaseSavedState { + int progress; + int secondaryProgress; + + /** + * Constructor called from {@link ProgressBar#onSaveInstanceState()} + */ + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + progress = in.readInt(); + secondaryProgress = in.readInt(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(progress); + out.writeInt(secondaryProgress); + } + + 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]; + } + }; + } + + @Override + public Parcelable onSaveInstanceState() { + // Force our ancestor class to save its state + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + + ss.progress = mProgress; + ss.secondaryProgress = mSecondaryProgress; + + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + + setProgress(ss.progress); + setSecondaryProgress(ss.secondaryProgress); + } } diff --git a/core/java/android/widget/ScrollBarDrawable.java b/core/java/android/widget/ScrollBarDrawable.java index 5df2b6d..17f9128 100644 --- a/core/java/android/widget/ScrollBarDrawable.java +++ b/core/java/android/widget/ScrollBarDrawable.java @@ -111,7 +111,10 @@ public class ScrollBarDrawable extends Drawable { } Rect r = getBounds(); - + if (canvas.quickReject(r.left, r.top, r.right, r.bottom, + Canvas.EdgeType.AA)) { + return; + } if (drawTrack) { drawTrack(canvas, r, vertical); } diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index 23a27ac..a2133b2 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -20,6 +20,8 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Rect; import android.util.AttributeSet; +import android.util.Config; +import android.util.Log; import android.view.FocusFinder; import android.view.KeyEvent; import android.view.MotionEvent; @@ -57,6 +59,9 @@ import java.util.List; * <p>ScrollView only supports vertical scrolling. */ public class ScrollView extends FrameLayout { + static final String TAG = "ScrollView"; + static final boolean localLOGV = false || Config.LOGV; + private static final int ANIMATED_SCROLL_GAP = 250; /** @@ -194,6 +199,7 @@ public class ScrollView extends FrameLayout { setFocusable(true); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setWillNotDraw(false); + setScrollContainer(true); } @Override @@ -839,12 +845,16 @@ public class ScrollView extends FrameLayout { public final void smoothScrollBy(int dx, int dy) { long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; if (duration > ANIMATED_SCROLL_GAP) { + if (localLOGV) Log.v(TAG, "Smooth scroll: mScrollY=" + mScrollY + + " dy=" + dy); mScroller.startScroll(mScrollX, mScrollY, dx, dy); invalidate(); } else { if (!mScroller.isFinished()) { mScroller.abortAnimation(); } + if (localLOGV) Log.v(TAG, "Immediate scroll: mScrollY=" + mScrollY + + " dy=" + dy); scrollBy(dx, dy); } mLastScroll = AnimationUtils.currentAnimationTimeMillis(); @@ -927,14 +937,19 @@ public class ScrollView extends FrameLayout { View child = getChildAt(0); mScrollX = clamp(x, this.getWidth(), child.getWidth()); mScrollY = clamp(y, this.getHeight(), child.getHeight()); + if (localLOGV) Log.v(TAG, "mScrollY=" + mScrollY + " y=" + y + + " height=" + this.getHeight() + + " child height=" + child.getHeight()); } else { mScrollX = x; mScrollY = y; } if (oldX != mScrollX || oldY != mScrollY) { onScrollChanged(mScrollX, mScrollY, oldX, oldY); - postInvalidate(); // So we draw again } + + // Keep on drawing until the animation has finished. + postInvalidate(); } } @@ -1005,6 +1020,9 @@ public class ScrollView extends FrameLayout { int scrollYDelta = 0; + if (localLOGV) Log.v(TAG, "child=" + rect.toShortString() + + " screenTop=" + screenTop + " screenBottom=" + screenBottom + + " height=" + height); if (rect.bottom > screenBottom && rect.top > screenTop) { // need to move down to get it in view: move down just enough so // that the entire rectangle is in view (or at least the first @@ -1021,6 +1039,8 @@ public class ScrollView extends FrameLayout { // make sure we aren't scrolling beyond the end of our content int bottom = getChildAt(getChildCount() - 1).getBottom(); int distanceToBottom = bottom - screenBottom; + if (localLOGV) Log.v(TAG, "scrollYDelta=" + scrollYDelta + + " distanceToBottom=" + distanceToBottom); scrollYDelta = Math.min(scrollYDelta, distanceToBottom); } else if (rect.top < screenTop && rect.bottom < screenBottom) { @@ -1098,8 +1118,7 @@ public class ScrollView extends FrameLayout { rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY()); - // note: until bug 1137695 is fixed, disable smooth scrolling for this api - return scrollToChildRect(rectangle, true);//immediate); + return scrollToChildRect(rectangle, immediate); } @Override @@ -1122,6 +1141,24 @@ public class ScrollView extends FrameLayout { scrollTo(mScrollX, mScrollY); } + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + View currentFocused = findFocus(); + if (null == currentFocused || this == currentFocused) + return; + + final int maxJump = mBottom - mTop; + + if (isWithinDeltaOfScreen(currentFocused, maxJump)) { + currentFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(currentFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScrollY(scrollDelta); + } + } + /** * Return true if child is an descendant of parent, (or equal to the parent). */ diff --git a/core/java/android/widget/SectionIndexer.java b/core/java/android/widget/SectionIndexer.java new file mode 100644 index 0000000..24f894c --- /dev/null +++ b/core/java/android/widget/SectionIndexer.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.widget; + +/** + * Interface that should be implemented on Adapters to enable fast scrolling + * in an {@link AbsListView} between sections of the list. A section is a group of list items + * to jump to that have something in common. For example, they may begin with the + * same letter or they may be songs from the same artist. + */ +public interface SectionIndexer { + /** + * This provides the list view with an array of section objects. In the simplest + * case these are Strings, each containing one letter of the alphabet. + * They could be more complex objects that indicate the grouping for the adapter's + * consumption. The list view will call toString() on the objects to get the + * preview letter to display while scrolling. + * @return the array of objects that indicate the different sections of the list. + */ + Object[] getSections(); + + /** + * Provides the starting index in the list for a given section. + * @param section the index of the section to jump to. + * @return the starting position of that section. If the section is out of bounds, the + * position must be clipped to fall within the size of the list. + */ + int getPositionForSection(int section); + + /** + * This is a reverse mapping to fetch the section index for a given position + * in the list. + * @param position the position for which to return the section + * @return the section index. If the position is out of bounds, the section index + * must be clipped to fall within the size of the section array. + */ + int getSectionForPosition(int position); +} diff --git a/core/java/android/widget/SimpleAdapter.java b/core/java/android/widget/SimpleAdapter.java index df52b69..261da9f 100644 --- a/core/java/android/widget/SimpleAdapter.java +++ b/core/java/android/widget/SimpleAdapter.java @@ -115,10 +115,22 @@ public class SimpleAdapter extends BaseAdapter implements Filterable { View v; if (convertView == null) { v = mInflater.inflate(resource, parent, false); + + final int[] to = mTo; + final int count = to.length; + final View[] holder = new View[count]; + + for (int i = 0; i < count; i++) { + holder[i] = v.findViewById(to[i]); + } + + v.setTag(holder); } else { v = convertView; } + bindView(position, v); + return v; } @@ -143,12 +155,14 @@ public class SimpleAdapter extends BaseAdapter implements Filterable { return; } + final ViewBinder binder = mViewBinder; + final View[] holder = (View[]) view.getTag(); final String[] from = mFrom; final int[] to = mTo; - final int len = to.length; + final int count = to.length; - for (int i = 0; i < len; i++) { - final View v = view.findViewById(to[i]); + for (int i = 0; i < count; i++) { + final View v = holder[i]; if (v != null) { final Object data = dataSet.get(from[i]); String text = data == null ? "" : data.toString(); @@ -157,8 +171,8 @@ public class SimpleAdapter extends BaseAdapter implements Filterable { } boolean bound = false; - if (mViewBinder != null) { - bound = mViewBinder.setViewValue(v, data, text); + if (binder != null) { + bound = binder.setViewValue(v, data, text); } if (!bound) { diff --git a/core/java/android/widget/SimpleCursorAdapter.java b/core/java/android/widget/SimpleCursorAdapter.java index 4d2fab3..74a9964 100644 --- a/core/java/android/widget/SimpleCursorAdapter.java +++ b/core/java/android/widget/SimpleCursorAdapter.java @@ -20,6 +20,7 @@ import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.view.View; +import android.view.ViewGroup; /** * An easy adapter to map columns from a cursor to TextViews or ImageViews @@ -79,14 +80,36 @@ public class SimpleCursorAdapter extends ResourceCursorAdapter { * are given the values of the first N columns in the from * parameter. */ - public SimpleCursorAdapter(Context context, int layout, Cursor c, - String[] from, int[] to) { + public SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to) { super(context, layout, c); mTo = to; mOriginalFrom = from; findColumns(from); } - + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return generateViewHolder(super.newView(context, cursor, parent)); + } + + @Override + public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) { + return generateViewHolder(super.newDropDownView(context, cursor, parent)); + } + + private View generateViewHolder(View v) { + final int[] to = mTo; + final int count = to.length; + final View[] holder = new View[count]; + + for (int i = 0; i < count; i++) { + holder[i] = v.findViewById(to[i]); + } + v.setTag(holder); + + return v; + } + /** * Binds all of the field names passed into the "to" parameter of the * constructor with their corresponding cursor columns as specified in the @@ -113,17 +136,22 @@ public class SimpleCursorAdapter extends ResourceCursorAdapter { */ @Override public void bindView(View view, Context context, Cursor cursor) { - for (int i = 0; i < mTo.length; i++) { - final View v = view.findViewById(mTo[i]); + final View[] holder = (View[]) view.getTag(); + final ViewBinder binder = mViewBinder; + final int count = mTo.length; + final int[] from = mFrom; + + for (int i = 0; i < count; i++) { + final View v = holder[i]; if (v != null) { - String text = cursor.getString(mFrom[i]); + String text = cursor.getString(from[i]); if (text == null) { text = ""; } boolean bound = false; - if (mViewBinder != null) { - bound = mViewBinder.setViewValue(v, cursor, mFrom[i]); + if (binder != null) { + bound = binder.setViewValue(v, cursor, from[i]); } if (!bound) { diff --git a/core/java/android/widget/TableLayout.java b/core/java/android/widget/TableLayout.java index d72ffb1..afa2f3b 100644 --- a/core/java/android/widget/TableLayout.java +++ b/core/java/android/widget/TableLayout.java @@ -101,11 +101,9 @@ public class TableLayout extends LinearLayout { public TableLayout(Context context, AttributeSet attrs) { super(context, attrs); - TypedArray a = - context.obtainStyledAttributes(attrs, R.styleable.TableLayout); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TableLayout); - String stretchedColumns = - a.getString(R.styleable.TableLayout_stretchColumns); + String stretchedColumns = a.getString(R.styleable.TableLayout_stretchColumns); if (stretchedColumns != null) { if (stretchedColumns.charAt(0) == '*') { mStretchAllColumns = true; @@ -114,8 +112,7 @@ public class TableLayout extends LinearLayout { } } - String shrinkedColumns = - a.getString(R.styleable.TableLayout_shrinkColumns); + String shrinkedColumns = a.getString(R.styleable.TableLayout_shrinkColumns); if (shrinkedColumns != null) { if (shrinkedColumns.charAt(0) == '*') { mShrinkAllColumns = true; @@ -124,8 +121,7 @@ public class TableLayout extends LinearLayout { } } - String collapsedColumns = - a.getString(R.styleable.TableLayout_collapseColumns); + String collapsedColumns = a.getString(R.styleable.TableLayout_collapseColumns); if (collapsedColumns != null) { mCollapsedColumns = parseColumns(collapsedColumns); } @@ -356,7 +352,7 @@ public class TableLayout extends LinearLayout { * @return true if the column is shrinkable, false otherwise. Default is false. */ public boolean isColumnShrinkable(int columnIndex) { - return mShrinkAllColumns || mStretchableColumns.get(columnIndex); + return mShrinkAllColumns || mShrinkableColumns.get(columnIndex); } /** diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index bd5db33..9e5f019 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; @@ -27,10 +28,12 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; +import android.os.Message; import android.text.BoringLayout; import android.text.DynamicLayout; import android.text.Editable; @@ -49,12 +52,16 @@ import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextWatcher; +import android.text.method.DateKeyListener; +import android.text.method.DateTimeKeyListener; import android.text.method.DialerKeyListener; import android.text.method.DigitsKeyListener; import android.text.method.KeyListener; import android.text.method.LinkMovementMethod; import android.text.method.MetaKeyKeyListener; import android.text.method.MovementMethod; +import android.text.method.TimeKeyListener; + import android.text.method.PasswordTransformationMethod; import android.text.method.SingleLineTransformationMethod; import android.text.method.TextKeyListener; @@ -78,12 +85,22 @@ import android.view.ViewDebug; import android.view.ViewTreeObserver; import android.view.ViewGroup.LayoutParams; import android.view.animation.AnimationUtils; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.EditorInfo; import android.widget.RemoteViews.RemoteView; +import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; import com.android.internal.util.FastMath; +import com.android.internal.widget.EditableInputConnection; + +import org.xmlpull.v1.XmlPullParserException; /** * Displays text to the user and optionally allows them to edit it. A TextView @@ -146,6 +163,7 @@ import com.android.internal.util.FastMath; * @attr ref android.R.styleable#TextView_drawableLeft * @attr ref android.R.styleable#TextView_lineSpacingExtra * @attr ref android.R.styleable#TextView_lineSpacingMultiplier + * @attr ref android.R.styleable#TextView_marqueeRepeatLimit */ @RemoteView public class TextView extends View implements ViewTreeObserver.OnPreDrawListener { @@ -182,22 +200,44 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private static final int SIGNED = 2; private static final int DECIMAL = 4; - private Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight; - private int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight; - private int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight; - private boolean mDrawables; - private int mDrawablePadding; + class Drawables { + final Rect mCompoundRect = new Rect(); + Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight; + int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight; + int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight; + int mDrawablePadding; + }; + private Drawables mDrawables; private CharSequence mError; private boolean mErrorWasChanged; private PopupWindow mPopup; private CharWrapper mCharWrapper = null; - private Rect mCompoundRect; private boolean mSelectionMoved = false; - /* + private Marquee mMarquee; + private boolean mRestartMarquee; + + private int mMarqueeRepeatLimit = 3; + + class InputContentType { + String privateContentType; + Bundle extras; + } + InputContentType mInputContentType; + + class InputMethodState { + Rect mCursorRectInWindow = new Rect(); + RectF mTmpRectF = new RectF(); + float[] mTmpOffset = new float[2]; + ExtractedTextRequest mExtracting; + final ExtractedText mTmpExtracted = new ExtractedText(); + } + InputMethodState mInputMethodState; + + /* * Kick-start the font cache for the zygote process (to pay the cost of * initializing freetype for our default font only once). */ @@ -221,7 +261,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mText = ""; mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); @@ -250,7 +289,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * Look the appearance up without checking first if it exists because * almost every TextView has one and it greatly simplifies the logic * to be able to parse the appearance first and then let specific tags - * for this View override it. + * for this View override it. */ TypedArray appearance = null; int ap = a.getResourceId(com.android.internal.R.styleable.TextView_textAppearance, -1); @@ -317,6 +356,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int shadowcolor = 0; float dx = 0, dy = 0, r = 0; boolean password = false; + int contentType = EditorInfo.TYPE_NULL; int n = a.getIndexCount(); for (int i = 0; i < n; i++) { @@ -461,6 +501,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ellipsize = a.getInt(attr, ellipsize); break; + case com.android.internal.R.styleable.TextView_marqueeRepeatLimit: + setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit)); + break; + case com.android.internal.R.styleable.TextView_includeFontPadding: if (!a.getBoolean(attr, true)) { setIncludeFontPadding(false); @@ -544,12 +588,37 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case com.android.internal.R.styleable.TextView_lineSpacingMultiplier: mSpacingMult = a.getFloat(attr, mSpacingMult); break; + + case com.android.internal.R.styleable.TextView_inputType: + contentType = a.getInt(attr, mInputType); + break; + + case com.android.internal.R.styleable.TextView_editorPrivateContentType: + setPrivateContentType(a.getString(attr)); + break; + + case com.android.internal.R.styleable.TextView_editorExtras: + try { + setInputExtras(a.getResourceId(attr, 0)); + } catch (XmlPullParserException e) { + Log.w("TextView", "Failure reading input extras", e); + } catch (IOException e) { + Log.w("TextView", "Failure reading input extras", e); + } + break; } } a.recycle(); BufferType bufferType = BufferType.EDITABLE; + if ((contentType&(EditorInfo.TYPE_MASK_CLASS + |EditorInfo.TYPE_MASK_VARIATION)) + == (EditorInfo.TYPE_CLASS_TEXT + |EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) { + password = true; + } + if (inputMethod != null) { Class c; @@ -566,27 +635,58 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } catch (IllegalAccessException ex) { throw new RuntimeException(ex); } + try { + mInputType = contentType != EditorInfo.TYPE_NULL + ? contentType + : mInput.getInputType(); + } catch (IncompatibleClassChangeError e) { + mInputType = EditorInfo.TYPE_CLASS_TEXT; + } } else if (digits != null) { mInput = DigitsKeyListener.getInstance(digits.toString()); + mInputType = contentType; + } else if (contentType != EditorInfo.TYPE_NULL) { + setInputType(contentType, true); + singleLine = (contentType&(EditorInfo.TYPE_MASK_CLASS + | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) != + (EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); } else if (phone) { mInput = DialerKeyListener.getInstance(); + contentType = EditorInfo.TYPE_CLASS_PHONE; } else if (numeric != 0) { mInput = DigitsKeyListener.getInstance((numeric & SIGNED) != 0, (numeric & DECIMAL) != 0); + contentType = EditorInfo.TYPE_CLASS_NUMBER; + if ((numeric & SIGNED) != 0) { + contentType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED; + } + if ((numeric & DECIMAL) != 0) { + contentType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL; + } + mInputType = contentType; } else if (autotext || autocap != -1) { TextKeyListener.Capitalize cap; + contentType = EditorInfo.TYPE_CLASS_TEXT; + if (!singleLine) { + contentType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; + } + switch (autocap) { case 1: cap = TextKeyListener.Capitalize.SENTENCES; + contentType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES; break; case 2: cap = TextKeyListener.Capitalize.WORDS; + contentType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; break; case 3: cap = TextKeyListener.Capitalize.CHARACTERS; + contentType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS; break; default: @@ -595,8 +695,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } mInput = TextKeyListener.getInstance(autotext, cap); + mInputType = contentType; } else if (editable) { mInput = TextKeyListener.getInstance(); + mInputType = EditorInfo.TYPE_CLASS_TEXT; } else { mInput = null; @@ -611,6 +713,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener bufferType = BufferType.EDITABLE; break; } + mInputType = EditorInfo.TYPE_CLASS_TEXT; + } + + if (password) { + mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) + | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD; } if (selectallonfocus) { @@ -642,6 +750,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case 3: setEllipsize(TextUtils.TruncateAt.END); break; + case 4: + setHorizontalFadingEdgeEnabled(true); + setEllipsize(TextUtils.TruncateAt.MARQUEE); + break; } setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000)); @@ -774,11 +886,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * Return the text the TextView is displaying. If setText() was called - * with an argument of BufferType.SPANNABLE or BufferType.EDITABLE, - * you can cast the return value from this method to Spannable - * or Editable, respectively. + * Return the text the TextView is displaying. If setText() was called with + * an argument of BufferType.SPANNABLE or BufferType.EDITABLE, you can cast + * the return value from this method to Spannable or Editable, respectively. + * + * Note: The content of the return value should not be modified. If you want + * a modifiable one, you should make your own copy first. */ + @ViewDebug.CapturedViewProperty public CharSequence getText() { return mText; } @@ -791,6 +906,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Return the text the TextView is displaying as an Editable object. If + * the text is not editable, null is returned. + * + * @see #getText + */ + public Editable getEditableText() { + return (mText instanceof Editable) ? (Editable)mText : null; + } + + /** * @return the height of one standard line in pixels. Note that markup * within the text can cause individual lines to be taller or shorter * than this height, and the layout may contain additional first- @@ -819,7 +944,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Sets the key listener to be used with this TextView. This can be null - * to disallow user input. + * to disallow user input. Note that this method has significant and + * subtle interactions with soft keyboards and other input method: + * see {@link KeyListener#getInputType() KeyListener.getContentType()} + * for important details. Calling this method will replace the current + * content type of the text view with the content type returned by the + * key listener. * <p> * Be warned that if you want a TextView with a key listener or movement * method not to be focusable, or if you want a TextView without a @@ -835,13 +965,37 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_autoText */ public void setKeyListener(KeyListener input) { - mInput = input; + setKeyListenerOnly(input); + fixFocusableAndClickableSettings(); + + if (input != null) { + try { + mInputType = mInput.getInputType(); + } catch (IncompatibleClassChangeError e) { + mInputType = EditorInfo.TYPE_CLASS_TEXT; + } + if ((mInputType&EditorInfo.TYPE_MASK_CLASS) + == EditorInfo.TYPE_CLASS_TEXT) { + if (mSingleLine) { + mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; + } else { + mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; + } + } + } else { + mInputType = EditorInfo.TYPE_NULL; + } + + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) imm.restartInput(this); + } + private void setKeyListenerOnly(KeyListener input) { + mInput = input; if (mInput != null && !(mText instanceof Editable)) setText(mText); setFilters((Editable) mText, mFilters); - fixFocusableAndClickableSettings(); } /** @@ -873,7 +1027,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private void fixFocusableAndClickableSettings() { - if (mMovement != null || mInput != null) { + if ((mMovement != null) || mInput != null) { setFocusable(true); setClickable(true); setLongClickable(true); @@ -917,10 +1071,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * Drawable if any. */ public int getCompoundPaddingTop() { - if (mDrawableTop == null) { + final Drawables dr = mDrawables; + if (dr == null || dr.mDrawableTop == null) { return mPaddingTop; } else { - return mPaddingTop + mDrawablePadding + mDrawableSizeTop; + return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop; } } @@ -929,10 +1084,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * Drawable if any. */ public int getCompoundPaddingBottom() { - if (mDrawableBottom == null) { + final Drawables dr = mDrawables; + if (dr == null || dr.mDrawableBottom == null) { return mPaddingBottom; } else { - return mPaddingBottom + mDrawablePadding + mDrawableSizeBottom; + return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom; } } @@ -941,10 +1097,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * Drawable if any. */ public int getCompoundPaddingLeft() { - if (mDrawableLeft == null) { + final Drawables dr = mDrawables; + if (dr == null || dr.mDrawableLeft == null) { return mPaddingLeft; } else { - return mPaddingLeft + mDrawablePadding + mDrawableSizeLeft; + return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft; } } @@ -953,10 +1110,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * Drawable if any. */ public int getCompoundPaddingRight() { - if (mDrawableRight == null) { + final Drawables dr = mDrawables; + if (dr == null || dr.mDrawableRight == null) { return mPaddingRight; } else { - return mPaddingRight + mDrawablePadding + mDrawableSizeRight; + return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight; } } @@ -1043,7 +1201,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * Returns the total top padding of the view, including the top + * Returns the total top padding of the view, including the top * Drawable if any, the extra space to keep more than maxLines * from showing, and the vertical offset for gravity, if any. */ @@ -1073,62 +1231,79 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom) { - mDrawableLeft = left; - mDrawableTop = top; - mDrawableRight = right; - mDrawableBottom = bottom; + Drawables dr = mDrawables; - mDrawables = mDrawableLeft != null - || mDrawableRight != null - || mDrawableTop != null - || mDrawableBottom != null; + final boolean drawables = left != null || top != null + || right != null || bottom != null; - if (mCompoundRect == null && - (left != null || top != null || right != null || bottom != null)) { - mCompoundRect = new Rect(); - } + if (!drawables) { + // Clearing drawables... can we free the data structure? + if (dr != null) { + if (dr.mDrawablePadding == 0) { + mDrawables = null; + } else { + // We need to retain the last set padding, so just clear + // out all of the fields in the existing structure. + dr.mDrawableLeft = null; + dr.mDrawableTop = null; + dr.mDrawableRight = null; + dr.mDrawableBottom = null; + dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0; + dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0; + dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; + dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; + } + } + } else { + if (dr == null) { + mDrawables = dr = new Drawables(); + } + + dr.mDrawableLeft = left; + dr.mDrawableTop = top; + dr.mDrawableRight = right; + dr.mDrawableBottom = bottom; + + final Rect compoundRect = dr.mCompoundRect; + int[] state = null; - final Rect compoundRect = mCompoundRect; - int[] state = null; - - if (mDrawables) { state = getDrawableState(); - } - - if (mDrawableLeft != null) { - mDrawableLeft.setState(state); - mDrawableLeft.copyBounds(compoundRect); - mDrawableSizeLeft = compoundRect.width(); - mDrawableHeightLeft = compoundRect.height(); - } else { - mDrawableSizeLeft = mDrawableHeightLeft = 0; - } - if (mDrawableRight != null) { - mDrawableRight.setState(state); - mDrawableRight.copyBounds(compoundRect); - mDrawableSizeRight = compoundRect.width(); - mDrawableHeightRight = compoundRect.height(); - } else { - mDrawableSizeRight = mDrawableHeightRight = 0; - } + if (left != null) { + left.setState(state); + left.copyBounds(compoundRect); + dr.mDrawableSizeLeft = compoundRect.width(); + dr.mDrawableHeightLeft = compoundRect.height(); + } else { + dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0; + } - if (mDrawableTop != null) { - mDrawableTop.setState(state); - mDrawableTop.copyBounds(compoundRect); - mDrawableSizeTop = compoundRect.height(); - mDrawableWidthTop = compoundRect.width(); - } else { - mDrawableSizeTop = mDrawableWidthTop = 0; - } + if (right != null) { + right.setState(state); + right.copyBounds(compoundRect); + dr.mDrawableSizeRight = compoundRect.width(); + dr.mDrawableHeightRight = compoundRect.height(); + } else { + dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0; + } - if (mDrawableBottom != null) { - mDrawableBottom.setState(state); - mDrawableBottom.copyBounds(compoundRect); - mDrawableSizeBottom = compoundRect.height(); - mDrawableWidthBottom = compoundRect.width(); - } else { - mDrawableSizeBottom = mDrawableWidthBottom = 0; + if (top != null) { + top.setState(state); + top.copyBounds(compoundRect); + dr.mDrawableSizeTop = compoundRect.height(); + dr.mDrawableWidthTop = compoundRect.width(); + } else { + dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; + } + + if (bottom != null) { + bottom.setState(state); + bottom.copyBounds(compoundRect); + dr.mDrawableSizeBottom = compoundRect.height(); + dr.mDrawableWidthBottom = compoundRect.width(); + } else { + dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; + } } invalidate(); @@ -1137,8 +1312,32 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Sets the Drawables (if any) to appear to the left of, above, + * to the right of, and below the text. Use 0 if you do not + * want a Drawable there. The Drawables' bounds will be set to + * their intrinsic bounds. + * + * @param left Resource identifier of the left Drawable. + * @param top Resource identifier of the top Drawable. + * @param right Resource identifier of the right Drawable. + * @param bottom Resource identifier of the bottom Drawable. + * + * @attr ref android.R.styleable#TextView_drawableLeft + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableRight + * @attr ref android.R.styleable#TextView_drawableBottom + */ + public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) { + final Resources resources = getContext().getResources(); + setCompoundDrawables(left != 0 ? resources.getDrawable(left) : null, + top != 0 ? resources.getDrawable(top) : null, + right != 0 ? resources.getDrawable(right) : null, + bottom != 0 ? resources.getDrawable(bottom) : null); + } + + /** + * Sets the Drawables (if any) to appear to the left of, above, * to the right of, and below the text. Use null if you do not - * want a Drawable there. The Drawables' bounds will be set to + * want a Drawable there. The Drawables' bounds will be set to * their intrinsic bounds. * * @attr ref android.R.styleable#TextView_drawableLeft @@ -1146,24 +1345,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_drawableRight * @attr ref android.R.styleable#TextView_drawableBottom */ - public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, - Drawable top, - Drawable right, Drawable bottom) { + public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top, + Drawable right, Drawable bottom) { + if (left != null) { - left.setBounds(0, 0, - left.getIntrinsicWidth(), left.getIntrinsicHeight()); + left.setBounds(0, 0, left.getIntrinsicWidth(), left.getIntrinsicHeight()); } if (right != null) { - right.setBounds(0, 0, - right.getIntrinsicWidth(), right.getIntrinsicHeight()); + right.setBounds(0, 0, right.getIntrinsicWidth(), right.getIntrinsicHeight()); } if (top != null) { - top.setBounds(0, 0, - top.getIntrinsicWidth(), top.getIntrinsicHeight()); + top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight()); } if (bottom != null) { - bottom.setBounds(0, 0, - bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight()); + bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight()); } setCompoundDrawables(left, top, right, bottom); } @@ -1172,9 +1367,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * Returns drawables for the left, top, right, and bottom borders. */ public Drawable[] getCompoundDrawables() { - return new Drawable[] { - mDrawableLeft, mDrawableTop, mDrawableRight, mDrawableBottom - }; + final Drawables dr = mDrawables; + if (dr != null) { + return new Drawable[] { + dr.mDrawableLeft, dr.mDrawableTop, dr.mDrawableRight, dr.mDrawableBottom + }; + } else { + return new Drawable[] { null, null, null, null }; + } } /** @@ -1184,7 +1384,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_drawablePadding */ public void setCompoundDrawablePadding(int pad) { - mDrawablePadding = pad; + Drawables dr = mDrawables; + if (pad == 0) { + if (dr != null) { + dr.mDrawablePadding = pad; + } + } else { + if (dr == null) { + mDrawables = dr = new Drawables(); + } + dr.mDrawablePadding = pad; + } invalidate(); requestLayout(); @@ -1194,7 +1404,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * Returns the padding between the compound drawables and the text. */ public int getCompoundDrawablePadding() { - return mDrawablePadding; + final Drawables dr = mDrawables; + return dr != null ? dr.mDrawablePadding : 0; } @Override @@ -1910,18 +2121,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener updateTextColors(); } - int[] state = getDrawableState(); - if (mDrawableTop != null && mDrawableTop.isStateful()) { - mDrawableTop.setState(state); - } - if (mDrawableBottom != null && mDrawableBottom.isStateful()) { - mDrawableBottom.setState(state); - } - if (mDrawableLeft != null && mDrawableLeft.isStateful()) { - mDrawableLeft.setState(state); - } - if (mDrawableRight != null && mDrawableRight.isStateful()) { - mDrawableRight.setState(state); + final Drawables dr = mDrawables; + if (dr != null) { + int[] state = getDrawableState(); + if (dr.mDrawableTop != null && dr.mDrawableTop.isStateful()) { + dr.mDrawableTop.setState(state); + } + if (dr.mDrawableBottom != null && dr.mDrawableBottom.isStateful()) { + dr.mDrawableBottom.setState(state); + } + if (dr.mDrawableLeft != null && dr.mDrawableLeft.isStateful()) { + dr.mDrawableLeft.setState(state); + } + if (dr.mDrawableRight != null && dr.mDrawableRight.isStateful()) { + dr.mDrawableRight.setState(state); + } } } @@ -1982,12 +2196,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); - + // Save state if we are forced to boolean save = mFreezesText; int start = 0; int end = 0; - + if (mText != null) { start = Selection.getSelectionStart(mText); end = Selection.getSelectionEnd(mText); @@ -1996,7 +2210,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener save = true; } } - + if (save) { SavedState ss = new SavedState(superState); // XXX Should also save the current scroll position! @@ -2030,15 +2244,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return ss; } - - return null; + + return superState; } @Override public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + SavedState ss = (SavedState)state; super.onRestoreInstanceState(ss.getSuperState()); - + // XXX restore buffer type too, as well as lots of other stuff if (ss.text != null) { setText(ss.text); @@ -2165,6 +2384,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener text = ""; } + if (text instanceof Spanned && + ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) { + setHorizontalFadingEdgeEnabled(true); + setEllipsize(TextUtils.TruncateAt.MARQUEE); + } + int n = mFilters.length; for (int i = 0; i < n; i++) { CharSequence out = mFilters[i].filter(text, 0, text.length(), @@ -2183,11 +2408,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - if (type == BufferType.EDITABLE || mInput != null) { + boolean needEditableForNotification = false; + + if (mListeners != null && mListeners.size() != 0) { + needEditableForNotification = true; + } + + if (type == BufferType.EDITABLE || mInput != null || + needEditableForNotification) { Editable t = mEditableFactory.newEditable(text); text = t; - setFilters(t, mFilters); + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) imm.restartInput(this); } else if (type == BufferType.SPANNABLE || mMovement != null) { text = mSpannableFactory.newSpannable(text); } else if (!(text instanceof CharWrapper)) { @@ -2256,7 +2489,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (mMovement != null) { - mMovement.initialize(this, (Spannable) text); + mMovement.initialize(this, (Spannable) text); /* * Initializing the movement method will have set the @@ -2273,6 +2506,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener sendOnTextChanged(text, 0, oldlen, textLength); onTextChanged(text, 0, oldlen, textLength); + + if (needEditableForNotification) { + sendAfterTextChanged((Editable) text); + } } /** @@ -2401,8 +2638,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Sets the text to be displayed when the text of the TextView is empty. - * Null means to use the normal empty text. The hint does not - * currently participate in determining the size of the view. + * Null means to use the normal empty text. The hint does not currently + * participate in determining the size of the view. + * + * This method is deprecated. Use {link #setHint(int, String)} or + * {link #setHint(CharSequence, String)} instead. * * @attr ref android.R.styleable#TextView_hint */ @@ -2421,6 +2661,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * Sets the text to be displayed when the text of the TextView is empty, * from a resource. * + * This method is deprecated. Use {link #setHint(int, String)} or + * {link #setHint(CharSequence, String)} instead. + * * @attr ref android.R.styleable#TextView_hint */ public final void setHint(int resid) { @@ -2433,11 +2676,177 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @attr ref android.R.styleable#TextView_hint */ + @ViewDebug.CapturedViewProperty public CharSequence getHint() { return mHint; } /** + * Set the type of the content with a constant as defined for + * {@link EditorInfo#inputType}. This will take care of changing + * the key listener, by calling {@link #setKeyListener(KeyListener)}, to + * match the given content type. If the given content type is + * {@link EditorInfo#TYPE_NULL} then a soft keyboard will + * not be displayed for this text view. + * + * @see #getInputType() + * @see #setRawInputType(int) + * @see android.text.InputType + * @attr ref android.R.styleable#TextView_inputType + */ + public void setInputType(int type) { + setInputType(type, false); + if ((type&(EditorInfo.TYPE_MASK_CLASS + |EditorInfo.TYPE_MASK_VARIATION)) + == (EditorInfo.TYPE_CLASS_TEXT + |EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) { + setTransformationMethod(PasswordTransformationMethod.getInstance()); + setTypefaceByIndex(MONOSPACE, 0); + } + boolean multiLine = (type&(EditorInfo.TYPE_MASK_CLASS + | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) == + (EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); + if (mSingleLine == multiLine) { + setSingleLine(!multiLine); + } + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) imm.restartInput(this); + } + + /** + * Directly change the content type integer of the text view, without + * modifying any other state. + * @see #setContentType + * @see android.text.InputType + * @attr ref android.R.styleable#TextView_inputType + */ + public void setRawInputType(int type) { + mInputType = type; + } + + private void setInputType(int type, boolean direct) { + final int cls = type & EditorInfo.TYPE_MASK_CLASS; + KeyListener input; + if (cls == EditorInfo.TYPE_CLASS_TEXT) { + boolean autotext = (type & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) + != 0; + TextKeyListener.Capitalize cap; + if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) { + cap = TextKeyListener.Capitalize.CHARACTERS; + } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) { + cap = TextKeyListener.Capitalize.WORDS; + } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) { + cap = TextKeyListener.Capitalize.SENTENCES; + } else { + cap = TextKeyListener.Capitalize.NONE; + } + input = TextKeyListener.getInstance(autotext, cap); + } else if (cls == EditorInfo.TYPE_CLASS_NUMBER) { + input = DigitsKeyListener.getInstance( + (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0, + (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0); + } else if (cls == EditorInfo.TYPE_CLASS_DATETIME) { + switch (type & EditorInfo.TYPE_MASK_VARIATION) { + case EditorInfo.TYPE_DATETIME_VARIATION_DATE: + input = DateKeyListener.getInstance(); + break; + case EditorInfo.TYPE_DATETIME_VARIATION_TIME: + input = TimeKeyListener.getInstance(); + break; + default: + input = DateTimeKeyListener.getInstance(); + break; + } + } else if (cls == EditorInfo.TYPE_CLASS_PHONE) { + input = DialerKeyListener.getInstance(); + } else { + input = TextKeyListener.getInstance(); + } + mInputType = type; + if (direct) mInput = input; + else { + setKeyListenerOnly(input); + } + } + + /** + * Get the type of the content. + * + * @see #setInputType(int) + * @see android.text.InputType + */ + public int getInputType() { + return mInputType; + } + + /** + * Set the private content type of the text, which is the + * {@link EditorInfo#privateContentType TextBoxAttribute.privateContentType} + * field that will be filled in when creating an input connection. + * + * @see #getPrivateContentType() + * @see EditorInfo#privateContentType + * @attr ref android.R.styleable#TextView_editorPrivateContentType + */ + public void setPrivateContentType(String type) { + if (mInputContentType == null) mInputContentType = new InputContentType(); + mInputContentType.privateContentType = type; + } + + /** + * Get the private type of the content. + * + * @see #setPrivateContentType(String) + * @see EditorInfo#privateContentType + */ + public String getPrivateContentType() { + return mInputContentType != null + ? mInputContentType.privateContentType : null; + } + + /** + * Set the extra input data of the text, which is the + * {@link EditorInfo#extras TextBoxAttribute.extras} + * Bundle that will be filled in when creating an input connection. The + * given integer is the resource ID of an XML resource holding an + * {@link android.R.styleable#InputExtras <input-extras>} XML tree. + * + * @see #getInputExtras() + * @see EditorInfo#extras + * @attr ref android.R.styleable#TextView_editorExtras + */ + public void setInputExtras(int xmlResId) + throws XmlPullParserException, IOException { + XmlResourceParser parser = getResources().getXml(xmlResId); + if (mInputContentType == null) mInputContentType = new InputContentType(); + mInputContentType.extras = new Bundle(); + getResources().parseBundleExtras(parser, mInputContentType.extras); + } + + /** + * Retrieve the input extras currently associated with the text view, which + * can be viewed as well as modified. + * + * @param create If true, the extras will be created if they don't already + * exist. Otherwise, null will be returned if none have been created. + * @see #setInputExtras(int) + * @see EditorInfo#extras + * @attr ref android.R.styleable#TextView_editorExtras + */ + public Bundle getInputExtras(boolean create) { + if (mInputContentType == null) { + if (!create) return null; + mInputContentType = new InputContentType(); + } + if (mInputContentType.extras == null) { + if (!create) return null; + mInputContentType.extras = new Bundle(); + } + return mInputContentType.extras; + } + + /** * Returns the error message that was set to be displayed with * {@link #setError}, or <code>null</code> if no error was set * or if it the error was cleared by the widget after user input. @@ -2481,8 +2890,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mError = error; mErrorWasChanged = true; - setCompoundDrawables(mDrawableLeft, mDrawableTop, - icon, mDrawableBottom); + final Drawables dr = mDrawables; + if (dr != null) { + setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, + icon, dr.mDrawableBottom); + } else { + setCompoundDrawables(null, null, icon, null); + } if (error == null) { if (mPopup != null) { @@ -2525,9 +2939,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * The "25" is the distance between the point and the right edge * of the background */ - + + final Drawables dr = mDrawables; return getWidth() - mPopup.getWidth() - - getPaddingRight() - mDrawableSizeRight / 2 + 25; + - getPaddingRight() + - (dr != null ? dr.mDrawableSizeRight : 0) / 2 + 25; } /** @@ -2542,17 +2958,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int vspace = mBottom - mTop - getCompoundPaddingBottom() - getCompoundPaddingTop(); - int icontop = getCompoundPaddingTop() + - (vspace - mDrawableHeightRight) / 2; + final Drawables dr = mDrawables; + int icontop = getCompoundPaddingTop() + + (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2; /* * The "2" is the distance between the point and the top edge * of the background. */ - return icontop + mDrawableHeightRight - getHeight() - 2; + return icontop + (dr != null ? dr.mDrawableHeightRight : 0) + - getHeight() - 2; } - + private void hideError() { if (mPopup != null) { if (mPopup.isShowing()) { @@ -2599,6 +3017,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mPopup.update(this, getErrorX(), getErrorY(), -1, -1); } + if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) { + mRestartMarquee = false; + startMarquee(); + } + return result; } @@ -2659,10 +3082,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int boxht; if (l == mHintLayout) { - boxht = getMeasuredHeight() - getCompoundPaddingTop() - + boxht = getMeasuredHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom(); } else { - boxht = getMeasuredHeight() - getExtendedPaddingTop() - + boxht = getMeasuredHeight() - getExtendedPaddingTop() - getExtendedPaddingBottom(); } int textht = l.getHeight(); @@ -2690,10 +3113,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int boxht; if (l == mHintLayout) { - boxht = getMeasuredHeight() - getCompoundPaddingTop() - + boxht = getMeasuredHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom(); } else { - boxht = getMeasuredHeight() - getExtendedPaddingTop() - + boxht = getMeasuredHeight() - getExtendedPaddingTop() - getExtendedPaddingBottom(); } int textht = l.getHeight(); @@ -2713,15 +3136,32 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener invalidateCursor(); } else { synchronized (sTempRect) { + /* + * The reason for this concern about the thickness of the + * cursor and doing the floor/ceil on the coordinates is that + * some EditTexts (notably textfields in the Browser) have + * anti-aliased text where not all the characters are + * necessarily at integer-multiple locations. This should + * make sure the entire cursor gets invalidated instead of + * sometimes missing half a pixel. + */ + + float thick = FloatMath.ceil(mTextPaint.getStrokeWidth()); + if (thick < 1.0f) { + thick = 1.0f; + } + + thick /= 2; + mHighlightPath.computeBounds(sTempRect, false); int left = getCompoundPaddingLeft(); int top = getExtendedPaddingTop() + getVerticalOffset(true); - invalidate((int) sTempRect.left + left, - (int) sTempRect.top + top, - (int) sTempRect.right + left + 1, - (int) sTempRect.bottom + top + 1); + invalidate((int) FloatMath.floor(left + sTempRect.left - thick), + (int) FloatMath.floor(top + sTempRect.top - thick), + (int) FloatMath.ceil(left + sTempRect.right + thick), + (int) FloatMath.ceil(top + sTempRect.bottom + thick)); } } } @@ -2837,6 +3277,35 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mPreDrawState = PREDRAW_NOT_REGISTERED; } } + + if (mError != null) { + hideError(); + } + } + + @Override + protected boolean isPaddingOffsetRequired() { + return mShadowRadius != 0; + } + + @Override + protected int getLeftPaddingOffset() { + return (int) Math.min(0, mShadowDx - mShadowRadius); + } + + @Override + protected int getTopPaddingOffset() { + return (int) Math.min(0, mShadowDy - mShadowRadius); + } + + @Override + protected int getBottomPaddingOffset() { + return (int) Math.max(0, mShadowDy + mShadowRadius); + } + + @Override + protected int getRightPaddingOffset() { + return (int) Math.max(0, mShadowDx + mShadowRadius); } @Override @@ -2855,7 +3324,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int bottom = mBottom; final int top = mTop; - if (mDrawables) { + final Drawables dr = mDrawables; + if (dr != null) { /* * Compound, not extended, because the icon is not clipped * if the text height is smaller. @@ -2864,37 +3334,37 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop; int hspace = right - left - compoundPaddingRight - compoundPaddingLeft; - if (mDrawableLeft != null) { + if (dr.mDrawableLeft != null) { canvas.save(); canvas.translate(scrollX + mPaddingLeft, scrollY + compoundPaddingTop + - (vspace - mDrawableHeightLeft) / 2); - mDrawableLeft.draw(canvas); + (vspace - dr.mDrawableHeightLeft) / 2); + dr.mDrawableLeft.draw(canvas); canvas.restore(); } - if (mDrawableRight != null) { + if (dr.mDrawableRight != null) { canvas.save(); - canvas.translate(scrollX + right - left - mPaddingRight - mDrawableSizeRight, - scrollY + compoundPaddingTop + (vspace - mDrawableHeightRight) / 2); - mDrawableRight.draw(canvas); + canvas.translate(scrollX + right - left - mPaddingRight - dr.mDrawableSizeRight, + scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2); + dr.mDrawableRight.draw(canvas); canvas.restore(); } - if (mDrawableTop != null) { + if (dr.mDrawableTop != null) { canvas.save(); - canvas.translate(scrollX + compoundPaddingLeft + (hspace - mDrawableWidthTop) / 2, + canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2, scrollY + mPaddingTop); - mDrawableTop.draw(canvas); + dr.mDrawableTop.draw(canvas); canvas.restore(); } - if (mDrawableBottom != null) { + if (dr.mDrawableBottom != null) { canvas.save(); canvas.translate(scrollX + compoundPaddingLeft + - (hspace - mDrawableWidthBottom) / 2, - scrollY + bottom - top - mPaddingBottom - mDrawableSizeBottom); - mDrawableBottom.draw(canvas); + (hspace - dr.mDrawableWidthBottom) / 2, + scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom); + dr.mDrawableBottom.draw(canvas); canvas.restore(); } } @@ -2929,7 +3399,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener canvas.save(); /* Would be faster if we didn't have to do this. Can we chop the - (displayable) text so that we don't need to do this ever? + (displayable) text so that we don't need to do this ever? */ int extendedPaddingTop = getExtendedPaddingTop(); @@ -2963,7 +3433,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText); } + if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { + if (!mSingleLine && getLineCount() == 1 && canMarquee() && + (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) { + canvas.translate(mLayout.getLineRight(0) - (mRight - mLeft - + getCompoundPaddingLeft() - getCompoundPaddingRight()), 0.0f); + } + + if (mMarquee != null && mMarquee.isRunning()) { + canvas.translate(-mMarquee.mScroll, 0.0f); + } + } + Path highlight = null; + int selStart = -1, selEnd = -1; // If there is no movement method, then there can be no selection. // Check that first and attempt to skip everything having to do with @@ -2971,19 +3454,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // XXX This is not strictly true -- a program could set the // selection manually if it really wanted to. if (mMovement != null && (isFocused() || isPressed())) { - int start = Selection.getSelectionStart(mText); - int end = Selection.getSelectionEnd(mText); + selStart = Selection.getSelectionStart(mText); + selEnd = Selection.getSelectionEnd(mText); - if (mCursorVisible && start >= 0 && isEnabled()) { + if (mCursorVisible && selStart >= 0 && isEnabled()) { if (mHighlightPath == null) mHighlightPath = new Path(); - if (start == end) { + if (selStart == selEnd) { if ((SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) < BLINK) { if (mHighlightPathBogus) { mHighlightPath.reset(); - mLayout.getCursorPath(start, mHighlightPath, mText); + mLayout.getCursorPath(selStart, mHighlightPath, mText); mHighlightPathBogus = false; } @@ -2996,7 +3479,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else { if (mHighlightPathBogus) { mHighlightPath.reset(); - mLayout.getSelectionPath(start, end, mHighlightPath); + mLayout.getSelectionPath(selStart, selEnd, mHighlightPath); mHighlightPathBogus = false; } @@ -3020,6 +3503,31 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } */ + InputMethodManager imm = InputMethodManager.peekInstance(); + if (highlight != null && mInputMethodState != null && imm != null) { + imm.updateSelection(this, selStart, selEnd); + + if (imm.isWatchingCursor(this)) { + final InputMethodState ims = mInputMethodState; + highlight.computeBounds(ims.mTmpRectF, true); + ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0; + + canvas.getMatrix().mapPoints(ims.mTmpOffset); + ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]); + + ims.mTmpRectF.offset(0, voffsetCursor - voffsetText); + + ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5), + (int)(ims.mTmpRectF.top + 0.5), + (int)(ims.mTmpRectF.right + 0.5), + (int)(ims.mTmpRectF.bottom + 0.5)); + + imm.updateCursor(this, + ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top, + ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom); + } + } + layout.draw(canvas, highlight, mHighlightPaint, voffsetCursor - voffsetText); /* Comment out until we decide what to do about animations @@ -3050,6 +3558,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener r.left = (int) mLayout.getPrimaryHorizontal(sel); r.right = r.left + 1; + + // Adjust for padding and gravity. + int paddingLeft = getCompoundPaddingLeft(); + int paddingTop = getExtendedPaddingTop(); + if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { + paddingTop += getVerticalOffset(false); + } + r.offset(paddingLeft, paddingTop); } /** @@ -3106,20 +3622,67 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (!isEnabled()) { + int which = doKeyDown(keyCode, event); + if (which == 0) { + // Go through default dispatching. return super.onKeyDown(keyCode, event); } + return true; + } + + @Override + public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + KeyEvent down = new KeyEvent(event, KeyEvent.ACTION_DOWN); + + int which = doKeyDown(keyCode, down); + if (which == 0) { + // Go through default dispatching. + return super.onKeyMultiple(keyCode, repeatCount, event); + } + + // We are going to dispatch the remaining events to either the input + // or movement method. To do this, we will just send a repeated stream + // of down and up events until we have done the complete repeatCount. + // It would be nice if those interfaces had an onKeyMultiple() method, + // but adding that is a more complicated change. + KeyEvent up = new KeyEvent(event, KeyEvent.ACTION_UP); + if (which == 1) { + mInput.onKeyUp(this, (Editable)mText, keyCode, up); + while (--repeatCount > 0) { + mInput.onKeyDown(this, (Editable)mText, keyCode, down); + mInput.onKeyUp(this, (Editable)mText, keyCode, up); + } + if (mError != null && !mErrorWasChanged) { + setError(null, null); + } + + } else if (which == 2) { + mMovement.onKeyUp(this, (Spannable)mText, keyCode, up); + while (--repeatCount > 0) { + mMovement.onKeyDown(this, (Spannable)mText, keyCode, down); + mMovement.onKeyUp(this, (Spannable)mText, keyCode, up); + } + } + + return true; + } + + private int doKeyDown(int keyCode, KeyEvent event) { + if (!isEnabled()) { + return 0; + } + switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: if (mSingleLine && mInput != null) { - return super.onKeyDown(keyCode, event); + return 0; } } if (mInput != null) { - /* + /* * Keep track of what the error was before doing the input * so that if an input filter changed the error, we leave * that error showing. Otherwise, we take down whatever @@ -3131,7 +3694,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mError != null && !mErrorWasChanged) { setError(null, null); } - return true; + return 1; } } @@ -3140,9 +3703,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mMovement != null && mLayout != null) if (mMovement.onKeyDown(this, (Spannable)mText, keyCode, event)) - return true; + return 2; - return super.onKeyDown(keyCode, event); + return 0; } @Override @@ -3199,6 +3762,108 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return super.onKeyUp(keyCode, event); } + @Override public InputConnection createInputConnection(EditorInfo outAttrs) { + if (mInputType != EditorInfo.TYPE_NULL) { + if (mInputMethodState == null) { + mInputMethodState = new InputMethodState(); + } + outAttrs.inputType = mInputType; + outAttrs.hintText = mHint; + if (mInputContentType != null) { + outAttrs.privateContentType = mInputContentType.privateContentType; + outAttrs.extras = mInputContentType.extras; + } + if (mText instanceof Editable) { + InputConnection ic = new EditableInputConnection(this); + outAttrs.initialSelStart = Selection.getSelectionStart(mText); + outAttrs.initialSelEnd = Selection.getSelectionEnd(mText); + outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType); + return ic; + } + } + return null; + } + + /** + * If this TextView contains editable content, extract a portion of it + * based on the information in <var>request</var> in to <var>outText</var>. + * @return Returns true if the text is editable and was successfully + * extracted, else false. + */ + public boolean extractText(ExtractedTextRequest request, + ExtractedText outText) { + Editable content = getEditableText(); + if (content != null) { + outText.text = content.subSequence(0, content.length()); + outText.startOffset = 0; + outText.selectionStart = Selection.getSelectionStart(content); + outText.selectionEnd = Selection.getSelectionEnd(content); + return true; + } + return false; + } + + void reportExtractedText() { + if (mInputMethodState != null) { + final ExtractedTextRequest req = mInputMethodState.mExtracting; + if (req != null) { + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + if (extractText(req, mInputMethodState.mTmpExtracted)) { + imm.updateExtractedText(this, req.token, + mInputMethodState.mTmpExtracted); + } + } + } + } + } + + /** + * Apply to this text view the given extracted text, as previously + * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}. + */ + public void setExtractedText(ExtractedText text) { + setText(text.text, TextView.BufferType.EDITABLE); + Selection.setSelection((Spannable)getText(), + text.selectionStart, text.selectionEnd); + } + + /** + * @hide + */ + public void setExtracting(ExtractedTextRequest req) { + if (mInputMethodState != null) { + mInputMethodState.mExtracting = req; + } + } + + /** + * Called by the framework in response to a text completion from + * the current input method, provided by it calling + * {@link InputConnection#commitCompletion + * InputConnection.commitCompletion()}. The default implementation does + * nothing; text views that are supporting auto-completion should override + * this to do their desired behavior. + * + * @param text The auto complete text the user has selected. + */ + public void onCommitCompletion(CompletionInfo text) { + } + + /** + * Called by the framework in response to a private command from the + * current method, provided by it calling + * {@link InputConnection#performPrivateCommand + * InputConnection.performPrivateCommand()}. + * + * @param action The action name of the command. + * @param data Any additional data for the command. This may be null. + * @return Return true if you handled the command, else false. + */ + public boolean onPrivateIMECommand(String action, Bundle data) { + return false; + } + private void nullLayouts() { if (mLayout instanceof BoringLayout && mSavedLayout == null) { mSavedLayout = (BoringLayout) mLayout; @@ -3240,6 +3905,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener BoringLayout.Metrics boring, BoringLayout.Metrics hintBoring, int ellipsisWidth, boolean bringIntoView) { + stopMarquee(); + mHighlightPathBogus = true; if (w < 0) { @@ -3371,6 +4038,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (bringIntoView) { registerForPreDraw(); } + + if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { + final int height = mLayoutParams.height; + // If the size of the view does not depend on the size of the text, try to + // start the marquee immediately + if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.FILL_PARENT) { + startMarquee(); + } else { + // Defer the start of the marquee until we know our width (see setFrame()) + mRestartMarquee = true; + } + } } private static int desired(Layout layout) { @@ -3458,8 +4137,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener width = boring.width; } - width = Math.max(width, mDrawableWidthTop); - width = Math.max(width, mDrawableWidthBottom); + final Drawables dr = mDrawables; + if (dr != null) { + width = Math.max(width, dr.mDrawableWidthTop); + width = Math.max(width, dr.mDrawableWidthBottom); + } if (mHint != null) { int hintDes = -1; @@ -3509,7 +4191,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // Check against our minimum width width = Math.max(width, getSuggestedMinimumWidth()); - + if (widthMode == MeasureSpec.AT_MOST) { width = Math.min(widthSize, width); } @@ -3596,8 +4278,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int pad = getCompoundPaddingTop() + getCompoundPaddingBottom(); int desired = layout.getLineTop(linecount); - desired = Math.max(desired, mDrawableHeightLeft); - desired = Math.max(desired, mDrawableHeightRight); + final Drawables dr = mDrawables; + if (dr != null) { + desired = Math.max(desired, dr.mDrawableHeightLeft); + desired = Math.max(desired, dr.mDrawableHeightRight); + } desired += pad; @@ -3611,8 +4296,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener desired = layout.getLineTop(mMaximum) + layout.getBottomPadding(); - desired = Math.max(desired, mDrawableHeightLeft); - desired = Math.max(desired, mDrawableHeightRight); + if (dr != null) { + desired = Math.max(desired, dr.mDrawableHeightLeft); + desired = Math.max(desired, dr.mDrawableHeightRight); + } desired += pad; linecount = mMaximum; @@ -3632,7 +4319,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // Check against our minimum height desired = Math.max(desired, getSuggestedMinimumHeight()); - + return desired; } @@ -3978,8 +4665,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private void getInterestingRect(Rect r, int h, int top, int bottom, int line) { - top += getExtendedPaddingTop(); - bottom += getExtendedPaddingTop(); + int paddingTop = getExtendedPaddingTop(); + if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { + paddingTop += getVerticalOffset(false); + } + top += paddingTop; + bottom += paddingTop; h += getCompoundPaddingLeft(); if (line == 0) @@ -3987,7 +4678,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (line == mLayout.getLineCount() - 1) bottom += getExtendedPaddingBottom(); - r.set(h, top, h, bottom); + r.set(h, top, h+1, bottom); r.offset(-mScrollX, -mScrollY); } @@ -4056,6 +4747,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void setSingleLine(boolean singleLine) { mSingleLine = singleLine; + if ((mInputType&EditorInfo.TYPE_MASK_CLASS) + == EditorInfo.TYPE_CLASS_TEXT) { + if (singleLine) { + mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; + } else { + mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; + } + } if (singleLine) { setLines(1); @@ -4089,9 +4788,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Sets how many times to repeat the marquee animation. Only applied if the + * TextView has marquee enabled. Set to -1 to repeat indefinitely. + * + * @attr ref android.R.styleable#TextView_marqueeRepeatLimit + */ + public void setMarqueeRepeatLimit(int marqueeLimit) { + mMarqueeRepeatLimit = marqueeLimit; + } + + /** * Returns where, if anywhere, words that are longer than the view * is wide should be ellipsized. */ + @ViewDebug.ExportedProperty public TextUtils.TruncateAt getEllipsize() { return mEllipsize; } @@ -4126,6 +4836,145 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + private boolean canMarquee() { + int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight()); + return width > 0 && mLayout.getLineWidth(0) > width; + } + + private void startMarquee() { + if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) && + getLineCount() == 1 && canMarquee()) { + if (mMarquee == null) mMarquee = new Marquee(this); + mMarquee.start(mMarqueeRepeatLimit); + } + } + + private void stopMarquee() { + if (mMarquee != null && !mMarquee.isStopped()) { + mMarquee.stop(); + } + } + + private void startStopMarquee(boolean start) { + if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { + if (start) { + startMarquee(); + } else { + stopMarquee(); + } + } + } + + private static final class Marquee extends Handler { + // TODO: Add an option to configure this + private static final int MARQUEE_DELAY = 1200; + private static final int MARQUEE_RESTART_DELAY = 1200; + private static final int MARQUEE_RESOLUTION = 1000 / 30; + private static final int MARQUEE_PIXELS_PER_SECOND = 30; + + private static final byte MARQUEE_STOPPED = 0x0; + private static final byte MARQUEE_STARTING = 0x1; + private static final byte MARQUEE_RUNNING = 0x2; + + private static final int MESSAGE_START = 0x1; + private static final int MESSAGE_TICK = 0x2; + private static final int MESSAGE_RESTART = 0x3; + + private final WeakReference<TextView> mView; + + private byte mStatus = MARQUEE_STOPPED; + private float mScrollUnit; + private float mMaxScroll; + private int mRepeatLimit; + + float mScroll; + + Marquee(TextView v) { + mView = new WeakReference<TextView>(v); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_START: + mStatus = MARQUEE_RUNNING; + tick(); + break; + case MESSAGE_TICK: + tick(); + break; + case MESSAGE_RESTART: + if (mStatus == MARQUEE_RUNNING) { + if (mRepeatLimit >= 0) { + mRepeatLimit--; + } + start(mRepeatLimit); + } + break; + } + } + + void tick() { + if (mStatus != MARQUEE_RUNNING) { + return; + } + + removeMessages(MESSAGE_TICK); + + final TextView textView = mView.get(); + if (textView != null && (textView.isFocused() || textView.isSelected())) { + mScroll += mScrollUnit; + if (mScroll > mMaxScroll) { + mScroll = mMaxScroll; + sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY); + } else { + sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION); + } + textView.invalidate(); + } + } + + void stop() { + mStatus = MARQUEE_STOPPED; + removeMessages(MESSAGE_START); + removeMessages(MESSAGE_RESTART); + removeMessages(MESSAGE_TICK); + resetScroll(); + } + + private void resetScroll() { + mScroll = 0.0f; + final TextView textView = mView.get(); + if (textView != null) textView.invalidate(); + } + + void start(int repeatLimit) { + if (repeatLimit == 0) { + stop(); + return; + } + mRepeatLimit = repeatLimit; + final TextView textView = mView.get(); + if (textView != null && textView.mLayout != null) { + mStatus = MARQUEE_STARTING; + mScroll = 0.0f; + mScrollUnit = MARQUEE_PIXELS_PER_SECOND / (float) MARQUEE_RESOLUTION; + mMaxScroll = textView.mLayout.getLineWidth(0) - (textView.getWidth() - + textView.getCompoundPaddingLeft() - textView.getCompoundPaddingRight()); + textView.invalidate(); + sendEmptyMessageDelayed(MESSAGE_START, MARQUEE_DELAY); + } + } + + boolean isRunning() { + return mStatus == MARQUEE_RUNNING; + } + + boolean isStopped() { + return mStatus == MARQUEE_STOPPED; + } + } + /** * This method is called when the text is changed, in case any * subclasses would like to know. @@ -4151,6 +5000,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Adds a TextWatcher to the list of those whose methods are called * whenever this TextView's text changes. + * <p> + * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously + * not called after {@link #setText} calls. Now, doing {@link #setText} + * if there are any text changed listeners forces the buffer type to + * Editable if it would not otherwise be and does call this method. */ public void addTextChangedListener(TextWatcher watcher) { if (mListeners == null) { @@ -4186,7 +5040,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void sendOnTextChanged(CharSequence text, int start, int before, + /** + * Not private so it can be called from an inner class without going + * through a thunk. + */ + void sendOnTextChanged(CharSequence text, int start, int before, int after) { if (mListeners != null) { final ArrayList<TextWatcher> list = mListeners; @@ -4197,7 +5055,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void sendAfterTextChanged(Editable text) { + /** + * Not private so it can be called from an inner class without going + * through a thunk. + */ + void sendAfterTextChanged(Editable text) { if (mListeners != null) { final ArrayList<TextWatcher> list = mListeners; final int count = list.size(); @@ -4207,105 +5069,118 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private class ChangeWatcher - extends Handler - implements TextWatcher, SpanWatcher { - public void beforeTextChanged(CharSequence buffer, int start, - int before, int after) { - TextView.this.sendBeforeTextChanged(buffer, start, before, after); + /** + * Not private so it can be called from an inner class without going + * through a thunk. + */ + void handleTextChanged(CharSequence buffer, int start, + int before, int after) { + invalidate(); + + int curs = Selection.getSelectionStart(buffer); + + if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == + Gravity.BOTTOM) { + registerForPreDraw(); } - public void onTextChanged(CharSequence buffer, int start, - int before, int after) { - invalidate(); + if (curs >= 0) { + mHighlightPathBogus = true; - int curs = Selection.getSelectionStart(buffer); + if (isFocused()) { + mShowCursor = SystemClock.uptimeMillis(); + makeBlink(); + } + } - if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == - Gravity.BOTTOM) { - registerForPreDraw(); + checkForResize(); + + sendOnTextChanged(buffer, start, before, after); + onTextChanged(buffer, start, before, after); + } + + /** + * Not private so it can be called from an inner class without going + * through a thunk. + */ + void spanChange(Spanned buf, Object what, int o, int n) { + // XXX Make the start and end move together if this ends up + // spending too much time invalidating. + + if (what == Selection.SELECTION_END) { + mHighlightPathBogus = true; + + if (!isFocused()) { + mSelectionMoved = true; } - if (curs >= 0) { - mHighlightPathBogus = true; + if (o >= 0 || n >= 0) { + invalidateCursor(Selection.getSelectionStart(buf), o, n); + registerForPreDraw(); if (isFocused()) { mShowCursor = SystemClock.uptimeMillis(); makeBlink(); } } - - checkForResize(); - - TextView.this.sendOnTextChanged(buffer, start, before, after); - TextView.this.onTextChanged(buffer, start, before, after); } - public void afterTextChanged(Editable buffer) { - TextView.this.sendAfterTextChanged(buffer); - } - - private void spanChange(Spanned buf, Object what, int o, int n) { - // XXX Make the start and end move together if this ends up - // spending too much time invalidating. + if (what == Selection.SELECTION_START) { + mHighlightPathBogus = true; - if (what == Selection.SELECTION_END) { - mHighlightPathBogus = true; - - if (!isFocused()) { - mSelectionMoved = true; - } - - if (o >= 0 || n >= 0) { - invalidateCursor(Selection.getSelectionStart(buf), o, n); - registerForPreDraw(); + if (!isFocused()) { + mSelectionMoved = true; + } - if (isFocused()) { - mShowCursor = SystemClock.uptimeMillis(); - makeBlink(); - } - } + if (o >= 0 || n >= 0) { + invalidateCursor(Selection.getSelectionEnd(buf), o, n); } + } - if (what == Selection.SELECTION_START) { - mHighlightPathBogus = true; + if (what instanceof UpdateLayout || + what instanceof ParagraphStyle) { + invalidate(); + mHighlightPathBogus = true; + checkForResize(); + } - if (!isFocused()) { - mSelectionMoved = true; - } + if (MetaKeyKeyListener.isMetaTracker(buf, what)) { + mHighlightPathBogus = true; - if (o >= 0 || n >= 0) { - invalidateCursor(Selection.getSelectionEnd(buf), o, n); - } + if (Selection.getSelectionStart(buf) >= 0) { + invalidateCursor(); } + } + } - if (what instanceof UpdateLayout || - what instanceof ParagraphStyle) { - invalidate(); - mHighlightPathBogus = true; - checkForResize(); - } + private class ChangeWatcher + implements TextWatcher, SpanWatcher { + public void beforeTextChanged(CharSequence buffer, int start, + int before, int after) { + TextView.this.sendBeforeTextChanged(buffer, start, before, after); + } - if (MetaKeyKeyListener.isMetaTracker(buf, what)) { - mHighlightPathBogus = true; + public void onTextChanged(CharSequence buffer, int start, + int before, int after) { + TextView.this.handleTextChanged(buffer, start, before, after); + } - if (Selection.getSelectionStart(buf) >= 0) { - invalidateCursor(); - } - } + public void afterTextChanged(Editable buffer) { + TextView.this.sendAfterTextChanged(buffer); + TextView.this.reportExtractedText(); } public void onSpanChanged(Spannable buf, Object what, int s, int e, int st, int en) { - spanChange(buf, what, s, st); + TextView.this.spanChange(buf, what, s, st); } public void onSpanAdded(Spannable buf, Object what, int s, int e) { - spanChange(buf, what, -1, s); + TextView.this.spanChange(buf, what, -1, s); } public void onSpanRemoved(Spannable buf, Object what, int s, int e) { - spanChange(buf, what, s, -1); + TextView.this.spanChange(buf, what, s, -1); } } @@ -4378,6 +5253,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + startStopMarquee(focused); + if (mTransformation != null) { mTransformation.onFocusChanged(this, mText, focused, direction, previouslyFocusedRect); @@ -4386,6 +5263,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener super.onFocusChanged(focused, direction, previouslyFocusedRect); } + @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); @@ -4403,6 +5281,23 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mBlink.cancel(); } } + + startStopMarquee(hasWindowFocus); + } + + @Override + public void setSelected(boolean selected) { + boolean wasSelected = isSelected(); + + super.setSelected(selected); + + if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) { + if (selected) { + startMarquee(); + } else { + stopMarquee(); + } + } } @Override @@ -4421,7 +5316,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mMovement != null && mText instanceof Spannable && mLayout != null) { - if (mMovement.onTouchEvent(this, (Spannable) mText, event)) { + boolean moved = mMovement.onTouchEvent(this, (Spannable) mText, event); + + if (mText instanceof Editable + && mInputType != EditorInfo.TYPE_NULL) { + if (event.getAction() == MotionEvent.ACTION_UP && isFocused()) { + InputMethodManager imm = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(this); + } + } + + if (moved) { return true; } } @@ -4490,6 +5396,52 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Override + protected float getLeftFadingEdgeStrength() { + if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { + if (mMarquee != null && !mMarquee.isStopped()) { + final Marquee marquee = mMarquee; + return marquee.mScroll / getHorizontalFadingEdgeLength(); + } else if (getLineCount() == 1) { + switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.LEFT: + return 0.0f; + case Gravity.RIGHT: + return (mLayout.getLineRight(0) - (mRight - mLeft) - + getCompoundPaddingLeft() - getCompoundPaddingRight() - + mLayout.getLineLeft(0)) / getHorizontalFadingEdgeLength(); + case Gravity.CENTER_HORIZONTAL: + return 0.0f; + } + } + } + return super.getLeftFadingEdgeStrength(); + } + + @Override + protected float getRightFadingEdgeStrength() { + if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { + if (mMarquee != null && !mMarquee.isStopped()) { + final Marquee marquee = mMarquee; + return (marquee.mMaxScroll - marquee.mScroll) / getHorizontalFadingEdgeLength(); + } else if (getLineCount() == 1) { + switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.LEFT: + return (mLayout.getLineRight(0) - mScrollX - (mRight - mLeft) - + getCompoundPaddingLeft() - getCompoundPaddingRight()) / + getHorizontalFadingEdgeLength(); + case Gravity.RIGHT: + return 0.0f; + case Gravity.CENTER_HORIZONTAL: + return (mLayout.getLineWidth(0) - ((mRight - mLeft) - + getCompoundPaddingLeft() - getCompoundPaddingRight())) / + getHorizontalFadingEdgeLength(); + } + } + } + return super.getRightFadingEdgeStrength(); + } + + @Override protected int computeHorizontalScrollRange() { if (mLayout != null) return mLayout.getWidth(); @@ -4599,6 +5551,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private boolean canCut() { + if (mTransformation instanceof PasswordTransformationMethod) { + return false; + } + if (mText.length() > 0 && getSelectionStart() >= 0) { if (mText instanceof Editable && mInput != null) { return true; @@ -4609,6 +5565,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private boolean canCopy() { + if (mTransformation instanceof PasswordTransformationMethod) { + return false; + } + if (mText.length() > 0 && getSelectionStart() >= 0) { return true; } @@ -4632,8 +5592,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override protected void onCreateContextMenu(ContextMenu menu) { super.onCreateContextMenu(menu); + boolean added = false; if (!isFocused()) { + if (isFocusable() && mInput != null) { + if (canCopy()) { + MenuHandler handler = new MenuHandler(); + int name = com.android.internal.R.string.copyAll; + + menu.add(0, ID_COPY, 0, name). + setOnMenuItemClickListener(handler). + setAlphabeticShortcut('c'); + menu.setHeaderTitle(com.android.internal.R.string. + editTextMenuTitle); + } + } + return; } @@ -4644,6 +5618,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener com.android.internal.R.string.selectAll). setOnMenuItemClickListener(handler). setAlphabeticShortcut('a'); + added = true; } boolean selection = getSelectionStart() != getSelectionEnd(); @@ -4659,6 +5634,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener menu.add(0, ID_CUT, 0, name). setOnMenuItemClickListener(handler). setAlphabeticShortcut('x'); + added = true; } if (canCopy()) { @@ -4672,12 +5648,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener menu.add(0, ID_COPY, 0, name). setOnMenuItemClickListener(handler). setAlphabeticShortcut('c'); + added = true; } if (canPaste()) { menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste). setOnMenuItemClickListener(handler). setAlphabeticShortcut('v'); + added = true; } if (mText instanceof Spanned) { @@ -4693,15 +5671,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener menu.add(0, ID_COPY_URL, 0, com.android.internal.R.string.copyUrl). setOnMenuItemClickListener(handler); + added = true; } } + + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null && imm.isActive(this)) { + menu.add(1, ID_SWITCH_IME, 0, com.android.internal.R.string.inputMethod). + setOnMenuItemClickListener(handler); + added = true; + } + + if (added) { + menu.setHeaderTitle(com.android.internal.R.string.editTextMenuTitle); + } } - private static final int ID_SELECT_ALL = 101; - private static final int ID_CUT = 102; - private static final int ID_COPY = 103; - private static final int ID_PASTE = 104; - private static final int ID_COPY_URL = 105; + private static final int ID_SELECT_ALL = com.android.internal.R.id.selectAll; + private static final int ID_CUT = com.android.internal.R.id.cut; + private static final int ID_COPY = com.android.internal.R.id.copy; + private static final int ID_PASTE = com.android.internal.R.id.paste; + private static final int ID_COPY_URL = com.android.internal.R.id.copyUrl; + private static final int ID_SWITCH_IME = com.android.internal.R.id.inputMethod; private class MenuHandler implements MenuItem.OnMenuItemClickListener { public boolean onMenuItemClick(MenuItem item) { @@ -4713,6 +5704,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int selStart = getSelectionStart(); int selEnd = getSelectionEnd(); + if (!isFocused()) { + selStart = 0; + selEnd = mText.length(); + } + int min = Math.min(selStart, selEnd); int max = Math.max(selStart, selEnd); @@ -4769,7 +5765,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } return true; - } + + case ID_SWITCH_IME: + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + imm.showInputMethodPicker(); + } + return true; + } return false; } @@ -4790,10 +5793,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private CharSequence mTransformed; private BufferType mBufferType = BufferType.NORMAL; + private int mInputType = EditorInfo.TYPE_NULL; private CharSequence mHint; private Layout mHintLayout; private KeyListener mInput; + private MovementMethod mMovement; private TransformationMethod mTransformation; private ChangeWatcher mChangeWatcher; @@ -4842,7 +5847,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // tmp primitives, so we don't alloc them on each draw private Path mHighlightPath; private boolean mHighlightPathBogus = true; - private static RectF sTempRect = new RectF(); + private static final RectF sTempRect = new RectF(); // XXX should be much larger private static final int VERY_WIDE = 16384; @@ -4858,8 +5863,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private BoringLayout mSavedLayout, mSavedHintLayout; - - private static final InputFilter[] NO_FILTERS = new InputFilter[0]; private InputFilter[] mFilters = NO_FILTERS; private static final Spanned EMPTY_SPANNED = new SpannedString(""); diff --git a/core/java/android/widget/VideoView.java b/core/java/android/widget/VideoView.java index da3c2aa..df40156 100644 --- a/core/java/android/widget/VideoView.java +++ b/core/java/android/widget/VideoView.java @@ -182,6 +182,7 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { try { mMediaPlayer = new MediaPlayer(); mMediaPlayer.setOnPreparedListener(mPreparedListener); + mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener); mIsPrepared = false; mMediaPlayer.setOnCompletionListener(mCompletionListener); mMediaPlayer.setOnErrorListener(mErrorListener); @@ -220,6 +221,17 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { } } + MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener = + new MediaPlayer.OnVideoSizeChangedListener() { + public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { + mVideoWidth = mp.getVideoWidth(); + mVideoHeight = mp.getVideoHeight(); + if (mVideoWidth != 0 && mVideoHeight != 0) { + getHolder().setFixedSize(mVideoWidth, mVideoHeight); + } + } + }; + MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() { public void onPrepared(MediaPlayer mp) { // briefly show the mediacontroller @@ -241,26 +253,32 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { // start the video here instead of in the callback. if (mSeekWhenPrepared != 0) { mMediaPlayer.seekTo(mSeekWhenPrepared); + mSeekWhenPrepared = 0; } if (mStartWhenPrepared) { mMediaPlayer.start(); + mStartWhenPrepared = false; if (mMediaController != null) { mMediaController.show(); } - } else if (!isPlaying() && (mSeekWhenPrepared != 0 || getCurrentPosition() > 0)) { + } else if (!isPlaying() && + (mSeekWhenPrepared != 0 || getCurrentPosition() > 0)) { if (mMediaController != null) { - mMediaController.show(0); // show the media controls when we're paused into a video and make 'em stick. + // Show the media controls when we're paused into a video and make 'em stick. + mMediaController.show(0); } } } } else { - Log.d("VideoView", "Couldn't get video size after prepare(): " + - mVideoWidth + "/" + mVideoHeight); - // The file was probably truncated or corrupt. Start anyway, so - // that we play whatever short snippet is there and then get - // the "playback completed" event. + // We don't know the video size yet, but should start anyway. + // The video size might be reported to us later. + if (mSeekWhenPrepared != 0) { + mMediaPlayer.seekTo(mSeekWhenPrepared); + mSeekWhenPrepared = 0; + } if (mStartWhenPrepared) { mMediaPlayer.start(); + mStartWhenPrepared = false; } } } @@ -370,9 +388,10 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { { mSurfaceWidth = w; mSurfaceHeight = h; - if (mIsPrepared && mVideoWidth == w && mVideoHeight == h) { + if (mMediaPlayer != null && mIsPrepared && mVideoWidth == w && mVideoHeight == h) { if (mSeekWhenPrepared != 0) { mMediaPlayer.seekTo(mSeekWhenPrepared); + mSeekWhenPrepared = 0; } mMediaPlayer.start(); if (mMediaController != null) { diff --git a/core/java/com/android/internal/app/AlertController.java b/core/java/com/android/internal/app/AlertController.java index 53b9654..989f972 100644 --- a/core/java/com/android/internal/app/AlertController.java +++ b/core/java/com/android/internal/app/AlertController.java @@ -34,6 +34,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.Window; +import android.view.WindowManager; import android.view.ViewGroup.LayoutParams; import android.widget.AdapterView; import android.widget.ArrayAdapter; @@ -68,23 +69,33 @@ public class AlertController { private View mView; - private Button mButton1; + private int mViewSpacingLeft; + + private int mViewSpacingTop; + + private int mViewSpacingRight; + + private int mViewSpacingBottom; + + private boolean mViewSpacingSpecified = false; + + private Button mButtonPositive; - private CharSequence mButton1Text; + private CharSequence mButtonPositiveText; - private Message mButton1Message; + private Message mButtonPositiveMessage; - private Button mButton2; + private Button mButtonNegative; - private CharSequence mButton2Text; + private CharSequence mButtonNegativeText; - private Message mButton2Message; + private Message mButtonNegativeMessage; - private Button mButton3; + private Button mButtonNeutral; - private CharSequence mButton3Text; + private CharSequence mButtonNeutralText; - private Message mButton3Message; + private Message mButtonNeutralMessage; private ScrollView mScrollView; @@ -111,12 +122,12 @@ public class AlertController { View.OnClickListener mButtonHandler = new View.OnClickListener() { public void onClick(View v) { Message m = null; - if (v == mButton1 && mButton1Message != null) { - m = Message.obtain(mButton1Message); - } else if (v == mButton2 && mButton2Message != null) { - m = Message.obtain(mButton2Message); - } else if (v == mButton3 && mButton3Message != null) { - m = Message.obtain(mButton3Message); + if (v == mButtonPositive && mButtonPositiveMessage != null) { + m = Message.obtain(mButtonPositiveMessage); + } else if (v == mButtonNegative && mButtonNegativeMessage != null) { + m = Message.obtain(mButtonNegativeMessage); + } else if (v == mButtonNeutral && mButtonNeutralMessage != null) { + m = Message.obtain(mButtonNeutralMessage); } if (m != null) { m.sendToTarget(); @@ -142,9 +153,9 @@ public class AlertController { public void handleMessage(Message msg) { switch (msg.what) { - case DialogInterface.BUTTON1: - case DialogInterface.BUTTON2: - case DialogInterface.BUTTON3: + case DialogInterface.BUTTON_POSITIVE: + case DialogInterface.BUTTON_NEGATIVE: + case DialogInterface.BUTTON_NEUTRAL: ((DialogInterface.OnClickListener) msg.obj).onClick(mDialog.get(), msg.what); break; @@ -165,6 +176,10 @@ public class AlertController { /* We use a custom title so never request a window title */ mWindow.requestFeature(Window.FEATURE_NO_TITLE); + if (mView == null) { + mWindow.setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, + WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + } mWindow.setContentView(com.android.internal.R.layout.alert_dialog); setupView(); } @@ -191,66 +206,64 @@ public class AlertController { } /** - * Set the view to display in that dialog. + * Set the view to display in the dialog. */ public void setView(View view) { mView = view; + mViewSpacingSpecified = false; } - - public void setButton(CharSequence text, Message msg) { - mButton1Text = text; - mButton1Message = msg; - } - - public void setButton2(CharSequence text, Message msg) { - mButton2Text = text; - mButton2Message = msg; - } - - public void setButton3(CharSequence text, Message msg) { - mButton3Text = text; - mButton3Message = msg; - } - + /** - * Set a listener to be invoked when button 1 of the dialog is pressed. - * @param text The text to display in button 1. - * @param listener The {@link DialogInterface.OnClickListener} to use. + * Set the view to display in the dialog along with the spacing around that view */ - public void setButton(CharSequence text, final DialogInterface.OnClickListener listener) { - mButton1Text = text; - if (listener != null) { - mButton1Message = mHandler.obtainMessage(DialogInterface.BUTTON1, listener); - } else { - mButton1Message = null; - } + public void setView(View view, int viewSpacingLeft, int viewSpacingTop, int viewSpacingRight, + int viewSpacingBottom) { + mView = view; + mViewSpacingSpecified = true; + mViewSpacingLeft = viewSpacingLeft; + mViewSpacingTop = viewSpacingTop; + mViewSpacingRight = viewSpacingRight; + mViewSpacingBottom = viewSpacingBottom; } /** - * Set a listener to be invoked when button 2 of the dialog is pressed. - * @param text The text to display in button 2. + * Sets a click listener or a message to be sent when the button is clicked. + * You only need to pass one of {@code listener} or {@code msg}. + * + * @param whichButton Which button, can be one of + * {@link DialogInterface#BUTTON_POSITIVE}, + * {@link DialogInterface#BUTTON_NEGATIVE}, or + * {@link DialogInterface#BUTTON_NEUTRAL} + * @param text The text to display in positive button. * @param listener The {@link DialogInterface.OnClickListener} to use. + * @param msg The {@link Message} to be sent when clicked. */ - public void setButton2(CharSequence text, final DialogInterface.OnClickListener listener) { - mButton2Text = text; - if (listener != null) { - mButton2Message = mHandler.obtainMessage(DialogInterface.BUTTON2, listener); - } else { - mButton2Message = null; + public void setButton(int whichButton, CharSequence text, + DialogInterface.OnClickListener listener, Message msg) { + + if (msg == null && listener != null) { + msg = mHandler.obtainMessage(whichButton, listener); } - } + + switch (whichButton) { - /** - * Set a listener to be invoked when button 3 of the dialog is pressed. - * @param text The text to display in button 3. - * @param listener The {@link DialogInterface.OnClickListener} to use. - */ - public void setButton3(CharSequence text, final DialogInterface.OnClickListener listener) { - mButton3Text = text; - if (listener != null) { - mButton3Message = mHandler.obtainMessage(DialogInterface.BUTTON3, listener); - } else { - mButton3Message = null; + case DialogInterface.BUTTON_POSITIVE: + mButtonPositiveText = text; + mButtonPositiveMessage = msg; + break; + + case DialogInterface.BUTTON_NEGATIVE: + mButtonNegativeText = text; + mButtonNegativeMessage = msg; + break; + + case DialogInterface.BUTTON_NEUTRAL: + mButtonNeutralText = text; + mButtonNeutralMessage = msg; + break; + + default: + throw new IllegalArgumentException("Button does not exist"); } } @@ -285,6 +298,19 @@ public class AlertController { return mListView; } + public Button getButton(int whichButton) { + switch (whichButton) { + case DialogInterface.BUTTON_POSITIVE: + return mButtonPositiveMessage != null ? mButtonPositive : null; + case DialogInterface.BUTTON_NEGATIVE: + return mButtonNegativeMessage != null ? mButtonNegative : null; + case DialogInterface.BUTTON_NEUTRAL: + return mButtonNeutralMessage != null ? mButtonNeutral : null; + default: + return null; + } + } + public boolean onKeyDown(int keyCode, KeyEvent event) { if (mScrollView != null && mScrollView.executeKeyEvent(event)) return true; return false; @@ -303,7 +329,7 @@ public class AlertController { LinearLayout topPanel = (LinearLayout) mWindow.findViewById(R.id.topPanel); TypedArray a = mContext.obtainStyledAttributes( null, com.android.internal.R.styleable.AlertDialog, com.android.internal.R.attr.alertDialogStyle, 0); - boolean hasTitle = setupTitle(topPanel, hasButtons); + boolean hasTitle = setupTitle(topPanel); View buttonPanel = mWindow.findViewById(R.id.buttonPanel); if (!hasButtons) { @@ -315,6 +341,10 @@ public class AlertController { customPanel = (FrameLayout) mWindow.findViewById(R.id.customPanel); FrameLayout custom = (FrameLayout) mWindow.findViewById(R.id.custom); custom.addView(mView, new LayoutParams(FILL_PARENT, WRAP_CONTENT)); + if (mViewSpacingSpecified) { + custom.setPadding(mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, + mViewSpacingBottom); + } } else { mWindow.findViewById(R.id.customPanel).setVisibility(View.GONE); } @@ -331,7 +361,7 @@ public class AlertController { a.recycle(); } - private boolean setupTitle(LinearLayout topPanel, boolean hasButtons) { + private boolean setupTitle(LinearLayout topPanel) { boolean hasTitle = true; if (mCustomTitleView != null) { @@ -352,40 +382,13 @@ public class AlertController { /* Display the title if a title is supplied, else hide it */ mTitleView = (TextView) mWindow.findViewById(R.id.alertTitle); - + mTitleView.setText(mTitle); - - /* The title font size and icon varies depending on - * what else is displayed within the dialog. - */ - if (mListView != null) { - - /* If a ListView is displayed then ensure the title - * is only 1 line and use a special icon. - */ - mTitleView.setSingleLine(); - mTitleView.setEllipsize(TruncateAt.END); - mIconView.setImageResource( - R.drawable.ic_dialog_menu_generic); - } else if ((mMessage != null) && hasButtons) { - - /* Has a message and buttons, we want the title to - * be a single line but large. - */ - mTitleView.setSingleLine(); - mTitleView.setEllipsize(TruncateAt.END); - mTitleView.setTextSize(getLargeTextSize()); - } else { - - /* We have a Title and buttons or we have title, - * and custom content. In either case the layout - * handles it so do nothing. - */ - } + mIconView.setImageResource(R.drawable.ic_dialog_menu_generic); /* Do this last so that if the user has supplied any * icons we use them instead of the default ones. If the - * user has specified 0 then make it dissapear. + * user has specified 0 then make it disappear. */ if (mIconId > 0) { mIconView.setImageResource(mIconId); @@ -414,17 +417,6 @@ public class AlertController { return hasTitle; } - private int getLargeTextSize() { - TypedArray a = - mContext.obtainStyledAttributes( - R.style.TextAppearance_Large, - R.styleable.TextAppearance); - int textSize = a.getDimensionPixelSize( - R.styleable.TextAppearance_textSize, 22); - a.recycle(); - return textSize; - } - private void setupContent(LinearLayout contentPanel) { mScrollView = (ScrollView) mWindow.findViewById(R.id.scrollView); mScrollView.setFocusable(false); @@ -452,65 +444,65 @@ public class AlertController { private boolean setupButtons() { View defaultButton = null; - int BUTTON1 = 1; - int BUTTON2 = 2; - int BUTTON3 = 4; - int whichButton = 0; - mButton1 = (Button) mWindow.findViewById(R.id.button1); - mButton1.setOnClickListener(mButtonHandler); - - if (TextUtils.isEmpty(mButton1Text)) { - mButton1.setVisibility(View.GONE); + int BIT_BUTTON_POSITIVE = 1; + int BIT_BUTTON_NEGATIVE = 2; + int BIT_BUTTON_NEUTRAL = 4; + int whichButtons = 0; + mButtonPositive = (Button) mWindow.findViewById(R.id.button1); + mButtonPositive.setOnClickListener(mButtonHandler); + + if (TextUtils.isEmpty(mButtonPositiveText)) { + mButtonPositive.setVisibility(View.GONE); } else { - mButton1.setText(mButton1Text); - mButton1.setVisibility(View.VISIBLE); - defaultButton = mButton1; - whichButton = whichButton | BUTTON1; + mButtonPositive.setText(mButtonPositiveText); + mButtonPositive.setVisibility(View.VISIBLE); + defaultButton = mButtonPositive; + whichButtons = whichButtons | BIT_BUTTON_POSITIVE; } - mButton2 = (Button) mWindow.findViewById(R.id.button2); - mButton2.setOnClickListener(mButtonHandler); + mButtonNegative = (Button) mWindow.findViewById(R.id.button2); + mButtonNegative.setOnClickListener(mButtonHandler); - if (TextUtils.isEmpty(mButton2Text)) { - mButton2.setVisibility(View.GONE); + if (TextUtils.isEmpty(mButtonNegativeText)) { + mButtonNegative.setVisibility(View.GONE); } else { - mButton2.setText(mButton2Text); - mButton2.setVisibility(View.VISIBLE); + mButtonNegative.setText(mButtonNegativeText); + mButtonNegative.setVisibility(View.VISIBLE); if (defaultButton == null) { - defaultButton = mButton2; + defaultButton = mButtonNegative; } - whichButton = whichButton | BUTTON2; + whichButtons = whichButtons | BIT_BUTTON_NEGATIVE; } - mButton3 = (Button) mWindow.findViewById(R.id.button3); - mButton3.setOnClickListener(mButtonHandler); + mButtonNeutral = (Button) mWindow.findViewById(R.id.button3); + mButtonNeutral.setOnClickListener(mButtonHandler); - if (TextUtils.isEmpty(mButton3Text)) { - mButton3.setVisibility(View.GONE); + if (TextUtils.isEmpty(mButtonNeutralText)) { + mButtonNeutral.setVisibility(View.GONE); } else { - mButton3.setText(mButton3Text); - mButton3.setVisibility(View.VISIBLE); + mButtonNeutral.setText(mButtonNeutralText); + mButtonNeutral.setVisibility(View.VISIBLE); if (defaultButton == null) { - defaultButton = mButton3; + defaultButton = mButtonNeutral; } - whichButton = whichButton | BUTTON3; + whichButtons = whichButtons | BIT_BUTTON_NEUTRAL; } /* * If we only have 1 button it should be centered on the layout and * expand to fill 50% of the available space. */ - if (whichButton == BUTTON1) { - centerButton(mButton1); - } else if (whichButton == BUTTON2) { - centerButton(mButton3); - } else if (whichButton == BUTTON3) { - centerButton(mButton3); + if (whichButtons == BIT_BUTTON_POSITIVE) { + centerButton(mButtonPositive); + } else if (whichButtons == BIT_BUTTON_NEGATIVE) { + centerButton(mButtonNeutral); + } else if (whichButtons == BIT_BUTTON_NEUTRAL) { + centerButton(mButtonNeutral); } - return whichButton != 0; + return whichButtons != 0; } private void centerButton(Button button) { @@ -677,6 +669,11 @@ public class AlertController { public ListAdapter mAdapter; public DialogInterface.OnClickListener mOnClickListener; public View mView; + public int mViewSpacingLeft; + public int mViewSpacingTop; + public int mViewSpacingRight; + public int mViewSpacingBottom; + public boolean mViewSpacingSpecified = false; public boolean[] mCheckedItems; public boolean mIsMultiChoice; public boolean mIsSingleChoice; @@ -726,13 +723,16 @@ public class AlertController { dialog.setMessage(mMessage); } if (mPositiveButtonText != null) { - dialog.setButton(mPositiveButtonText, mPositiveButtonListener); + dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText, + mPositiveButtonListener, null); } if (mNegativeButtonText != null) { - dialog.setButton2(mNegativeButtonText, mNegativeButtonListener); + dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText, + mNegativeButtonListener, null); } if (mNeutralButtonText != null) { - dialog.setButton3(mNeutralButtonText, mNeutralButtonListener); + dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText, + mNeutralButtonListener, null); } if (mForceInverseBackground) { dialog.setInverseBackgroundForced(true); @@ -743,7 +743,12 @@ public class AlertController { createListView(dialog); } if (mView != null) { - dialog.setView(mView); + if (mViewSpacingSpecified) { + dialog.setView(mView, mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, + mViewSpacingBottom); + } else { + dialog.setView(mView); + } } /* @@ -764,8 +769,7 @@ public class AlertController { adapter = new ArrayAdapter<CharSequence>( mContext, R.layout.select_dialog_multichoice, R.id.text1, mItems) { @Override - public View getView(int position, View convertView, - ViewGroup parent) { + public View getView(int position, View convertView, ViewGroup parent) { View view = super.getView(position, convertView, parent); if (mCheckedItems != null) { boolean isItemChecked = mCheckedItems[position]; @@ -778,24 +782,27 @@ public class AlertController { }; } else { adapter = new CursorAdapter(mContext, mCursor, false) { - + private final int mLabelIndex; + private final int mIsCheckedIndex; + + { + final Cursor cursor = getCursor(); + mLabelIndex = cursor.getColumnIndexOrThrow(mLabelColumn); + mIsCheckedIndex = cursor.getColumnIndexOrThrow(mIsCheckedColumn); + } + @Override - public void bindView(View view, Context context, - Cursor cursor) { + public void bindView(View view, Context context, Cursor cursor) { CheckedTextView text = (CheckedTextView) view.findViewById(R.id.text1); - text.setText(cursor.getString(cursor.getColumnIndexOrThrow(mLabelColumn))); + text.setText(cursor.getString(mLabelIndex)); + listView.setItemChecked(cursor.getPosition(), + cursor.getInt(mIsCheckedIndex) == 1); } @Override - public View newView(Context context, Cursor cursor, - ViewGroup parent) { - View view = mInflater.inflate( - R.layout.select_dialog_multichoice, parent, false); - bindView(view, context, cursor); - if (cursor.getInt(cursor.getColumnIndexOrThrow(mIsCheckedColumn)) == 1) { - listView.setItemChecked(cursor.getPosition(), true); - } - return view; + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.select_dialog_multichoice, + parent, false); } }; diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl index bb9fa0b..434850c 100755..100644 --- a/core/java/com/android/internal/app/IBatteryStats.aidl +++ b/core/java/com/android/internal/app/IBatteryStats.aidl @@ -16,9 +16,14 @@ package com.android.internal.app; +import com.android.internal.os.BatteryStatsImpl; + interface IBatteryStats { + BatteryStatsImpl getStatistics(); void noteStartWakelock(int uid, String name, int type); void noteStopWakelock(int uid, String name, int type); + void noteStartSensor(int uid, int sensor); + void noteStopSensor(int uid, int sensor); void setOnBattery(boolean onBattery); long getAwakeTimeBattery(); long getAwakeTimePlugged(); diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index d5a9f8c..e907a04 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -17,9 +17,6 @@ package com.android.internal.app; import com.android.internal.R; - -import android.app.Activity; -import android.app.ListActivity; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -33,19 +30,17 @@ import android.os.Bundle; import android.os.PatternMatcher; import android.util.Config; import android.util.Log; -import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.LayoutInflater; -import android.view.Window; import android.widget.BaseAdapter; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.ImageView; -import android.widget.ListView; import android.widget.TextView; - +import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; @@ -55,35 +50,37 @@ import java.util.Set; * which there is more than one matching activity, allowing the user to decide * which to go to. It is not normally used directly by application developers. */ -public class ResolverActivity extends AlertActivity implements +public class ResolverActivity extends AlertActivity implements DialogInterface.OnClickListener, CheckBox.OnCheckedChangeListener { private ResolveListAdapter mAdapter; private CheckBox mAlwaysCheck; private TextView mClearDefaultHint; - + private PackageManager mPm; + @Override protected void onCreate(Bundle savedInstanceState) { onCreate(savedInstanceState, new Intent(getIntent()), getResources().getText(com.android.internal.R.string.whichApplication), true); } - + protected void onCreate(Bundle savedInstanceState, Intent intent, CharSequence title, boolean alwaysUseOption) { super.onCreate(savedInstanceState); - + mPm = getPackageManager(); intent.setComponent(null); AlertController.AlertParams ap = mAlertParams; - + ap.mTitle = title; ap.mOnClickListener = this; - + if (alwaysUseOption) { LayoutInflater inflater = (LayoutInflater) getSystemService( Context.LAYOUT_INFLATER_SERVICE); ap.mView = inflater.inflate(R.layout.always_use_checkbox, null); mAlwaysCheck = (CheckBox)ap.mView.findViewById(com.android.internal.R.id.alwaysUse); + mAlwaysCheck.setText(R.string.alwaysUse); mAlwaysCheck.setOnCheckedChangeListener(this); mClearDefaultHint = (TextView)ap.mView.findViewById( com.android.internal.R.id.clearDefaultHint); @@ -99,10 +96,10 @@ public class ResolverActivity extends AlertActivity implements } else { ap.mMessage = getResources().getText(com.android.internal.R.string.noApplications); } - + setupAlert(); } - + public void onClick(DialogInterface dialog, int which) { ResolveInfo ri = mAdapter.resolveInfoForPosition(which); Intent intent = mAdapter.intentForPosition(which); @@ -110,7 +107,7 @@ public class ResolverActivity extends AlertActivity implements if ((mAlwaysCheck != null) && mAlwaysCheck.isChecked()) { // Build a reasonable intent filter, based on what matched. IntentFilter filter = new IntentFilter(); - + if (intent.getAction() != null) { filter.addAction(intent.getAction()); } @@ -121,7 +118,7 @@ public class ResolverActivity extends AlertActivity implements } } filter.addCategory(Intent.CATEGORY_DEFAULT); - + int cat = ri.match&IntentFilter.MATCH_CATEGORY_MASK; Uri data = intent.getData(); if (cat == IntentFilter.MATCH_CATEGORY_TYPE) { @@ -136,7 +133,7 @@ public class ResolverActivity extends AlertActivity implements } } else if (data != null && data.getScheme() != null) { filter.addDataScheme(data.getScheme()); - + // Look through the resolved filter to determine which part // of it matched the original Intent. Iterator<IntentFilter.AuthorityEntry> aIt = ri.filter.authoritiesIterator(); @@ -163,13 +160,13 @@ public class ResolverActivity extends AlertActivity implements } } } - + if (filter != null) { final int N = mAdapter.mList.size(); ComponentName[] set = new ComponentName[N]; int bestMatch = 0; for (int i=0; i<N; i++) { - ResolveInfo r = mAdapter.mList.get(i); + ResolveInfo r = mAdapter.mList.get(i).ri; set[i] = new ComponentName(r.activityInfo.packageName, r.activityInfo.name); if (r.match > bestMatch) bestMatch = r.match; @@ -178,51 +175,135 @@ public class ResolverActivity extends AlertActivity implements intent.getComponent()); } } - + if (intent != null) { startActivity(intent); } finish(); } - + + private final class DisplayResolveInfo { + ResolveInfo ri; + CharSequence displayLabel; + CharSequence extendedInfo; + + DisplayResolveInfo(ResolveInfo pri, CharSequence pLabel, CharSequence pInfo) { + ri = pri; + displayLabel = pLabel; + extendedInfo = pInfo; + } + } + private final class ResolveListAdapter extends BaseAdapter { private final Intent mIntent; private final LayoutInflater mInflater; - private List<ResolveInfo> mList; - + private List<DisplayResolveInfo> mList; + public ResolveListAdapter(Context context, Intent intent) { mIntent = new Intent(intent); mIntent.setComponent(null); mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - PackageManager pm = context.getPackageManager(); - mList = pm.queryIntentActivities( + List<ResolveInfo> rList = mPm.queryIntentActivities( intent, PackageManager.MATCH_DEFAULT_ONLY | (mAlwaysCheck != null ? PackageManager.GET_RESOLVED_FILTER : 0)); - if (mList != null) { - int N = mList.size(); + int N; + if ((rList != null) && ((N = rList.size()) > 0)) { + // Only display the first matches that are either of equal + // priority or have asked to be default options. + ResolveInfo r0 = rList.get(0); + for (int i=1; i<N; i++) { + ResolveInfo ri = rList.get(i); + if (Config.LOGV) Log.v( + "ResolveListActivity", + r0.activityInfo.name + "=" + + r0.priority + "/" + r0.isDefault + " vs " + + ri.activityInfo.name + "=" + + ri.priority + "/" + ri.isDefault); + if (r0.priority != ri.priority || + r0.isDefault != ri.isDefault) { + while (i < N) { + rList.remove(i); + N--; + } + } + } if (N > 1) { - // Only display the first matches that are either of equal - // priority or have asked to be default options. - ResolveInfo r0 = mList.get(0); - for (int i=1; i<N; i++) { - ResolveInfo ri = mList.get(i); - if (Config.LOGV) Log.v( - "ResolveListActivity", - r0.activityInfo.name + "=" + - r0.priority + "/" + r0.isDefault + " vs " + - ri.activityInfo.name + "=" + - ri.priority + "/" + ri.isDefault); - if (r0.priority != ri.priority || - r0.isDefault != ri.isDefault) { - while (i < N) { - mList.remove(i); - N--; - } + ResolveInfo.DisplayNameComparator rComparator = + new ResolveInfo.DisplayNameComparator(mPm); + Collections.sort(rList, rComparator); + } + // Check for applications with same name and use application name or + // package name if necessary + mList = new ArrayList<DisplayResolveInfo>(); + r0 = rList.get(0); + int start = 0; + CharSequence r0Label = r0.loadLabel(mPm); + for (int i = 1; i < N; i++) { + if (r0Label == null) { + r0Label = r0.activityInfo.packageName; + } + ResolveInfo ri = rList.get(i); + CharSequence riLabel = ri.loadLabel(mPm); + if (riLabel == null) { + riLabel = ri.activityInfo.packageName; + } + if (riLabel.equals(r0Label)) { + continue; + } + processGroup(rList, start, (i-1), r0, r0Label); + r0 = ri; + r0Label = riLabel; + start = i; + } + // Process last group + processGroup(rList, start, (N-1), r0, r0Label); + } + } + + private void processGroup(List<ResolveInfo> rList, int start, int end, ResolveInfo ro, + CharSequence roLabel) { + // Process labels from start to i + int num = end - start+1; + if (num == 1) { + // No duplicate labels. Use label for entry at start + mList.add(new DisplayResolveInfo(ro, roLabel, null)); + } else { + boolean usePkg = false; + CharSequence startApp = ro.activityInfo.applicationInfo.loadLabel(mPm); + if (startApp == null) { + usePkg = true; + } + if (!usePkg) { + // Use HashSet to track duplicates + HashSet<CharSequence> duplicates = + new HashSet<CharSequence>(); + duplicates.add(startApp); + for (int j = start+1; j <= end ; j++) { + ResolveInfo jRi = rList.get(j); + CharSequence jApp = jRi.activityInfo.applicationInfo.loadLabel(mPm); + if ( (jApp == null) || (duplicates.contains(jApp))) { + usePkg = true; + break; + } else { + duplicates.add(jApp); } } - Collections.sort(mList, new ResolveInfo.DisplayNameComparator(pm)); + // Clear HashSet for later use + duplicates.clear(); + } + for (int k = start; k <= end; k++) { + ResolveInfo add = rList.get(k); + if (usePkg) { + // Use application name for all entries from start to end-1 + mList.add(new DisplayResolveInfo(add, roLabel, + add.activityInfo.packageName)); + } else { + // Use package name for all entries from start to end-1 + mList.add(new DisplayResolveInfo(add, roLabel, + add.activityInfo.applicationInfo.loadLabel(mPm))); + } } } } @@ -232,7 +313,7 @@ public class ResolverActivity extends AlertActivity implements return null; } - return mList.get(position); + return mList.get(position).ri; } public Intent intentForPosition(int position) { @@ -243,7 +324,7 @@ public class ResolverActivity extends AlertActivity implements Intent intent = new Intent(mIntent); intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT |Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); - ActivityInfo ai = mList.get(position).activityInfo; + ActivityInfo ai = mList.get(position).ri.activityInfo; intent.setComponent(new ComponentName( ai.applicationInfo.packageName, ai.name)); return intent; @@ -273,22 +354,24 @@ public class ResolverActivity extends AlertActivity implements return view; } - private final void bindView(View view, ResolveInfo info) { + private final void bindView(View view, DisplayResolveInfo info) { TextView text = (TextView)view.findViewById(com.android.internal.R.id.text1); + TextView text2 = (TextView)view.findViewById(com.android.internal.R.id.text2); ImageView icon = (ImageView)view.findViewById(R.id.icon); - - PackageManager pm = getPackageManager(); - - CharSequence label = info.loadLabel(pm); - if (label == null) label = info.activityInfo.name; - text.setText(label); - icon.setImageDrawable(info.loadIcon(pm)); + text.setText(info.displayLabel); + if (info.extendedInfo != null) { + text2.setVisibility(View.VISIBLE); + text2.setText(info.extendedInfo); + } else { + text2.setVisibility(View.GONE); + } + icon.setImageDrawable(info.ri.loadIcon(mPm)); } } public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (mClearDefaultHint == null) return; - + if(isChecked) { mClearDefaultHint.setVisibility(View.VISIBLE); } else { diff --git a/core/java/com/android/internal/app/RingtonePickerActivity.java b/core/java/com/android/internal/app/RingtonePickerActivity.java index 63fe952..9c83aa3 100644 --- a/core/java/com/android/internal/app/RingtonePickerActivity.java +++ b/core/java/com/android/internal/app/RingtonePickerActivity.java @@ -139,6 +139,9 @@ public final class RingtonePickerActivity extends AlertActivity implements } mCursor = mRingtoneManager.getCursor(); + + // The volume keys will control the stream that we are choosing a ringtone for + setVolumeControlStream(mRingtoneManager.inferStreamType()); // Get the URI whose list item should have a checkmark mExistingUri = intent diff --git a/core/java/com/android/internal/app/UsbStorageActivity.java b/core/java/com/android/internal/app/UsbStorageActivity.java index 5b2bde0..b8a2136 100644 --- a/core/java/com/android/internal/app/UsbStorageActivity.java +++ b/core/java/com/android/internal/app/UsbStorageActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2007 The Android Open Source Project + * Copyright (C) 2007 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ public class UsbStorageActivity extends AlertActivity implements DialogInterface p.mPositiveButtonText = getString(com.android.internal.R.string.usb_storage_button_mount); p.mPositiveButtonListener = this; p.mNegativeButtonText = getString(com.android.internal.R.string.usb_storage_button_unmount); - p.mPositiveButtonListener = this; + p.mNegativeButtonListener = this; setupAlert(); } diff --git a/core/java/com/android/internal/database/ArrayListCursor.java b/core/java/com/android/internal/database/ArrayListCursor.java index bcbcc41..2e1d8f1 100644 --- a/core/java/com/android/internal/database/ArrayListCursor.java +++ b/core/java/com/android/internal/database/ArrayListCursor.java @@ -17,6 +17,7 @@ package com.android.internal.database; import android.database.AbstractCursor; +import android.database.CursorWindow; import java.lang.System; import java.util.ArrayList; @@ -25,105 +26,146 @@ import java.util.ArrayList; * A convenience class that presents a two-dimensional ArrayList * as a Cursor. */ -public class ArrayListCursor extends AbstractCursor -{ - public ArrayListCursor(String[] columnNames, ArrayList<ArrayList> rows) - { +public class ArrayListCursor extends AbstractCursor { + private String[] mColumnNames; + private ArrayList<Object>[] mRows; + + @SuppressWarnings({"unchecked"}) + public ArrayListCursor(String[] columnNames, ArrayList<ArrayList> rows) { int colCount = columnNames.length; boolean foundID = false; // Add an _id column if not in columnNames - for (int i=0; i<colCount; ++i) { + for (int i = 0; i < colCount; ++i) { if (columnNames[i].compareToIgnoreCase("_id") == 0) { mColumnNames = columnNames; foundID = true; break; } } - + if (!foundID) { mColumnNames = new String[colCount + 1]; System.arraycopy(columnNames, 0, mColumnNames, 0, columnNames.length); mColumnNames[colCount] = "_id"; } - + int rowCount = rows.size(); mRows = new ArrayList[rowCount]; - - for (int i = 0; i<rowCount; ++i) { + + for (int i = 0; i < rowCount; ++i) { mRows[i] = rows.get(i); if (!foundID) { - mRows[i].add(Long.valueOf(i)); + mRows[i].add(i); } } + } - } + @Override + public void fillWindow(int position, CursorWindow window) { + if (position < 0 || position > getCount()) { + return; + } + + window.acquireReference(); + try { + int oldpos = mPos; + mPos = position - 1; + window.clear(); + window.setStartPosition(position); + int columnNum = getColumnCount(); + window.setNumColumns(columnNum); + while (moveToNext() && window.allocRow()) { + for (int i = 0; i < columnNum; i++) { + final Object data = mRows[mPos].get(i); + if (data != null) { + if (data instanceof byte[]) { + byte[] field = (byte[]) data; + if (!window.putBlob(field, mPos, i)) { + window.freeLastRow(); + break; + } + } else { + String field = data.toString(); + if (!window.putString(field, mPos, i)) { + window.freeLastRow(); + break; + } + } + } else { + if (!window.putNull(mPos, i)) { + window.freeLastRow(); + break; + } + } + } + } + + mPos = oldpos; + } catch (IllegalStateException e){ + // simply ignore it + } finally { + window.releaseReference(); + } + } @Override - public int getCount() - { + public int getCount() { return mRows.length; } @Override - public boolean deleteRow() - { + public boolean deleteRow() { return false; } @Override - public String[] getColumnNames() - { + public String[] getColumnNames() { return mColumnNames; } @Override - public String getString(int columnIndex) - { + public byte[] getBlob(int columnIndex) { + return (byte[]) mRows[mPos].get(columnIndex); + } + + @Override + public String getString(int columnIndex) { Object cell = mRows[mPos].get(columnIndex); return (cell == null) ? null : cell.toString(); } - + @Override - public short getShort(int columnIndex) - { - Number num = (Number)mRows[mPos].get(columnIndex); + public short getShort(int columnIndex) { + Number num = (Number) mRows[mPos].get(columnIndex); return num.shortValue(); } @Override - public int getInt(int columnIndex) - { - Number num = (Number)mRows[mPos].get(columnIndex); + public int getInt(int columnIndex) { + Number num = (Number) mRows[mPos].get(columnIndex); return num.intValue(); } @Override - public long getLong(int columnIndex) - { - Number num = (Number)mRows[mPos].get(columnIndex); + public long getLong(int columnIndex) { + Number num = (Number) mRows[mPos].get(columnIndex); return num.longValue(); } @Override - public float getFloat(int columnIndex) - { - Number num = (Number)mRows[mPos].get(columnIndex); + public float getFloat(int columnIndex) { + Number num = (Number) mRows[mPos].get(columnIndex); return num.floatValue(); } @Override - public double getDouble(int columnIndex) - { - Number num = (Number)mRows[mPos].get(columnIndex); + public double getDouble(int columnIndex) { + Number num = (Number) mRows[mPos].get(columnIndex); return num.doubleValue(); } @Override - public boolean isNull(int columnIndex) - { + public boolean isNull(int columnIndex) { return mRows[mPos].get(columnIndex) == null; } - - private String[] mColumnNames; - private ArrayList<Object>[] mRows; } diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.aidl b/core/java/com/android/internal/os/BatteryStatsImpl.aidl new file mode 100644 index 0000000..0c26a3c --- /dev/null +++ b/core/java/com/android/internal/os/BatteryStatsImpl.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.os; + +parcelable BatteryStatsImpl; diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java new file mode 100644 index 0000000..8912960 --- /dev/null +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -0,0 +1,1661 @@ +/* + * Copyright (C) 2006-2007 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.os; + +import android.os.BatteryStats; +import android.os.Parcel; +import android.os.ParcelFormatException; +import android.os.Parcelable; +import android.os.SystemClock; +import android.util.Log; +import android.util.SparseArray; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** + * All information we are collecting about things that can happen that impact + * battery life. All times are represented in microseconds except where indicated + * otherwise. + */ +public final class BatteryStatsImpl extends BatteryStats implements Parcelable { + + // In-memory Parcel magic number, used to detect attempts to unmarshall bad data + private static final int MAGIC = 0xBA757475; // 'BATSTATS' + + // Current on-disk Parcel version + private static final int VERSION = 13; + + private final File mFile; + private final File mBackupFile; + + /** + * The statistics we have collected organized by uids. + */ + final SparseArray<BatteryStatsImpl.Uid> mUidStats = + new SparseArray<BatteryStatsImpl.Uid>(); + + // A set of pools of currently active timers. When a timer is queried, we will divide the + // elapsed time by the number of active timers to arrive at that timer's share of the time. + // In order to do this, we must refresh each timer whenever the number of active timers + // changes. + final ArrayList<Timer> mPartialTimers = new ArrayList<Timer>(); + final ArrayList<Timer> mFullTimers = new ArrayList<Timer>(); + final ArrayList<Timer> mWindowTimers = new ArrayList<Timer>(); + final ArrayList<Timer> mSensorTimers = new ArrayList<Timer>(); + + int mStartCount; + + long mBatteryUptime; + long mBatteryLastUptime; + long mBatteryRealtime; + long mBatteryLastRealtime; + + long mUptime; + long mUptimeStart; + long mLastUptime; + long mRealtime; + long mRealtimeStart; + long mLastRealtime; + + /** + * These provide time bases that discount the time the device is plugged + * in to power. + */ + boolean mOnBattery; + long mTrackBatteryPastUptime; + long mTrackBatteryUptimeStart; + long mTrackBatteryPastRealtime; + long mTrackBatteryRealtimeStart; + + long mLastWriteTime = 0; // Milliseconds + + // For debugging + public BatteryStatsImpl() { + mFile = mBackupFile = null; + } + + /** + * State for keeping track of timing information. + */ + public static final class Timer extends BatteryStats.Timer { + ArrayList<Timer> mTimerPool; + + int mType; + int mNesting; + + int mCount; + int mLoadedCount; + int mLastCount; + + // Times are in microseconds for better accuracy when dividing by the lock count + + long mTotalTime; // Add mUnpluggedTotalTime to get true value + long mLoadedTotalTime; + long mLastTotalTime; + long mStartTime; + long mUpdateTime; + + /** + * The value of mTotalTime when unplug() was last called, initially 0. + */ + long mTotalTimeAtLastUnplug; + + /** Constructor used for unmarshalling only. */ + Timer() {} + + Timer(int type, ArrayList<Timer> timerPool) { + mType = type; + mTimerPool = timerPool; + } + + public void writeToParcel(Parcel out) { + out.writeInt(mType); + out.writeInt(mNesting); + out.writeInt(mCount); + out.writeInt(mLoadedCount); + out.writeInt(mLastCount); + out.writeLong(mTotalTime); + out.writeLong(mLoadedTotalTime); + out.writeLong(mLastTotalTime); + out.writeLong(mStartTime); + out.writeLong(mUpdateTime); + out.writeLong(mTotalTimeAtLastUnplug); + } + + public void readFromParcel(Parcel in) { + mType = in.readInt(); + mNesting = in.readInt(); + mCount = in.readInt(); + mLoadedCount = in.readInt(); + mLastCount = in.readInt(); + mTotalTime = in.readLong(); + mLoadedTotalTime = in.readLong(); + mLastTotalTime = in.readLong(); + mStartTime = in.readLong(); + mUpdateTime = in.readLong(); + mTotalTimeAtLastUnplug = in.readLong(); + } + + private void unplug() { + mTotalTimeAtLastUnplug += mTotalTime; + mTotalTime = 0; + } + + /** + * Writes a possibly null Timer to a Parcel. + * + * @param out the Parcel to be written to. + * @param timer a Timer, or null. + */ + public static void writeTimerToParcel(Parcel out, Timer timer) { + if (timer == null) { + out.writeInt(0); // indicates null + return; + } + out.writeInt(1); // indicates non-null + + timer.writeToParcel(out); + } + + @Override + public long getTotalTime(long now, int which) { + long val; + if (which == STATS_LAST) { + val = mLastTotalTime; + } else { + val = computeRunTimeLocked(now); + if (which != STATS_UNPLUGGED) { + val += mTotalTimeAtLastUnplug; + } + if ((which == STATS_CURRENT) || (which == STATS_UNPLUGGED)) { + val -= mLoadedTotalTime; + } + } + + return val; + } + + @Override + public int getCount(int which) { + int val; + if (which == STATS_LAST) { + val = mLastCount; + } else { + val = mCount; + if ((which == STATS_CURRENT) || (which == STATS_UNPLUGGED)) { + val -= mLoadedCount; + } + } + + return val; + } + + void startRunningLocked(BatteryStatsImpl stats) { + if (mNesting++ == 0) { + mStartTime = mUpdateTime = + stats.getBatteryUptimeLocked(SystemClock.elapsedRealtime() * 1000); + // Accumulate time to all other active counters with the current value of mCount + refreshTimersLocked(stats); + // Add this timer to the active pool + mTimerPool.add(this); + // Increment the count + mCount++; + } + } + + void stopRunningLocked(BatteryStatsImpl stats) { + // Ignore attempt to stop a timer that isn't running + if (mNesting == 0) { + return; + } + if (--mNesting == 0) { + // Accumulate time to all active counters with the current value of mCount + refreshTimersLocked(stats); + // Remove this timer from the active pool + mTimerPool.remove(this); + // Decrement the count + mCount--; + } + } + + // Update the total time for all other running Timers with the same type as this Timer + // due to a change in timer count + private void refreshTimersLocked(BatteryStatsImpl stats) { + for (Timer t : mTimerPool) { + t.updateTimeLocked(stats); + } + } + + /** + * Update totalTime and reset updateTime + * @param stats + */ + private void updateTimeLocked(BatteryStatsImpl stats) { + long realtime = SystemClock.elapsedRealtime() * 1000; + long heldTime = stats.getBatteryUptimeLocked(realtime) - mUpdateTime; + if (heldTime > 0) { + mTotalTime += (heldTime * 1000) / mCount; + } + mUpdateTime = stats.getBatteryUptimeLocked(realtime); + } + + private long computeRunTimeLocked(long curBatteryUptime) { + return mTotalTime + + (mNesting > 0 ? ((curBatteryUptime * 1000) - mUpdateTime) / mCount : 0); + } + + void writeSummaryFromParcelLocked(Parcel out, long curBatteryUptime) { + long runTime = computeRunTimeLocked(curBatteryUptime); + // Divide by 1000 for backwards compatibility + out.writeLong((runTime + 500) / 1000); + out.writeLong(((runTime - mLoadedTotalTime) + 500) / 1000); + out.writeInt(mCount); + out.writeInt(mCount - mLoadedCount); + } + + void readSummaryFromParcelLocked(Parcel in) { + // Multiply by 1000 for backwards compatibility + mTotalTime = mLoadedTotalTime = in.readLong() * 1000; + mLastTotalTime = in.readLong(); + mCount = mLoadedCount = in.readInt(); + mLastCount = in.readInt(); + mNesting = 0; + } + } + + public void unplugTimers() { + ArrayList<Timer> timers; + + timers = mPartialTimers; + for (int i = timers.size() - 1; i >= 0; i--) { + timers.get(i).unplug(); + } + timers = mFullTimers; + for (int i = timers.size() - 1; i >= 0; i--) { + timers.get(i).unplug(); + } + timers = mWindowTimers; + for (int i = timers.size() - 1; i >= 0; i--) { + timers.get(i).unplug(); + } + timers = mSensorTimers; + for (int i = timers.size() - 1; i >= 0; i--) { + timers.get(i).unplug(); + } + } + + @Override + public SparseArray<? extends BatteryStats.Uid> getUidStats() { + return mUidStats; + } + + /** + * The statistics associated with a particular uid. + */ + public final class Uid extends BatteryStats.Uid { + + /** + * The statistics we have collected for this uid's wake locks. + */ + final HashMap<String, Wakelock> mWakelockStats = new HashMap<String, Wakelock>(); + + /** + * The statistics we have collected for this uid's sensor activations. + */ + final HashMap<Integer, Sensor> mSensorStats = new HashMap<Integer, Sensor>(); + + /** + * The statistics we have collected for this uid's processes. + */ + final HashMap<String, Proc> mProcessStats = new HashMap<String, Proc>(); + + /** + * The statistics we have collected for this uid's processes. + */ + final HashMap<String, Pkg> mPackageStats = new HashMap<String, Pkg>(); + + @Override + public Map<String, ? extends BatteryStats.Uid.Wakelock> getWakelockStats() { + return mWakelockStats; + } + + @Override + public Map<Integer, ? extends BatteryStats.Uid.Sensor> getSensorStats() { + return mSensorStats; + } + + @Override + public Map<String, ? extends BatteryStats.Uid.Proc> getProcessStats() { + return mProcessStats; + } + + @Override + public Map<String, ? extends BatteryStats.Uid.Pkg> getPackageStats() { + return mPackageStats; + } + + void writeToParcelLocked(Parcel out) { + out.writeInt(mWakelockStats.size()); + for (Map.Entry<String, Uid.Wakelock> wakelockEntry : mWakelockStats.entrySet()) { + out.writeString(wakelockEntry.getKey()); + Uid.Wakelock wakelock = wakelockEntry.getValue(); + wakelock.writeToParcelLocked(out); + } + + out.writeInt(mSensorStats.size()); + for (Map.Entry<Integer, Uid.Sensor> sensorEntry : mSensorStats.entrySet()) { + out.writeInt(sensorEntry.getKey()); + Uid.Sensor sensor = sensorEntry.getValue(); + sensor.writeToParcelLocked(out); + } + + out.writeInt(mProcessStats.size()); + for (Map.Entry<String, Uid.Proc> procEntry : mProcessStats.entrySet()) { + out.writeString(procEntry.getKey()); + Uid.Proc proc = procEntry.getValue(); + proc.writeToParcelLocked(out); + } + + out.writeInt(mPackageStats.size()); + for (Map.Entry<String, Uid.Pkg> pkgEntry : mPackageStats.entrySet()) { + out.writeString(pkgEntry.getKey()); + Uid.Pkg pkg = pkgEntry.getValue(); + pkg.writeToParcelLocked(out); + } + } + + void readFromParcelLocked(Parcel in) { + int numWakelocks = in.readInt(); + mWakelockStats.clear(); + for (int j = 0; j < numWakelocks; j++) { + String wakelockName = in.readString(); + Uid.Wakelock wakelock = new Wakelock(); + wakelock.readFromParcelLocked(in); + mWakelockStats.put(wakelockName, wakelock); + } + + int numSensors = in.readInt(); + mSensorStats.clear(); + for (int k = 0; k < numSensors; k++) { + int sensorNumber = in.readInt(); + Uid.Sensor sensor = new Sensor(); + sensor.readFromParcelLocked(in); + mSensorStats.put(sensorNumber, sensor); + } + + int numProcs = in.readInt(); + mProcessStats.clear(); + for (int k = 0; k < numProcs; k++) { + String processName = in.readString(); + Uid.Proc proc = new Proc(); + proc.readFromParcelLocked(in); + mProcessStats.put(processName, proc); + } + + int numPkgs = in.readInt(); + mPackageStats.clear(); + for (int l = 0; l < numPkgs; l++) { + String packageName = in.readString(); + Uid.Pkg pkg = new Pkg(); + pkg.readFromParcelLocked(in); + mPackageStats.put(packageName, pkg); + } + } + + /** + * The statistics associated with a particular wake lock. + */ + public final class Wakelock extends BatteryStats.Uid.Wakelock { + /** + * How long (in ms) this uid has been keeping the device partially awake. + */ + Timer wakeTimePartial; + + /** + * How long (in ms) this uid has been keeping the device fully awake. + */ + Timer wakeTimeFull; + + /** + * How long (in ms) this uid has had a window keeping the device awake. + */ + Timer wakeTimeWindow; + + /** + * Reads a possibly null Timer from a Parcel. The timer is associated with the + * proper timer pool from the given BatteryStatsImpl object. + * + * @param in the Parcel to be read from. + * return a new Timer, or null. + */ + private Timer readTimerFromParcel(Parcel in) { + if (in.readInt() == 0) { + return null; + } + + Timer timer = new Timer(); + timer.readFromParcel(in); + // Set the timer pool for the timer according to its type + switch (timer.mType) { + case WAKE_TYPE_PARTIAL: + timer.mTimerPool = mPartialTimers; + break; + case WAKE_TYPE_FULL: + timer.mTimerPool = mFullTimers; + break; + case WAKE_TYPE_WINDOW: + timer.mTimerPool = mWindowTimers; + break; + } + // If the timer is active, add it to the pool + if (timer.mNesting > 0) { + timer.mTimerPool.add(timer); + } + return timer; + } + + void readFromParcelLocked(Parcel in) { + wakeTimePartial = readTimerFromParcel(in); + wakeTimeFull = readTimerFromParcel(in); + wakeTimeWindow = readTimerFromParcel(in); + } + + void writeToParcelLocked(Parcel out) { + Timer.writeTimerToParcel(out, wakeTimePartial); + Timer.writeTimerToParcel(out, wakeTimeFull); + Timer.writeTimerToParcel(out, wakeTimeWindow); + } + + @Override + public Timer getWakeTime(int type) { + switch (type) { + case WAKE_TYPE_FULL: return wakeTimeFull; + case WAKE_TYPE_PARTIAL: return wakeTimePartial; + case WAKE_TYPE_WINDOW: return wakeTimeWindow; + default: throw new IllegalArgumentException("type = " + type); + } + } + } + + public final class Sensor extends BatteryStats.Uid.Sensor { + Timer sensorTime; + + private Timer readTimerFromParcel(Parcel in) { + if (in.readInt() == 0) { + return null; + } + + Timer timer = new Timer(); + timer.readFromParcel(in); + // Set the timer pool for the timer + timer.mTimerPool = mSensorTimers; + + // If the timer is active, add it to the pool + if (timer.mNesting > 0) { + timer.mTimerPool.add(timer); + } + return timer; + } + + void readFromParcelLocked(Parcel in) { + sensorTime = readTimerFromParcel(in); + } + + void writeToParcelLocked(Parcel out) { + Timer.writeTimerToParcel(out, sensorTime); + } + + @Override + public Timer getSensorTime() { + return sensorTime; + } + } + + /** + * The statistics associated with a particular process. + */ + public final class Proc extends BatteryStats.Uid.Proc { + /** + * Total time (in 1/100 sec) spent executing in user code. + */ + long mUserTime; + + /** + * Total time (in 1/100 sec) spent executing in kernel code. + */ + long mSystemTime; + + /** + * Number of times the process has been started. + */ + int mStarts; + + /** + * The amount of user time loaded from a previous save. + */ + long mLoadedUserTime; + + /** + * The amount of system time loaded from a previous save. + */ + long mLoadedSystemTime; + + /** + * The number of times the process has started from a previous save. + */ + int mLoadedStarts; + + /** + * The amount of user time loaded from the previous run. + */ + long mLastUserTime; + + /** + * The amount of system time loaded from the previous run. + */ + long mLastSystemTime; + + /** + * The number of times the process has started from the previous run. + */ + int mLastStarts; + + void writeToParcelLocked(Parcel out) { + out.writeLong(mUserTime); + out.writeLong(mSystemTime); + out.writeInt(mStarts); + out.writeLong(mLoadedUserTime); + out.writeLong(mLoadedSystemTime); + out.writeInt(mLoadedStarts); + out.writeLong(mLastUserTime); + out.writeLong(mLastSystemTime); + out.writeInt(mLastStarts); + } + + void readFromParcelLocked(Parcel in) { + mUserTime = in.readLong(); + mSystemTime = in.readLong(); + mStarts = in.readInt(); + mLoadedUserTime = in.readLong(); + mLoadedSystemTime = in.readLong(); + mLoadedStarts = in.readInt(); + mLastUserTime = in.readLong(); + mLastSystemTime = in.readLong(); + mLastStarts = in.readInt(); + } + + public BatteryStatsImpl getBatteryStats() { + return BatteryStatsImpl.this; + } + + public void addCpuTimeLocked(int utime, int stime) { + mUserTime += utime; + mSystemTime += stime; + } + + public void incStartsLocked() { + mStarts++; + } + + @Override + public long getUserTime(int which) { + long val; + if (which == STATS_LAST) { + val = mLastUserTime; + } else { + val = mUserTime; + if (which == STATS_CURRENT) { + val -= mLoadedUserTime; + } + } + return val; + } + + @Override + public long getSystemTime(int which) { + long val; + if (which == STATS_LAST) { + val = mLastSystemTime; + } else { + val = mSystemTime; + if (which == STATS_CURRENT) { + val -= mLoadedSystemTime; + } + } + return val; + } + + @Override + public int getStarts(int which) { + int val; + if (which == STATS_LAST) { + val = mLastStarts; + } else { + val = mStarts; + if (which == STATS_CURRENT) { + val -= mLoadedStarts; + } + } + return val; + } + } + + /** + * The statistics associated with a particular package. + */ + public final class Pkg extends BatteryStats.Uid.Pkg { + /** + * Number of times this package has done something that could wake up the + * device from sleep. + */ + int mWakeups; + + /** + * Number of things that could wake up the device loaded from a + * previous save. + */ + int mLoadedWakeups; + + /** + * Number of things that could wake up the device as of the + * last run. + */ + int mLastWakeups; + + /** + * The statics we have collected for this package's services. + */ + final HashMap<String, Serv> mServiceStats = new HashMap<String, Serv>(); + + void readFromParcelLocked(Parcel in) { + mWakeups = in.readInt(); + mLoadedWakeups = in.readInt(); + mLastWakeups = in.readInt(); + + int numServs = in.readInt(); + mServiceStats.clear(); + for (int m = 0; m < numServs; m++) { + String serviceName = in.readString(); + Uid.Pkg.Serv serv = new Serv(); + mServiceStats.put(serviceName, serv); + + serv.readFromParcelLocked(in); + } + } + + void writeToParcelLocked(Parcel out) { + out.writeInt(mWakeups); + out.writeInt(mLoadedWakeups); + out.writeInt(mLastWakeups); + + out.writeInt(mServiceStats.size()); + for (Map.Entry<String, Uid.Pkg.Serv> servEntry : mServiceStats.entrySet()) { + out.writeString(servEntry.getKey()); + Uid.Pkg.Serv serv = servEntry.getValue(); + + serv.writeToParcelLocked(out); + } + } + + @Override + public Map<String, ? extends BatteryStats.Uid.Pkg.Serv> getServiceStats() { + return mServiceStats; + } + + @Override + public int getWakeups(int which) { + int val; + if (which == STATS_LAST) { + val = mLastWakeups; + } else { + val = mWakeups; + if (which == STATS_CURRENT) { + val -= mLoadedWakeups; + } + } + + return val; + } + + /** + * The statistics associated with a particular service. + */ + public final class Serv extends BatteryStats.Uid.Pkg.Serv { + /** + * Total time (ms) the service has been left started. + */ + long mStartTime; + + /** + * If service has been started and not yet stopped, this is + * when it was started. + */ + long mRunningSince; + + /** + * True if we are currently running. + */ + boolean mRunning; + + /** + * Total number of times startService() has been called. + */ + int mStarts; + + /** + * Total time (ms) the service has been left launched. + */ + long mLaunchedTime; + + /** + * If service has been launched and not yet exited, this is + * when it was launched. + */ + long mLaunchedSince; + + /** + * True if we are currently launched. + */ + boolean mLaunched; + + /** + * Total number times the service has been launched. + */ + int mLaunches; + + /** + * The amount of time spent started loaded from a previous save. + */ + long mLoadedStartTime; + + /** + * The number of starts loaded from a previous save. + */ + int mLoadedStarts; + + /** + * The number of launches loaded from a previous save. + */ + int mLoadedLaunches; + + /** + * The amount of time spent started as of the last run. + */ + long mLastStartTime; + + /** + * The number of starts as of the last run. + */ + int mLastStarts; + + /** + * The number of launches as of the last run. + */ + int mLastLaunches; + + void readFromParcelLocked(Parcel in) { + mStartTime = in.readLong(); + mRunningSince = in.readLong(); + mRunning = in.readInt() != 0; + mStarts = in.readInt(); + mLaunchedTime = in.readLong(); + mLaunchedSince = in.readLong(); + mLaunched = in.readInt() != 0; + mLaunches = in.readInt(); + mLoadedStartTime = in.readLong(); + mLoadedStarts = in.readInt(); + mLoadedLaunches = in.readInt(); + mLastStartTime = in.readLong(); + mLastStarts = in.readInt(); + mLastLaunches = in.readInt(); + } + + void writeToParcelLocked(Parcel out) { + out.writeLong(mStartTime); + out.writeLong(mRunningSince); + out.writeInt(mRunning ? 1 : 0); + out.writeInt(mStarts); + out.writeLong(mLaunchedTime); + out.writeLong(mLaunchedSince); + out.writeInt(mLaunched ? 1 : 0); + out.writeInt(mLaunches); + out.writeLong(mLoadedStartTime); + out.writeInt(mLoadedStarts); + out.writeInt(mLoadedLaunches); + out.writeLong(mLastStartTime); + out.writeInt(mLastStarts); + out.writeInt(mLastLaunches); + } + + long getLaunchTimeToNowLocked(long batteryUptime) { + if (!mLaunched) return mLaunchedTime; + return mLaunchedTime + batteryUptime - mLaunchedSince; + } + + long getStartTimeToNowLocked(long batteryUptime) { + if (!mRunning) return mStartTime; + return mStartTime + batteryUptime - mRunningSince; + } + + public void startLaunchedLocked() { + if (!mLaunched) { + mLaunches++; + mLaunchedSince = getBatteryUptimeLocked(); + mLaunched = true; + } + } + + public void stopLaunchedLocked() { + if (mLaunched) { + long time = getBatteryUptimeLocked() - mLaunchedSince; + if (time > 0) { + mLaunchedTime += time; + } else { + mLaunches--; + } + mLaunched = false; + } + } + + public void startRunningLocked() { + if (!mRunning) { + mStarts++; + mRunningSince = getBatteryUptimeLocked(); + mRunning = true; + } + } + + public void stopRunningLocked() { + if (mRunning) { + long time = getBatteryUptimeLocked() - mRunningSince; + if (time > 0) { + mStartTime += time; + } else { + mStarts--; + } + mRunning = false; + } + } + + public BatteryStatsImpl getBatteryStats() { + return BatteryStatsImpl.this; + } + + @Override + public int getLaunches(int which) { + int val; + + if (which == STATS_LAST) { + val = mLastLaunches; + } else { + val = mLaunches; + if (which == STATS_CURRENT) { + val -= mLoadedLaunches; + } + } + + return val; + } + + @Override + public long getStartTime(long now, int which) { + long val; + if (which == STATS_LAST) { + val = mLastStartTime; + } else { + val = getStartTimeToNowLocked(now); + if (which == STATS_CURRENT) { + val -= mLoadedStartTime; + } + } + + return val; + } + + @Override + public int getStarts(int which) { + int val; + if (which == STATS_LAST) { + val = mLastStarts; + } else { + val = mStarts; + if (which == STATS_CURRENT) { + val -= mLoadedStarts; + } + } + + return val; + } + } + + public BatteryStatsImpl getBatteryStats() { + return BatteryStatsImpl.this; + } + + public void incWakeupsLocked() { + mWakeups++; + } + + final Serv newServiceStatsLocked() { + return new Serv(); + } + } + + /** + * Retrieve the statistics object for a particular process, creating + * if needed. + */ + public Proc getProcessStatsLocked(String name) { + Proc ps = mProcessStats.get(name); + if (ps == null) { + ps = new Proc(); + mProcessStats.put(name, ps); + } + + return ps; + } + + /** + * Retrieve the statistics object for a particular service, creating + * if needed. + */ + public Pkg getPackageStatsLocked(String name) { + Pkg ps = mPackageStats.get(name); + if (ps == null) { + ps = new Pkg(); + mPackageStats.put(name, ps); + } + + return ps; + } + + /** + * Retrieve the statistics object for a particular service, creating + * if needed. + */ + public Pkg.Serv getServiceStatsLocked(String pkg, String serv) { + Pkg ps = getPackageStatsLocked(pkg); + Pkg.Serv ss = ps.mServiceStats.get(serv); + if (ss == null) { + ss = ps.newServiceStatsLocked(); + ps.mServiceStats.put(serv, ss); + } + + return ss; + } + + public Timer getWakeTimerLocked(String name, int type) { + Wakelock wl = mWakelockStats.get(name); + if (wl == null) { + wl = new Wakelock(); + mWakelockStats.put(name, wl); + } + Timer t = null; + switch (type) { + case WAKE_TYPE_PARTIAL: + t = wl.wakeTimePartial; + if (t == null) { + t = new Timer(WAKE_TYPE_PARTIAL, mPartialTimers); + wl.wakeTimePartial = t; + } + return t; + case WAKE_TYPE_FULL: + t = wl.wakeTimeFull; + if (t == null) { + t = new Timer(WAKE_TYPE_FULL, mFullTimers); + wl.wakeTimeFull = t; + } + return t; + case WAKE_TYPE_WINDOW: + t = wl.wakeTimeWindow; + if (t == null) { + t = new Timer(WAKE_TYPE_WINDOW, mWindowTimers); + wl.wakeTimeWindow = t; + } + return t; + default: + throw new IllegalArgumentException("type=" + type); + } + } + + public Timer getSensorTimerLocked(int sensor, boolean create) { + Integer sId = Integer.valueOf(sensor); + Sensor se = mSensorStats.get(sId); + if (se == null) { + if (!create) { + return null; + } + se = new Sensor(); + mSensorStats.put(sId, se); + } + Timer t = se.sensorTime; + if (t == null) { + t = new Timer(0, mSensorTimers); + se.sensorTime = t; + } + return t; + } + + public void noteStartWakeLocked(String name, int type) { + Timer t = getWakeTimerLocked(name, type); + if (t != null) { + t.startRunningLocked(BatteryStatsImpl.this); + } + } + + public void noteStopWakeLocked(String name, int type) { + Timer t = getWakeTimerLocked(name, type); + if (t != null) { + t.stopRunningLocked(BatteryStatsImpl.this); + } + } + + public void noteStartSensor(int sensor) { + Timer t = getSensorTimerLocked(sensor, true); + if (t != null) { + t.startRunningLocked(BatteryStatsImpl.this); + } + } + + public void noteStopSensor(int sensor) { + // Don't create a timer if one doesn't already exist + Timer t = getSensorTimerLocked(sensor, false); + if (t != null) { + t.stopRunningLocked(BatteryStatsImpl.this); + } + } + + public BatteryStatsImpl getBatteryStats() { + return BatteryStatsImpl.this; + } + } + + public BatteryStatsImpl(String filename) { + mFile = new File(filename); + mBackupFile = new File(filename + ".bak"); + mStartCount++; + mOnBattery = true; + mTrackBatteryPastUptime = 0; + mTrackBatteryPastRealtime = 0; + mUptimeStart = mTrackBatteryUptimeStart = SystemClock.uptimeMillis() * 1000; + mRealtimeStart = mTrackBatteryRealtimeStart = SystemClock.elapsedRealtime() * 1000; + } + + public BatteryStatsImpl(Parcel p) { + mFile = mBackupFile = null; + readFromParcel(p); + } + + @Override + public int getStartCount() { + return mStartCount; + } + + public boolean isOnBattery() { + return mOnBattery; + } + + public void setOnBattery(boolean onBattery) { + synchronized(this) { + if (mOnBattery != onBattery) { + long uptime = SystemClock.uptimeMillis() * 1000; + long mSecRealtime = SystemClock.elapsedRealtime(); + long realtime = mSecRealtime * 1000; + if (onBattery) { + mTrackBatteryUptimeStart = uptime; + mTrackBatteryRealtimeStart = realtime; + unplugTimers(); + } else { + mTrackBatteryPastUptime += uptime - mTrackBatteryUptimeStart; + mTrackBatteryPastRealtime += realtime - mTrackBatteryRealtimeStart; + } + mOnBattery = onBattery; + if ((mLastWriteTime + (60 * 1000)) < mSecRealtime) { + if (mFile != null) { + writeLocked(); + } + } + } + } + } + + public long getAwakeTimeBattery() { + return computeBatteryUptime(getBatteryUptimeLocked(), STATS_CURRENT); + } + + public long getAwakeTimePlugged() { + return (SystemClock.uptimeMillis() * 1000) - getAwakeTimeBattery(); + } + + @Override + public long computeUptime(long curTime, int which) { + switch (which) { + case STATS_TOTAL: return mUptime + (curTime-mUptimeStart); + case STATS_LAST: return mLastUptime; + case STATS_CURRENT: return (curTime-mUptimeStart); + case STATS_UNPLUGGED: return (curTime-mTrackBatteryUptimeStart); + } + return 0; + } + + @Override + public long computeRealtime(long curTime, int which) { + switch (which) { + case STATS_TOTAL: return mRealtime + (curTime-mRealtimeStart); + case STATS_LAST: return mLastRealtime; + case STATS_CURRENT: return (curTime-mRealtimeStart); + case STATS_UNPLUGGED: return (curTime-mTrackBatteryRealtimeStart); + } + return 0; + } + + @Override + public long computeBatteryUptime(long curTime, int which) { + switch (which) { + case STATS_TOTAL: + return mBatteryUptime + getBatteryUptime(curTime); + case STATS_LAST: + return mBatteryLastUptime; + case STATS_CURRENT: + case STATS_UNPLUGGED: + return getBatteryUptime(curTime); + } + return 0; + } + + @Override + public long computeBatteryRealtime(long curTime, int which) { + switch (which) { + case STATS_TOTAL: + return mBatteryRealtime + getBatteryRealtimeLocked(curTime); + case STATS_LAST: + return mBatteryLastRealtime; + case STATS_CURRENT: + case STATS_UNPLUGGED: + return getBatteryRealtimeLocked(curTime); + } + return 0; + } + + long getBatteryUptimeLocked(long curTime) { + long time = mTrackBatteryPastUptime; + if (mOnBattery) { + time += curTime - mTrackBatteryUptimeStart; + } + return time; + } + + long getBatteryUptimeLocked() { + return getBatteryUptime(SystemClock.uptimeMillis() * 1000); + } + + @Override + public long getBatteryUptime(long curTime) { + return getBatteryUptimeLocked(curTime); + } + + long getBatteryRealtimeLocked(long curTime) { + long time = mTrackBatteryPastRealtime; + if (mOnBattery) { + time += curTime - mTrackBatteryRealtimeStart; + } + return time; + } + + @Override + public long getBatteryRealtime(long curTime) { + return getBatteryRealtimeLocked(curTime); + } + + /** + * Retrieve the statistics object for a particular uid, creating if needed. + */ + public Uid getUidStatsLocked(int uid) { + Uid u = mUidStats.get(uid); + if (u == null) { + u = new Uid(); + mUidStats.put(uid, u); + } + return u; + } + + /** + * Remove the statistics object for a particular uid. + */ + public void removeUidStatsLocked(int uid) { + mUidStats.remove(uid); + } + + /** + * Retrieve the statistics object for a particular process, creating + * if needed. + */ + public Uid.Proc getProcessStatsLocked(int uid, String name) { + Uid u = getUidStatsLocked(uid); + return u.getProcessStatsLocked(name); + } + + /** + * Retrieve the statistics object for a particular process, creating + * if needed. + */ + public Uid.Pkg getPackageStatsLocked(int uid, String pkg) { + Uid u = getUidStatsLocked(uid); + return u.getPackageStatsLocked(pkg); + } + + /** + * Retrieve the statistics object for a particular service, creating + * if needed. + */ + public Uid.Pkg.Serv getServiceStatsLocked(int uid, String pkg, String name) { + Uid u = getUidStatsLocked(uid); + return u.getServiceStatsLocked(pkg, name); + } + + public void writeLocked() { + if ((mFile == null) || (mBackupFile == null)) { + Log.w("BatteryStats", "writeLocked: no file associated with this instance"); + return; + } + + // Keep the old file around until we know the new one has + // been successfully written. + if (mFile.exists()) { + if (mBackupFile.exists()) { + mBackupFile.delete(); + } + mFile.renameTo(mBackupFile); + } + + try { + FileOutputStream stream = new FileOutputStream(mFile); + Parcel out = Parcel.obtain(); + writeSummaryToParcel(out); + stream.write(out.marshall()); + out.recycle(); + + stream.flush(); + stream.close(); + mBackupFile.delete(); + + mLastWriteTime = SystemClock.elapsedRealtime(); + } catch (IOException e) { + Log.e("BatteryStats", "Error writing battery statistics", e); + } + } + + static byte[] readFully(FileInputStream stream) throws java.io.IOException { + int pos = 0; + int avail = stream.available(); + byte[] data = new byte[avail]; + while (true) { + int amt = stream.read(data, pos, data.length-pos); + //Log.i("foo", "Read " + amt + " bytes at " + pos + // + " of avail " + data.length); + if (amt <= 0) { + //Log.i("foo", "**** FINISHED READING: pos=" + pos + // + " len=" + data.length); + return data; + } + pos += amt; + avail = stream.available(); + if (avail > data.length-pos) { + byte[] newData = new byte[pos+avail]; + System.arraycopy(data, 0, newData, 0, pos); + data = newData; + } + } + } + + public void readLocked() { + if ((mFile == null) || (mBackupFile == null)) { + Log.w("BatteryStats", "readLocked: no file associated with this instance"); + return; + } + + mUidStats.clear(); + + FileInputStream stream = null; + if (mBackupFile.exists()) { + try { + stream = new FileInputStream(mBackupFile); + } catch (java.io.IOException e) { + // We'll try for the normal settings file. + } + } + + try { + if (stream == null) { + if (!mFile.exists()) { + return; + } + stream = new FileInputStream(mFile); + } + + byte[] raw = readFully(stream); + Parcel in = Parcel.obtain(); + in.unmarshall(raw, 0, raw.length); + in.setDataPosition(0); + stream.close(); + + readSummaryFromParcel(in); + } catch(java.io.IOException e) { + Log.e("BatteryStats", "Error reading battery statistics", e); + } + } + + public int describeContents() { + return 0; + } + + private void readSummaryFromParcel(Parcel in) { + final int version = in.readInt(); + if (version != VERSION) { + Log.e("BatteryStats", "readFromParcel: version got " + version + + ", expected " + VERSION); + return; + } + + mStartCount = in.readInt(); + mBatteryUptime = in.readLong(); + mBatteryLastUptime = in.readLong(); + mBatteryRealtime = in.readLong(); + mBatteryLastRealtime = in.readLong(); + mUptime = in.readLong(); + mLastUptime = in.readLong(); + mRealtime = in.readLong(); + mLastRealtime = in.readLong(); + mStartCount++; + + final int NU = in.readInt(); + for (int iu=0; iu<NU; iu++) { + int uid = in.readInt(); + Uid u = new Uid(); + mUidStats.put(uid, u); + + int NW = in.readInt(); + for (int iw=0; iw<NW; iw++) { + String wlName = in.readString(); + if (in.readInt() != 0) { + u.getWakeTimerLocked(wlName, WAKE_TYPE_FULL).readSummaryFromParcelLocked(in); + } + if (in.readInt() != 0) { + u.getWakeTimerLocked(wlName, WAKE_TYPE_PARTIAL).readSummaryFromParcelLocked(in); + } + if (in.readInt() != 0) { + u.getWakeTimerLocked(wlName, WAKE_TYPE_WINDOW).readSummaryFromParcelLocked(in); + } + } + + if (version >= 12) { + int NSE = in.readInt(); + for (int is=0; is<NSE; is++) { + int seNumber = in.readInt(); + if (in.readInt() != 0) { + u.getSensorTimerLocked(seNumber, true).readSummaryFromParcelLocked(in); + } + } + } + + int NP = in.readInt(); + for (int ip=0; ip<NP; ip++) { + String procName = in.readString(); + Uid.Proc p = u.getProcessStatsLocked(procName); + p.mUserTime = p.mLoadedUserTime = in.readLong(); + p.mLastUserTime = in.readLong(); + p.mSystemTime = p.mLoadedSystemTime = in.readLong(); + p.mLastSystemTime = in.readLong(); + p.mStarts = p.mLoadedStarts = in.readInt(); + p.mLastStarts = in.readInt(); + } + + NP = in.readInt(); + for (int ip=0; ip<NP; ip++) { + String pkgName = in.readString(); + Uid.Pkg p = u.getPackageStatsLocked(pkgName); + p.mWakeups = p.mLoadedWakeups = in.readInt(); + p.mLastWakeups = in.readInt(); + final int NS = in.readInt(); + for (int is=0; is<NS; is++) { + String servName = in.readString(); + Uid.Pkg.Serv s = u.getServiceStatsLocked(pkgName, servName); + s.mStartTime = s.mLoadedStartTime = in.readLong(); + s.mLastStartTime = in.readLong(); + s.mStarts = s.mLoadedStarts = in.readInt(); + s.mLastStarts = in.readInt(); + s.mLaunches = s.mLoadedLaunches = in.readInt(); + s.mLastLaunches = in.readInt(); + } + } + } + } + + /** + * Writes a summary of the statistics to a Parcel, in a format suitable to be written to + * disk. This format does not allow a lossless round-trip. + * + * @param out the Parcel to be written to. + */ + public void writeSummaryToParcel(Parcel out) { + final long NOW = getBatteryUptimeLocked(); + final long NOW_SYS = SystemClock.uptimeMillis() * 1000; + final long NOWREAL_SYS = SystemClock.elapsedRealtime() * 1000; + + out.writeInt(VERSION); + + out.writeInt(mStartCount); + out.writeLong(computeBatteryUptime(NOW_SYS, STATS_TOTAL)); + out.writeLong(computeBatteryUptime(NOW_SYS, STATS_CURRENT)); + out.writeLong(computeBatteryRealtime(NOWREAL_SYS, STATS_TOTAL)); + out.writeLong(computeBatteryRealtime(NOWREAL_SYS, STATS_CURRENT)); + out.writeLong(computeUptime(NOW_SYS, STATS_TOTAL)); + out.writeLong(computeUptime(NOW_SYS, STATS_CURRENT)); + out.writeLong(computeRealtime(NOWREAL_SYS, STATS_TOTAL)); + out.writeLong(computeRealtime(NOWREAL_SYS, STATS_CURRENT)); + + final int NU = mUidStats.size(); + out.writeInt(NU); + for (int iu=0; iu<NU; iu++) { + out.writeInt(mUidStats.keyAt(iu)); + Uid u = mUidStats.valueAt(iu); + + int NW = u.mWakelockStats.size(); + out.writeInt(NW); + if (NW > 0) { + for (Map.Entry<String, BatteryStatsImpl.Uid.Wakelock> ent + : u.mWakelockStats.entrySet()) { + out.writeString(ent.getKey()); + Uid.Wakelock wl = ent.getValue(); + if (wl.wakeTimeFull != null) { + out.writeInt(1); + wl.wakeTimeFull.writeSummaryFromParcelLocked(out, NOW); + } else { + out.writeInt(0); + } + if (wl.wakeTimePartial != null) { + out.writeInt(1); + wl.wakeTimePartial.writeSummaryFromParcelLocked(out, NOW); + } else { + out.writeInt(0); + } + if (wl.wakeTimeWindow != null) { + out.writeInt(1); + wl.wakeTimeWindow.writeSummaryFromParcelLocked(out, NOW); + } else { + out.writeInt(0); + } + } + } + + int NSE = u.mSensorStats.size(); + out.writeInt(NSE); + if (NSE > 0) { + for (Map.Entry<Integer, BatteryStatsImpl.Uid.Sensor> ent + : u.mSensorStats.entrySet()) { + out.writeInt(ent.getKey()); + Uid.Sensor se = ent.getValue(); + if (se.sensorTime != null) { + out.writeInt(1); + se.sensorTime.writeSummaryFromParcelLocked(out, NOW); + } else { + out.writeInt(0); + } + } + } + + int NP = u.mProcessStats.size(); + out.writeInt(NP); + if (NP > 0) { + for (Map.Entry<String, BatteryStatsImpl.Uid.Proc> ent + : u.mProcessStats.entrySet()) { + out.writeString(ent.getKey()); + Uid.Proc ps = ent.getValue(); + out.writeLong(ps.mUserTime); + out.writeLong(ps.mUserTime - ps.mLoadedUserTime); + out.writeLong(ps.mSystemTime); + out.writeLong(ps.mSystemTime - ps.mLoadedSystemTime); + out.writeInt(ps.mStarts); + out.writeInt(ps.mStarts - ps.mLoadedStarts); + } + } + + NP = u.mPackageStats.size(); + out.writeInt(NP); + if (NP > 0) { + for (Map.Entry<String, BatteryStatsImpl.Uid.Pkg> ent + : u.mPackageStats.entrySet()) { + out.writeString(ent.getKey()); + Uid.Pkg ps = ent.getValue(); + out.writeInt(ps.mWakeups); + out.writeInt(ps.mWakeups - ps.mLoadedWakeups); + final int NS = ps.mServiceStats.size(); + out.writeInt(NS); + if (NS > 0) { + for (Map.Entry<String, BatteryStatsImpl.Uid.Pkg.Serv> sent + : ps.mServiceStats.entrySet()) { + out.writeString(sent.getKey()); + BatteryStatsImpl.Uid.Pkg.Serv ss = sent.getValue(); + long time = ss.getStartTimeToNowLocked(NOW); + out.writeLong(time); + out.writeLong(time - ss.mLoadedStartTime); + out.writeInt(ss.mStarts); + out.writeInt(ss.mStarts - ss.mLoadedStarts); + out.writeInt(ss.mLaunches); + out.writeInt(ss.mLaunches - ss.mLoadedLaunches); + } + } + } + } + } + } + + public void readFromParcel(Parcel in) { + readFromParcelLocked(in); + } + + void readFromParcelLocked(Parcel in) { + int magic = in.readInt(); + if (magic != MAGIC) { + throw new ParcelFormatException("Bad magic number"); + } + + mStartCount = in.readInt(); + mBatteryUptime = in.readLong(); + mBatteryLastUptime = in.readLong(); + mBatteryRealtime = in.readLong(); + mBatteryLastRealtime = in.readLong(); + mUptime = in.readLong(); + mUptimeStart = in.readLong(); + mLastUptime = in.readLong(); + mRealtime = in.readLong(); + mRealtimeStart = in.readLong(); + mLastRealtime = in.readLong(); + mOnBattery = in.readInt() != 0; + mTrackBatteryPastUptime = in.readLong(); + mTrackBatteryUptimeStart = in.readLong(); + mTrackBatteryPastRealtime = in.readLong(); + mTrackBatteryRealtimeStart = in.readLong(); + mLastWriteTime = in.readLong(); + + mPartialTimers.clear(); + mFullTimers.clear(); + mWindowTimers.clear(); + + int numUids = in.readInt(); + mUidStats.clear(); + for (int i = 0; i < numUids; i++) { + int key = in.readInt(); + Uid uid = new Uid(); + uid.readFromParcelLocked(in); + mUidStats.append(key, uid); + } + } + + public void writeToParcel(Parcel out, int flags) { + writeToParcelLocked(out, flags); + } + + @SuppressWarnings("unused") + void writeToParcelLocked(Parcel out, int flags) { + out.writeInt(MAGIC); + out.writeInt(mStartCount); + out.writeLong(mBatteryUptime); + out.writeLong(mBatteryLastUptime); + out.writeLong(mBatteryRealtime); + out.writeLong(mBatteryLastRealtime); + out.writeLong(mUptime); + out.writeLong(mUptimeStart); + out.writeLong(mLastUptime); + out.writeLong(mRealtime); + out.writeLong(mRealtimeStart); + out.writeLong(mLastRealtime); + out.writeInt(mOnBattery ? 1 : 0); + out.writeLong(mTrackBatteryPastUptime); + out.writeLong(mTrackBatteryUptimeStart); + out.writeLong(mTrackBatteryPastRealtime); + out.writeLong(mTrackBatteryRealtimeStart); + out.writeLong(mLastWriteTime); + + int size = mUidStats.size(); + out.writeInt(size); + for (int i = 0; i < size; i++) { + out.writeInt(mUidStats.keyAt(i)); + Uid uid = mUidStats.valueAt(i); + + uid.writeToParcelLocked(out); + } + } + + public static final Parcelable.Creator<BatteryStatsImpl> CREATOR = + new Parcelable.Creator<BatteryStatsImpl>() { + public BatteryStatsImpl createFromParcel(Parcel in) { + return new BatteryStatsImpl(in); + } + + public BatteryStatsImpl[] newArray(int size) { + return new BatteryStatsImpl[size]; + } + }; +} diff --git a/core/java/com/android/internal/os/HandlerCaller.java b/core/java/com/android/internal/os/HandlerCaller.java new file mode 100644 index 0000000..fd8fd5a --- /dev/null +++ b/core/java/com/android/internal/os/HandlerCaller.java @@ -0,0 +1,166 @@ +package com.android.internal.os; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +public class HandlerCaller { + private static final String TAG = "HandlerCaller"; + private static final boolean DEBUG = false; + + public final Context mContext; + + final Looper mMainLooper; + final Handler mH; + + final Callback mCallback; + + public static class SomeArgs { + SomeArgs next; + + public Object arg1; + public Object arg2; + Object arg3; + public int argi1; + public int argi2; + public int argi3; + public int argi4; + } + + static final int ARGS_POOL_MAX_SIZE = 10; + int mArgsPoolSize; + SomeArgs mArgsPool; + + class MyHandler extends Handler { + MyHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + mCallback.executeMessage(msg); + } + } + + public interface Callback { + public void executeMessage(Message msg); + } + + public HandlerCaller(Context context, Callback callback) { + mContext = context; + mMainLooper = context.getMainLooper(); + mH = new MyHandler(mMainLooper); + mCallback = callback; + } + + public SomeArgs obtainArgs() { + synchronized (mH) { + SomeArgs args = mArgsPool; + if (args != null) { + mArgsPool = args.next; + args.next = null; + mArgsPoolSize--; + return args; + } + } + return new SomeArgs(); + } + + public void recycleArgs(SomeArgs args) { + synchronized (mH) { + if (mArgsPoolSize < ARGS_POOL_MAX_SIZE) { + args.next = mArgsPool; + mArgsPool = args; + mArgsPoolSize++; + } + } + } + + public void executeOrSendMessage(Message msg) { + // If we are calling this from the main thread, then we can call + // right through. Otherwise, we need to send the message to the + // main thread. + if (Looper.myLooper() == mMainLooper) { + mCallback.executeMessage(msg); + msg.recycle(); + return; + } + + mH.sendMessage(msg); + } + + public void sendMessage(Message msg) { + mH.sendMessage(msg); + } + + public Message obtainMessage(int what) { + return mH.obtainMessage(what); + } + + public Message obtainMessageBO(int what, boolean arg1, Object arg2) { + return mH.obtainMessage(what, arg1 ? 1 : 0, 0, arg2); + } + + public Message obtainMessageBOO(int what, boolean arg1, Object arg2, Object arg3) { + SomeArgs args = obtainArgs(); + args.arg1 = arg2; + args.arg2 = arg3; + return mH.obtainMessage(what, arg1 ? 1 : 0, 0, args); + } + + public Message obtainMessageO(int what, Object arg1) { + return mH.obtainMessage(what, 0, 0, arg1); + } + + public Message obtainMessageIO(int what, int arg1, Object arg2) { + return mH.obtainMessage(what, arg1, 0, arg2); + } + + public Message obtainMessageIO(int what, int arg1, int arg2, Object arg3) { + return mH.obtainMessage(what, arg1, arg2, arg3); + } + + public Message obtainMessageIOO(int what, int arg1, Object arg2, Object arg3) { + SomeArgs args = obtainArgs(); + args.arg1 = arg2; + args.arg2 = arg3; + return mH.obtainMessage(what, arg1, 0, args); + } + + public Message obtainMessageOO(int what, Object arg1, Object arg2) { + SomeArgs args = obtainArgs(); + args.arg1 = arg1; + args.arg2 = arg2; + return mH.obtainMessage(what, 0, 0, args); + } + + public Message obtainMessageOOO(int what, Object arg1, Object arg2, Object arg3) { + SomeArgs args = obtainArgs(); + args.arg1 = arg1; + args.arg2 = arg2; + args.arg3 = arg3; + return mH.obtainMessage(what, 0, 0, args); + } + + public Message obtainMessageIIII(int what, int arg1, int arg2, + int arg3, int arg4) { + SomeArgs args = obtainArgs(); + args.argi1 = arg1; + args.argi2 = arg2; + args.argi3 = arg3; + args.argi4 = arg4; + return mH.obtainMessage(what, 0, 0, args); + } + + public Message obtainMessageIIIIO(int what, int arg1, int arg2, + int arg3, int arg4, Object arg5) { + SomeArgs args = obtainArgs(); + args.arg1 = arg5; + args.argi1 = arg1; + args.argi2 = arg2; + args.argi3 = arg3; + args.argi4 = arg4; + return mH.obtainMessage(what, 0, 0, args); + } +} diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java index 566332b..631e7d8 100644 --- a/core/java/com/android/internal/os/ZygoteConnection.java +++ b/core/java/com/android/internal/os/ZygoteConnection.java @@ -210,7 +210,7 @@ class ZygoteConnection { } pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, - parsedArgs.gids, parsedArgs.enableDebugger, rlimits); + parsedArgs.gids, parsedArgs.debugFlags, rlimits); } catch (IllegalArgumentException ex) { logAndPrintError (newStderr, "Invalid zygote arguments", ex); pid = -1; @@ -295,8 +295,8 @@ class ZygoteConnection { /** from --peer-wait */ boolean peerWait; - /** from --enable-debugger */ - boolean enableDebugger; + /** from --enable-debugger, --enable-checkjni, --enable-assert */ + int debugFlags; /** from --classpath */ String classpath; @@ -362,7 +362,11 @@ class ZygoteConnection { gid = Integer.parseInt( arg.substring(arg.indexOf('=') + 1)); } else if (arg.equals("--enable-debugger")) { - enableDebugger = true; + debugFlags |= Zygote.DEBUG_ENABLE_DEBUGGER; + } else if (arg.equals("--enable-checkjni")) { + debugFlags |= Zygote.DEBUG_ENABLE_CHECKJNI; + } else if (arg.equals("--enable-assert")) { + debugFlags |= Zygote.DEBUG_ENABLE_ASSERT; } else if (arg.equals("--peer-wait")) { peerWait = true; } else if (arg.equals("--runtime-init")) { @@ -567,7 +571,7 @@ class ZygoteConnection { */ private static void applyDebuggerSecurityPolicy(Arguments args) { if ("1".equals(SystemProperties.get("ro.debuggable"))) { - args.enableDebugger = true; + args.debugFlags |= Zygote.DEBUG_ENABLE_DEBUGGER; } } diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index 5714b99..f21b62f 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -260,7 +260,7 @@ public class ZygoteInit { int count = 0; String line; - boolean gotMissingClass = false; + String missingClasses = null; while ((line = br.readLine()) != null) { // Skip comments and blank lines. line = line.trim(); @@ -285,14 +285,19 @@ public class ZygoteInit { count++; } catch (ClassNotFoundException e) { Log.e(TAG, "Class not found for preloading: " + line); - gotMissingClass = true; + if (missingClasses == null) { + missingClasses = line; + } else { + missingClasses += " " + line; + } } } - if (gotMissingClass && + if (missingClasses != null && "1".equals(SystemProperties.get("persist.service.adb.enable"))) { throw new IllegalStateException( - "Missing class(es) for preloading, update preloaded-classes"); + "Missing class(es) for preloading, update preloaded-classes [" + + missingClasses + "]"); } Log.i(TAG, "...preloaded " + count + " classes in " @@ -425,7 +430,7 @@ public class ZygoteInit { "--setuid=1000", "--setgid=1000", "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,3001,3002,3003", - "--capabilities=88161312,88161312", + "--capabilities=121715744,121715744", "--runtime-init", "--nice-name=system_server", "com.android.server.SystemServer", @@ -442,13 +447,14 @@ public class ZygoteInit { * indicate it should be debuggable or the ro.debuggable system property * is set to "1" */ - boolean debuggableBuild = "1".equals(SystemProperties.get("ro.debuggable")); - boolean enableDebugger = parsedArgs.enableDebugger || debuggableBuild; + int debugFlags = parsedArgs.debugFlags; + if ("1".equals(SystemProperties.get("ro.debuggable"))) + debugFlags |= Zygote.DEBUG_ENABLE_DEBUGGER; /* Request to fork the system server process */ pid = Zygote.forkSystemServer( parsedArgs.uid, parsedArgs.gid, - parsedArgs.gids, enableDebugger, null); + parsedArgs.gids, debugFlags, null); } catch (IllegalArgumentException ex) { throw new RuntimeException(ex); } diff --git a/core/java/com/android/internal/provider/Settings.java b/core/java/com/android/internal/provider/Settings.java deleted file mode 100644 index 85ef17e..0000000 --- a/core/java/com/android/internal/provider/Settings.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.provider; - -import android.provider.BaseColumns; -import android.net.Uri; - -/** - * Settings related utilities. - */ -public class Settings { - /** - * Favorite intents - */ - public static final class Favorites implements BaseColumns { - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = Uri.parse("content://" + - android.provider.Settings.AUTHORITY + "/favorites?notify=true"); - - /** - * The content:// style URL for this table. When this Uri is used, no notification is - * sent if the content changes. - */ - public static final Uri CONTENT_URI_NO_NOTIFICATION = - Uri.parse("content://" + android.provider.Settings.AUTHORITY + - "/favorites?notify=false"); - - /** - * The content:// style URL for a given row, identified by its id. - * - * @param id The row id. - * @param notify True to send a notification is the content changes. - * - * @return The unique content URL for the specified row. - */ - public static Uri getContentUri(long id, boolean notify) { - return Uri.parse("content://" + android.provider.Settings.AUTHORITY + - "/favorites/" + id + "?notify=" + notify); - } - - /** - * The row ID. - * <p>Type: INTEGER</p> - */ - public static final String ID = "_id"; - - /** - * Descriptive name of the favorite that can be displayed to the user. - * <P>Type: TEXT</P> - */ - public static final String TITLE = "title"; - - /** - * The Intent URL of the favorite, describing what it points to. This - * value is given to {@link android.content.Intent#getIntent} to create - * an Intent that can be launched. - * <P>Type: TEXT</P> - */ - public static final String INTENT = "intent"; - - /** - * The container holding the favorite - * <P>Type: INTEGER</P> - */ - public static final String CONTAINER = "container"; - - /** - * The icon is a resource identified by a package name and an integer id. - */ - public static final int CONTAINER_DESKTOP = -100; - - /** - * The screen holding the favorite (if container is CONTAINER_DESKTOP) - * <P>Type: INTEGER</P> - */ - public static final String SCREEN = "screen"; - - /** - * The X coordinate of the cell holding the favorite - * (if container is CONTAINER_DESKTOP or CONTAINER_DOCK) - * <P>Type: INTEGER</P> - */ - public static final String CELLX = "cellX"; - - /** - * The Y coordinate of the cell holding the favorite - * (if container is CONTAINER_DESKTOP) - * <P>Type: INTEGER</P> - */ - public static final String CELLY = "cellY"; - - /** - * The X span of the cell holding the favorite - * <P>Type: INTEGER</P> - */ - public static final String SPANX = "spanX"; - - /** - * The Y span of the cell holding the favorite - * <P>Type: INTEGER</P> - */ - public static final String SPANY = "spanY"; - - /** - * The type of the favorite - * - * <P>Type: INTEGER</P> - */ - public static final String ITEM_TYPE = "itemType"; - - /** - * The favorite is an application - */ - public static final int ITEM_TYPE_APPLICATION = 0; - - /** - * The favorite is an application created shortcut - */ - public static final int ITEM_TYPE_SHORTCUT = 1; - - /** - * The favorite is a user created folder - */ - public static final int ITEM_TYPE_USER_FOLDER = 2; - - /** - * The favorite is a clock - */ - public static final int ITEM_TYPE_WIDGET_CLOCK = 1000; - - /** - * The favorite is a search widget - */ - public static final int ITEM_TYPE_WIDGET_SEARCH = 1001; - - /** - * The favorite is a photo frame - */ - public static final int ITEM_TYPE_WIDGET_PHOTO_FRAME = 1002; - - /** - * Indicates whether this favorite is an application-created shortcut or not. - * If the value is 0, the favorite is not an application-created shortcut, if the - * value is 1, it is an application-created shortcut. - * <P>Type: INTEGER</P> - */ - public static final String IS_SHORTCUT = "isShortcut"; - - /** - * The icon type. - * <P>Type: INTEGER</P> - */ - public static final String ICON_TYPE = "iconType"; - - /** - * The icon is a resource identified by a package name and an integer id. - */ - public static final int ICON_TYPE_RESOURCE = 0; - - /** - * The icon is a bitmap. - */ - public static final int ICON_TYPE_BITMAP = 1; - - /** - * The icon package name, if icon type is ICON_TYPE_RESOURCE. - * <P>Type: TEXT</P> - */ - public static final String ICON_PACKAGE = "iconPackage"; - - /** - * The icon resource id, if icon type is ICON_TYPE_RESOURCE. - * <P>Type: TEXT</P> - */ - public static final String ICON_RESOURCE = "iconResource"; - - /** - * The custom icon bitmap, if icon type is ICON_TYPE_BITMAP. - * <P>Type: BLOB</P> - */ - public static final String ICON = "icon"; - } -} diff --git a/core/java/com/android/internal/util/FastXmlSerializer.java b/core/java/com/android/internal/util/FastXmlSerializer.java index b4dae94..592a8fa 100644 --- a/core/java/com/android/internal/util/FastXmlSerializer.java +++ b/core/java/com/android/internal/util/FastXmlSerializer.java @@ -125,7 +125,7 @@ public class FastXmlSerializer implements XmlSerializer { String escape = escapes[c]; if (escape == null) continue; if (lastPos < pos) append(string, lastPos, pos-lastPos); - lastPos = pos; + lastPos = pos + 1; append(escape); } if (lastPos < pos) append(string, lastPos, pos-lastPos); @@ -143,7 +143,7 @@ public class FastXmlSerializer implements XmlSerializer { String escape = escapes[c]; if (escape == null) continue; if (lastPos < pos) append(buf, lastPos, pos-lastPos); - lastPos = pos; + lastPos = pos + 1; append(escape); } if (lastPos < pos) append(buf, lastPos, pos-lastPos); diff --git a/core/java/com/android/internal/view/IInputConnectionCallback.aidl b/core/java/com/android/internal/view/IInputConnectionCallback.aidl new file mode 100644 index 0000000..5b5b3df --- /dev/null +++ b/core/java/com/android/internal/view/IInputConnectionCallback.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view; + +import android.graphics.Rect; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.TextBoxAttribute; +import com.android.internal.view.IInputContext; +import android.os.IBinder; + +/** + * {@hide} + */ +oneway interface IInputMethodCallback { + void finishedEvent(int seq, boolean handled); +} diff --git a/core/java/com/android/internal/view/IInputConnectionWrapper.java b/core/java/com/android/internal/view/IInputConnectionWrapper.java new file mode 100644 index 0000000..c5966ee --- /dev/null +++ b/core/java/com/android/internal/view/IInputConnectionWrapper.java @@ -0,0 +1,252 @@ +package com.android.internal.view; + +import com.android.internal.view.IInputContext; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; +import android.view.KeyEvent; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; + +public class IInputConnectionWrapper extends IInputContext.Stub { + static final String TAG = "IInputConnectionWrapper"; + + private static final int DO_GET_TEXT_AFTER_CURSOR = 10; + private static final int DO_GET_TEXT_BEFORE_CURSOR = 20; + private static final int DO_GET_CURSOR_CAPS_MODE = 30; + private static final int DO_GET_EXTRACTED_TEXT = 40; + private static final int DO_COMMIT_TEXT = 50; + private static final int DO_COMMIT_COMPLETION = 55; + private static final int DO_SET_COMPOSING_TEXT = 60; + private static final int DO_SEND_KEY_EVENT = 70; + private static final int DO_DELETE_SURROUNDING_TEXT = 80; + private static final int DO_HIDE_STATUS_ICON = 100; + private static final int DO_SHOW_STATUS_ICON = 110; + private static final int DO_PERFORM_PRIVATE_COMMAND = 120; + private static final int DO_CLEAR_META_KEY_STATES = 130; + + private InputConnection mInputConnection; + + private Looper mMainLooper; + private Handler mH; + + static class SomeArgs { + Object arg1; + Object arg2; + IInputContextCallback callback; + int seq; + } + + class MyHandler extends Handler { + MyHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + executeMessage(msg); + } + } + + public IInputConnectionWrapper(Looper mainLooper, InputConnection conn) { + mInputConnection = conn; + mMainLooper = mainLooper; + mH = new MyHandler(mMainLooper); + } + + public void getTextAfterCursor(int length, int seq, IInputContextCallback callback) { + dispatchMessage(obtainMessageISC(DO_GET_TEXT_AFTER_CURSOR, length, seq, callback)); + } + + public void getTextBeforeCursor(int length, int seq, IInputContextCallback callback) { + dispatchMessage(obtainMessageISC(DO_GET_TEXT_BEFORE_CURSOR, length, seq, callback)); + } + + public void getCursorCapsMode(int reqModes, int seq, IInputContextCallback callback) { + dispatchMessage(obtainMessageISC(DO_GET_CURSOR_CAPS_MODE, reqModes, seq, callback)); + } + + public void getExtractedText(ExtractedTextRequest request, + int flags, int seq, IInputContextCallback callback) { + dispatchMessage(obtainMessageIOSC(DO_GET_EXTRACTED_TEXT, flags, + request, seq, callback)); + } + + public void commitText(CharSequence text, int newCursorPosition) { + dispatchMessage(obtainMessageIO(DO_COMMIT_TEXT, newCursorPosition, text)); + } + + public void commitCompletion(CompletionInfo text) { + dispatchMessage(obtainMessageO(DO_COMMIT_COMPLETION, text)); + } + + public void setComposingText(CharSequence text, int newCursorPosition) { + dispatchMessage(obtainMessageIO(DO_SET_COMPOSING_TEXT, newCursorPosition, text)); + } + + public void sendKeyEvent(KeyEvent event) { + dispatchMessage(obtainMessageO(DO_SEND_KEY_EVENT, event)); + } + + public void clearMetaKeyStates(int states) { + dispatchMessage(obtainMessageII(DO_CLEAR_META_KEY_STATES, states, 0)); + } + + public void deleteSurroundingText(int leftLength, int rightLength) { + dispatchMessage(obtainMessageII(DO_DELETE_SURROUNDING_TEXT, + leftLength, rightLength)); + } + + public void hideStatusIcon() { + dispatchMessage(obtainMessage(DO_HIDE_STATUS_ICON)); + } + + public void showStatusIcon(String packageName, int resId) { + dispatchMessage(obtainMessageIO(DO_SHOW_STATUS_ICON, resId, packageName)); + } + + public void performPrivateCommand(String action, Bundle data) { + dispatchMessage(obtainMessageOO(DO_PERFORM_PRIVATE_COMMAND, action, data)); + } + + void dispatchMessage(Message msg) { + // If we are calling this from the main thread, then we can call + // right through. Otherwise, we need to send the message to the + // main thread. + if (Looper.myLooper() == mMainLooper) { + executeMessage(msg); + msg.recycle(); + return; + } + + mH.sendMessage(msg); + } + + void executeMessage(Message msg) { + switch (msg.what) { + case DO_GET_TEXT_AFTER_CURSOR: { + SomeArgs args = (SomeArgs)msg.obj; + try { + args.callback.setTextAfterCursor(mInputConnection.getTextAfterCursor(msg.arg1), + args.seq); + } catch (RemoteException e) { + Log.w(TAG, "Got RemoteException calling setTextAfterCursor", e); + } + return; + } + case DO_GET_TEXT_BEFORE_CURSOR: { + SomeArgs args = (SomeArgs)msg.obj; + try { + args.callback.setTextBeforeCursor(mInputConnection.getTextBeforeCursor(msg.arg1), + args.seq); + } catch (RemoteException e) { + Log.w(TAG, "Got RemoteException calling setTextBeforeCursor", e); + } + return; + } + case DO_GET_CURSOR_CAPS_MODE: { + SomeArgs args = (SomeArgs)msg.obj; + try { + args.callback.setCursorCapsMode(mInputConnection.getCursorCapsMode(msg.arg1), + args.seq); + } catch (RemoteException e) { + Log.w(TAG, "Got RemoteException calling setCursorCapsMode", e); + } + return; + } + case DO_GET_EXTRACTED_TEXT: { + SomeArgs args = (SomeArgs)msg.obj; + try { + args.callback.setExtractedText(mInputConnection.getExtractedText( + (ExtractedTextRequest)args.arg1, msg.arg1), args.seq); + } catch (RemoteException e) { + Log.w(TAG, "Got RemoteException calling setExtractedText", e); + } + return; + } + case DO_COMMIT_TEXT: { + mInputConnection.commitText((CharSequence)msg.obj, msg.arg1); + return; + } + case DO_COMMIT_COMPLETION: { + mInputConnection.commitCompletion((CompletionInfo)msg.obj); + return; + } + case DO_SET_COMPOSING_TEXT: { + mInputConnection.setComposingText((CharSequence)msg.obj, msg.arg1); + return; + } + case DO_SEND_KEY_EVENT: { + mInputConnection.sendKeyEvent((KeyEvent)msg.obj); + return; + } + case DO_CLEAR_META_KEY_STATES: { + mInputConnection.clearMetaKeyStates(msg.arg1); + return; + } + case DO_DELETE_SURROUNDING_TEXT: { + mInputConnection.deleteSurroundingText(msg.arg1, msg.arg2); + return; + } + case DO_HIDE_STATUS_ICON: { + mInputConnection.hideStatusIcon(); + return; + } + case DO_SHOW_STATUS_ICON: { + mInputConnection.showStatusIcon((String)msg.obj, msg.arg1); + return; + } + case DO_PERFORM_PRIVATE_COMMAND: { + SomeArgs args = (SomeArgs)msg.obj; + mInputConnection.performPrivateCommand((String)args.arg1, + (Bundle)args.arg2); + return; + } + } + Log.w(TAG, "Unhandled message code: " + msg.what); + } + + Message obtainMessage(int what) { + return mH.obtainMessage(what); + } + + Message obtainMessageII(int what, int arg1, int arg2) { + return mH.obtainMessage(what, arg1, arg2); + } + + Message obtainMessageO(int what, Object arg1) { + return mH.obtainMessage(what, 0, 0, arg1); + } + + Message obtainMessageISC(int what, int arg1, int seq, IInputContextCallback callback) { + SomeArgs args = new SomeArgs(); + args.callback = callback; + args.seq = seq; + return mH.obtainMessage(what, arg1, 0, args); + } + + Message obtainMessageIOSC(int what, int arg1, Object arg2, int seq, + IInputContextCallback callback) { + SomeArgs args = new SomeArgs(); + args.arg1 = arg2; + args.callback = callback; + args.seq = seq; + return mH.obtainMessage(what, arg1, 0, args); + } + + Message obtainMessageIO(int what, int arg1, Object arg2) { + return mH.obtainMessage(what, arg1, 0, arg2); + } + + Message obtainMessageOO(int what, Object arg1, Object arg2) { + SomeArgs args = new SomeArgs(); + args.arg1 = arg1; + args.arg2 = arg2; + return mH.obtainMessage(what, 0, 0, args); + } +} diff --git a/core/java/com/android/internal/view/IInputContext.aidl b/core/java/com/android/internal/view/IInputContext.aidl new file mode 100644 index 0000000..7ea65a0 --- /dev/null +++ b/core/java/com/android/internal/view/IInputContext.aidl @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view; + +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.ExtractedTextRequest; + +import com.android.internal.view.IInputContextCallback; + +/** + * Interface from an input method to the application, allowing it to perform + * edits on the current input field and other interactions with the application. + * {@hide} + */ + oneway interface IInputContext { + void getTextBeforeCursor(int length, int seq, IInputContextCallback callback); + + void getTextAfterCursor(int length, int seq, IInputContextCallback callback); + + void getCursorCapsMode(int reqModes, int seq, IInputContextCallback callback); + + void getExtractedText(in ExtractedTextRequest request, int flags, int seq, + IInputContextCallback callback); + + void deleteSurroundingText(int leftLength, int rightLength); + + void setComposingText(CharSequence text, int newCursorPosition); + + void commitText(CharSequence text, int newCursorPosition); + + void commitCompletion(in CompletionInfo completion); + + void sendKeyEvent(in KeyEvent event); + + void clearMetaKeyStates(int states); + + void performPrivateCommand(String action, in Bundle data); + + void showStatusIcon(String packageName, int resId); + + void hideStatusIcon(); +} diff --git a/core/java/com/android/internal/view/IInputContextCallback.aidl b/core/java/com/android/internal/view/IInputContextCallback.aidl new file mode 100644 index 0000000..9b8c43c --- /dev/null +++ b/core/java/com/android/internal/view/IInputContextCallback.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view; + +import android.view.inputmethod.ExtractedText; + +/** + * {@hide} + */ +oneway interface IInputContextCallback { + void setTextBeforeCursor(CharSequence textBeforeCursor, int seq); + void setTextAfterCursor(CharSequence textAfterCursor, int seq); + void setCursorCapsMode(int capsMode, int seq); + void setExtractedText(in ExtractedText extractedText, int seq); +} diff --git a/core/java/com/android/internal/view/IInputMethod.aidl b/core/java/com/android/internal/view/IInputMethod.aidl new file mode 100644 index 0000000..87bf473 --- /dev/null +++ b/core/java/com/android/internal/view/IInputMethod.aidl @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view; + +import android.graphics.Rect; +import android.os.IBinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputBinding; +import com.android.internal.view.IInputContext; +import com.android.internal.view.IInputMethodCallback; +import com.android.internal.view.IInputMethodSession; + +/** + * Top-level interface to an input method component (implemented in a + * Service). + * {@hide} + */ +oneway interface IInputMethod { + void attachToken(IBinder token); + + void bindInput(in InputBinding binding); + + void unbindInput(); + + void startInput(in EditorInfo attribute); + + void restartInput(in EditorInfo attribute); + + void createSession(IInputMethodCallback callback); + + void setSessionEnabled(IInputMethodSession session, boolean enabled); + + void revokeSession(IInputMethodSession session); + + void showSoftInput(); + + void hideSoftInput(); +} diff --git a/core/java/com/android/internal/view/IInputMethodCallback.aidl b/core/java/com/android/internal/view/IInputMethodCallback.aidl new file mode 100644 index 0000000..480cc0e --- /dev/null +++ b/core/java/com/android/internal/view/IInputMethodCallback.aidl @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view; + +import android.graphics.Rect; +import android.view.KeyEvent; +import android.view.MotionEvent; +import com.android.internal.view.IInputContext; +import com.android.internal.view.IInputMethodSession; +import android.os.IBinder; + +/** + * Helper interface for IInputMethod to allow the input method to call back + * to its client with results from incoming calls. + * {@hide} + */ +oneway interface IInputMethodCallback { + void finishedEvent(int seq, boolean handled); + void sessionCreated(IInputMethodSession session); +} diff --git a/core/java/com/android/internal/view/IInputMethodClient.aidl b/core/java/com/android/internal/view/IInputMethodClient.aidl new file mode 100644 index 0000000..ce4312d --- /dev/null +++ b/core/java/com/android/internal/view/IInputMethodClient.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view; + +import com.android.internal.view.InputBindResult; + +/** + * Interface a client of the IInputMethodManager implements, to identify + * itself and receive information about changes to the global manager state. + */ +oneway interface IInputMethodClient { + void setUsingInputMethod(boolean state); + void onBindMethod(in InputBindResult res); + void onUnbindMethod(int sequence); + void setActive(boolean active); +} diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl new file mode 100644 index 0000000..b4cfe26 --- /dev/null +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view; + +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.EditorInfo; +import com.android.internal.view.InputBindResult; +import com.android.internal.view.IInputContext; +import com.android.internal.view.IInputMethodClient; + +/** + * Public interface to the global input method manager, used by all client + * applications. + */ +interface IInputMethodManager { + List<InputMethodInfo> getInputMethodList(); + List<InputMethodInfo> getEnabledInputMethodList(); + + void addClient(in IInputMethodClient client, + in IInputContext inputContext, int uid, int pid); + void removeClient(in IInputMethodClient client); + + InputBindResult startInput(in IInputMethodClient client, + in EditorInfo attribute, boolean initial, boolean needResult); + void finishInput(in IInputMethodClient client); + void showSoftInput(in IInputMethodClient client); + void hideSoftInput(in IInputMethodClient client); + void windowGainedFocus(in IInputMethodClient client, + boolean viewHasFocus, int softInputMode, boolean first, + int windowFlags); + + void showInputMethodPickerFromClient(in IInputMethodClient client); + void setInputMethod(in IBinder token, String id); + void hideMySoftInput(in IBinder token); + void updateStatusIcon(int iconId, String iconPackage); +} + diff --git a/core/java/com/android/internal/view/IInputMethodSession.aidl b/core/java/com/android/internal/view/IInputMethodSession.aidl new file mode 100644 index 0000000..4f28593 --- /dev/null +++ b/core/java/com/android/internal/view/IInputMethodSession.aidl @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view; + +import android.graphics.Rect; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.ExtractedText; +import com.android.internal.view.IInputMethodCallback; + +/** + * Sub-interface of IInputMethod which is safe to give to client applications. + * {@hide} + */ +oneway interface IInputMethodSession { + void finishInput(); + + void updateExtractedText(int token, in ExtractedText text); + + void updateSelection(int oldSelStart, int oldSelEnd, + int newSelStart, int newSelEnd); + + void updateCursor(in Rect newCursor); + + void displayCompletions(in CompletionInfo[] completions); + + void dispatchKeyEvent(int seq, in KeyEvent event, IInputMethodCallback callback); + + void dispatchTrackballEvent(int seq, in MotionEvent event, IInputMethodCallback callback); + + void appPrivateCommand(String action, in Bundle data); +} diff --git a/core/java/com/android/internal/view/InputBindResult.aidl b/core/java/com/android/internal/view/InputBindResult.aidl new file mode 100644 index 0000000..7ff5c4e --- /dev/null +++ b/core/java/com/android/internal/view/InputBindResult.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view; + +parcelable InputBindResult; diff --git a/core/java/com/android/internal/view/InputBindResult.java b/core/java/com/android/internal/view/InputBindResult.java new file mode 100644 index 0000000..658f098 --- /dev/null +++ b/core/java/com/android/internal/view/InputBindResult.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.internal.view; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Bundle of information returned by input method manager about a successful + * binding to an input method. + */ +public final class InputBindResult implements Parcelable { + static final String TAG = "InputBindResult"; + + /** + * The input method service. + */ + public final IInputMethodSession method; + + /** + * The ID for this input method, as found in InputMethodInfo; null if + * no input method will be bound. + */ + public final String id; + + /** + * Sequence number of this binding. + */ + public final int sequence; + + public InputBindResult(IInputMethodSession _method, String _id, int _sequence) { + method = _method; + id = _id; + sequence = _sequence; + } + + InputBindResult(Parcel source) { + method = IInputMethodSession.Stub.asInterface(source.readStrongBinder()); + id = source.readString(); + sequence = source.readInt(); + } + + @Override + public String toString() { + return "InputBindResult{" + method + " " + id + + " #" + sequence + "}"; + } + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongInterface(method); + dest.writeString(id); + dest.writeInt(sequence); + } + + /** + * Used to make this class parcelable. + */ + public static final Parcelable.Creator<InputBindResult> CREATOR = new Parcelable.Creator<InputBindResult>() { + public InputBindResult createFromParcel(Parcel source) { + return new InputBindResult(source); + } + + public InputBindResult[] newArray(int size) { + return new InputBindResult[size]; + } + }; + + public int describeContents() { + return 0; + } +} diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java new file mode 100644 index 0000000..5bfcfe9 --- /dev/null +++ b/core/java/com/android/internal/view/InputConnectionWrapper.java @@ -0,0 +1,306 @@ +package com.android.internal.view; + +import com.android.internal.view.IInputContext; + +import android.os.Bundle; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.Log; +import android.view.KeyEvent; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; + +public class InputConnectionWrapper implements InputConnection { + private static final int MAX_WAIT_TIME_MILLIS = 2000; + private final IInputContext mIInputContext; + + static class InputContextCallback extends IInputContextCallback.Stub { + private static final String TAG = "InputConnectionWrapper.ICC"; + public int mSeq; + public boolean mHaveValue; + public CharSequence mTextBeforeCursor; + public CharSequence mTextAfterCursor; + public ExtractedText mExtractedText; + public int mCursorCapsMode; + + // A 'pool' of one InputContextCallback. Each ICW request will attempt to gain + // exclusive access to this object. + private static InputContextCallback sInstance = new InputContextCallback(); + private static int sSequenceNumber = 1; + + /** + * Returns an InputContextCallback object that is guaranteed not to be in use by + * any other thread. The returned object's 'have value' flag is cleared and its expected + * sequence number is set to a new integer. We use a sequence number so that replies that + * occur after a timeout has expired are not interpreted as replies to a later request. + */ + private static InputContextCallback getInstance() { + synchronized (InputContextCallback.class) { + // Return sInstance if it's non-null, otherwise construct a new callback + InputContextCallback callback; + if (sInstance != null) { + callback = sInstance; + sInstance = null; + + // Reset the callback + callback.mHaveValue = false; + } else { + callback = new InputContextCallback(); + } + + // Set the sequence number + callback.mSeq = sSequenceNumber++; + return callback; + } + } + + /** + * Makes the given InputContextCallback available for use in the future. + */ + private void dispose() { + synchronized (InputContextCallback.class) { + // If sInstance is non-null, just let this object be garbage-collected + if (sInstance == null) { + // Allow any objects being held to be gc'ed + mTextAfterCursor = null; + mTextBeforeCursor = null; + mExtractedText = null; + sInstance = this; + } + } + } + + public void setTextBeforeCursor(CharSequence textBeforeCursor, int seq) { + synchronized (this) { + if (seq == mSeq) { + mTextBeforeCursor = textBeforeCursor; + mHaveValue = true; + notifyAll(); + } else { + Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq + + ") in setTextBeforeCursor, ignoring."); + } + } + } + + public void setTextAfterCursor(CharSequence textAfterCursor, int seq) { + synchronized (this) { + if (seq == mSeq) { + mTextAfterCursor = textAfterCursor; + mHaveValue = true; + notifyAll(); + } else { + Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq + + ") in setTextAfterCursor, ignoring."); + } + } + } + + public void setCursorCapsMode(int capsMode, int seq) { + synchronized (this) { + if (seq == mSeq) { + mCursorCapsMode = capsMode; + mHaveValue = true; + notifyAll(); + } else { + Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq + + ") in setCursorCapsMode, ignoring."); + } + } + } + + public void setExtractedText(ExtractedText extractedText, int seq) { + synchronized (this) { + if (seq == mSeq) { + mExtractedText = extractedText; + mHaveValue = true; + notifyAll(); + } else { + Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq + + ") in setExtractedText, ignoring."); + } + } + } + + /** + * Waits for a result for up to {@link #MAX_WAIT_TIME_MILLIS} milliseconds. + * + * <p>The caller must be synchronized on this callback object. + */ + void waitForResultLocked() { + long startTime = SystemClock.uptimeMillis(); + long endTime = startTime + MAX_WAIT_TIME_MILLIS; + + while (!mHaveValue) { + long remainingTime = endTime - SystemClock.uptimeMillis(); + if (remainingTime <= 0) { + Log.w(TAG, "Timed out waiting on IInputContextCallback"); + return; + } + try { + wait(remainingTime); + } catch (InterruptedException e) { + } + } + } + } + + public InputConnectionWrapper(IInputContext inputContext) { + mIInputContext = inputContext; + } + + public CharSequence getTextAfterCursor(int length) { + CharSequence value = null; + try { + InputContextCallback callback = InputContextCallback.getInstance(); + mIInputContext.getTextAfterCursor(length, callback.mSeq, callback); + synchronized (callback) { + callback.waitForResultLocked(); + if (callback.mHaveValue) { + value = callback.mTextAfterCursor; + } + } + callback.dispose(); + } catch (RemoteException e) { + return null; + } + return value; + } + + public CharSequence getTextBeforeCursor(int length) { + CharSequence value = null; + try { + InputContextCallback callback = InputContextCallback.getInstance(); + mIInputContext.getTextBeforeCursor(length, callback.mSeq, callback); + synchronized (callback) { + callback.waitForResultLocked(); + if (callback.mHaveValue) { + value = callback.mTextBeforeCursor; + } + } + callback.dispose(); + } catch (RemoteException e) { + return null; + } + return value; + } + + public int getCursorCapsMode(int reqModes) { + int value = 0; + try { + InputContextCallback callback = InputContextCallback.getInstance(); + mIInputContext.getCursorCapsMode(reqModes, callback.mSeq, callback); + synchronized (callback) { + callback.waitForResultLocked(); + if (callback.mHaveValue) { + value = callback.mCursorCapsMode; + } + } + callback.dispose(); + } catch (RemoteException e) { + return 0; + } + return value; + } + + public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { + ExtractedText value = null; + try { + InputContextCallback callback = InputContextCallback.getInstance(); + mIInputContext.getExtractedText(request, flags, callback.mSeq, callback); + synchronized (callback) { + callback.waitForResultLocked(); + if (callback.mHaveValue) { + value = callback.mExtractedText; + } + } + callback.dispose(); + } catch (RemoteException e) { + return null; + } + return value; + } + + public boolean commitText(CharSequence text, int newCursorPosition) { + try { + mIInputContext.commitText(text, newCursorPosition); + return true; + } catch (RemoteException e) { + return false; + } + } + + public boolean commitCompletion(CompletionInfo text) { + try { + mIInputContext.commitCompletion(text); + return true; + } catch (RemoteException e) { + return false; + } + } + + public boolean setComposingText(CharSequence text, int newCursorPosition) { + try { + mIInputContext.setComposingText(text, newCursorPosition); + return true; + } catch (RemoteException e) { + return false; + } + } + + public boolean sendKeyEvent(KeyEvent event) { + try { + mIInputContext.sendKeyEvent(event); + return true; + } catch (RemoteException e) { + return false; + } + } + + public boolean clearMetaKeyStates(int states) { + try { + mIInputContext.clearMetaKeyStates(states); + return true; + } catch (RemoteException e) { + return false; + } + } + + public boolean deleteSurroundingText(int leftLength, int rightLength) { + try { + mIInputContext.deleteSurroundingText(leftLength, rightLength); + return true; + } catch (RemoteException e) { + return false; + } + } + + public boolean hideStatusIcon() { + try { + mIInputContext.showStatusIcon(null, 0); + return true; + } catch (RemoteException e) { + return false; + } + } + + public boolean showStatusIcon(String packageName, int resId) { + try { + mIInputContext.showStatusIcon(packageName, resId); + return true; + } catch (RemoteException e) { + return false; + } + } + + public boolean performPrivateCommand(String action, Bundle data) { + try { + mIInputContext.performPrivateCommand(action, data); + return true; + } catch (RemoteException e) { + return false; + } + } +} diff --git a/core/java/com/android/internal/view/menu/IconMenuItemView.java b/core/java/com/android/internal/view/menu/IconMenuItemView.java index 156e20a..3b11a64 100644 --- a/core/java/com/android/internal/view/menu/IconMenuItemView.java +++ b/core/java/com/android/internal/view/menu/IconMenuItemView.java @@ -26,6 +26,7 @@ import android.util.AttributeSet; import android.view.Gravity; import android.view.SoundEffectConstants; import android.view.View; +import android.view.ViewDebug; import android.widget.TextView; /** @@ -176,6 +177,9 @@ public final class IconMenuItemView extends TextView implements MenuView.ItemVie // Set the compound drawables setCompoundDrawables(null, icon, null, null); + + // When there is an icon, make sure the text is at the bottom + setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); /* * Request a layout to reposition the icon. The positioning of icon @@ -185,13 +189,17 @@ public final class IconMenuItemView extends TextView implements MenuView.ItemVie requestLayout(); } else { setCompoundDrawables(null, null, null, null); + + // When there is no icon, make sure the text is centered vertically + setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL); } } public void setItemInvoker(ItemInvoker itemInvoker) { mItemInvoker = itemInvoker; } - + + @ViewDebug.CapturedViewProperty(retrieveReturn = true) public MenuItemImpl getItemData() { return mItemData; } diff --git a/core/java/com/android/internal/view/menu/IconMenuView.java b/core/java/com/android/internal/view/menu/IconMenuView.java index fc76a04..781c608 100644 --- a/core/java/com/android/internal/view/menu/IconMenuView.java +++ b/core/java/com/android/internal/view/menu/IconMenuView.java @@ -26,6 +26,7 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; +import android.text.Layout; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.View; @@ -57,6 +58,8 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi private int mRowHeight; /** Maximum number of rows to be shown */ private int mMaxRows; + /** Maximum number of items to show in the icon menu. */ + private int mMaxItems; /** Maximum number of items per row */ private int mMaxItemsPerRow; /** Actual number of items (the 'More' view does not count as an item) shown */ @@ -76,15 +79,15 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi /** Set of vertical divider positions where the vertical divider will be drawn */ private ArrayList<Rect> mVerticalDividerRects; + /** Icon for the 'More' button */ + private Drawable mMoreIcon; + /** Item view for the 'More' button */ private IconMenuItemView mMoreItemView; /** Background of each item (should contain the selected and focused states) */ private Drawable mItemBackground; - /** Icon for the 'More' button */ - private Drawable mMoreIcon; - /** Default animations for this menu */ private int mAnimations; @@ -108,6 +111,20 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi * we broadcasted to children. */ private boolean mLastChildrenCaptionMode; + + /** + * The layout to use for menu items. Each index is the row number (0 is the + * top-most). Each value contains the number of items in that row. + * <p> + * The length of this array should not be used to get the number of rows in + * the current layout, instead use {@link #mLayoutNumRows}. + */ + private int[] mLayout; + + /** + * The number of rows in the current layout. + */ + private int mLayoutNumRows; /** * Instantiates the IconMenuView that is linked with the provided MenuBuilder. @@ -119,6 +136,7 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.IconMenuView, 0, 0); mRowHeight = a.getDimensionPixelSize(com.android.internal.R.styleable.IconMenuView_rowHeight, 64); mMaxRows = a.getInt(com.android.internal.R.styleable.IconMenuView_maxRows, 2); + mMaxItems = a.getInt(com.android.internal.R.styleable.IconMenuView_maxItems, 6); mMaxItemsPerRow = a.getInt(com.android.internal.R.styleable.IconMenuView_maxItemsPerRow, 3); mMoreIcon = a.getDrawable(com.android.internal.R.styleable.IconMenuView_moreIcon); a.recycle(); @@ -144,6 +162,8 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi if (mVerticalDividerWidth == -1) mVerticalDividerWidth = 1; } + mLayout = new int[mMaxRows]; + // This view will be drawing the dividers setWillNotDraw(false); @@ -152,13 +172,106 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi // This is so our children can still be arrow-key focused setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); } - + + /** + * Figures out the layout for the menu items. + * + * @param width The available width for the icon menu. + */ + private void layoutItems(int width) { + int numItems = getChildCount(); + + // Start with the least possible number of rows + int curNumRows = + Math.min((int) Math.ceil(numItems / (float) mMaxItemsPerRow), mMaxRows); + + /* + * Increase the number of rows until we find a configuration that fits + * all of the items' titles. Worst case, we use mMaxRows. + */ + for (; curNumRows <= mMaxRows; curNumRows++) { + layoutItemsUsingGravity(curNumRows, numItems); + + if (curNumRows >= numItems) { + // Can't have more rows than items + break; + } + + if (doItemsFit()) { + // All the items fit, so this is a good configuration + break; + } + } + } + /** - * Calculates the minimum number of rows needed to the items to be shown. - * @return the minimum number of rows + * Figures out the layout for the menu items by equally distributing, and + * adding any excess items equally to lower rows. + * + * @param numRows The total number of rows for the menu view + * @param numItems The total number of items (across all rows) contained in + * the menu view + * @return int[] Where the value of index i contains the number of items for row i */ - private int calculateNumberOfRows() { - return Math.min((int) Math.ceil(getChildCount() / (double) mMaxItemsPerRow), mMaxRows); + private void layoutItemsUsingGravity(int numRows, int numItems) { + int numBaseItemsPerRow = numItems / numRows; + int numLeftoverItems = numItems % numRows; + /** + * The bottom rows will each get a leftover item. Rows (indexed at 0) + * that are >= this get a leftover item. Note: if there are 0 leftover + * items, no rows will get them since this value will be greater than + * the last row. + */ + int rowsThatGetALeftoverItem = numRows - numLeftoverItems; + + int[] layout = mLayout; + for (int i = 0; i < numRows; i++) { + layout[i] = numBaseItemsPerRow; + + // Fill the bottom rows with a leftover item each + if (i >= rowsThatGetALeftoverItem) { + layout[i]++; + } + } + + mLayoutNumRows = numRows; + } + + /** + * Checks whether each item's title is fully visible using the current + * layout. + * + * @return True if the items fit (each item's text is fully visible), false + * otherwise. + */ + private boolean doItemsFit() { + int itemPos = 0; + + int[] layout = mLayout; + int numRows = mLayoutNumRows; + for (int row = 0; row < numRows; row++) { + int numItemsOnRow = layout[row]; + + /* + * If there is only one item on this row, increasing the + * number of rows won't help. + */ + if (numItemsOnRow == 1) { + itemPos++; + continue; + } + + for (int itemsOnRowCounter = numItemsOnRow; itemsOnRowCounter > 0; + itemsOnRowCounter--) { + View child = getChildAt(itemPos++); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.maxNumItemsOnRow < numItemsOnRow) { + return false; + } + } + } + + return true; } /** @@ -166,7 +279,7 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi * @param itemView The item's view to add */ private void addItemView(IconMenuItemView itemView) { - ViewGroup.LayoutParams lp = itemView.getLayoutParams(); + LayoutParams lp = (LayoutParams) itemView.getLayoutParams(); if (lp == null) { // Default layout parameters @@ -182,6 +295,9 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi // This class is the invoker for all its item views itemView.setItemInvoker(this); + // Set the desired width of item + lp.desiredWidth = (int) Layout.getDesiredWidth(itemView.getText(), itemView.getPaint()); + addView(itemView, lp); } @@ -228,7 +344,7 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi final ArrayList<MenuItemImpl> itemsToShow = mMenu.getVisibleItems(); final int numItems = itemsToShow.size(); - final int numItemsThatCanFit = mMaxItemsPerRow * mMaxRows; + final int numItemsThatCanFit = mMaxItems; // Minimum of the num that can fit and the num that we have final int minFitMinus1AndNumItems = Math.min(numItemsThatCanFit - 1, numItems); @@ -263,30 +379,6 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi } /** - * Calculates the number of items that should go on each row of this menu view. - * @param numRows the total number of rows for the menu view - * @param numItems the total number of items (across all rows) contained in the menu view - * @return int[] where index i contains the number of items for row i - */ - private int[] calculateNumberOfItemsPerRow(final int numRows, final int numItems) { - // TODO: get from theme? or write a best-fit algorithm? either way, this hard-coding needs - // to be dropped (946635). Right now, this is according to UI spec. - final int numItemsForRow[] = new int[numRows]; - if (numRows == 2) { - if (numItems <= 5) { - numItemsForRow[0] = 2; - numItemsForRow[1] = numItems - 2; - } else { - numItemsForRow[0] = numItemsForRow[1] = mMaxItemsPerRow; - } - } else if (numRows == 1) { - numItemsForRow[0] = numItems; - } - - return numItemsForRow; - } - - /** * The positioning algorithm that gets called from onMeasure. It * just computes positions for each child, and then stores them in the child's layout params. * @param menuWidth The width of this menu to assume for positioning @@ -298,10 +390,9 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi if (mVerticalDivider != null) mVerticalDividerRects.clear(); // Get the minimum number of rows needed - final int numRows = calculateNumberOfRows(); + final int numRows = mLayoutNumRows; final int numRowsMinus1 = numRows - 1; - final int numItems = getChildCount(); - final int numItemsForRow[] = calculateNumberOfItemsPerRow(numRows, numItems); + final int numItemsForRow[] = mLayout; // The item position across all rows int itemPos = 0; @@ -382,13 +473,17 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi updateChildren(false); } + int measuredWidth = resolveSize(Integer.MAX_VALUE, widthMeasureSpec); + calculateItemFittingMetadata(measuredWidth); + layoutItems(measuredWidth); + // Get the desired height of the icon menu view (last row of items does // not have a divider below) - final int desiredHeight = (mRowHeight + mHorizontalDividerHeight) * calculateNumberOfRows() - - mHorizontalDividerHeight; + final int desiredHeight = (mRowHeight + mHorizontalDividerHeight) * + mLayoutNumRows - mHorizontalDividerHeight; // Maximum possible width and desired height - setMeasuredDimension(resolveSize(Integer.MAX_VALUE, widthMeasureSpec), + setMeasuredDimension(measuredWidth, resolveSize(desiredHeight, heightMeasureSpec)); // Position the children @@ -472,6 +567,32 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi return mAnimations; } + /** + * Returns the number of items per row. + * <p> + * This should only be used for testing. + * + * @return The length of the array is the number of rows. A value at a + * position is the number of items in that row. + * @hide + */ + public int[] getLayout() { + return mLayout; + } + + /** + * Returns the number of rows in the layout. + * <p> + * This should only be used for testing. + * + * @return The length of the array is the number of rows. A value at a + * position is the number of items in that row. + * @hide + */ + public int getLayoutNumRows() { + return mLayoutNumRows; + } + @Override public boolean dispatchKeyEvent(KeyEvent event) { @@ -499,6 +620,13 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi } @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + requestFocus(); + } + + @Override protected void onDetachedFromWindow() { setCycleShortcutCaptionMode(false); super.onDetachedFromWindow(); @@ -580,6 +708,31 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi ((IconMenuItemView) getChildAt(i)).setCaptionMode(shortcut); } } + + /** + * For each item, calculates the most dense row that fully shows the item's + * title. + * + * @param width The available width of the icon menu. + */ + private void calculateItemFittingMetadata(int width) { + int maxNumItemsPerRow = mMaxItemsPerRow; + int numItems = getChildCount(); + for (int i = 0; i < numItems; i++) { + LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); + // Start with 1, since that case does not get covered in the loop below + lp.maxNumItemsOnRow = 1; + for (int curNumItemsPerRow = maxNumItemsPerRow; curNumItemsPerRow > 0; + curNumItemsPerRow--) { + // Check whether this item can fit into a row containing curNumItemsPerRow + if (lp.desiredWidth < width / curNumItemsPerRow) { + // It can, mark this value as the most dense row it can fit into + lp.maxNumItemsOnRow = curNumItemsPerRow; + break; + } + } + } + } @Override protected Parcelable onSaveInstanceState() { @@ -655,6 +808,8 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi public static class LayoutParams extends ViewGroup.MarginLayoutParams { int left, top, right, bottom; + int desiredWidth; + int maxNumItemsOnRow; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); diff --git a/core/java/com/android/internal/view/menu/MenuBuilder.java b/core/java/com/android/internal/view/menu/MenuBuilder.java index 835e4a9..2987602 100644 --- a/core/java/com/android/internal/view/menu/MenuBuilder.java +++ b/core/java/com/android/internal/view/menu/MenuBuilder.java @@ -432,8 +432,9 @@ public class MenuBuilder implements Menu { rintent.setComponent(new ComponentName( ri.activityInfo.applicationInfo.packageName, ri.activityInfo.name)); - final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm)); - item.setIntent(rintent); + final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm)) + .setIcon(ri.loadIcon(pm)) + .setIntent(rintent); if (outSpecificItems != null && ri.specificIndex >= 0) { outSpecificItems[ri.specificIndex] = item; } @@ -624,7 +625,8 @@ public class MenuBuilder implements Menu { return mItems.size(); } - public MenuItem get(int index) { + /** {@inheritDoc} */ + public MenuItem getItem(int index) { return mItems.get(index); } @@ -773,7 +775,8 @@ public class MenuBuilder implements Menu { (shortcutAlphaChar != 0) && (shortcutAlphaChar == possibleChars.meta[0] || shortcutAlphaChar == possibleChars.meta[2] - || (shortcutAlphaChar == '\b' && keyCode == KeyEvent.KEYCODE_DEL))) { + || (shortcutAlphaChar == '\b' && keyCode == KeyEvent.KEYCODE_DEL)) && + item.isEnabled()) { return item; } } else { @@ -781,7 +784,8 @@ public class MenuBuilder implements Menu { if (((metaState & (KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON)) == 0) && (shortcutNumericChar != 0) && (shortcutNumericChar == possibleChars.meta[0] - || shortcutNumericChar == possibleChars.meta[2])) { + || shortcutNumericChar == possibleChars.meta[2]) && + item.isEnabled()) { return item; } } @@ -829,13 +833,18 @@ public class MenuBuilder implements Menu { * sub menu is about to be shown, <var>allMenusAreClosing</var> * is false. */ - public final void close(boolean allMenusAreClosing) { + final void close(boolean allMenusAreClosing) { Callback callback = getCallback(); if (callback != null) { callback.onCloseMenu(this, allMenusAreClosing); } } + /** {@inheritDoc} */ + public void close() { + close(true); + } + /** * Called when an item is added or removed. * diff --git a/core/java/com/android/internal/view/menu/MenuDialogHelper.java b/core/java/com/android/internal/view/menu/MenuDialogHelper.java index 6dfc7a2..bc51cf3 100644 --- a/core/java/com/android/internal/view/menu/MenuDialogHelper.java +++ b/core/java/com/android/internal/view/menu/MenuDialogHelper.java @@ -72,10 +72,11 @@ public class MenuDialogHelper implements DialogInterface.OnKeyListener, DialogIn mDialog = builder.create(); WindowManager.LayoutParams lp = mDialog.getWindow().getAttributes(); - lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; + lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; if (windowToken != null) { lp.token = windowToken; } + lp.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; mDialog.show(); } diff --git a/core/java/com/android/internal/view/menu/MenuItemImpl.java b/core/java/com/android/internal/view/menu/MenuItemImpl.java index c89f2e9..43dba6f 100644 --- a/core/java/com/android/internal/view/menu/MenuItemImpl.java +++ b/core/java/com/android/internal/view/menu/MenuItemImpl.java @@ -24,6 +24,7 @@ import android.view.LayoutInflater; import android.view.MenuItem; import android.view.SubMenu; import android.view.View; +import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ContextMenu.ContextMenuInfo; @@ -183,6 +184,7 @@ public final class MenuItemImpl implements MenuItem { return mGroup; } + @ViewDebug.CapturedViewProperty public int getItemId() { return mId; } @@ -353,6 +355,7 @@ public final class MenuItemImpl implements MenuItem { subMenu.setHeaderTitle(getTitle()); } + @ViewDebug.CapturedViewProperty public CharSequence getTitle() { return mTitle; } diff --git a/core/java/com/android/internal/view/package.html b/core/java/com/android/internal/view/package.html new file mode 100644 index 0000000..783d0a1 --- /dev/null +++ b/core/java/com/android/internal/view/package.html @@ -0,0 +1,3 @@ +<body> +{@hide} +</body> diff --git a/core/java/com/android/internal/widget/DialogTitle.java b/core/java/com/android/internal/widget/DialogTitle.java new file mode 100644 index 0000000..2eef0b6 --- /dev/null +++ b/core/java/com/android/internal/widget/DialogTitle.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2008 Google Inc. + * + * 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.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.text.Layout; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.TextView; + +/** + * Used by dialogs to change the font size and number of lines to try to fit + * the text to the available space. + */ +public class DialogTitle extends TextView { + + public DialogTitle(Context context, AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + } + + public DialogTitle(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DialogTitle(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + final Layout layout = getLayout(); + if (layout != null) { + final int lineCount = layout.getLineCount(); + if (lineCount > 0) { + final int ellipsisCount = layout.getEllipsisCount(lineCount - 1); + if (ellipsisCount > 0) { + setSingleLine(false); + + TypedArray a = mContext.obtainStyledAttributes( + android.R.style.TextAppearance_Medium, + android.R.styleable.TextAppearance); + final int textSize = a.getDimensionPixelSize( + android.R.styleable.TextAppearance_textSize, 20); + + setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize); + setMaxLines(2); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + } + } + +}
\ No newline at end of file diff --git a/core/java/com/android/internal/widget/EditableInputConnection.java b/core/java/com/android/internal/widget/EditableInputConnection.java new file mode 100644 index 0000000..efe15f3 --- /dev/null +++ b/core/java/com/android/internal/widget/EditableInputConnection.java @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2007-2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.internal.widget; + +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Handler; +import android.text.Editable; +import android.text.Selection; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.method.KeyListener; +import android.util.Log; +import android.util.LogPrinter; +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.widget.TextView; + +class ComposingText { +} + +public class EditableInputConnection extends BaseInputConnection { + private static final boolean DEBUG = false; + private static final String TAG = "EditableInputConnection"; + + public static final Object COMPOSING = new ComposingText(); + + private final TextView mTextView; + private final Handler mUiHandler; + + private Object[] mDefaultComposingSpans; + + public EditableInputConnection(TextView textview) { + super(textview); + mTextView = textview; + mUiHandler = textview.getHandler(); + } + + public boolean setComposingText(CharSequence text, int newCursorPosition) { + if (DEBUG) Log.v(TAG, "setComposingText " + text); + replaceText(text, newCursorPosition, true); + return true; + } + + public boolean commitText(CharSequence text, int newCursorPosition) { + if (DEBUG) Log.v(TAG, "commitText " + text); + replaceText(text, newCursorPosition, false); + return true; + } + + public boolean commitCompletion(CompletionInfo text) { + if (DEBUG) Log.v(TAG, "commitCompletion " + text); + mTextView.onCommitCompletion(text); + return true; + } + + public CharSequence getTextBeforeCursor(int length) { + final Editable content = getEditable(); + if (content == null) return null; + + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + if (a > b) { + int tmp = a; + a = b; + b = tmp; + } + + if (length > a) { + length = a; + } + + return content.subSequence(a - length, a); + } + + public CharSequence getTextAfterCursor(int length) { + final Editable content = getEditable(); + if (content == null) return null; + + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + if (a > b) { + int tmp = a; + a = b; + b = tmp; + } + + if (b + length > content.length()) { + length = content.length() - b; + } + + return content.subSequence(b, b + length); + } + + public int getCursorCapsMode(int reqModes) { + final Editable content = getEditable(); + if (content == null) return 0; + + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + if (a > b) { + int tmp = a; + a = b; + b = tmp; + } + + return TextUtils.getCapsMode(content, a, reqModes); + } + + public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { + if (mTextView != null) { + ExtractedText et = new ExtractedText(); + if (mTextView.extractText(request, et)) { + if ((flags&EXTRACTED_TEXT_MONITOR) != 0) { + mTextView.setExtracting(request); + } + return et; + } + } + return null; + } + + public boolean deleteSurroundingText(int leftLength, int rightLength) { + if (DEBUG) Log.v(TAG, "deleteSurroundingText " + leftLength + + " / " + rightLength); + final Editable content = getEditable(); + if (content == null) return false; + + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + if (a > b) { + int tmp = a; + a = b; + b = tmp; + } + + // ignore the composing text. + int ca = content.getSpanStart(COMPOSING); + int cb = content.getSpanEnd(COMPOSING); + if (cb < ca) { + int tmp = ca; + ca = cb; + cb = tmp; + } + if (ca != -1 && cb != -1) { + if (ca < a) a = ca; + if (cb > b) b = cb; + } + + int deleted = 0; + + if (leftLength > 0) { + int start = a - leftLength; + if (start < 0) start = 0; + content.delete(start, a); + deleted = a - start; + } + + if (rightLength > 0) { + b = b - deleted; + + int end = b + rightLength; + if (end > content.length()) end = content.length(); + + content.delete(b, end); + } + + return true; + } + + public boolean clearMetaKeyStates(int states) { + final Editable content = getEditable(); + if (content == null) return false; + KeyListener kl = mTextView.getKeyListener(); + if (kl != null) kl.clearMetaKeyState(mTextView, content, states); + return true; + } + + public boolean performPrivateCommand(String action, Bundle data) { + if (mTextView == null) return false; + mTextView.onPrivateIMECommand(action, data); + return true; + } + + private Editable getEditable() { + TextView tv = mTextView; + if (tv != null) { + return tv.getEditableText(); + } + return null; + } + + public static void setComposingSpans(Spannable text) { + final Object[] sps = text.getSpans(0, text.length(), Object.class); + if (sps != null) { + for (int i=sps.length-1; i>=0; i--) { + final Object o = sps[i]; + if (o == COMPOSING) { + text.removeSpan(o); + continue; + } + final int fl = text.getSpanFlags(o); + if ((fl&(Spanned.SPAN_COMPOSING|Spanned.SPAN_POINT_MARK_MASK)) + != (Spanned.SPAN_COMPOSING|Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)) { + text.setSpan(o, text.getSpanStart(o), text.getSpanEnd(o), + (fl&Spanned.SPAN_POINT_MARK_MASK) + | Spanned.SPAN_COMPOSING + | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + + text.setSpan(COMPOSING, 0, text.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); + } + + public static final void removeComposingSpans(Spannable text) { + text.removeSpan(COMPOSING); + Object[] sps = text.getSpans(0, text.length(), Object.class); + if (sps != null) { + for (int i=sps.length-1; i>=0; i--) { + Object o = sps[i]; + if ((text.getSpanFlags(o)&Spanned.SPAN_COMPOSING) != 0) { + text.removeSpan(o); + } + } + } + } + + private void replaceText(CharSequence text, int newCursorPosition, + boolean composing) { + final Editable content = getEditable(); + + // delete composing text set previously. + int a = content.getSpanStart(COMPOSING); + int b = content.getSpanEnd(COMPOSING); + + if (DEBUG) Log.v(TAG, "Composing span: " + a + " to " + b); + + if (b < a) { + int tmp = a; + a = b; + b = tmp; + } + + if (a != -1 && b != -1) { + removeComposingSpans(content); + } else { + a = Selection.getSelectionStart(content); + b = Selection.getSelectionEnd(content); + if (a >=0 && b>= 0 && a != b) { + if (b < a) { + int tmp = a; + a = b; + b = tmp; + } + } + } + + if (composing) { + Spannable sp = null; + if (!(text instanceof Spannable)) { + sp = new SpannableStringBuilder(text); + text = sp; + if (mDefaultComposingSpans == null) { + TypedArray ta = mTextView.getContext().getTheme() + .obtainStyledAttributes(new int[] { + com.android.internal.R.attr.candidatesTextStyleSpans + }); + CharSequence style = ta.getText(0); + ta.recycle(); + if (style != null && style instanceof Spanned) { + mDefaultComposingSpans = ((Spanned)style).getSpans( + 0, style.length(), Object.class); + } + } + if (mDefaultComposingSpans != null) { + for (int i = 0; i < mDefaultComposingSpans.length; ++i) { + sp.setSpan(mDefaultComposingSpans[i], 0, sp.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } else { + sp = (Spannable)text; + } + setComposingSpans(sp); + } + + // Adjust newCursorPosition to be relative the start of the text. + newCursorPosition += a; + + if (DEBUG) Log.v(TAG, "Replacing from " + a + " to " + b + " with \"" + + text + "\", composing=" + composing + + ", type=" + text.getClass().getCanonicalName()); + + if (DEBUG) { + LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG); + lp.println("Current text:"); + TextUtils.dumpSpans(content, lp, " "); + lp.println("Composing text:"); + TextUtils.dumpSpans(text, lp, " "); + } + + content.replace(a, b, text); + if (newCursorPosition < 0) newCursorPosition = 0; + if (newCursorPosition > content.length()) + newCursorPosition = content.length(); + Selection.setSelection(content, newCursorPosition); + + if (DEBUG) { + LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG); + lp.println("Final text:"); + TextUtils.dumpSpans(content, lp, " "); + } + } +} diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 1c75daa..ed1cd58 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -256,6 +256,20 @@ public class LockPatternUtils { } /** + * @return Whether tactile feedback for the pattern is enabled. + */ + public boolean isTactileFeedbackEnabled() { + return getBoolean(Settings.System.LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED); + } + + /** + * Set whether tactile feedback for the pattern is enabled. + */ + public void setTactileFeedbackEnabled(boolean enabled) { + setBoolean(Settings.System.LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED, enabled); + } + + /** * Store the lockout deadline, meaning the user can't attempt his/her unlock * pattern until the deadline has passed. Does not persist across reboots. * @param deadline The elapsed real time in millis in future. diff --git a/core/java/com/android/internal/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java index bf00eff..7f99ac8 100644 --- a/core/java/com/android/internal/widget/LockPatternView.java +++ b/core/java/com/android/internal/widget/LockPatternView.java @@ -32,6 +32,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.os.Debug; +import android.os.Vibrator; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; @@ -48,6 +49,9 @@ import java.util.List; * "correct" states. */ public class LockPatternView extends View { + // Vibrator pattern for creating a tactile bump + private static final long[] VIBE_PATTERN = {0, 1, 40, 41}; + private static final boolean PROFILE_DRAWING = false; private boolean mDrawingProfilingStarted = false; @@ -88,6 +92,7 @@ public class LockPatternView extends View { private DisplayMode mPatternDisplayMode = DisplayMode.Correct; private boolean mInputEnabled = true; private boolean mInStealthMode = false; + private boolean mTactileFeedbackEnabled = true; private boolean mPatternInProgress = false; private float mDiameterFactor = 0.5f; @@ -112,6 +117,8 @@ public class LockPatternView extends View { private int mBitmapHeight; + private Vibrator vibe; // Vibrator for creating tactile feedback + /** * Represents a cell in the 3 X 3 matrix of the unlock pattern view. */ @@ -219,6 +226,7 @@ public class LockPatternView extends View { public LockPatternView(Context context, AttributeSet attrs) { super(context, attrs); + vibe = new Vibrator(); setClickable(true); @@ -257,6 +265,13 @@ public class LockPatternView extends View { } /** + * @return Whether the view has tactile feedback enabled. + */ + public boolean isTactileFeedbackEnabled() { + return mTactileFeedbackEnabled; + } + + /** * Set whether the view is in stealth mode. If true, there will be no * visible feedback as the user enters the pattern. * @@ -267,6 +282,16 @@ public class LockPatternView extends View { } /** + * Set whether the view will use tactile feedback. If true, there will be + * tactile feedback as the user enters the pattern. + * + * @param tactileFeedbackEnabled Whether tactile feedback is enabled + */ + public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) { + mTactileFeedbackEnabled = tactileFeedbackEnabled; + } + + /** * Set the call back for pattern detection. * @param onPatternListener The call back. */ @@ -420,6 +445,9 @@ public class LockPatternView extends View { addCellToPattern(fillInGapCell); } addCellToPattern(cell); + if (mTactileFeedbackEnabled){ + vibe.vibrate(VIBE_PATTERN, -1); // Generate tactile feedback + } return cell; } return null; @@ -900,7 +928,7 @@ public class LockPatternView extends View { return new SavedState(superState, LockPatternUtils.patternToString(mPattern), mPatternDisplayMode.ordinal(), - mInputEnabled, mInStealthMode); + mInputEnabled, mInStealthMode, mTactileFeedbackEnabled); } @Override @@ -913,6 +941,7 @@ public class LockPatternView extends View { mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()]; mInputEnabled = ss.isInputEnabled(); mInStealthMode = ss.isInStealthMode(); + mTactileFeedbackEnabled = ss.isTactileFeedbackEnabled(); } /** @@ -924,17 +953,19 @@ public class LockPatternView extends View { private final int mDisplayMode; private final boolean mInputEnabled; private final boolean mInStealthMode; + private final boolean mTactileFeedbackEnabled; /** * Constructor called from {@link LockPatternView#onSaveInstanceState()} */ private SavedState(Parcelable superState, String serializedPattern, int displayMode, - boolean inputEnabled, boolean inStealthMode) { + boolean inputEnabled, boolean inStealthMode, boolean tactileFeedbackEnabled) { super(superState); mSerializedPattern = serializedPattern; mDisplayMode = displayMode; mInputEnabled = inputEnabled; mInStealthMode = inStealthMode; + mTactileFeedbackEnabled = tactileFeedbackEnabled; } /** @@ -946,6 +977,7 @@ public class LockPatternView extends View { mDisplayMode = in.readInt(); mInputEnabled = (Boolean) in.readValue(null); mInStealthMode = (Boolean) in.readValue(null); + mTactileFeedbackEnabled = (Boolean) in.readValue(null); } public String getSerializedPattern() { @@ -964,6 +996,10 @@ public class LockPatternView extends View { return mInStealthMode; } + public boolean isTactileFeedbackEnabled(){ + return mTactileFeedbackEnabled; + } + @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); @@ -971,6 +1007,7 @@ public class LockPatternView extends View { dest.writeInt(mDisplayMode); dest.writeValue(mInputEnabled); dest.writeValue(mInStealthMode); + dest.writeValue(mTactileFeedbackEnabled); } public static final Parcelable.Creator<SavedState> CREATOR = diff --git a/core/java/com/android/internal/widget/NumberPicker.java b/core/java/com/android/internal/widget/NumberPicker.java index 5a7ddcb..5590f1a 100644 --- a/core/java/com/android/internal/widget/NumberPicker.java +++ b/core/java/com/android/internal/widget/NumberPicker.java @@ -19,6 +19,7 @@ package com.android.internal.widget; import android.content.Context; import android.os.Handler; import android.text.InputFilter; +import android.text.InputType; import android.text.Spanned; import android.text.method.NumberKeyListener; import android.util.AttributeSet; @@ -359,6 +360,12 @@ public class NumberPicker extends LinearLayout implements OnClickListener, private class NumberRangeKeyListener extends NumberKeyListener { + // XXX This doesn't allow for range limits when controlled by a + // soft input method! + public int getInputType() { + return InputType.TYPE_CLASS_NUMBER; + } + @Override protected char[] getAcceptedChars() { return DIGIT_CHARACTERS; @@ -421,4 +428,10 @@ public class NumberPicker extends LinearLayout implements OnClickListener, return mStart; } -} + /** + * @return the current value. + */ + public int getCurrent() { + return mCurrent; + } +}
\ No newline at end of file diff --git a/core/java/com/android/internal/widget/SlidingDrawer.java b/core/java/com/android/internal/widget/SlidingDrawer.java index 90a548a..a4045d5 100644 --- a/core/java/com/android/internal/widget/SlidingDrawer.java +++ b/core/java/com/android/internal/widget/SlidingDrawer.java @@ -74,6 +74,7 @@ import com.android.internal.R; * @attr ref com.android.internal.R.styleable#SlidingDrawer_topOffset * @attr ref com.android.internal.R.styleable#SlidingDrawer_bottomOffset * @attr ref com.android.internal.R.styleable#SlidingDrawer_orientation + * @attr ref com.android.internal.R.styleable#SlidingDrawer_allowSingleTap * @attr ref com.android.internal.R.styleable#SlidingDrawer_animateOnClick */ public class SlidingDrawer extends ViewGroup { @@ -124,6 +125,7 @@ public class SlidingDrawer extends ViewGroup { private long mCurrentAnimationTime; private int mTouchDelta; private boolean mAnimating; + private boolean mAllowSingleTap; private boolean mAnimateOnClick; /** @@ -186,6 +188,7 @@ public class SlidingDrawer extends ViewGroup { mVertical = orientation == ORIENTATION_VERTICAL; mBottomOffset = (int) a.getDimension(R.styleable.SlidingDrawer_bottomOffset, 0.0f); mTopOffset = (int) a.getDimension(R.styleable.SlidingDrawer_topOffset, 0.0f); + mAllowSingleTap = a.getBoolean(R.styleable.SlidingDrawer_allowSingleTap, true); mAnimateOnClick = a.getBoolean(R.styleable.SlidingDrawer_animateOnClick, true); int handleId = a.getResourceId(R.styleable.SlidingDrawer_handle, 0); @@ -241,11 +244,11 @@ public class SlidingDrawer extends ViewGroup { measureChild(handle, widthMeasureSpec, heightMeasureSpec); if (mVertical) { - int height = heightSpecSize - handle.getMeasuredHeight(); + int height = heightSpecSize - handle.getMeasuredHeight() - mTopOffset; mContent.measure(MeasureSpec.makeMeasureSpec(widthSpecSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); } else { - int width = widthSpecSize - handle.getMeasuredWidth(); + int width = widthSpecSize - handle.getMeasuredWidth() - mTopOffset; mContent.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSpecSize, MeasureSpec.EXACTLY)); } @@ -269,6 +272,12 @@ public class SlidingDrawer extends ViewGroup { } else { canvas.drawBitmap(cache, handle.getRight(), 0, null); } + } else { + canvas.save(); + canvas.translate(isVertical ? 0 : handle.getLeft() - mTopOffset, + isVertical ? handle.getTop() - mTopOffset : 0); + drawChild(canvas, mContent, drawingTime); + canvas.restore(); } } else if (mExpanded) { drawChild(canvas, mContent, drawingTime); @@ -415,12 +424,14 @@ public class SlidingDrawer extends ViewGroup { (!mExpanded && left > mBottomOffset + mRight - mLeft - mHandleWidth - TAP_THRESHOLD)) { - playSoundEffect(SoundEffectConstants.CLICK); + if (mAllowSingleTap) { + playSoundEffect(SoundEffectConstants.CLICK); - if (mExpanded) { - animateClose(vertical ? top : left); - } else { - animateOpen(vertical ? top : left); + if (mExpanded) { + animateClose(vertical ? top : left); + } else { + animateOpen(vertical ? top : left); + } } } else { @@ -889,6 +900,9 @@ public class SlidingDrawer extends ViewGroup { if (mLocked) { return; } + // mAllowSingleTap isn't relevant here; you're *always* + // allowed to open/close the drawer by clicking with the + // trackball. if (mAnimateOnClick) { animateToggle(); diff --git a/core/java/com/google/android/mms/pdu/PduComposer.java b/core/java/com/google/android/mms/pdu/PduComposer.java index acece47..094e992 100644 --- a/core/java/com/google/android/mms/pdu/PduComposer.java +++ b/core/java/com/google/android/mms/pdu/PduComposer.java @@ -24,7 +24,6 @@ import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.HashMap; @@ -461,7 +460,7 @@ public class PduComposer { int version = mPduHeader.getOctet(field); if (0 == version) { - appendShortInteger(PduHeaders.MMS_VERSION_1_3); + appendShortInteger(PduHeaders.CURRENT_MMS_VERSION); } else { appendShortInteger(version); } @@ -952,7 +951,7 @@ public class PduComposer { appendQuotedString("<" + new String(contentId) + ">"); } } - + // content-location byte[] contentLocation = part.getContentLocation(); if (null != contentLocation) { diff --git a/core/java/com/google/android/mms/pdu/PduHeaders.java b/core/java/com/google/android/mms/pdu/PduHeaders.java index 3769349..4313815 100644 --- a/core/java/com/google/android/mms/pdu/PduHeaders.java +++ b/core/java/com/google/android/mms/pdu/PduHeaders.java @@ -150,13 +150,14 @@ public class PduHeaders { /** * X-Mms-MMS-Version field types. */ - // Current version is 1.3. public static final int MMS_VERSION_1_3 = ((1 << 4) | 3); - public static final int MMS_VERSION_1_2 = ((1 << 4) | 2); public static final int MMS_VERSION_1_1 = ((1 << 4) | 1); public static final int MMS_VERSION_1_0 = ((1 << 4) | 0); + // Current version is 1.2. + public static final int CURRENT_MMS_VERSION = MMS_VERSION_1_2; + /** * From field type components. */ @@ -475,7 +476,7 @@ public class PduHeaders { break; case MMS_VERSION: if ((value < MMS_VERSION_1_0)|| (value > MMS_VERSION_1_3)) { - value = MMS_VERSION_1_3; //1.3 is the default version. + value = CURRENT_MMS_VERSION; // Current version is the default value. } break; case MESSAGE_TYPE: diff --git a/core/java/com/google/android/mms/pdu/PduPersister.java b/core/java/com/google/android/mms/pdu/PduPersister.java index 4089c58..d2a41f1 100644 --- a/core/java/com/google/android/mms/pdu/PduPersister.java +++ b/core/java/com/google/android/mms/pdu/PduPersister.java @@ -729,36 +729,13 @@ public class PduPersister { } is = mContentResolver.openInputStream(dataUri); - boolean fakeRawAmr = contentType.equals("audio/amr"); - if (LOCAL_LOGV) { Log.v(TAG, "Saving data to: " + uri); } byte[] buffer = new byte[256]; for (int len = 0; (len = is.read(buffer)) != -1; ) { - if (fakeRawAmr && len > 32) { - // This is a Gross Hack. We can only record audio to amr format in a 3gpp container. - // Millions of handsets out there only support what is essentially raw AMR. - // We work around this issue by extracting the AMR data out of the 3gpp container - // (in a really stupid and non-portable way), prepending a little header, and then - // using that as the attachment. - // This also requires some cooperation from the SoundRecorder, which ends up saving - // a "recording.amr" file with mime type audio/amr, even though it's a 3gpp file. - if (buffer[4] == 0x66 && // f - buffer[5] == 0x74 && // t - buffer[6] == 0x79 && // y - buffer[7] == 0x70) { // p - byte [] amrHeader = new byte [] { 0x23, 0x21, 0x41, 0x4d, 0x52, 0x0a }; - os.write(amrHeader); - os.write(buffer, 32, len - 32); - } else { - os.write(buffer, 0, len); - } - fakeRawAmr = false; - } else { - os.write(buffer, 0, len); - } + os.write(buffer, 0, len); } } else { if (LOCAL_LOGV) { diff --git a/core/java/com/google/android/mms/pdu/SendReq.java b/core/java/com/google/android/mms/pdu/SendReq.java index e2f1101..9081b0c 100644 --- a/core/java/com/google/android/mms/pdu/SendReq.java +++ b/core/java/com/google/android/mms/pdu/SendReq.java @@ -29,7 +29,7 @@ public class SendReq extends MultimediaMessagePdu { try { setMessageType(PduHeaders.MESSAGE_TYPE_SEND_REQ); - setMmsVersion(PduHeaders.MMS_VERSION_1_3); + setMmsVersion(PduHeaders.CURRENT_MMS_VERSION); // FIXME: Content-type must be decided according to whether // SMIL part present. setContentType("application/vnd.wap.multipart.related".getBytes()); diff --git a/core/java/com/google/android/net/ParentalControl.java b/core/java/com/google/android/net/ParentalControl.java index 368b885..71a3958 100644 --- a/core/java/com/google/android/net/ParentalControl.java +++ b/core/java/com/google/android/net/ParentalControl.java @@ -23,7 +23,13 @@ import android.os.ServiceManager; import android.util.Log; public class ParentalControl { - + /** + * Strings to identify your app. To enable parental control checking for + * new apps, please add it here, and configure GServices accordingly. + */ + public static final String VENDING = "vending"; + public static final String YOUTUBE = "youtube"; + /** * This interface is supplied to getParentalControlState and is callback upon with * the state of parental control. @@ -36,28 +42,29 @@ public class ParentalControl { */ void onResult(ParentalControlState state); } - + private static class RemoteCallback extends IParentalControlCallback.Stub { private Callback mCallback; - + public RemoteCallback(Callback callback) { mCallback = callback; } - + public void onResult(ParentalControlState state) { if (mCallback != null) { mCallback.onResult(state); } } }; - - public static void getParentalControlState(Callback callback) { + + public static void getParentalControlState(Callback callback, + String requestingApp) { ICheckinService service = ICheckinService.Stub.asInterface(ServiceManager.getService("checkin")); - + RemoteCallback remoteCallback = new RemoteCallback(callback); try { - service.getParentalControlState(remoteCallback); + service.getParentalControlState(remoteCallback, requestingApp); } catch (RemoteException e) { // This should never happen. Log.e("ParentalControl", "Failed to talk to the checkin service."); diff --git a/core/java/com/google/android/util/GoogleWebContentHelper.java b/core/java/com/google/android/util/GoogleWebContentHelper.java index 5709522..7500ec3 100644 --- a/core/java/com/google/android/util/GoogleWebContentHelper.java +++ b/core/java/com/google/android/util/GoogleWebContentHelper.java @@ -22,6 +22,7 @@ import android.net.http.SslError; import android.os.Message; import android.provider.Settings; import android.text.TextUtils; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -81,10 +82,26 @@ public class GoogleWebContentHelper { */ public GoogleWebContentHelper setUrlsFromGservices(String secureSetting, String prettySetting) { ContentResolver contentResolver = mContext.getContentResolver(); - mSecureUrl = fillUrl(Settings.Gservices.getString(contentResolver, secureSetting)); - mPrettyUrl = fillUrl(Settings.Gservices.getString(contentResolver, prettySetting)); + mSecureUrl = fillUrl(Settings.Gservices.getString(contentResolver, secureSetting), + mContext); + mPrettyUrl = fillUrl(Settings.Gservices.getString(contentResolver, prettySetting), + mContext); return this; } + + /** + * Fetch directly from provided urls. + * + * @param secureUrl The HTTPS URL. + * @param prettyUrl The pretty URL. + * @return This {@link GoogleWebContentHelper} so methods can be chained. + */ + public GoogleWebContentHelper setUrls(String secureUrl, String prettyUrl) { + mSecureUrl = fillUrl(secureUrl, mContext); + mPrettyUrl = fillUrl(prettyUrl, mContext); + return this; + } + /** * Sets the message that will be shown if we are unable to load the page. @@ -113,6 +130,22 @@ public class GoogleWebContentHelper { mWebView.loadUrl(mSecureUrl); return this; } + + /** + * Helper to handle the back key. Returns true if the back key was handled, + * otherwise returns false. + * @param event the key event sent to {@link Activity#dispatchKeyEvent()} + */ + public boolean handleKey(KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK + && event.getAction() == KeyEvent.ACTION_DOWN) { + if (mWebView.canGoBack()) { + mWebView.goBack(); + return true; + } + } + return false; + } /** * Returns the layout containing the web view, progress bar, and text view. @@ -138,12 +171,23 @@ public class GoogleWebContentHelper { * @param url The URL in Formatter style for the extra info to be filled in. * @return The filled URL. */ - private static String fillUrl(String url) { + private static String fillUrl(String url, Context context) { if (TextUtils.isEmpty(url)) { return ""; } - + + /* We add another layer of indirection here to allow mcc's to fill + * in Locales for TOS. TODO - REMOVE when needed locales supported + * natively (when not shipping devices to country X without support + * for their locale). + */ + String localeReplacement = context. + getString(com.android.internal.R.string.locale_replacement); + if (localeReplacement != null && localeReplacement.length() != 0) { + url = String.format(url, localeReplacement); + } + Locale locale = Locale.getDefault(); String tmp = locale.getLanguage() + "_" + locale.getCountry().toLowerCase(); return String.format(url, tmp); diff --git a/core/java/jarjar-rules.txt b/core/java/jarjar-rules.txt new file mode 100644 index 0000000..5fdb022 --- /dev/null +++ b/core/java/jarjar-rules.txt @@ -0,0 +1,2 @@ +rule org.apache.commons com.android.internal.apache.commons + |