From c19c065b6d0e1a088780c4dca27a1404d5926765 Mon Sep 17 00:00:00 2001 From: Martijn Coenen Date: Mon, 8 Sep 2014 21:34:22 -0700 Subject: Reinstate RF field events. These are generic enough to maintain in AOSP, and can still be protected by nfcee_access.xml like they used to be. Bug: 16653617 Change-Id: Id604fcc45d4263b24fea2f16ca6a24e25f8580c3 --- etc/sample_nfcee_access.xml | 45 ++++ nci/jni/NativeNfcManager.cpp | 22 ++ .../com/android/nfc/dhimpl/NativeNfcManager.java | 9 + nxp/jni/com_android_nfc_NativeNfcManager.cpp | 10 + .../com/android/nfc/dhimpl/NativeNfcManager.java | 8 + src/com/android/nfc/DeviceHost.java | 4 + src/com/android/nfc/NfcService.java | 105 +++++++- src/com/android/nfc/NfceeAccessControl.java | 289 +++++++++++++++++++++ 8 files changed, 484 insertions(+), 8 deletions(-) create mode 100644 etc/sample_nfcee_access.xml create mode 100644 src/com/android/nfc/NfceeAccessControl.java diff --git a/etc/sample_nfcee_access.xml b/etc/sample_nfcee_access.xml new file mode 100644 index 00000000..d6c8707d --- /dev/null +++ b/etc/sample_nfcee_access.xml @@ -0,0 +1,45 @@ + + + + diff --git a/nci/jni/NativeNfcManager.cpp b/nci/jni/NativeNfcManager.cpp index fa1b2d21..57dcb809 100755 --- a/nci/jni/NativeNfcManager.cpp +++ b/nci/jni/NativeNfcManager.cpp @@ -84,6 +84,8 @@ namespace android jmethodID gCachedNfcManagerNotifyHostEmuActivated; jmethodID gCachedNfcManagerNotifyHostEmuData; jmethodID gCachedNfcManagerNotifyHostEmuDeactivated; + jmethodID gCachedNfcManagerNotifyRfFieldActivated; + jmethodID gCachedNfcManagerNotifyRfFieldDeactivated; const char* gNativeP2pDeviceClassName = "com/android/nfc/dhimpl/NativeP2pDevice"; const char* gNativeLlcpServiceSocketClassName = "com/android/nfc/dhimpl/NativeLlcpServiceSocket"; const char* gNativeLlcpConnectionlessSocketClassName = "com/android/nfc/dhimpl/NativeLlcpConnectionlessSocket"; @@ -562,6 +564,11 @@ static jboolean nfcManager_initNativeStruc (JNIEnv* e, jobject o) gCachedNfcManagerNotifyHostEmuDeactivated = e->GetMethodID(cls.get(), "notifyHostEmuDeactivated", "()V"); + gCachedNfcManagerNotifyRfFieldActivated = e->GetMethodID(cls.get(), + "notifyRfFieldActivated", "()V"); + gCachedNfcManagerNotifyRfFieldDeactivated = e->GetMethodID(cls.get(), + "notifyRfFieldDeactivated", "()V"); + if (nfc_jni_cache_object(e, gNativeNfcTagClassName, &(nat->cached_NfcTag)) == -1) { ALOGE ("%s: fail cache NativeNfcTag", __FUNCTION__); @@ -647,6 +654,21 @@ void nfaDeviceManagementCallback (UINT8 dmEvent, tNFA_DM_CBACK_DATA* eventData) case NFA_DM_RF_FIELD_EVT: ALOGD ("%s: NFA_DM_RF_FIELD_EVT; status=0x%X; field status=%u", __FUNCTION__, eventData->rf_field.status, eventData->rf_field.rf_field_status); + if (!sP2pActive && eventData->rf_field.status == NFA_STATUS_OK) + { + struct nfc_jni_native_data *nat = getNative(NULL, NULL); + JNIEnv* e = NULL; + ScopedAttach attach(nat->vm, &e); + if (e == NULL) + { + ALOGE ("jni env is null"); + return; + } + if (eventData->rf_field.rf_field_status == NFA_DM_RF_FIELD_ON) + e->CallVoidMethod (nat->manager, android::gCachedNfcManagerNotifyRfFieldActivated); + else + e->CallVoidMethod (nat->manager, android::gCachedNfcManagerNotifyRfFieldDeactivated); + } break; case NFA_DM_NFCC_TRANSPORT_ERR_EVT: diff --git a/nci/src/com/android/nfc/dhimpl/NativeNfcManager.java b/nci/src/com/android/nfc/dhimpl/NativeNfcManager.java index 080bca01..d327f62a 100755 --- a/nci/src/com/android/nfc/dhimpl/NativeNfcManager.java +++ b/nci/src/com/android/nfc/dhimpl/NativeNfcManager.java @@ -335,4 +335,13 @@ public class NativeNfcManager implements DeviceHost { private void notifyHostEmuDeactivated() { mListener.onHostCardEmulationDeactivated(); } + + private void notifyRfFieldActivated() { + mListener.onRemoteFieldActivated(); + } + + private void notifyRfFieldDeactivated() { + mListener.onRemoteFieldDeactivated(); + } + } diff --git a/nxp/jni/com_android_nfc_NativeNfcManager.cpp b/nxp/jni/com_android_nfc_NativeNfcManager.cpp index 489a615b..c80141e6 100644 --- a/nxp/jni/com_android_nfc_NativeNfcManager.cpp +++ b/nxp/jni/com_android_nfc_NativeNfcManager.cpp @@ -50,6 +50,8 @@ static jmethodID cached_NfcManager_notifyLlcpLinkActivation; static jmethodID cached_NfcManager_notifyLlcpLinkDeactivated; static jmethodID cached_NfcManager_notifyTargetDeselected; +static jmethodID cached_NfcManager_notifyRfFieldActivated; +static jmethodID cached_NfcManager_notifyRfFieldDeactivated; namespace android { phLibNfc_Handle storedHandle = 0; @@ -1169,11 +1171,13 @@ static void nfc_jni_transaction_callback(void *context, case phLibNfc_eSE_EvtFieldOn: { TRACE("> SE EVT_FIELD_ON"); + e->CallVoidMethod(nat->manager, cached_NfcManager_notifyRfFieldActivated); }break; case phLibNfc_eSE_EvtFieldOff: { TRACE("> SE EVT_FIELD_OFF"); + e->CallVoidMethod(nat->manager, cached_NfcManager_notifyRfFieldDeactivated); }break; default: @@ -1605,6 +1609,12 @@ static jboolean com_android_nfc_NfcManager_init_native_struc(JNIEnv *e, jobject cached_NfcManager_notifyLlcpLinkDeactivated = e->GetMethodID(cls, "notifyLlcpLinkDeactivated","(Lcom/android/nfc/dhimpl/NativeP2pDevice;)V"); + cached_NfcManager_notifyRfFieldActivated = e->GetMethodID(cls, + "notifyRfFieldActivated", "()V"); + + cached_NfcManager_notifyRfFieldDeactivated = e->GetMethodID(cls, + "notifyRfFieldDeactivated", "()V"); + if(nfc_jni_cache_object(e,"com/android/nfc/dhimpl/NativeNfcTag",&(nat->cached_NfcTag)) == -1) { ALOGD("Native Structure initialization failed"); diff --git a/nxp/src/com/android/nfc/dhimpl/NativeNfcManager.java b/nxp/src/com/android/nfc/dhimpl/NativeNfcManager.java index 18336233..cc73db3b 100755 --- a/nxp/src/com/android/nfc/dhimpl/NativeNfcManager.java +++ b/nxp/src/com/android/nfc/dhimpl/NativeNfcManager.java @@ -373,4 +373,12 @@ public class NativeNfcManager implements DeviceHost { private void notifyLlcpLinkDeactivated(NativeP2pDevice device) { mListener.onLlcpLinkDeactivated(device); } + + private void notifyRfFieldActivated() { + mListener.onRemoteFieldActivated(); + } + + private void notifyRfFieldDeactivated() { + mListener.onRemoteFieldDeactivated(); + } } diff --git a/src/com/android/nfc/DeviceHost.java b/src/com/android/nfc/DeviceHost.java index 58b20696..07710119 100644 --- a/src/com/android/nfc/DeviceHost.java +++ b/src/com/android/nfc/DeviceHost.java @@ -43,6 +43,10 @@ public interface DeviceHost { public void onLlcpLinkDeactivated(NfcDepEndpoint device); public void onLlcpFirstPacketReceived(NfcDepEndpoint device); + + public void onRemoteFieldActivated(); + + public void onRemoteFieldDeactivated(); } public interface TagEndpoint { diff --git a/src/com/android/nfc/NfcService.java b/src/com/android/nfc/NfcService.java index a646c246..0b110c77 100755 --- a/src/com/android/nfc/NfcService.java +++ b/src/com/android/nfc/NfcService.java @@ -27,6 +27,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Resources.NotFoundException; import android.media.AudioManager; @@ -78,6 +79,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -98,14 +100,16 @@ public class NfcService implements DeviceHostListener { static final String PREF_AIRPLANE_OVERRIDE = "airplane_override"; static final int MSG_NDEF_TAG = 0; - static final int MSG_LLCP_LINK_ACTIVATION = 2; - static final int MSG_LLCP_LINK_DEACTIVATED = 3; - static final int MSG_MOCK_NDEF = 7; - static final int MSG_LLCP_LINK_FIRST_PACKET = 15; - static final int MSG_ROUTE_AID = 16; - static final int MSG_UNROUTE_AID = 17; - static final int MSG_COMMIT_ROUTING = 18; - static final int MSG_INVOKE_BEAM = 19; + static final int MSG_LLCP_LINK_ACTIVATION = 1; + static final int MSG_LLCP_LINK_DEACTIVATED = 2; + static final int MSG_MOCK_NDEF = 3; + static final int MSG_LLCP_LINK_FIRST_PACKET = 4; + static final int MSG_ROUTE_AID = 5; + static final int MSG_UNROUTE_AID = 6; + static final int MSG_COMMIT_ROUTING = 7; + static final int MSG_INVOKE_BEAM = 8; + static final int MSG_RF_FIELD_ACTIVATED = 9; + static final int MSG_RF_FIELD_DEACTIVATED = 10; static final int TASK_ENABLE = 1; static final int TASK_DISABLE = 2; @@ -138,6 +142,12 @@ public class NfcService implements DeviceHostListener { // the Beam animation when called through the share menu. static final int INVOKE_BEAM_DELAY_MS = 1000; + // RF field events as defined in NFC extras + public static final String ACTION_RF_FIELD_ON_DETECTED = + "com.android.nfc_extras.action.RF_FIELD_ON_DETECTED"; + public static final String ACTION_RF_FIELD_OFF_DETECTED = + "com.android.nfc_extras.action.RF_FIELD_OFF_DETECTED"; + // for use with playSound() public static final int SOUND_START = 0; public static final int SOUND_END = 1; @@ -157,6 +167,10 @@ public class NfcService implements DeviceHostListener { new ReaderModeDeathRecipient(); private final NfcUnlockManager mNfcUnlockManager; + private final NfceeAccessControl mNfceeAccessControl; + + List mInstalledPackages; // cached version of installed packages + // fields below are used in multiple threads and protected by synchronized(this) final HashMap mObjectMap = new HashMap(); int mScreenState; @@ -260,6 +274,15 @@ public class NfcService implements DeviceHostListener { sendMessage(NfcService.MSG_LLCP_LINK_FIRST_PACKET, device); } + @Override + public void onRemoteFieldActivated() { + sendMessage(NfcService.MSG_RF_FIELD_ACTIVATED, null); + } + + public void onRemoteFieldDeactivated() { + sendMessage(NfcService.MSG_RF_FIELD_DEACTIVATED, null); + } + final class ReaderModeParams { public int flags; public IAppCallback callback; @@ -305,6 +328,8 @@ public class NfcService implements DeviceHostListener { mPrefs = mContext.getSharedPreferences(PREF, Context.MODE_PRIVATE); mPrefsEditor = mPrefs.edit(); + mNfceeAccessControl = new NfceeAccessControl(mContext); + mState = NfcAdapter.STATE_OFF; mIsNdefPushEnabled = mPrefs.getBoolean(PREF_NDEF_PUSH_ON, NDEF_PUSH_ON_DEFAULT); setBeamShareActivityState(mIsNdefPushEnabled); @@ -331,6 +356,18 @@ public class NfcService implements DeviceHostListener { registerForAirplaneMode(filter); mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, null); + IntentFilter ownerFilter = new IntentFilter(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); + ownerFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + mContext.registerReceiver(mOwnerReceiver, ownerFilter); + + ownerFilter = new IntentFilter(); + ownerFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + ownerFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + ownerFilter.addDataScheme("package"); + mContext.registerReceiver(mOwnerReceiver, ownerFilter); + + updatePackageCache(); + PackageManager pm = mContext.getPackageManager(); mIsHceCapable = pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION); if (mIsHceCapable) { @@ -376,6 +413,14 @@ public class NfcService implements DeviceHostListener { } } + void updatePackageCache() { + PackageManager pm = mContext.getPackageManager(); + List packages = pm.getInstalledPackages(0, UserHandle.USER_OWNER); + synchronized (this) { + mInstalledPackages = packages; + } + } + /** * Manages tasks that involve turning on/off the NFC controller. *

@@ -1664,12 +1709,37 @@ public class NfcService implements DeviceHostListener { case MSG_LLCP_LINK_FIRST_PACKET: mP2pLinkManager.onLlcpFirstPacketReceived(); break; + case MSG_RF_FIELD_ACTIVATED: + Intent fieldOnIntent = new Intent(ACTION_RF_FIELD_ON_DETECTED); + sendNfcEeAccessProtectedBroadcast(fieldOnIntent); + break; + case MSG_RF_FIELD_DEACTIVATED: + Intent fieldOffIntent = new Intent(ACTION_RF_FIELD_OFF_DETECTED); + sendNfcEeAccessProtectedBroadcast(fieldOffIntent); + break; default: Log.e(TAG, "Unknown message received"); break; } } + private void sendNfcEeAccessProtectedBroadcast(Intent intent) { + intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); + // Resume app switches so the receivers can start activites without delay + mNfcDispatcher.resumeAppSwitches(); + + synchronized (this) { + for (PackageInfo pkg : mInstalledPackages) { + if (pkg != null && pkg.applicationInfo != null) { + if (mNfceeAccessControl.check(pkg.applicationInfo)) { + intent.setPackage(pkg.packageName); + mContext.sendBroadcast(intent); + } + } + } + } + } + private boolean llcpActivated(NfcDepEndpoint device) { Log.d(TAG, "LLCP Activation message"); @@ -1832,6 +1902,25 @@ public class NfcService implements DeviceHostListener { } }; + private final BroadcastReceiver mOwnerReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(Intent.ACTION_PACKAGE_REMOVED) || + action.equals(Intent.ACTION_PACKAGE_ADDED) || + action.equals(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE) || + action.equals(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE)) { + updatePackageCache(); + + if (action.equals(Intent.ACTION_PACKAGE_REMOVED)) { + // Clear the NFCEE access cache in case a UID gets recycled + mNfceeAccessControl.invalidateCache(); + } + } + } + }; + + /** * Returns true if airplane mode is currently on */ diff --git a/src/com/android/nfc/NfceeAccessControl.java b/src/com/android/nfc/NfceeAccessControl.java new file mode 100644 index 00000000..e872d284 --- /dev/null +++ b/src/com/android/nfc/NfceeAccessControl.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.nfc; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Environment; +import android.util.Log; + +public class NfceeAccessControl { + static final String TAG = "NfceeAccess"; + static final boolean DBG = true; + + public static final String NFCEE_ACCESS_PATH = "/etc/nfcee_access.xml"; + + /** + * Map of signatures to valid packages names, as read from nfcee_access.xml. + * An empty list of package names indicates that any package + * with this signature is allowed. + */ + final HashMap mNfceeAccess; // contents final after onCreate() + + /** + * Map from UID to NFCEE access, used as a cache. + * Note: if a UID contains multiple packages they must all be + * signed with the same certificate so in effect UID == certificate + * used to sign the package. + */ + final HashMap mUidCache; // contents guarded by this + + final Context mContext; + final boolean mDebugPrintSignature; + + NfceeAccessControl(Context context) { + mContext = context; + mNfceeAccess = new HashMap(); + mUidCache = new HashMap(); + mDebugPrintSignature = parseNfceeAccess(); + } + + /** + * Check if the {uid, pkg} combination may use NFCEE. + * Also verify with package manager that this {uid, pkg} combination + * is valid if it is not cached. + */ + public boolean check(int uid, String pkg) { + synchronized (this) { + Boolean cached = mUidCache.get(uid); + if (cached != null) { + return cached; + } + + boolean access = false; + + // Ensure the claimed package is present in the calling UID + PackageManager pm = mContext.getPackageManager(); + String[] pkgs = pm.getPackagesForUid(uid); + for (String uidPkg : pkgs) { + if (uidPkg.equals(pkg)) { + // Ensure the package has access permissions + if (checkPackageNfceeAccess(pkg)) { + access = true; + } + break; + } + } + + mUidCache.put(uid, access); + return access; + } + } + + /** + * Check if the given ApplicationInfo may use the NFCEE. + * Assumes ApplicationInfo came from package manager, + * so no need to confirm {uid, pkg} is valid. + */ + public boolean check(ApplicationInfo info) { + synchronized (this) { + Boolean access = mUidCache.get(info.uid); + if (access == null) { + access = checkPackageNfceeAccess(info.packageName); + mUidCache.put(info.uid, access); + } + return access; + } + } + + public void invalidateCache() { + synchronized (this) { + mUidCache.clear(); + } + } + + /** + * Check with package manager if the pkg may use NFCEE. + * Does not use cache. + */ + boolean checkPackageNfceeAccess(String pkg) { + PackageManager pm = mContext.getPackageManager(); + try { + PackageInfo info = pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES); + if (info.signatures == null) { + return false; + } + + for (Signature s : info.signatures){ + if (s == null) { + continue; + } + String[] packages = mNfceeAccess.get(s); + if (packages == null) { + continue; + } + if (packages.length == 0) { + // wildcard access + if (DBG) Log.d(TAG, "Granted NFCEE access to " + pkg + " (wildcard)"); + return true; + } + for (String p : packages) { + if (pkg.equals(p)) { + // explicit package access + if (DBG) Log.d(TAG, "Granted access to " + pkg + " (explicit)"); + return true; + } + } + } + + if (mDebugPrintSignature) { + Log.w(TAG, "denied NFCEE access for " + pkg + " with signature:"); + for (Signature s : info.signatures) { + if (s != null) { + Log.w(TAG, s.toCharsString()); + } + } + } + } catch (NameNotFoundException e) { + // ignore + } + return false; + } + + /** + * Parse nfcee_access.xml, populate mNfceeAccess + * Policy is to ignore unexpected XML elements and continue processing, + * except for obvious errors within a group since they might cause + * package names to by ignored and therefore wildcard access granted + * by mistake. Those errors invalidate the entire group. + */ + boolean parseNfceeAccess() { + File file = new File(Environment.getRootDirectory(), NFCEE_ACCESS_PATH); + FileReader reader = null; + boolean debug = false; + try { + reader = new FileReader(file); + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + XmlPullParser parser = factory.newPullParser(); + parser.setInput(reader); + + int event; + ArrayList packages = new ArrayList(); + Signature signature = null; + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); + while (true) { + event = parser.next(); + String tag = parser.getName(); + if (event == XmlPullParser.START_TAG && "signer".equals(tag)) { + signature = null; + packages.clear(); + for (int i = 0; i < parser.getAttributeCount(); i++) { + if ("android:signature".equals(parser.getAttributeName(i))) { + signature = new Signature(parser.getAttributeValue(i)); + break; + } + } + if (signature == null) { + Log.w(TAG, "signer tag is missing android:signature attribute, igorning"); + continue; + } + if (mNfceeAccess.containsKey(signature)) { + Log.w(TAG, "duplicate signature, ignoring"); + signature = null; + continue; + } + } else if (event == XmlPullParser.END_TAG && "signer".equals(tag)) { + if (signature == null) { + Log.w(TAG, "mis-matched signer tag"); + continue; + } + mNfceeAccess.put(signature, packages.toArray(new String[0])); + packages.clear(); + } else if (event == XmlPullParser.START_TAG && "package".equals(tag)) { + if (signature == null) { + Log.w(TAG, "ignoring unnested packge tag"); + continue; + } + String name = null; + for (int i = 0; i < parser.getAttributeCount(); i++) { + if ("android:name".equals(parser.getAttributeName(i))) { + name = parser.getAttributeValue(i); + break; + } + } + if (name == null) { + Log.w(TAG, "package missing android:name, ignoring signer group"); + signature = null; // invalidate signer + continue; + } + // check for duplicate package names + if (packages.contains(name)) { + Log.w(TAG, "duplicate package name in signer group, ignoring"); + continue; + } + packages.add(name); + } else if (event == XmlPullParser.START_TAG && "debug".equals(tag)) { + debug = true; + } else if (event == XmlPullParser.END_DOCUMENT) { + break; + } + } + } catch (XmlPullParserException e) { + Log.w(TAG, "failed to load NFCEE access list", e); + mNfceeAccess.clear(); // invalidate entire access list + } catch (FileNotFoundException e) { + Log.w(TAG, "could not find " + NFCEE_ACCESS_PATH + ", no NFCEE access allowed"); + } catch (IOException e) { + Log.e(TAG, "Failed to load NFCEE access list", e); + mNfceeAccess.clear(); // invalidate entire access list + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e2) { } + } + } + Log.i(TAG, "read " + mNfceeAccess.size() + " signature(s) for NFCEE access"); + return debug; + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("mNfceeAccess="); + for (Signature s : mNfceeAccess.keySet()) { + pw.printf("\t%s [", s.toCharsString()); + String[] ps = mNfceeAccess.get(s); + for (String p : ps) { + pw.printf("%s, ", p); + } + pw.println("]"); + } + synchronized (this) { + pw.println("mNfceeUidCache="); + for (Integer uid : mUidCache.keySet()) { + Boolean b = mUidCache.get(uid); + pw.printf("\t%d %s\n", uid, b); + } + } + } +} -- cgit v1.2.3