diff options
Diffstat (limited to 'core/java/android/server')
-rw-r--r-- | core/java/android/server/BluetoothDeviceService.java | 1175 | ||||
-rw-r--r-- | core/java/android/server/BluetoothEventLoop.java | 289 | ||||
-rw-r--r-- | core/java/android/server/checkin/CheckinProvider.java | 388 | ||||
-rw-r--r-- | core/java/android/server/checkin/FallbackCheckinService.java | 45 | ||||
-rw-r--r-- | core/java/android/server/checkin/package.html | 5 | ||||
-rw-r--r-- | core/java/android/server/data/BuildData.java | 89 | ||||
-rw-r--r-- | core/java/android/server/data/CrashData.java | 145 | ||||
-rw-r--r-- | core/java/android/server/data/StackTraceElementData.java | 80 | ||||
-rw-r--r-- | core/java/android/server/data/ThrowableData.java | 138 | ||||
-rwxr-xr-x | core/java/android/server/data/package.html | 5 | ||||
-rwxr-xr-x | core/java/android/server/package.html | 5 | ||||
-rw-r--r-- | core/java/android/server/search/SearchManagerService.java | 156 | ||||
-rw-r--r-- | core/java/android/server/search/SearchableInfo.aidl | 19 | ||||
-rw-r--r-- | core/java/android/server/search/SearchableInfo.java | 747 | ||||
-rw-r--r-- | core/java/android/server/search/package.html | 5 |
15 files changed, 3291 insertions, 0 deletions
diff --git a/core/java/android/server/BluetoothDeviceService.java b/core/java/android/server/BluetoothDeviceService.java new file mode 100644 index 0000000..10f9f7c --- /dev/null +++ b/core/java/android/server/BluetoothDeviceService.java @@ -0,0 +1,1175 @@ +/* + * 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/BluetoothDeviceService.java + * and make the contructor package private again. + * + * @hide + */ + +package android.server; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadset; // just for dump() +import android.bluetooth.BluetoothIntent; +import android.bluetooth.IBluetoothDevice; +import android.bluetooth.IBluetoothDeviceCallback; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.provider.Settings; +import android.util.Log; +import android.os.Binder; +import android.os.Handler; +import android.os.Message; +import android.os.SystemService; + +import java.io.IOException; +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +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; + + static { + classInitNative(); + } + private native static void classInitNative(); + + public BluetoothDeviceService(Context context) { + mContext = context; + } + + /** Must be called after construction, and before any other method. + */ + public synchronized void init() { + initializeNativeDataNative(); + mIsEnabled = (isEnabledNative() == 1); + mIsDiscovering = false; + mEventLoop = new BluetoothEventLoop(mContext, this); + registerForAirplaneMode(); + + disableEsco(); // TODO: enable eSCO support once its fully supported + } + private native void initializeNativeDataNative(); + + @Override + protected void finalize() throws Throwable { + if (mIsAirplaneSensitive) { + mContext.unregisterReceiver(mReceiver); + } + try { + cleanupNativeDataNative(); + } finally { + super.finalize(); + } + } + private native void cleanupNativeDataNative(); + + public boolean isEnabled() { + checkPermissionBluetooth(); + return mIsEnabled; + } + private native int isEnabledNative(); + + /** + * Disable bluetooth. Returns true on success. + */ + public synchronized boolean disable() { + checkPermissionBluetoothAdmin(); + + if (mEnableThread != null && mEnableThread.isAlive()) { + return false; + } + if (!mIsEnabled) { + return true; + } + mEventLoop.stop(); + disableNative(); + mIsEnabled = false; + mIsDiscovering = false; + Intent intent = new Intent(BluetoothIntent.DISABLED_ACTION); + mContext.sendBroadcast(intent); + return true; + } + + /** + * Enable this Bluetooth device, asynchronously. + * This turns on/off the underlying hardware. + * + * @return True on success (so far), guarenteeing the callback with be + * notified when complete. + */ + public synchronized boolean enable(IBluetoothDeviceCallback callback) { + checkPermissionBluetoothAdmin(); + + // Airplane mode can prevent Bluetooth radio from being turned on. + if (mIsAirplaneSensitive && isAirplaneModeOn()) { + return false; + } + if (mIsEnabled) { + return false; + } + if (mEnableThread != null && mEnableThread.isAlive()) { + return false; + } + mEnableThread = new EnableThread(callback); + mEnableThread.start(); + return true; + } + + private static final int REGISTER_SDP_RECORDS = 1; + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case REGISTER_SDP_RECORDS: + //TODO: Don't assume HSP/HFP is running, don't use sdptool, + if (isEnabled()) { + SystemService.start("hsag"); + SystemService.start("hfag"); + } + } + } + }; + + private EnableThread mEnableThread; + private class EnableThread extends Thread { + private final IBluetoothDeviceCallback mEnableCallback; + public EnableThread(IBluetoothDeviceCallback callback) { + mEnableCallback = callback; + } + public void run() { + boolean res = (enableNative() == 0); + if (res) { + mEventLoop.start(); + } + + if (mEnableCallback != null) { + try { + mEnableCallback.onEnableResult(res ? + BluetoothDevice.RESULT_SUCCESS : + BluetoothDevice.RESULT_FAILURE); + } catch (RemoteException e) {} + } + + if (res) { + mIsEnabled = true; + mIsDiscovering = false; + Intent intent = new Intent(BluetoothIntent.ENABLED_ACTION); + mContext.sendBroadcast(intent); + mHandler.sendMessageDelayed(mHandler.obtainMessage(REGISTER_SDP_RECORDS), 3000); + } + mEnableThread = null; + } + }; + + private native int enableNative(); + private native int disableNative(); + + public synchronized String getAddress() { + checkPermissionBluetooth(); + return getAddressNative(); + } + private native String getAddressNative(); + + public synchronized String getName() { + checkPermissionBluetooth(); + return getNameNative(); + } + private native String getNameNative(); + + public synchronized boolean setName(String name) { + checkPermissionBluetoothAdmin(); + if (name == null) { + return false; + } + // hcid handles persistance of the bluetooth name + return setNameNative(name); + } + private native boolean setNameNative(String name); + + public synchronized String[] listBondings() { + checkPermissionBluetooth(); + return listBondingsNative(); + } + private native String[] listBondingsNative(); + + public synchronized String getMajorClass() { + checkPermissionBluetooth(); + return getMajorClassNative(); + } + private native String getMajorClassNative(); + + public synchronized String getMinorClass() { + checkPermissionBluetooth(); + return getMinorClassNative(); + } + private native String getMinorClassNative(); + + /** + * Returns the user-friendly name of a remote device. This value is + * retrned from our local cache, which is updated during device discovery. + * Do not expect to retrieve the updated remote name immediately after + * changing the name on the remote device. + * + * @param address Bluetooth address of remote device. + * + * @return The user-friendly name of the specified remote device. + */ + public synchronized String getRemoteName(String address) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return null; + } + return getRemoteNameNative(address); + } + private native String getRemoteNameNative(String address); + + /* pacakge */ native String getAdapterPathNative(); + + /** + * Initiate a remote-device-discovery procedure. This procedure may be + * canceled by calling {@link #stopDiscovery}. Remote-device discoveries + * are returned as intents + * <p> + * Typically, when a remote device is found, your + * android.bluetooth.DiscoveryEventNotifier#notifyRemoteDeviceFound + * method will be invoked, and subsequently, your + * android.bluetooth.RemoteDeviceEventNotifier#notifyRemoteNameUpdated + * will tell you the user-friendly name of the remote device. However, + * it is possible that the name update may fail for various reasons, so you + * should display the device's Bluetooth address as soon as you get a + * notifyRemoteDeviceFound event, and update the name when you get the + * remote name. + * + * @return true if discovery has started, + * false otherwise. + */ + public synchronized boolean startDiscovery(boolean resolveNames) { + checkPermissionBluetoothAdmin(); + return startDiscoveryNative(resolveNames); + } + private native boolean startDiscoveryNative(boolean resolveNames); + + /** + * Cancel a remote-device discovery. + * + * Note: you may safely call this method even when discovery has not been + * started. + */ + public synchronized boolean cancelDiscovery() { + checkPermissionBluetoothAdmin(); + return cancelDiscoveryNative(); + } + private native boolean cancelDiscoveryNative(); + + public synchronized boolean isDiscovering() { + checkPermissionBluetooth(); + return mIsDiscovering; + } + + /* package */ void setIsDiscovering(boolean isDiscovering) { + mIsDiscovering = isDiscovering; + } + + public synchronized boolean startPeriodicDiscovery() { + checkPermissionBluetoothAdmin(); + return startPeriodicDiscoveryNative(); + } + private native boolean startPeriodicDiscoveryNative(); + + public synchronized boolean stopPeriodicDiscovery() { + checkPermissionBluetoothAdmin(); + return stopPeriodicDiscoveryNative(); + } + private native boolean stopPeriodicDiscoveryNative(); + + public synchronized boolean isPeriodicDiscovery() { + checkPermissionBluetooth(); + return isPeriodicDiscoveryNative(); + } + private native boolean isPeriodicDiscoveryNative(); + + /** + * Set the discoverability window for the device. A timeout of zero + * makes the device permanently discoverable (if the device is + * discoverable). Setting the timeout to a nonzero value does not make + * a device discoverable; you need to call setMode() to make the device + * explicitly discoverable. + * + * @param timeout_s The discoverable timeout in seconds. + */ + public synchronized boolean setDiscoverableTimeout(int timeout) { + checkPermissionBluetoothAdmin(); + return setDiscoverableTimeoutNative(timeout); + } + private native boolean setDiscoverableTimeoutNative(int timeout_s); + + /** + * Get the discoverability window for the device. A timeout of zero + * means that the device is permanently discoverable (if the device is + * in the discoverable mode). + * + * @return The discoverability window of the device, in seconds. A negative + * value indicates an error. + */ + public synchronized int getDiscoverableTimeout() { + checkPermissionBluetooth(); + return getDiscoverableTimeoutNative(); + } + private native int getDiscoverableTimeoutNative(); + + public synchronized boolean isAclConnected(String address) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return false; + } + return isConnectedNative(address); + } + private native boolean isConnectedNative(String address); + + /** + * Detetermines whether this device is connectable (that is, whether remote + * devices can connect to it.) + * <p> + * Note: A Bluetooth adapter has separate connectable and discoverable + * states, and you could have any combination of those. Although + * any combination is possible (such as discoverable but not + * connectable), we restrict the possible combinations to one of + * three possibilities: discoverable and connectable, connectable + * but not discoverable, and neither connectable nor discoverable. + * + * @return true if this adapter is connectable + * false otherwise + * + * @see #isDiscoverable + * @see #getMode + * @see #setMode + */ + public synchronized boolean isConnectable() { + checkPermissionBluetooth(); + return isConnectableNative(); + } + private native boolean isConnectableNative(); + + /** + * Detetermines whether this device is discoverable. + * + * Note: a Bluetooth adapter has separate connectable and discoverable + * states, and you could have any combination of those. Although + * any combination is possible (such as discoverable but not + * connectable), we restrict the possible combinations to one of + * three possibilities: discoverable and connectable, connectable + * but not discoverable, and neither connectable nor discoverable. + * + * @return true if this adapter is discoverable + * false otherwise + * + * @see #isConnectable + * @see #getMode + * @see #setMode + */ + public synchronized boolean isDiscoverable() { + checkPermissionBluetooth(); + return isDiscoverableNative(); + } + private native boolean isDiscoverableNative(); + + /** + * Determines which one of three modes this adapter is in: discoverable and + * connectable, not discoverable but connectable, or neither. + * + * @return Mode enumeration containing the current mode. + * + * @see #setMode + */ + public synchronized int getMode() { + checkPermissionBluetooth(); + String mode = getModeNative(); + if (mode == null) { + return BluetoothDevice.MODE_UNKNOWN; + } + if (mode.equalsIgnoreCase("off")) { + return BluetoothDevice.MODE_OFF; + } + else if (mode.equalsIgnoreCase("connectable")) { + return BluetoothDevice.MODE_CONNECTABLE; + } + else if (mode.equalsIgnoreCase("discoverable")) { + return BluetoothDevice.MODE_DISCOVERABLE; + } + else { + return BluetoothDevice.MODE_UNKNOWN; + } + } + private native String getModeNative(); + + /** + * Set the discoverability and connectability mode of this adapter. The + * possibilities are discoverable and connectable (MODE_DISCOVERABLE), + * connectable but not discoverable (MODE_CONNECTABLE), and neither + * (MODE_OFF). + * + * Note: MODE_OFF does not mean that the adapter is physically off. It + * may be neither discoverable nor connectable, but it could still + * initiate outgoing connections, or could participate in a + * connection initiated by a remote device before its mode was set + * to MODE_OFF. + * + * @param mode the new mode + * @see #getMode + */ + public synchronized boolean setMode(int mode) { + checkPermissionBluetoothAdmin(); + switch (mode) { + case BluetoothDevice.MODE_OFF: + return setModeNative("off"); + case BluetoothDevice.MODE_CONNECTABLE: + return setModeNative("connectable"); + case BluetoothDevice.MODE_DISCOVERABLE: + return setModeNative("discoverable"); + } + return false; + } + private native boolean setModeNative(String mode); + + /** + * Retrieves the alias of a remote device. The alias is a local feature, + * and allows us to associate a name with a remote device that is different + * from that remote device's user-friendly name. The remote device knows + * nothing about this. The alias can be changed with + * {@link #setRemoteAlias}, and it may be removed with + * {@link #clearRemoteAlias} + * + * @param address Bluetooth address of remote device. + * + * @return The alias of the remote device. + */ + public synchronized String getRemoteAlias(String address) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return null; + } + return getRemoteAliasNative(address); + } + private native String getRemoteAliasNative(String address); + + /** + * Changes the alias of a remote device. The alias is a local feature, + * from that remote device's user-friendly name. The remote device knows + * nothing about this. The alias can be retrieved with + * {@link #getRemoteAlias}, and it may be removed with + * {@link #clearRemoteAlias}. + * + * @param address Bluetooth address of remote device + * @param alias Alias for the remote device + */ + public synchronized boolean setRemoteAlias(String address, String alias) { + checkPermissionBluetoothAdmin(); + if (alias == null || !BluetoothDevice.checkBluetoothAddress(address)) { + return false; + } + return setRemoteAliasNative(address, alias); + } + private native boolean setRemoteAliasNative(String address, String alias); + + /** + * Removes the alias of a remote device. The alias is a local feature, + * from that remote device's user-friendly name. The remote device knows + * nothing about this. The alias can be retrieved with + * {@link #getRemoteAlias}. + * + * @param address Bluetooth address of remote device + */ + public synchronized boolean clearRemoteAlias(String address) { + checkPermissionBluetoothAdmin(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return false; + } + return clearRemoteAliasNative(address); + } + private native boolean clearRemoteAliasNative(String address); + + public synchronized boolean disconnectRemoteDeviceAcl(String address) { + checkPermissionBluetoothAdmin(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return false; + } + return disconnectRemoteDeviceNative(address); + } + private native boolean disconnectRemoteDeviceNative(String address); + + private static final int MAX_OUTSTANDING_ASYNC = 32; + /** + * This method initiates a Bonding request to a remote device. + * + * + * @param address The Bluetooth address of the remote device + * + * @see #createBonding + * @see #cancelBondingProcess + * @see #removeBonding + * @see #hasBonding + * @see #listBondings + * + * @see android.bluetooth.PasskeyAgent + */ + public synchronized boolean createBonding(String address, IBluetoothDeviceCallback callback) { + checkPermissionBluetoothAdmin(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return false; + } + + HashMap<String, IBluetoothDeviceCallback> callbacks = mEventLoop.getCreateBondingCallbacks(); + if (callbacks.containsKey(address)) { + Log.w(TAG, "createBonding() already in progress for " + address); + return false; + } + + // Protect from malicious clients - limit number of outstanding requests + if (callbacks.size() > MAX_OUTSTANDING_ASYNC) { + Log.w(TAG, "Too many outstanding bonding requests, dropping request for " + address); + return false; + } + + callbacks.put(address, callback); + if (!createBondingNative(address, 60000 /* 1 minute */)) { + callbacks.remove(address); + return false; + } + return true; + } + private native boolean createBondingNative(String address, int timeout_ms); + + /** + * This method cancels a pending bonding request. + * + * @param address The Bluetooth address of the remote device to which a + * bonding request has been initiated. + * + * Note: When a request is canceled, method + * {@link CreateBondingResultNotifier#notifyAuthenticationFailed} + * will be called on the object passed to method + * {@link #createBonding}. + * + * Note: it is safe to call this method when there is no outstanding + * bonding request. + * + * @see #createBonding + * @see #cancelBondingProcess + * @see #removeBonding + * @see #hasBonding + * @see #listBondings + */ + public synchronized boolean cancelBondingProcess(String address) { + checkPermissionBluetoothAdmin(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return false; + } + return cancelBondingProcessNative(address); + } + private native boolean cancelBondingProcessNative(String address); + + /** + * This method removes a bonding to a remote device. This is a local + * operation only, resulting in this adapter "forgetting" the bonding + * information about the specified remote device. The other device itself + * does not know what the bonding has been torn down. The next time either + * device attemps to connect to the other, the connection will fail, and + * the pairing procedure will have to be re-initiated. + * + * @param address The Bluetooth address of the remote device. + * + * @see #createBonding + * @see #cancelBondingProcess + * @see #removeBonding + * @see #hasBonding + * @see #listBondings + */ + public synchronized boolean removeBonding(String address) { + checkPermissionBluetoothAdmin(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return false; + } + return removeBondingNative(address); + } + private native boolean removeBondingNative(String address); + + public synchronized boolean hasBonding(String address) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return false; + } + return hasBondingNative(address); + } + private native boolean hasBondingNative(String address); + + public synchronized String[] listAclConnections() { + checkPermissionBluetooth(); + return listConnectionsNative(); + } + private native String[] listConnectionsNative(); + + /** + * This method lists all remote devices that this adapter is aware of. + * This is a list not only of all most-recently discovered devices, but of + * all devices discovered by this adapter up to some point in the past. + * Note that many of these devices may not be in the neighborhood anymore, + * and attempting to connect to them will result in an error. + * + * @return An array of strings representing the Bluetooth addresses of all + * remote devices that this adapter is aware of. + */ + public synchronized String[] listRemoteDevices() { + checkPermissionBluetooth(); + return listRemoteDevicesNative(); + } + private native String[] listRemoteDevicesNative(); + + /** + * Returns the version of the Bluetooth chip. This version is compiled from + * the LMP version. In case of EDR the features attribute must be checked. + * Example: "Bluetooth 2.0 + EDR". + * + * @return a String representation of the this Adapter's underlying + * Bluetooth-chip version. + */ + public synchronized String getVersion() { + checkPermissionBluetooth(); + return getVersionNative(); + } + private native String getVersionNative(); + + /** + * Returns the revision of the Bluetooth chip. This is a vendor-specific + * value and in most cases it represents the firmware version. This might + * derive from the HCI revision and LMP subversion values or via extra + * vendord specific commands. + * In case the revision of a chip is not available. This method should + * return the LMP subversion value as a string. + * Example: "HCI 19.2" + * + * @return The HCI revision of this adapter. + */ + public synchronized String getRevision() { + checkPermissionBluetooth(); + return getRevisionNative(); + } + private native String getRevisionNative(); + + /** + * Returns the manufacturer of the Bluetooth chip. If the company id is not + * known the sting "Company ID %d" where %d should be replaced with the + * numeric value from the manufacturer field. + * Example: "Cambridge Silicon Radio" + * + * @return Manufacturer name. + */ + public synchronized String getManufacturer() { + checkPermissionBluetooth(); + return getManufacturerNative(); + } + private native String getManufacturerNative(); + + /** + * Returns the company name from the OUI database of the Bluetooth device + * address. This function will need a valid and up-to-date oui.txt from + * the IEEE. This value will be different from the manufacturer string in + * the most cases. + * If the oui.txt file is not present or the OUI part of the Bluetooth + * address is not listed, it should return the string "OUI %s" where %s is + * the actual OUI. + * + * Example: "Apple Computer" + * + * @return company name + */ + public synchronized String getCompany() { + checkPermissionBluetooth(); + return getCompanyNative(); + } + private native String getCompanyNative(); + + /** + * Like getVersion(), but for a remote device. + * + * @param address The Bluetooth address of the remote device. + * + * @return remote-device Bluetooth version + * + * @see #getVersion + */ + public synchronized String getRemoteVersion(String address) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return null; + } + return getRemoteVersionNative(address); + } + private native String getRemoteVersionNative(String address); + + /** + * Like getRevision(), but for a remote device. + * + * @param address The Bluetooth address of the remote device. + * + * @return remote-device HCI revision + * + * @see #getRevision + */ + public synchronized String getRemoteRevision(String address) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return null; + } + return getRemoteRevisionNative(address); + } + private native String getRemoteRevisionNative(String address); + + /** + * Like getManufacturer(), but for a remote device. + * + * @param address The Bluetooth address of the remote device. + * + * @return remote-device Bluetooth chip manufacturer + * + * @see #getManufacturer + */ + public synchronized String getRemoteManufacturer(String address) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return null; + } + return getRemoteManufacturerNative(address); + } + private native String getRemoteManufacturerNative(String address); + + /** + * Like getCompany(), but for a remote device. + * + * @param address The Bluetooth address of the remote device. + * + * @return remote-device company + * + * @see #getCompany + */ + public synchronized String getRemoteCompany(String address) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return null; + } + return getRemoteCompanyNative(address); + } + private native String getRemoteCompanyNative(String address); + + /** + * Returns the date and time when the specified remote device has been seen + * by a discover procedure. + * Example: "2006-02-08 12:00:00 GMT" + * + * @return a String with the timestamp. + */ + public synchronized String lastSeen(String address) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return null; + } + return lastSeenNative(address); + } + private native String lastSeenNative(String address); + + /** + * Returns the date and time when the specified remote device has last been + * connected to + * Example: "2006-02-08 12:00:00 GMT" + * + * @return a String with the timestamp. + */ + public synchronized String lastUsed(String address) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return null; + } + return lastUsedNative(address); + } + private native String lastUsedNative(String address); + + /** + * Gets the major device class of the specified device. + * Example: "computer" + * + * Note: This is simply a string desciption of the major class of the + * device-class information, which is returned as a 32-bit value + * during device discovery. + * + * @param address The Bluetooth address of the remote device. + * + * @return remote-device major class + * + * @see #getRemoteClass + */ + public synchronized String getRemoteMajorClass(String address) { + if (!BluetoothDevice.checkBluetoothAddress(address)) { + checkPermissionBluetooth(); + return null; + } + return getRemoteMajorClassNative(address); + } + private native String getRemoteMajorClassNative(String address); + + /** + * Gets the minor device class of the specified device. + * Example: "laptop" + * + * Note: This is simply a string desciption of the minor class of the + * device-class information, which is returned as a 32-bit value + * during device discovery. + * + * @param address The Bluetooth address of the remote device. + * + * @return remote-device minor class + * + * @see #getRemoteClass + */ + public synchronized String getRemoteMinorClass(String address) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return null; + } + return getRemoteMinorClassNative(address); + } + private native String getRemoteMinorClassNative(String address); + + /** + * Gets the service classes of the specified device. + * Example: ["networking", "object transfer"] + * + * @return a String array with the descriptions of the service classes. + * + * @see #getRemoteClass + */ + public synchronized String[] getRemoteServiceClasses(String address) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return null; + } + return getRemoteServiceClassesNative(address); + } + private native String[] getRemoteServiceClassesNative(String address); + + /** + * Gets the remote major, minor, and service classes encoded as a 32-bit + * integer. + * + * Note: this value is retrieved from cache, because we get it during + * remote-device discovery. + * + * @return 32-bit integer encoding the remote major, minor, and service + * classes. + * + * @see #getRemoteMajorClass + * @see #getRemoteMinorClass + * @see #getRemoteServiceClasses + */ + public synchronized int getRemoteClass(String address) { + if (!BluetoothDevice.checkBluetoothAddress(address)) { + checkPermissionBluetooth(); + return -1; + } + return getRemoteClassNative(address); + } + private native int getRemoteClassNative(String address); + + /** + * Gets the remote features encoded as bit mask. + * + * Note: This method may be obsoleted soon. + * + * @return byte array of features. + */ + public synchronized byte[] getRemoteFeatures(String address) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return null; + } + return getRemoteFeaturesNative(address); + } + private native byte[] getRemoteFeaturesNative(String address); + + /** + * This method and {@link #getRemoteServiceRecord} query the SDP service + * on a remote device. They do not interpret the data, but simply return + * it raw to the user. To read more about SDP service handles and records, + * consult the Bluetooth core documentation (www.bluetooth.com). + * + * @param address Bluetooth address of remote device. + * @param match a String match to narrow down the service-handle search. + * The only supported value currently is "hsp" for the headset + * profile. To retrieve all service handles, simply pass an empty + * match string. + * + * @return all service handles corresponding to the string match. + * + * @see #getRemoteServiceRecord + */ + public synchronized int[] getRemoteServiceHandles(String address, String match) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return null; + } + if (match == null) { + match = ""; + } + return getRemoteServiceHandlesNative(address, match); + } + private native int[] getRemoteServiceHandlesNative(String address, String match); + + /** + * This method retrieves the service records corresponding to a given + * service handle (method {@link #getRemoteServiceHandles} retrieves the + * service handles.) + * + * This method and {@link #getRemoteServiceHandles} do not interpret their + * data, but simply return it raw to the user. To read more about SDP + * service handles and records, consult the Bluetooth core documentation + * (www.bluetooth.com). + * + * @param address Bluetooth address of remote device. + * @param handle Service handle returned by {@link #getRemoteServiceHandles} + * + * @return a byte array of all service records corresponding to the + * specified service handle. + * + * @see #getRemoteServiceHandles + */ + public synchronized byte[] getRemoteServiceRecord(String address, int handle) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return null; + } + return getRemoteServiceRecordNative(address, handle); + } + private native byte[] getRemoteServiceRecordNative(String address, int handle); + + // AIDL does not yet support short's + public synchronized boolean getRemoteServiceChannel(String address, int uuid16, + IBluetoothDeviceCallback callback) { + checkPermissionBluetooth(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return false; + } + HashMap<String, IBluetoothDeviceCallback> callbacks = + mEventLoop.getRemoteServiceChannelCallbacks(); + if (callbacks.containsKey(address)) { + Log.w(TAG, "SDP request already in progress for " + address); + return false; + } + // Protect from malicious clients - only allow 32 bonding requests per minute. + if (callbacks.size() > MAX_OUTSTANDING_ASYNC) { + Log.w(TAG, "Too many outstanding SDP requests, dropping request for " + address); + return false; + } + callbacks.put(address, callback); + + if (!getRemoteServiceChannelNative(address, (short)uuid16)) { + callbacks.remove(address); + return false; + } + return true; + } + private native boolean getRemoteServiceChannelNative(String address, short uuid16); + + public synchronized boolean setPin(String address, byte[] pin) { + checkPermissionBluetoothAdmin(); + if (pin == null || pin.length <= 0 || pin.length > 16 || + !BluetoothDevice.checkBluetoothAddress(address)) { + return false; + } + Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); + if (data == null) { + Log.w(TAG, "setPin(" + address + ") called but no native data available, " + + "ignoring. Maybe the PasskeyAgent Request was cancelled by the remote device" + + " or by bluez.\n"); + return false; + } + // bluez API wants pin as a string + String pinString; + try { + pinString = new String(pin, "UTF8"); + } catch (UnsupportedEncodingException uee) { + Log.e(TAG, "UTF8 not supported?!?"); + return false; + } + return setPinNative(address, pinString, data.intValue()); + } + private native boolean setPinNative(String address, String pin, int nativeData); + + public synchronized boolean cancelPin(String address) { + checkPermissionBluetoothAdmin(); + if (!BluetoothDevice.checkBluetoothAddress(address)) { + return false; + } + Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); + if (data == null) { + Log.w(TAG, "cancelPin(" + address + ") called but no native data available, " + + "ignoring. Maybe the PasskeyAgent Request was already cancelled by the remote " + + "or by bluez.\n"); + return false; + } + return cancelPinNative(address, data.intValue()); + } + private native boolean cancelPinNative(String address, int natveiData); + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) { + ContentResolver resolver = context.getContentResolver(); + // Query the airplane mode from Settings.System just to make sure that + // 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 (enabled) { + enable(null); + } else { + disable(); + } + } + } + } + }; + + private void registerForAirplaneMode() { + String airplaneModeRadios = Settings.System.getString(mContext.getContentResolver(), + Settings.System.AIRPLANE_MODE_RADIOS); + mIsAirplaneSensitive = airplaneModeRadios == null + ? true : airplaneModeRadios.contains(Settings.System.RADIO_BLUETOOTH); + if (mIsAirplaneSensitive) { + mIntentFilter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED); + mContext.registerReceiver(mReceiver, mIntentFilter); + } + } + + /* Returns true if airplane mode is currently on */ + private final boolean isAirplaneModeOn() { + return Settings.System.getInt(mContext.getContentResolver(), + 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 { + FileWriter file = new FileWriter(DISABLE_ESCO_PATH); + file.write("Y"); + file.close(); + } catch (FileNotFoundException e) { + } catch (IOException e) {} + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mIsEnabled) { + pw.println("\nBluetooth ENABLED: " + getAddress() + " (" + getName() + ")"); + pw.println("\nisDiscovering() = " + isDiscovering()); + + BluetoothHeadset headset = new BluetoothHeadset(mContext); + + pw.println("\n--Bondings--"); + String[] addresses = listBondings(); + for (String address : addresses) { + String name = getRemoteName(address); + pw.println(address + " (" + name + ")"); + } + + pw.println("\n--Current ACL Connections--"); + addresses = listAclConnections(); + for (String address : addresses) { + String name = getRemoteName(address); + pw.println(address + " (" + name + ")"); + } + + pw.println("\n--Known Devices--"); + addresses = listRemoteDevices(); + for (String address : addresses) { + String name = getRemoteName(address); + pw.println(address + " (" + name + ")"); + } + + // Rather not do this from here, but no-where else and I need this + // dump + pw.println("\n--Headset Service--"); + switch (headset.getState()) { + case BluetoothHeadset.STATE_DISCONNECTED: + pw.println("getState() = STATE_DISCONNECTED"); + break; + case BluetoothHeadset.STATE_CONNECTING: + pw.println("getState() = STATE_CONNECTING"); + break; + case BluetoothHeadset.STATE_CONNECTED: + pw.println("getState() = STATE_CONNECTED"); + break; + case BluetoothHeadset.STATE_ERROR: + pw.println("getState() = STATE_ERROR"); + break; + } + pw.println("getHeadsetAddress() = " + headset.getHeadsetAddress()); + headset.close(); + + } else { + pw.println("\nBluetooth DISABLED"); + } + pw.println("\nmIsAirplaneSensitive = " + mIsAirplaneSensitive); + } +} diff --git a/core/java/android/server/BluetoothEventLoop.java b/core/java/android/server/BluetoothEventLoop.java new file mode 100644 index 0000000..5722f51 --- /dev/null +++ b/core/java/android/server/BluetoothEventLoop.java @@ -0,0 +1,289 @@ +/* + * 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.server; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothIntent; +import android.bluetooth.IBluetoothDeviceCallback; +import android.content.Context; +import android.content.Intent; +import android.os.RemoteException; +import android.util.Log; + +import java.io.IOException; +import java.lang.Thread; +import java.util.HashMap; + +/** + * TODO: Move this to + * java/services/com/android/server/BluetoothEventLoop.java + * and make the contructor package private again. + * + * @hide + */ +class BluetoothEventLoop { + private static final String TAG = "BluetoothEventLoop"; + private static final boolean DBG = false; + + private int mNativeData; + private Thread mThread; + private boolean mInterrupted; + private HashMap<String, IBluetoothDeviceCallback> mCreateBondingCallbacks; + private HashMap<String, Integer> mPasskeyAgentRequestData; + private HashMap<String, IBluetoothDeviceCallback> mGetRemoteServiceChannelCallbacks; + private BluetoothDeviceService mBluetoothService; + + private Context mContext; + + static { classInitNative(); } + private static native void classInitNative(); + + /* pacakge */ BluetoothEventLoop(Context context, BluetoothDeviceService bluetoothService) { + mBluetoothService = bluetoothService; + mContext = context; + mCreateBondingCallbacks = new HashMap(); + mPasskeyAgentRequestData = new HashMap(); + mGetRemoteServiceChannelCallbacks = new HashMap(); + initializeNativeDataNative(); + } + private native void initializeNativeDataNative(); + + protected void finalize() throws Throwable { + try { + cleanupNativeDataNative(); + } finally { + super.finalize(); + } + } + private native void cleanupNativeDataNative(); + + /* pacakge */ HashMap<String, IBluetoothDeviceCallback> getCreateBondingCallbacks() { + return mCreateBondingCallbacks; + } + /* pacakge */ HashMap<String, IBluetoothDeviceCallback> getRemoteServiceChannelCallbacks() { + return mGetRemoteServiceChannelCallbacks; + } + + /* pacakge */ HashMap<String, Integer> getPasskeyAgentRequestData() { + return mPasskeyAgentRequestData; + } + + private synchronized boolean waitForAndDispatchEvent(int timeout_ms) { + return waitForAndDispatchEventNative(timeout_ms); + } + private native boolean waitForAndDispatchEventNative(int timeout_ms); + + /* package */ synchronized void start() { + + if (mThread != null) { + // Already running. + return; + } + mThread = new Thread("Bluetooth Event Loop") { + @Override + public void run() { + try { + if (setUpEventLoopNative()) { + while (!mInterrupted) { + waitForAndDispatchEvent(0); + sleep(500); + } + tearDownEventLoopNative(); + } + } catch (InterruptedException e) { } + if (DBG) log("Event Loop thread finished"); + } + }; + if (DBG) log("Starting Event Loop thread"); + mInterrupted = false; + mThread.start(); + } + private native boolean setUpEventLoopNative(); + private native void tearDownEventLoopNative(); + + public synchronized void stop() { + if (mThread != null) { + + mInterrupted = true; + + try { + mThread.join(); + mThread = null; + } catch (InterruptedException e) { + Log.i(TAG, "Interrupted waiting for Event Loop thread to join"); + } + } + } + + public synchronized boolean isEventLoopRunning() { + return mThread != null; + } + + public void onModeChanged(String mode) { + Intent intent = new Intent(BluetoothIntent.MODE_CHANGED_ACTION); + int intMode = BluetoothDevice.MODE_UNKNOWN; + if (mode.equalsIgnoreCase("off")) { + intMode = BluetoothDevice.MODE_OFF; + } + else if (mode.equalsIgnoreCase("connectable")) { + intMode = BluetoothDevice.MODE_CONNECTABLE; + } + else if (mode.equalsIgnoreCase("discoverable")) { + intMode = BluetoothDevice.MODE_DISCOVERABLE; + } + intent.putExtra(BluetoothIntent.MODE, intMode); + mContext.sendBroadcast(intent); + } + + public void onDiscoveryStarted() { + mBluetoothService.setIsDiscovering(true); + Intent intent = new Intent(BluetoothIntent.DISCOVERY_STARTED_ACTION); + mContext.sendBroadcast(intent); + } + public void onDiscoveryCompleted() { + mBluetoothService.setIsDiscovering(false); + Intent intent = new Intent(BluetoothIntent.DISCOVERY_COMPLETED_ACTION); + mContext.sendBroadcast(intent); + } + + public void onPairingRequest() { + Intent intent = new Intent(BluetoothIntent.PAIRING_REQUEST_ACTION); + mContext.sendBroadcast(intent); + } + public void onPairingCancel() { + Intent intent = new Intent(BluetoothIntent.PAIRING_CANCEL_ACTION); + mContext.sendBroadcast(intent); + } + + public void onRemoteDeviceFound(String address, int deviceClass, short rssi) { + Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_FOUND_ACTION); + intent.putExtra(BluetoothIntent.ADDRESS, address); + intent.putExtra(BluetoothIntent.CLASS, deviceClass); + intent.putExtra(BluetoothIntent.RSSI, rssi); + mContext.sendBroadcast(intent); + } + public void onRemoteDeviceDisappeared(String address) { + Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_DISAPPEARED_ACTION); + intent.putExtra(BluetoothIntent.ADDRESS, address); + mContext.sendBroadcast(intent); + } + 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); + } + public void onRemoteDeviceConnected(String address) { + Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_CONNECTED_ACTION); + intent.putExtra(BluetoothIntent.ADDRESS, address); + mContext.sendBroadcast(intent); + } + public void onRemoteDeviceDisconnectRequested(String address) { + Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_DISCONNECT_REQUESTED_ACTION); + intent.putExtra(BluetoothIntent.ADDRESS, address); + mContext.sendBroadcast(intent); + } + public void onRemoteDeviceDisconnected(String address) { + Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_DISCONNECTED_ACTION); + intent.putExtra(BluetoothIntent.ADDRESS, address); + mContext.sendBroadcast(intent); + } + 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); + } + public void onRemoteNameFailed(String address) { + Intent intent = new Intent(BluetoothIntent.REMOTE_NAME_FAILED_ACTION); + intent.putExtra(BluetoothIntent.ADDRESS, address); + mContext.sendBroadcast(intent); + } + 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); + } + 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); + } + public void onRemoteAliasCleared(String address) { + Intent intent = new Intent(BluetoothIntent.REMOTE_ALIAS_CLEARED_ACTION); + intent.putExtra(BluetoothIntent.ADDRESS, address); + mContext.sendBroadcast(intent); + } + + private void onCreateBondingResult(String address, boolean result) { + IBluetoothDeviceCallback callback = mCreateBondingCallbacks.get(address); + if (callback != null) { + try { + callback.onCreateBondingResult(address, + result ? BluetoothDevice.RESULT_SUCCESS : + 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); + } + public void onBondingRemoved(String address) { + Intent intent = new Intent(BluetoothIntent.BONDING_REMOVED_ACTION); + intent.putExtra(BluetoothIntent.ADDRESS, address); + mContext.sendBroadcast(intent); + } + + public void onNameChanged(String name) { + Intent intent = new Intent(BluetoothIntent.NAME_CHANGED_ACTION); + intent.putExtra(BluetoothIntent.NAME, name); + mContext.sendBroadcast(intent); + } + + public void onPasskeyAgentRequest(String address, int nativeData) { + mPasskeyAgentRequestData.put(address, new Integer(nativeData)); + + Intent intent = new Intent(BluetoothIntent.PAIRING_REQUEST_ACTION); + intent.putExtra(BluetoothIntent.ADDRESS, address); + mContext.sendBroadcast(intent); + } + public void onPasskeyAgentCancel(String address) { + mPasskeyAgentRequestData.remove(address); + + Intent intent = new Intent(BluetoothIntent.PAIRING_CANCEL_ACTION); + intent.putExtra(BluetoothIntent.ADDRESS, address); + mContext.sendBroadcast(intent); + } + private void onGetRemoteServiceChannelResult(String address, int channel) { + IBluetoothDeviceCallback callback = mGetRemoteServiceChannelCallbacks.get(address); + if (callback != null) { + try { + callback.onGetRemoteServiceChannelResult(address, channel); + } catch (RemoteException e) {} + mGetRemoteServiceChannelCallbacks.remove(address); + } + } + + private static void log(String msg) { + Log.d(TAG, msg); + } +} diff --git a/core/java/android/server/checkin/CheckinProvider.java b/core/java/android/server/checkin/CheckinProvider.java new file mode 100644 index 0000000..86ece4a --- /dev/null +++ b/core/java/android/server/checkin/CheckinProvider.java @@ -0,0 +1,388 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.server.checkin; + +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.os.Environment; +import android.provider.BaseColumns; +import android.provider.Checkin; +import android.util.Log; + +import java.io.File; + +/** + * Content provider for the database used to store events and statistics + * while they wait to be uploaded by the checkin service. + */ +public class CheckinProvider extends ContentProvider { + /** Class identifier for logging. */ + private static final String TAG = "CheckinProvider"; + + /** Filename of database (in /data directory). */ + private static final String DATABASE_FILENAME = "checkin.db"; + + /** Version of database schema. */ + private static final int DATABASE_VERSION = 1; + + /** Maximum number of events recorded. */ + private static final int EVENT_LIMIT = 1000; + + /** Maximum size of individual event data. */ + private static final int EVENT_SIZE = 8192; + + /** Maximum number of crashes recorded. */ + private static final int CRASH_LIMIT = 25; + + /** Maximum size of individual crashes recorded. */ + private static final int CRASH_SIZE = 16384; + + /** Permission required for access to the 'properties' database. */ + private static final String PROPERTIES_PERMISSION = + "android.permission.ACCESS_CHECKIN_PROPERTIES"; + + /** Lock for stats read-modify-write update cycle (see {@link #insert}). */ + private final Object mStatsLock = new Object(); + + /** The underlying SQLite database. */ + private SQLiteOpenHelper mOpenHelper; + + private static class OpenHelper extends SQLiteOpenHelper { + public OpenHelper(Context context) { + super(context, DATABASE_FILENAME, null, DATABASE_VERSION); + + // The database used to live in /data/checkin.db. + File oldLocation = Environment.getDataDirectory(); + File old = new File(oldLocation, DATABASE_FILENAME); + File file = context.getDatabasePath(DATABASE_FILENAME); + + // Try to move the file to the new location. + // TODO: Remove this code before shipping. + if (old.exists() && !file.exists() && !old.renameTo(file)) { + Log.e(TAG, "Can't rename " + old + " to " + file); + } + if (old.exists() && !old.delete()) { + // Clean up the old data file in any case. + Log.e(TAG, "Can't remove " + old); + } + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + Checkin.Events.TABLE_NAME + " (" + + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Checkin.Events.TAG + " TEXT NOT NULL," + + Checkin.Events.VALUE + " TEXT DEFAULT \"\"," + + Checkin.Events.DATE + " INTEGER NOT NULL)"); + + db.execSQL("CREATE INDEX events_index ON " + + Checkin.Events.TABLE_NAME + " (" + + Checkin.Events.TAG + ")"); + + db.execSQL("CREATE TABLE " + Checkin.Stats.TABLE_NAME + " (" + + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Checkin.Stats.TAG + " TEXT UNIQUE," + + Checkin.Stats.COUNT + " INTEGER DEFAULT 0," + + Checkin.Stats.SUM + " REAL DEFAULT 0.0)"); + + db.execSQL("CREATE TABLE " + Checkin.Crashes.TABLE_NAME + " (" + + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Checkin.Crashes.DATA + " TEXT NOT NULL," + + Checkin.Crashes.LOGS + " TEXT)"); + + db.execSQL("CREATE TABLE " + Checkin.Properties.TABLE_NAME + " (" + + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Checkin.Properties.TAG + " TEXT UNIQUE ON CONFLICT REPLACE," + + Checkin.Properties.VALUE + " TEXT DEFAULT \"\")"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int old, int version) { + db.execSQL("DROP TABLE IF EXISTS " + Checkin.Events.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + Checkin.Stats.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + Checkin.Crashes.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + Checkin.Properties.TABLE_NAME); + onCreate(db); + } + } + + @Override public boolean onCreate() { + mOpenHelper = new OpenHelper(getContext()); + return true; + } + + @Override + public Cursor query(Uri uri, String[] select, + String where, String[] args, String sort) { + checkPermissions(uri); + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(uri.getPathSegments().get(0)); + if (uri.getPathSegments().size() == 2) { + qb.appendWhere("_id=" + ContentUris.parseId(uri)); + } else if (uri.getPathSegments().size() != 1) { + throw new IllegalArgumentException("Invalid query URI: " + uri); + } + + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + Cursor cursor = qb.query(db, select, where, args, null, null, sort); + cursor.setNotificationUri(getContext().getContentResolver(), uri); + return cursor; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + checkPermissions(uri); + if (uri.getPathSegments().size() != 1) { + throw new IllegalArgumentException("Invalid insert URI: " + uri); + } + + long id; + String table = uri.getPathSegments().get(0); + if (Checkin.Events.TABLE_NAME.equals(table)) { + id = insertEvent(values); + } else if (Checkin.Stats.TABLE_NAME.equals(table)) { + id = insertStats(values); + } else if (Checkin.Crashes.TABLE_NAME.equals(table)) { + id = insertCrash(values); + } else { + id = mOpenHelper.getWritableDatabase().insert(table, null, values); + } + + if (id < 0) { + return null; + } else { + uri = ContentUris.withAppendedId(uri, id); + getContext().getContentResolver().notifyChange(uri, null); + return uri; + } + } + + /** + * Insert an entry into the events table. + * Trims old events from the table to keep the size bounded. + * @param values to insert + * @return the row ID of the new entry + */ + private long insertEvent(ContentValues values) { + String value = values.getAsString(Checkin.Events.VALUE); + if (value != null && value.length() > EVENT_SIZE) { + // Event values are readable text, so they can be truncated. + value = value.substring(0, EVENT_SIZE - 3) + "..."; + values.put(Checkin.Events.VALUE, value); + } + + if (!values.containsKey(Checkin.Events.DATE)) { + values.put(Checkin.Events.DATE, System.currentTimeMillis()); + } + + // TODO: Make this more efficient; don't do it on every insert. + // Also, consider keeping the most recent instance of every tag, + // and possibly update a counter when events are deleted. + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.execSQL("DELETE FROM " + + Checkin.Events.TABLE_NAME + " WHERE " + + Checkin.Events._ID + " IN (SELECT " + + Checkin.Events._ID + " FROM " + + Checkin.Events.TABLE_NAME + " ORDER BY " + + Checkin.Events.DATE + " DESC LIMIT -1 OFFSET " + + (EVENT_LIMIT - 1) + ")"); + return db.insert(Checkin.Events.TABLE_NAME, null, values); + } + + /** + * Add an entry into the stats table. + * For statistics, instead of just inserting a row into the database, + * we add the count and sum values to the existing values (if any) + * for the specified tag. This must be done with a lock held, + * to avoid a race condition during the read-modify-write update. + * @param values to insert + * @return the row ID of the modified entry + */ + private long insertStats(ContentValues values) { + synchronized (mStatsLock) { + String tag = values.getAsString(Checkin.Stats.TAG); + if (tag == null) { + throw new IllegalArgumentException("Tag required:" + values); + } + + // Look for existing values with this tag. + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + Cursor cursor = db.query(false, + Checkin.Stats.TABLE_NAME, + new String[] { + Checkin.Stats._ID, + Checkin.Stats.COUNT, + Checkin.Stats.SUM + }, + Checkin.Stats.TAG + "=?", + new String[] { tag }, + null, null, null, null /* limit */); + + try { + if (cursor == null || !cursor.moveToNext()) { + // This is a new statistic, insert it directly. + return db.insert(Checkin.Stats.TABLE_NAME, null, values); + } else { + // Depend on SELECT column order to avoid getColumnIndex() + long id = cursor.getLong(0); + int count = cursor.getInt(1); + double sum = cursor.getDouble(2); + + Integer countAdd = values.getAsInteger(Checkin.Stats.COUNT); + if (countAdd != null) count += countAdd.intValue(); + + Double sumAdd = values.getAsDouble(Checkin.Stats.SUM); + if (sumAdd != null) sum += sumAdd.doubleValue(); + + if (count <= 0 && sum == 0.0) { + // Updated to nothing: delete the row! + cursor.deleteRow(); + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Checkin.Stats.CONTENT_URI, id), null); + return -1; + } else { + if (countAdd != null) cursor.updateInt(1, count); + if (sumAdd != null) cursor.updateDouble(2, sum); + cursor.commitUpdates(); + return id; + } + } + } finally { + // Always clean up the cursor. + if (cursor != null) cursor.close(); + } + } + } + + /** + * Add an entry into the crashes table. + * @param values to insert + * @return the row ID of the modified entry + */ + private long insertCrash(ContentValues values) { + try { + int crashSize = values.getAsString(Checkin.Crashes.DATA).length(); + if (crashSize > CRASH_SIZE) { + // The crash is too big. Don't report it, but do log a stat. + Checkin.updateStats(getContext().getContentResolver(), + Checkin.Stats.Tag.CRASHES_TRUNCATED, 1, 0.0); + throw new IllegalArgumentException("Too big: " + crashSize); + } + + // Count the number of crashes reported, even if they roll over. + Checkin.updateStats(getContext().getContentResolver(), + Checkin.Stats.Tag.CRASHES_REPORTED, 1, 0.0); + + // Trim the crashes database, if needed. + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.execSQL("DELETE FROM " + + Checkin.Crashes.TABLE_NAME + " WHERE " + + Checkin.Crashes._ID + " IN (SELECT " + + Checkin.Crashes._ID + " FROM " + + Checkin.Crashes.TABLE_NAME + " ORDER BY " + + Checkin.Crashes._ID + " DESC LIMIT -1 OFFSET " + + (CRASH_LIMIT - 1) + ")"); + + return db.insert(Checkin.Crashes.TABLE_NAME, null, values); + } catch (Throwable t) { + // To avoid an infinite crash-reporting loop, swallow the error. + Log.e("CheckinProvider", "Error inserting crash: " + t); + return -1; + } + } + + // TODO: optimize bulkInsert, especially for stats? + + @Override + public int update(Uri uri, ContentValues values, + String where, String[] args) { + checkPermissions(uri); + if (uri.getPathSegments().size() == 2) { + if (where != null && where.length() > 0) { + throw new UnsupportedOperationException( + "WHERE clause not supported for update: " + uri); + } + where = "_id=" + ContentUris.parseId(uri); + args = null; + } else if (uri.getPathSegments().size() != 1) { + throw new IllegalArgumentException("Invalid update URI: " + uri); + } + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + int count = db.update(uri.getPathSegments().get(0), values, where, args); + getContext().getContentResolver().notifyChange(uri, null); + return count; + } + + @Override + public int delete(Uri uri, String where, String[] args) { + checkPermissions(uri); + if (uri.getPathSegments().size() == 2) { + if (where != null && where.length() > 0) { + throw new UnsupportedOperationException( + "WHERE clause not supported for delete: " + uri); + } + where = "_id=" + ContentUris.parseId(uri); + args = null; + } else if (uri.getPathSegments().size() != 1) { + throw new IllegalArgumentException("Invalid delete URI: " + uri); + } + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + int count = db.delete(uri.getPathSegments().get(0), where, args); + getContext().getContentResolver().notifyChange(uri, null); + return count; + } + + @Override + public String getType(Uri uri) { + if (uri.getPathSegments().size() == 1) { + return "vnd.android.cursor.dir/" + uri.getPathSegments().get(0); + } else if (uri.getPathSegments().size() == 2) { + return "vnd.android.cursor.item/" + uri.getPathSegments().get(0); + } else { + throw new IllegalArgumentException("Invalid URI: " + uri); + } + } + + /** + * Make sure the caller has permission to the database. + * @param uri the caller is requesting access to + * @throws SecurityException if the caller is forbidden. + */ + private void checkPermissions(Uri uri) { + if (uri.getPathSegments().size() < 1) { + throw new IllegalArgumentException("Invalid query URI: " + uri); + } + + String table = uri.getPathSegments().get(0); + if (table.equals(Checkin.Properties.TABLE_NAME) && + getContext().checkCallingOrSelfPermission(PROPERTIES_PERMISSION) != + PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Cannot access checkin properties"); + } + } +} diff --git a/core/java/android/server/checkin/FallbackCheckinService.java b/core/java/android/server/checkin/FallbackCheckinService.java new file mode 100644 index 0000000..b450913 --- /dev/null +++ b/core/java/android/server/checkin/FallbackCheckinService.java @@ -0,0 +1,45 @@ +/* + * 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.server.checkin; + +import android.os.ICheckinService; +import android.os.RemoteException; +import android.os.IParentalControlCallback; +import com.google.android.net.ParentalControlState; + +/** + * @hide + */ +public final class FallbackCheckinService extends ICheckinService.Stub { + public FallbackCheckinService() { + } + + public void reportCrashSync(byte[] crashData) throws RemoteException { + } + + public void reportCrashAsync(byte[] crashData) throws RemoteException { + } + + public void masterClear() throws RemoteException { + } + + public void getParentalControlState(IParentalControlCallback p) throws RemoteException { + ParentalControlState state = new ParentalControlState(); + state.isEnabled = false; + p.onResult(state); + } +} diff --git a/core/java/android/server/checkin/package.html b/core/java/android/server/checkin/package.html new file mode 100644 index 0000000..1c9bf9d --- /dev/null +++ b/core/java/android/server/checkin/package.html @@ -0,0 +1,5 @@ +<html> +<body> + {@hide} +</body> +</html> diff --git a/core/java/android/server/data/BuildData.java b/core/java/android/server/data/BuildData.java new file mode 100644 index 0000000..53ffa3f --- /dev/null +++ b/core/java/android/server/data/BuildData.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 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 android.server.data; + +import android.os.Build; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import static com.android.internal.util.Objects.nonNull; + +/** + * Build data transfer object. Keep in sync. with the server side version. + */ +public class BuildData { + + /** The version of the data returned by write() and understood by the constructor. */ + private static final int VERSION = 0; + + private final String fingerprint; + private final String incrementalVersion; + private final long time; // in *seconds* since the epoch (not msec!) + + public BuildData() { + this.fingerprint = "android:" + Build.FINGERPRINT; + this.incrementalVersion = Build.VERSION.INCREMENTAL; + this.time = Build.TIME / 1000; // msec -> sec + } + + public BuildData(String fingerprint, String incrementalVersion, long time) { + this.fingerprint = nonNull(fingerprint); + this.incrementalVersion = incrementalVersion; + this.time = time; + } + + /*package*/ BuildData(DataInput in) throws IOException { + int dataVersion = in.readInt(); + if (dataVersion != VERSION) { + throw new IOException("Expected " + VERSION + ". Got: " + dataVersion); + } + + this.fingerprint = in.readUTF(); + this.incrementalVersion = Long.toString(in.readLong()); + this.time = in.readLong(); + } + + /*package*/ void write(DataOutput out) throws IOException { + out.writeInt(VERSION); + out.writeUTF(fingerprint); + + // TODO: change the format/version to expect a string for this field. + // Version 0, still used by the server side, expects a long. + long changelist; + try { + changelist = Long.parseLong(incrementalVersion); + } catch (NumberFormatException ex) { + changelist = -1; + } + out.writeLong(changelist); + out.writeLong(time); + } + + public String getFingerprint() { + return fingerprint; + } + + public String getIncrementalVersion() { + return incrementalVersion; + } + + public long getTime() { + return time; + } +} diff --git a/core/java/android/server/data/CrashData.java b/core/java/android/server/data/CrashData.java new file mode 100644 index 0000000..d652bb3 --- /dev/null +++ b/core/java/android/server/data/CrashData.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.server.data; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import static com.android.internal.util.Objects.nonNull; + +/** + * Crash data transfer object. Keep in sync. with the server side version. + */ +public class CrashData { + + final String id; + final String activity; + final long time; + final BuildData buildData; + final ThrowableData throwableData; + final byte[] state; + + public CrashData(String id, String activity, BuildData buildData, + ThrowableData throwableData) { + this.id = nonNull(id); + this.activity = nonNull(activity); + this.buildData = nonNull(buildData); + this.throwableData = nonNull(throwableData); + this.time = System.currentTimeMillis(); + this.state = null; + } + + public CrashData(String id, String activity, BuildData buildData, + ThrowableData throwableData, byte[] state) { + this.id = nonNull(id); + this.activity = nonNull(activity); + this.buildData = nonNull(buildData); + this.throwableData = nonNull(throwableData); + this.time = System.currentTimeMillis(); + this.state = state; + } + + public CrashData(DataInput in) throws IOException { + int dataVersion = in.readInt(); + if (dataVersion != 0 && dataVersion != 1) { + throw new IOException("Expected 0 or 1. Got: " + dataVersion); + } + + this.id = in.readUTF(); + this.activity = in.readUTF(); + this.time = in.readLong(); + this.buildData = new BuildData(in); + this.throwableData = new ThrowableData(in); + if (dataVersion == 1) { + int len = in.readInt(); + if (len == 0) { + this.state = null; + } else { + this.state = new byte[len]; + in.readFully(this.state, 0, len); + } + } else { + this.state = null; + } + } + + public CrashData(String tag, Throwable throwable) { + id = ""; + activity = tag; + buildData = new BuildData(); + throwableData = new ThrowableData(throwable); + time = System.currentTimeMillis(); + state = null; + } + + public void write(DataOutput out) throws IOException { + // version + if (this.state == null) { + out.writeInt(0); + } else { + out.writeInt(1); + } + + out.writeUTF(this.id); + out.writeUTF(this.activity); + out.writeLong(this.time); + buildData.write(out); + throwableData.write(out); + if (this.state != null) { + out.writeInt(this.state.length); + out.write(this.state, 0, this.state.length); + } + } + + public BuildData getBuildData() { + return buildData; + } + + public ThrowableData getThrowableData() { + return throwableData; + } + + public String getId() { + return id; + } + + public String getActivity() { + return activity; + } + + public long getTime() { + return time; + } + + public byte[] getState() { + return state; + } + + /** + * Return a brief description of this CrashData record. The details of the + * representation are subject to change. + * + * @return Returns a String representing the contents of the object. + */ + @Override + public String toString() { + return "[CrashData: id=" + id + " activity=" + activity + " time=" + time + + " buildData=" + buildData.toString() + + " throwableData=" + throwableData.toString() + "]"; + } +} diff --git a/core/java/android/server/data/StackTraceElementData.java b/core/java/android/server/data/StackTraceElementData.java new file mode 100644 index 0000000..07185a0 --- /dev/null +++ b/core/java/android/server/data/StackTraceElementData.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.server.data; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +/** + * Stack trace element data transfer object. Keep in sync. with the server side + * version. + */ +public class StackTraceElementData { + + final String className; + final String fileName; + final String methodName; + final int lineNumber; + + public StackTraceElementData(StackTraceElement element) { + this.className = element.getClassName(); + + String fileName = element.getFileName(); + this.fileName = fileName == null ? "[unknown source]" : fileName; + + this.methodName = element.getMethodName(); + this.lineNumber = element.getLineNumber(); + } + + public StackTraceElementData(DataInput in) throws IOException { + int dataVersion = in.readInt(); + if (dataVersion != 0) { + throw new IOException("Expected 0. Got: " + dataVersion); + } + + this.className = in.readUTF(); + this.fileName = in.readUTF(); + this.methodName = in.readUTF(); + this.lineNumber = in.readInt(); + } + + void write(DataOutput out) throws IOException { + out.writeInt(0); // version + + out.writeUTF(className); + out.writeUTF(fileName); + out.writeUTF(methodName); + out.writeInt(lineNumber); + } + + public String getClassName() { + return className; + } + + public String getFileName() { + return fileName; + } + + public String getMethodName() { + return methodName; + } + + public int getLineNumber() { + return lineNumber; + } +} diff --git a/core/java/android/server/data/ThrowableData.java b/core/java/android/server/data/ThrowableData.java new file mode 100644 index 0000000..e500aca --- /dev/null +++ b/core/java/android/server/data/ThrowableData.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.server.data; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +/** + * Throwable data transfer object. Keep in sync. with the server side version. + */ +public class ThrowableData { + + final String message; + final String type; + final StackTraceElementData[] stackTrace; + final ThrowableData cause; + + public ThrowableData(Throwable throwable) { + this.type = throwable.getClass().getName(); + String message = throwable.getMessage(); + this.message = message == null ? "" : message; + + StackTraceElement[] elements = throwable.getStackTrace(); + this.stackTrace = new StackTraceElementData[elements.length]; + for (int i = 0; i < elements.length; i++) { + this.stackTrace[i] = new StackTraceElementData(elements[i]); + } + + Throwable cause = throwable.getCause(); + this.cause = cause == null ? null : new ThrowableData(cause); + } + + public ThrowableData(DataInput in) throws IOException { + int dataVersion = in.readInt(); + if (dataVersion != 0) { + throw new IOException("Expected 0. Got: " + dataVersion); + } + + this.message = in.readUTF(); + this.type = in.readUTF(); + + int count = in.readInt(); + this.stackTrace = new StackTraceElementData[count]; + for (int i = 0; i < count; i++) { + this.stackTrace[i] = new StackTraceElementData(in); + } + + this.cause = in.readBoolean() ? new ThrowableData(in) : null; + } + + public void write(DataOutput out) throws IOException { + out.writeInt(0); // version + + out.writeUTF(message); + out.writeUTF(type); + + out.writeInt(stackTrace.length); + for (StackTraceElementData elementData : stackTrace) { + elementData.write(out); + } + + out.writeBoolean(cause != null); + if (cause != null) { + cause.write(out); + } + } + + public String getMessage() { + return message; + } + + public String getType() { + return type; + } + + public StackTraceElementData[] getStackTrace() { + return stackTrace; + } + + public ThrowableData getCause() { + return cause; + } + + + public String toString() { + return toString(null); + } + + public String toString(String prefix) { + StringBuilder builder = new StringBuilder(); + append(prefix, builder, this); + return builder.toString(); + } + + private static void append(String prefix, StringBuilder builder, + ThrowableData throwableData) { + if (prefix != null) builder.append(prefix); + builder.append(throwableData.getType()) + .append(": ") + .append(throwableData.getMessage()) + .append('\n'); + for (StackTraceElementData element : throwableData.getStackTrace()) { + if (prefix != null ) builder.append(prefix); + builder.append(" at ") + .append(element.getClassName()) + .append('.') + .append(element.getMethodName()) + .append("(") + .append(element.getFileName()) + .append(':') + .append(element.getLineNumber()) + .append(")\n"); + + } + + ThrowableData cause = throwableData.getCause(); + if (cause != null) { + if (prefix != null ) builder.append(prefix); + builder.append("Caused by: "); + append(prefix, builder, cause); + } + } +} diff --git a/core/java/android/server/data/package.html b/core/java/android/server/data/package.html new file mode 100755 index 0000000..1c9bf9d --- /dev/null +++ b/core/java/android/server/data/package.html @@ -0,0 +1,5 @@ +<html> +<body> + {@hide} +</body> +</html> diff --git a/core/java/android/server/package.html b/core/java/android/server/package.html new file mode 100755 index 0000000..c9f96a6 --- /dev/null +++ b/core/java/android/server/package.html @@ -0,0 +1,5 @@ +<body> + +{@hide} + +</body> diff --git a/core/java/android/server/search/SearchManagerService.java b/core/java/android/server/search/SearchManagerService.java new file mode 100644 index 0000000..fe15553 --- /dev/null +++ b/core/java/android/server/search/SearchManagerService.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 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 android.server.search; + +import android.app.ISearchManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.util.Config; + +/** + * This is a simplified version of the Search Manager service. It no longer handles + * presentation (UI). Its function is to maintain the map & list of "searchable" + * items, which provides a mapping from individual activities (where a user might have + * invoked search) to specific searchable activities (where the search will be dispatched). + */ +public class SearchManagerService extends ISearchManager.Stub +{ + // general debugging support + private static final String TAG = "SearchManagerService"; + private static final boolean DEBUG = false; + private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV; + + // configuration choices + private static final boolean IMMEDIATE_SEARCHABLES_UPDATE = true; + + // class maintenance and general shared data + private final Context mContext; + private final Handler mHandler; + private boolean mSearchablesDirty; + + /** + * Initialize the Search Manager service in the provided system context. + * Only one instance of this object should be created! + * + * @param context to use for accessing DB, window manager, etc. + */ + public SearchManagerService(Context context) { + mContext = context; + mHandler = new Handler(); + + // Setup the infrastructure for updating and maintaining the list + // of searchable activities. + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addDataScheme("package"); + mContext.registerReceiver(mIntentReceiver, filter, null, mHandler); + mSearchablesDirty = true; + + // After startup settles down, preload the searchables list, + // which will reduce the delay when the search UI is invoked. + if (IMMEDIATE_SEARCHABLES_UPDATE) { + mHandler.post(mRunUpdateSearchable); + } + } + + /** + * Listens for intent broadcasts. + * + * The primary purpose here is to refresh the "searchables" list + * if packages are added/removed. + */ + private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + // First, test for intents that matter at any time + if (action.equals(Intent.ACTION_PACKAGE_ADDED) || + action.equals(Intent.ACTION_PACKAGE_REMOVED) || + action.equals(Intent.ACTION_PACKAGE_CHANGED)) { + mSearchablesDirty = true; + if (IMMEDIATE_SEARCHABLES_UPDATE) { + mHandler.post(mRunUpdateSearchable); + } + return; + } + } + }; + + /** + * This runnable (for the main handler / UI thread) will update the searchables list. + */ + private Runnable mRunUpdateSearchable = new Runnable() { + public void run() { + if (mSearchablesDirty) { + updateSearchables(); + } + } + }; + + /** + * Update the list of searchables, either at startup or in response to + * a package add/remove broadcast message. + */ + private void updateSearchables() { + SearchableInfo.buildSearchableList(mContext); + mSearchablesDirty = false; + + // TODO This is a hack. This shouldn't be hardcoded here, it's probably + // a policy. +// ComponentName defaultSearch = new ComponentName( +// "com.android.contacts", +// "com.android.contacts.ContactsListActivity" ); + ComponentName defaultSearch = new ComponentName( + "com.android.googlesearch", + "com.android.googlesearch.GoogleSearch" ); + SearchableInfo.setDefaultSearchable(mContext, defaultSearch); + } + + /** + * Return the searchableinfo for a given activity + * + * @param launchActivity The activity from which we're launching this search. + * @return Returns a SearchableInfo record describing the parameters of the search, + * or null if no searchable metadata was available. + * @param globalSearch If false, this will only launch the search that has been specifically + * defined by the application (which is usually defined as a local search). If no default + * search is defined in the current application or activity, no search will be launched. + * If true, this will always launch a platform-global (e.g. web-based) search instead. + */ + public SearchableInfo getSearchableInfo(ComponentName launchActivity, boolean globalSearch) { + // final check. however we should try to avoid this, because + // it slows down the entry into the UI. + if (mSearchablesDirty) { + updateSearchables(); + } + SearchableInfo si = null; + if (globalSearch) { + si = SearchableInfo.getDefaultSearchable(); + } else { + si = SearchableInfo.getSearchableInfo(mContext, launchActivity); + } + + return si; + } +} diff --git a/core/java/android/server/search/SearchableInfo.aidl b/core/java/android/server/search/SearchableInfo.aidl new file mode 100644 index 0000000..9576c2b --- /dev/null +++ b/core/java/android/server/search/SearchableInfo.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.server.search; + +parcelable SearchableInfo; diff --git a/core/java/android/server/search/SearchableInfo.java b/core/java/android/server/search/SearchableInfo.java new file mode 100644 index 0000000..5b9942e --- /dev/null +++ b/core/java/android/server/search/SearchableInfo.java @@ -0,0 +1,747 @@ +/* + * Copyright (C) 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 android.server.search; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.pm.ResolveInfo; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public final class SearchableInfo implements Parcelable { + + // general debugging support + final static String LOG_TAG = "SearchableInfo"; + + // set this flag to 1 to prevent any apps from providing suggestions + final static int DBG_INHIBIT_SUGGESTIONS = 0; + + // static strings used for XML lookups, etc. + // TODO how should these be documented for the developer, in a more structured way than + // the current long wordy javadoc in SearchManager.java ? + private static final String MD_LABEL_DEFAULT_SEARCHABLE = "android.app.default_searchable"; + private static final String MD_LABEL_SEARCHABLE = "android.app.searchable"; + private static final String MD_SEARCHABLE_SYSTEM_SEARCH = "*"; + private static final String MD_XML_ELEMENT_SEARCHABLE = "searchable"; + private static final String MD_XML_ELEMENT_SEARCHABLE_ACTION_KEY = "actionkey"; + + // class maintenance and general shared data + private static HashMap<ComponentName, SearchableInfo> sSearchablesMap = null; + private static ArrayList<SearchableInfo> sSearchablesList = null; + private static SearchableInfo sDefaultSearchable = null; + + // true member variables - what we know about the searchability + // TO-DO replace public with getters + public boolean mSearchable = false; + private int mLabelId = 0; + public ComponentName mSearchActivity = null; + private int mHintId = 0; + private int mSearchMode = 0; + public boolean mBadgeLabel = false; + public boolean mBadgeIcon = false; + public boolean mQueryRewriteFromData = false; + public boolean mQueryRewriteFromText = false; + private int mIconId = 0; + private int mSearchButtonText = 0; + private String mSuggestAuthority = null; + private String mSuggestPath = null; + private String mSuggestSelection = null; + private String mSuggestIntentAction = null; + private String mSuggestIntentData = null; + private ActionKeyInfo mActionKeyList = null; + private String mSuggestProviderPackage = null; + private Context mCacheActivityContext = null; // use during setup only - don't hold memory! + + /** + * Set the default searchable activity (when none is specified). + */ + public static void setDefaultSearchable(Context context, + ComponentName activity) { + synchronized (SearchableInfo.class) { + SearchableInfo si = null; + if (activity != null) { + si = getSearchableInfo(context, activity); + if (si != null) { + // move to front of list + sSearchablesList.remove(si); + sSearchablesList.add(0, si); + } + } + sDefaultSearchable = si; + } + } + + /** + * Provides the system-default search activity, which you can use + * whenever getSearchableInfo() returns null; + * + * @return Returns the system-default search activity, null if never defined + */ + public static SearchableInfo getDefaultSearchable() { + synchronized (SearchableInfo.class) { + return sDefaultSearchable; + } + } + + /** + * Retrieve the authority for obtaining search suggestions. + * + * @return Returns a string containing the suggestions authority. + */ + public String getSuggestAuthority() { + return mSuggestAuthority; + } + + /** + * Retrieve the path for obtaining search suggestions. + * + * @return Returns a string containing the suggestions path, or null if not provided. + */ + public String getSuggestPath() { + return mSuggestPath; + } + + /** + * Retrieve the selection pattern for obtaining search suggestions. This must + * include a single ? which will be used for the user-typed characters. + * + * @return Returns a string containing the suggestions authority. + */ + public String getSuggestSelection() { + return mSuggestSelection; + } + + /** + * Retrieve the (optional) intent action for use with these suggestions. This is + * useful if all intents will have the same action (e.g. "android.intent.action.VIEW"). + * + * Can be overriden in any given suggestion via the AUTOSUGGEST_COLUMN_INTENT_ACTION column. + * + * @return Returns a string containing the default intent action. + */ + public String getSuggestIntentAction() { + return mSuggestIntentAction; + } + + /** + * Retrieve the (optional) intent data for use with these suggestions. This is + * useful if all intents will have similar data URIs (e.g. "android.intent.action.VIEW"), + * but you'll likely need to provide a specific ID as well via the column + * AUTOSUGGEST_COLUMN_INTENT_DATA_ID, which will be appended to the intent data URI. + * + * Can be overriden in any given suggestion via the AUTOSUGGEST_COLUMN_INTENT_DATA column. + * + * @return Returns a string containing the default intent data. + */ + public String getSuggestIntentData() { + return mSuggestIntentData; + } + + /** + * Get the context for the searchable activity. + * + * This is fairly expensive so do it on the original scan, or when an app is + * selected, but don't hang on to the result forever. + * + * @param context You need to supply a context to start with + * @return Returns a context related to the searchable activity + */ + public Context getActivityContext(Context context) { + Context theirContext = null; + try { + theirContext = context.createPackageContext(mSearchActivity.getPackageName(), 0); + } catch (PackageManager.NameNotFoundException e) { + // unexpected, but we deal with this by null-checking theirContext + } catch (java.lang.SecurityException e) { + // unexpected, but we deal with this by null-checking theirContext + } + + return theirContext; + } + + /** + * Get the context for the suggestions provider. + * + * This is fairly expensive so do it on the original scan, or when an app is + * selected, but don't hang on to the result forever. + * + * @param context You need to supply a context to start with + * @param activityContext If we can determine that the provider and the activity are the + * same, we'll just return this one. + * @return Returns a context related to the context provider + */ + public Context getProviderContext(Context context, Context activityContext) { + Context theirContext = null; + if (mSearchActivity.getPackageName().equals(mSuggestProviderPackage)) { + return activityContext; + } + if (mSuggestProviderPackage != null) + try { + theirContext = context.createPackageContext(mSuggestProviderPackage, 0); + } catch (PackageManager.NameNotFoundException e) { + // unexpected, but we deal with this by null-checking theirContext + } catch (java.lang.SecurityException e) { + // unexpected, but we deal with this by null-checking theirContext + } + + return theirContext; + } + + /** + * Factory. Look up, or construct, based on the activity. + * + * The activities fall into three cases, based on meta-data found in + * the manifest entry: + * <ol> + * <li>The activity itself implements search. This is indicated by the + * presence of a "android.app.searchable" meta-data attribute. + * The value is a reference to an XML file containing search information.</li> + * <li>A related activity implements search. This is indicated by the + * presence of a "android.app.default_searchable" meta-data attribute. + * The value is a string naming the activity implementing search. In this + * case the factory will "redirect" and return the searchable data.</li> + * <li>No searchability data is provided. We return null here and other + * code will insert the "default" (e.g. contacts) search. + * + * TODO: cache the result in the map, and check the map first. + * TODO: it might make sense to implement the searchable reference as + * an application meta-data entry. This way we don't have to pepper each + * and every activity. + * TODO: can we skip the constructor step if it's a non-searchable? + * TODO: does it make sense to plug the default into a slot here for + * automatic return? Probably not, but it's one way to do it. + * + * @param activity The name of the current activity, or null if the + * activity does not define any explicit searchable metadata. + */ + public static SearchableInfo getSearchableInfo(Context context, + ComponentName activity) { + // Step 1. Is the result already hashed? (case 1) + SearchableInfo result; + synchronized (SearchableInfo.class) { + result = sSearchablesMap.get(activity); + if (result != null) return result; + } + + // Step 2. See if the current activity references a searchable. + // Note: Conceptually, this could be a while(true) loop, but there's + // no point in implementing reference chaining here and risking a loop. + // References must point directly to searchable activities. + + ActivityInfo ai = null; + XmlPullParser xml = null; + try { + ai = context.getPackageManager(). + getActivityInfo(activity, PackageManager.GET_META_DATA ); + String refActivityName = null; + + // First look for activity-specific reference + Bundle md = ai.metaData; + if (md != null) { + refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE); + } + // If not found, try for app-wide reference + if (refActivityName == null) { + md = ai.applicationInfo.metaData; + if (md != null) { + refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE); + } + } + + // Irrespective of source, if a reference was found, follow it. + if (refActivityName != null) + { + // An app or activity can declare that we should simply launch + // "system default search" if search is invoked. + if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) { + return getDefaultSearchable(); + } + String pkg = activity.getPackageName(); + ComponentName referredActivity; + if (refActivityName.charAt(0) == '.') { + referredActivity = new ComponentName(pkg, pkg + refActivityName); + } else { + referredActivity = new ComponentName(pkg, refActivityName); + } + + // Now try the referred activity, and if found, cache + // it against the original name so we can skip the check + synchronized (SearchableInfo.class) { + result = sSearchablesMap.get(referredActivity); + if (result != null) { + sSearchablesMap.put(activity, result); + return result; + } + } + } + } catch (PackageManager.NameNotFoundException e) { + // case 3: no metadata + } + + // Step 3. None found. Return null. + return null; + + } + + /** + * Super-factory. Builds an entire list (suitable for display) of + * activities that are searchable, by iterating the entire set of + * ACTION_SEARCH intents. + * + * Also clears the hash of all activities -> searches which will + * refill as the user clicks "search". + * + * This should only be done at startup and again if we know that the + * list has changed. + * + * TODO: every activity that provides a ACTION_SEARCH intent should + * also provide searchability meta-data. There are a bunch of checks here + * that, if data is not found, silently skip to the next activity. This + * won't help a developer trying to figure out why their activity isn't + * showing up in the list, but an exception here is too rough. I would + * like to find a better notification mechanism. + * + * TODO: sort the list somehow? UI choice. + * + * @param context a context we can use during this work + */ + public static void buildSearchableList(Context context) { + + // create empty hash & list + HashMap<ComponentName, SearchableInfo> newSearchablesMap + = new HashMap<ComponentName, SearchableInfo>(); + ArrayList<SearchableInfo> newSearchablesList + = new ArrayList<SearchableInfo>(); + + // use intent resolver to generate list of ACTION_SEARCH receivers + final PackageManager pm = context.getPackageManager(); + List<ResolveInfo> infoList; + final Intent intent = new Intent(Intent.ACTION_SEARCH); + infoList = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA); + + // analyze each one, generate a Searchables record, and record + if (infoList != null) { + int count = infoList.size(); + for (int ii = 0; ii < count; ii++) { + // for each component, try to find metadata + ResolveInfo info = infoList.get(ii); + ActivityInfo ai = info.activityInfo; + XmlResourceParser xml = ai.loadXmlMetaData(context.getPackageManager(), + MD_LABEL_SEARCHABLE); + if (xml == null) { + continue; + } + ComponentName cName = new ComponentName( + info.activityInfo.packageName, + info.activityInfo.name); + + SearchableInfo searchable = getActivityMetaData(context, xml, cName); + xml.close(); + + if (searchable != null) { + // no need to keep the context any longer. setup time is over. + searchable.mCacheActivityContext = null; + + newSearchablesList.add(searchable); + newSearchablesMap.put(cName, searchable); + } + } + } + + // record the final values as a coherent pair + synchronized (SearchableInfo.class) { + sSearchablesList = newSearchablesList; + sSearchablesMap = newSearchablesMap; + } + } + + /** + * Constructor + * + * Given a ComponentName, get the searchability info + * and build a local copy of it. Use the factory, not this. + * + * @param context runtime context + * @param attr The attribute set we found in the XML file, contains the values that are used to + * construct the object. + * @param cName The component name of the searchable activity + */ + private SearchableInfo(Context context, AttributeSet attr, final ComponentName cName) { + // 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); + if (mCacheActivityContext != null) { + TypedArray a = mCacheActivityContext.obtainStyledAttributes(attr, + com.android.internal.R.styleable.Searchable); + mSearchMode = a.getInt(com.android.internal.R.styleable.Searchable_searchMode, 0); + mLabelId = a.getResourceId(com.android.internal.R.styleable.Searchable_label, 0); + mHintId = a.getResourceId(com.android.internal.R.styleable.Searchable_hint, 0); + 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( + com.android.internal.R.styleable.Searchable_searchSuggestAuthority); + mSuggestPath = a.getString( + com.android.internal.R.styleable.Searchable_searchSuggestPath); + mSuggestSelection = a.getString( + com.android.internal.R.styleable.Searchable_searchSuggestSelection); + mSuggestIntentAction = a.getString( + com.android.internal.R.styleable.Searchable_searchSuggestIntentAction); + mSuggestIntentData = a.getString( + com.android.internal.R.styleable.Searchable_searchSuggestIntentData); + } + a.recycle(); + + // get package info for suggestions provider (if any) + if (mSuggestAuthority != null) { + ProviderInfo pi = + context.getPackageManager().resolveContentProvider(mSuggestAuthority, + 0); + if (pi != null) { + mSuggestProviderPackage = pi.packageName; + } + } + } + + // for now, implement some form of rules - minimal data + if (mLabelId != 0) { + mSearchable = true; + } else { + // Provide some help for developers instead of just silently discarding + Log.w(LOG_TAG, "Insufficient metadata to configure searchability for " + + cName.flattenToShortString()); + } + } + + /** + * Convert searchmode to flags. + */ + private void setSearchModeFlags() { + // decompose searchMode attribute + // TODO How do I reconcile these hardcoded values with the flag bits defined in + // in attrs.xml? e.g. android.R.id.filterMode = 0x010200a4 instead of just "1" + /* mFilterMode = (0 != (mSearchMode & 1)); */ + /* mQuickStart = (0 != (mSearchMode & 2)); */ + mBadgeLabel = (0 != (mSearchMode & 4)); + mBadgeIcon = (0 != (mSearchMode & 8)) && (mIconId != 0); + mQueryRewriteFromData = (0 != (mSearchMode & 0x10)); + mQueryRewriteFromText = (0 != (mSearchMode & 0x20)); + } + + /** + * Private class used to hold the "action key" configuration + */ + public class ActionKeyInfo implements Parcelable { + + public int mKeyCode = 0; + public String mQueryActionMsg; + public String mSuggestActionMsg; + public String mSuggestActionMsgColumn; + private ActionKeyInfo mNext; + + /** + * Create one object using attributeset as input data. + * @param context runtime context + * @param attr The attribute set we found in the XML file, contains the values that are used to + * construct the object. + * @param next We'll build these up using a simple linked list (since there are usually + * just zero or one). + */ + public ActionKeyInfo(Context context, AttributeSet attr, ActionKeyInfo next) { + TypedArray a = mCacheActivityContext.obtainStyledAttributes(attr, + com.android.internal.R.styleable.SearchableActionKey); + + mKeyCode = a.getInt( + com.android.internal.R.styleable.SearchableActionKey_keycode, 0); + mQueryActionMsg = a.getString( + com.android.internal.R.styleable.SearchableActionKey_queryActionMsg); + if (DBG_INHIBIT_SUGGESTIONS == 0) { + mSuggestActionMsg = a.getString( + com.android.internal.R.styleable.SearchableActionKey_suggestActionMsg); + mSuggestActionMsgColumn = a.getString( + com.android.internal.R.styleable.SearchableActionKey_suggestActionMsgColumn); + } + a.recycle(); + + // initialize any other fields + mNext = next; + + // sanity check. must have at least one action message, or invalidate the object. + if ((mQueryActionMsg == null) && + (mSuggestActionMsg == null) && + (mSuggestActionMsgColumn == null)) { + mKeyCode = 0; + } + } + + /** + * Instantiate a new ActionKeyInfo from the data in a Parcel that was + * previously written with {@link #writeToParcel(Parcel, int)}. + * + * @param in The Parcel containing the previously written ActionKeyInfo, + * positioned at the location in the buffer where it was written. + * @param next The value to place in mNext, creating a linked list + */ + public ActionKeyInfo(Parcel in, ActionKeyInfo next) { + mKeyCode = in.readInt(); + mQueryActionMsg = in.readString(); + mSuggestActionMsg = in.readString(); + mSuggestActionMsgColumn = in.readString(); + mNext = next; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mKeyCode); + dest.writeString(mQueryActionMsg); + dest.writeString(mSuggestActionMsg); + dest.writeString(mSuggestActionMsgColumn); + } + } + + /** + * If any action keys were defined for this searchable activity, look up and return. + * + * @param keyCode The key that was pressed + * @return Returns the ActionKeyInfo record, or null if none defined + */ + public ActionKeyInfo findActionKey(int keyCode) { + ActionKeyInfo info = mActionKeyList; + while (info != null) { + if (info.mKeyCode == keyCode) { + return info; + } + info = info.mNext; + } + return null; + } + + /** + * Get the metadata for a given activity + * + * TODO: clean up where we return null vs. where we throw exceptions. + * + * @param context runtime context + * @param xml XML parser for reading attributes + * @param cName The component name of the searchable activity + * + * @result A completely constructed SearchableInfo, or null if insufficient XML data for it + */ + private static SearchableInfo getActivityMetaData(Context context, XmlPullParser xml, + final ComponentName cName) { + SearchableInfo result = null; + + // in order to use the attributes mechanism, we have to walk the parser + // forward through the file until it's reading the tag of interest. + try { + int tagType = xml.next(); + while (tagType != XmlPullParser.END_DOCUMENT) { + if (tagType == XmlPullParser.START_TAG) { + if (xml.getName().equals(MD_XML_ELEMENT_SEARCHABLE)) { + AttributeSet attr = Xml.asAttributeSet(xml); + if (attr != null) { + result = new SearchableInfo(context, attr, cName); + // if the constructor returned a bad object, exit now. + if (! result.mSearchable) { + return null; + } + } + } else if (xml.getName().equals(MD_XML_ELEMENT_SEARCHABLE_ACTION_KEY)) { + if (result == null) { + // Can't process an embedded element if we haven't seen the enclosing + return null; + } + AttributeSet attr = Xml.asAttributeSet(xml); + if (attr != null) { + ActionKeyInfo keyInfo = result.new ActionKeyInfo(context, attr, + result.mActionKeyList); + // only add to list if it is was useable + if (keyInfo.mKeyCode != 0) { + result.mActionKeyList = keyInfo; + } + } + } + } + tagType = xml.next(); + } + } catch (XmlPullParserException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + return result; + } + + /** + * Return the "label" (user-visible name) of this searchable context. This must be + * accessed using the target (searchable) Activity's resources, not simply the context of the + * caller. + * + * @return Returns the resource Id + */ + public int getLabelId() { + return mLabelId; + } + + /** + * Return the resource Id of the hint text. This must be + * accessed using the target (searchable) Activity's resources, not simply the context of the + * caller. + * + * @return Returns the resource Id, or 0 if not specified by this package. + */ + public int getHintId() { + return mHintId; + } + + /** + * Return the icon Id specified by the Searchable_icon meta-data entry. This must be + * accessed using the target (searchable) Activity's resources, not simply the context of the + * caller. + * + * @return Returns the resource id. + */ + public int getIconId() { + return mIconId; + } + + /** + * Return the resource Id of replacement text for the "Search" button. + * + * @return Returns the resource Id, or 0 if not specified by this package. + */ + public int getSearchButtonText() { + return mSearchButtonText; + } + + /** + * Return the list of searchable activities, for use in the drop-down. + */ + public static ArrayList<SearchableInfo> getSearchablesList() { + synchronized (SearchableInfo.class) { + ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(sSearchablesList); + return result; + } + } + + /** + * Support for parcelable and aidl operations. + */ + public static final Parcelable.Creator<SearchableInfo> CREATOR + = new Parcelable.Creator<SearchableInfo>() { + public SearchableInfo createFromParcel(Parcel in) { + return new SearchableInfo(in); + } + + public SearchableInfo[] newArray(int size) { + return new SearchableInfo[size]; + } + }; + + /** + * Instantiate a new SearchableInfo from the data in a Parcel that was + * previously written with {@link #writeToParcel(Parcel, int)}. + * + * @param in The Parcel containing the previously written SearchableInfo, + * positioned at the location in the buffer where it was written. + */ + public SearchableInfo(Parcel in) { + mLabelId = in.readInt(); + mSearchActivity = ComponentName.readFromParcel(in); + mHintId = in.readInt(); + mSearchMode = in.readInt(); + mIconId = in.readInt(); + mSearchButtonText = in.readInt(); + setSearchModeFlags(); + + mSuggestAuthority = in.readString(); + mSuggestPath = in.readString(); + mSuggestSelection = in.readString(); + mSuggestIntentAction = in.readString(); + mSuggestIntentData = in.readString(); + + mActionKeyList = null; + int count = in.readInt(); + while (count-- > 0) { + mActionKeyList = new ActionKeyInfo(in, mActionKeyList); + } + + mSuggestProviderPackage = in.readString(); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mLabelId); + mSearchActivity.writeToParcel(dest, flags); + dest.writeInt(mHintId); + dest.writeInt(mSearchMode); + dest.writeInt(mIconId); + dest.writeInt(mSearchButtonText); + + dest.writeString(mSuggestAuthority); + dest.writeString(mSuggestPath); + dest.writeString(mSuggestSelection); + dest.writeString(mSuggestIntentAction); + dest.writeString(mSuggestIntentData); + + // This is usually a very short linked list so we'll just pre-count it + ActionKeyInfo nextKeyInfo = mActionKeyList; + int count = 0; + while (nextKeyInfo != null) { + ++count; + nextKeyInfo = nextKeyInfo.mNext; + } + dest.writeInt(count); + // Now write count of 'em + nextKeyInfo = mActionKeyList; + while (count-- > 0) { + nextKeyInfo.writeToParcel(dest, flags); + } + + dest.writeString(mSuggestProviderPackage); + } +} diff --git a/core/java/android/server/search/package.html b/core/java/android/server/search/package.html new file mode 100644 index 0000000..c9f96a6 --- /dev/null +++ b/core/java/android/server/search/package.html @@ -0,0 +1,5 @@ +<body> + +{@hide} + +</body> |