summaryrefslogtreecommitdiffstats
path: root/core/java/android/server
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/server')
-rw-r--r--core/java/android/server/BluetoothDeviceService.java1175
-rw-r--r--core/java/android/server/BluetoothEventLoop.java289
-rw-r--r--core/java/android/server/checkin/CheckinProvider.java388
-rw-r--r--core/java/android/server/checkin/FallbackCheckinService.java45
-rw-r--r--core/java/android/server/checkin/package.html5
-rw-r--r--core/java/android/server/data/BuildData.java89
-rw-r--r--core/java/android/server/data/CrashData.java145
-rw-r--r--core/java/android/server/data/StackTraceElementData.java80
-rw-r--r--core/java/android/server/data/ThrowableData.java138
-rwxr-xr-xcore/java/android/server/data/package.html5
-rwxr-xr-xcore/java/android/server/package.html5
-rw-r--r--core/java/android/server/search/SearchManagerService.java156
-rw-r--r--core/java/android/server/search/SearchableInfo.aidl19
-rw-r--r--core/java/android/server/search/SearchableInfo.java747
-rw-r--r--core/java/android/server/search/package.html5
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>