Home | History | Annotate | Download | only in server
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server;
     18 
     19 import android.annotation.Nullable;
     20 
     21 import android.content.BroadcastReceiver;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.IntentFilter;
     25 import android.content.pm.ActivityInfo;
     26 import android.content.pm.ApplicationInfo;
     27 import android.content.pm.PackageManager;
     28 import android.content.pm.ResolveInfo;
     29 import android.net.Uri;
     30 import android.os.Binder;
     31 import android.os.Build;
     32 import android.os.Handler;
     33 import android.os.Looper;
     34 import android.os.Message;
     35 import android.os.UserHandle;
     36 import android.provider.MediaStore;
     37 import android.system.ErrnoException;
     38 import android.system.Os;
     39 import android.system.OsConstants;
     40 import android.system.StructStat;
     41 import android.util.ArraySet;
     42 import android.util.Slog;
     43 
     44 import com.android.internal.app.ResolverActivity;
     45 import com.android.internal.os.BackgroundThread;
     46 import com.android.internal.util.DumpUtils;
     47 
     48 import dalvik.system.DexFile;
     49 import dalvik.system.VMRuntime;
     50 
     51 import java.io.FileDescriptor;
     52 import java.io.Closeable;
     53 import java.io.InputStream;
     54 import java.io.DataInputStream;
     55 import java.io.IOException;
     56 import java.io.EOFException;
     57 import java.io.PrintWriter;
     58 import java.util.ArrayList;
     59 
     60 import java.util.zip.ZipFile;
     61 import java.util.zip.ZipException;
     62 import java.util.zip.ZipEntry;
     63 
     64 /**
     65  * <p>PinnerService pins important files for key processes in memory.</p>
     66  * <p>Files to pin are specified in the config_defaultPinnerServiceFiles
     67  * overlay.</p>
     68  * <p>Pin the default camera application if specified in config_pinnerCameraApp.</p>
     69  */
     70 public final class PinnerService extends SystemService {
     71     private static final boolean DEBUG = false;
     72     private static final String TAG = "PinnerService";
     73     private static final int MAX_CAMERA_PIN_SIZE = 80 * (1 << 20); //80MB max
     74     private static final String PIN_META_FILENAME = "pinlist.meta";
     75     private static final int PAGE_SIZE = (int) Os.sysconf(OsConstants._SC_PAGESIZE);
     76 
     77     private final Context mContext;
     78     private final boolean mShouldPinCamera;
     79 
     80     /* These lists protected by PinnerService monitor lock */
     81     private final ArrayList<PinnedFile> mPinnedFiles = new ArrayList<PinnedFile>();
     82     private final ArrayList<PinnedFile> mPinnedCameraFiles = new ArrayList<PinnedFile>();
     83 
     84     private BinderService mBinderService;
     85     private PinnerHandler mPinnerHandler = null;
     86 
     87     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
     88         @Override
     89         public void onReceive(Context context, Intent intent) {
     90           // If this user's camera app has been updated, update pinned files accordingly.
     91           if (intent.getAction() == Intent.ACTION_PACKAGE_REPLACED) {
     92                 Uri packageUri = intent.getData();
     93                 String packageName = packageUri.getSchemeSpecificPart();
     94                 ArraySet<String> updatedPackages = new ArraySet<>();
     95                 updatedPackages.add(packageName);
     96                 update(updatedPackages);
     97             }
     98         }
     99     };
    100 
    101     public PinnerService(Context context) {
    102         super(context);
    103 
    104         mContext = context;
    105         mShouldPinCamera = context.getResources().getBoolean(
    106                 com.android.internal.R.bool.config_pinnerCameraApp);
    107         mPinnerHandler = new PinnerHandler(BackgroundThread.get().getLooper());
    108 
    109         IntentFilter filter = new IntentFilter();
    110         filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
    111         filter.addDataScheme("package");
    112         mContext.registerReceiver(mBroadcastReceiver, filter);
    113     }
    114 
    115     @Override
    116     public void onStart() {
    117         if (DEBUG) {
    118             Slog.i(TAG, "Starting PinnerService");
    119         }
    120         mBinderService = new BinderService();
    121         publishBinderService("pinner", mBinderService);
    122         publishLocalService(PinnerService.class, this);
    123 
    124         mPinnerHandler.obtainMessage(PinnerHandler.PIN_ONSTART_MSG).sendToTarget();
    125         mPinnerHandler.obtainMessage(PinnerHandler.PIN_CAMERA_MSG, UserHandle.USER_SYSTEM, 0)
    126                 .sendToTarget();
    127     }
    128 
    129     /**
    130      * Pin camera on user switch.
    131      * If more than one user is using the device
    132      * each user may set a different preference for the camera app.
    133      * Make sure that user's preference is pinned into memory.
    134      */
    135     @Override
    136     public void onSwitchUser(int userHandle) {
    137         mPinnerHandler.obtainMessage(PinnerHandler.PIN_CAMERA_MSG, userHandle, 0).sendToTarget();
    138     }
    139 
    140     /**
    141      * Update the currently pinned files.
    142      * Specifically, this only updates camera pinning.
    143      * The other files pinned in onStart will not need to be updated.
    144      */
    145     public void update(ArraySet<String> updatedPackages) {
    146         ApplicationInfo cameraInfo = getCameraInfo(UserHandle.USER_SYSTEM);
    147         if (cameraInfo != null && updatedPackages.contains(cameraInfo.packageName)) {
    148             Slog.i(TAG, "Updating pinned files.");
    149             mPinnerHandler.obtainMessage(PinnerHandler.PIN_CAMERA_MSG, UserHandle.USER_SYSTEM, 0)
    150                     .sendToTarget();
    151         }
    152     }
    153 
    154     /**
    155      * Handler for on start pinning message
    156      */
    157     private void handlePinOnStart() {
    158          // Files to pin come from the overlay and can be specified per-device config
    159         String[] filesToPin = mContext.getResources().getStringArray(
    160                 com.android.internal.R.array.config_defaultPinnerServiceFiles);
    161         // Continue trying to pin each file even if we fail to pin some of them
    162         for (String fileToPin : filesToPin) {
    163             PinnedFile pf = pinFile(fileToPin,
    164                                     Integer.MAX_VALUE,
    165                                     /*attemptPinIntrospection=*/false);
    166             if (pf == null) {
    167                 Slog.e(TAG, "Failed to pin file = " + fileToPin);
    168                 continue;
    169             }
    170 
    171             synchronized (this) {
    172                 mPinnedFiles.add(pf);
    173             }
    174         }
    175     }
    176 
    177     /**
    178      * Handler for camera pinning message
    179      */
    180     private void handlePinCamera(int userHandle) {
    181         if (!mShouldPinCamera) return;
    182         if (!pinCamera(userHandle)) {
    183             if (DEBUG) {
    184                 Slog.v(TAG, "Failed to pin camera.");
    185             }
    186         }
    187     }
    188 
    189     private void unpinCameraApp() {
    190         ArrayList<PinnedFile> pinnedCameraFiles;
    191         synchronized (this) {
    192             pinnedCameraFiles = new ArrayList<>(mPinnedCameraFiles);
    193             mPinnedCameraFiles.clear();
    194         }
    195         for (PinnedFile pinnedFile : pinnedCameraFiles) {
    196             pinnedFile.close();
    197         }
    198     }
    199 
    200     private boolean isResolverActivity(ActivityInfo info) {
    201         return ResolverActivity.class.getName().equals(info.name);
    202     }
    203 
    204     private ApplicationInfo getCameraInfo(int userHandle) {
    205         //  find the camera via an intent
    206         //  use INTENT_ACTION_STILL_IMAGE_CAMERA instead of _SECURE.  On a
    207         //  device without a fbe enabled, the _SECURE intent will never get set.
    208         Intent cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
    209         PackageManager pm = mContext.getPackageManager();
    210         ResolveInfo cameraResolveInfo = pm.resolveActivityAsUser(cameraIntent,
    211                 PackageManager.MATCH_DEFAULT_ONLY | PackageManager.MATCH_DIRECT_BOOT_AWARE
    212                         | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
    213                 userHandle);
    214         if (cameraResolveInfo == null ) {
    215             //this is not necessarily an error
    216             if (DEBUG) {
    217               Slog.v(TAG, "Unable to resolve camera intent");
    218             }
    219             return null;
    220         }
    221 
    222         if (isResolverActivity(cameraResolveInfo.activityInfo))
    223         {
    224             if (DEBUG) {
    225               Slog.v(TAG, "cameraIntent returned resolverActivity");
    226             }
    227             return null;
    228         }
    229 
    230         return cameraResolveInfo.activityInfo.applicationInfo;
    231     }
    232 
    233     /**
    234      * If the camera app is already pinned, unpin and repin it.
    235      */
    236     private boolean pinCamera(int userHandle){
    237         ApplicationInfo cameraInfo = getCameraInfo(userHandle);
    238         if (cameraInfo == null) {
    239             return false;
    240         }
    241 
    242         //unpin after checking that the camera intent has resolved
    243         //this prevents us from thrashing when switching users with
    244         //FBE enabled, because the intent won't resolve until the unlock
    245         unpinCameraApp();
    246 
    247         //pin APK
    248         String camAPK = cameraInfo.sourceDir;
    249         PinnedFile pf = pinFile(camAPK,
    250                                 MAX_CAMERA_PIN_SIZE,
    251                                 /*attemptPinIntrospection=*/true);
    252         if (pf == null) {
    253             Slog.e(TAG, "Failed to pin " + camAPK);
    254             return false;
    255         }
    256         if (DEBUG) {
    257             Slog.i(TAG, "Pinned " + pf.fileName);
    258         }
    259         synchronized (this) {
    260             mPinnedCameraFiles.add(pf);
    261         }
    262 
    263         // determine the ABI from either ApplicationInfo or Build
    264         String arch = "arm";
    265         if (cameraInfo.primaryCpuAbi != null) {
    266             if (VMRuntime.is64BitAbi(cameraInfo.primaryCpuAbi)) {
    267                 arch = arch + "64";
    268             }
    269         } else {
    270             if (VMRuntime.is64BitAbi(Build.SUPPORTED_ABIS[0])) {
    271                 arch = arch + "64";
    272             }
    273         }
    274 
    275         // get the path to the odex or oat file
    276         String baseCodePath = cameraInfo.getBaseCodePath();
    277         String[] files = null;
    278         try {
    279             files = DexFile.getDexFileOutputPaths(baseCodePath, arch);
    280         } catch (IOException ioe) {}
    281         if (files == null) {
    282             return true;
    283         }
    284 
    285         //not pinning the oat/odex is not a fatal error
    286         for (String file : files) {
    287             pf = pinFile(file, MAX_CAMERA_PIN_SIZE, /*attemptPinIntrospection=*/false);
    288             if (pf != null) {
    289                 synchronized (this) {
    290                     mPinnedCameraFiles.add(pf);
    291                 }
    292                 if (DEBUG) {
    293                     Slog.i(TAG, "Pinned " + pf.fileName);
    294                 }
    295             }
    296         }
    297 
    298         return true;
    299     }
    300 
    301 
    302     /** mlock length bytes of fileToPin in memory
    303      *
    304      * If attemptPinIntrospection is true, then treat the file to pin as a zip file and
    305      * look for a "pinlist.meta" file in the archive root directory. The structure of this
    306      * file is a PINLIST_META as described below:
    307      *
    308      * <pre>
    309      *   PINLIST_META: PIN_RANGE*
    310      *   PIN_RANGE: PIN_START PIN_LENGTH
    311      *   PIN_START: big endian i32: offset in bytes of pin region from file start
    312      *   PIN_LENGTH: big endian i32: length of pin region in bytes
    313      * </pre>
    314      *
    315      * (We use big endian because that's what DataInputStream is hardcoded to use.)
    316      *
    317      * If attemptPinIntrospection is false, then we use a single implicit PIN_RANGE of (0,
    318      * maxBytesToPin); that is, we attempt to pin the first maxBytesToPin bytes of the file.
    319      *
    320      * After we open a file, we march through the list of pin ranges and attempt to pin
    321      * each one, stopping after we've pinned maxBytesToPin bytes. (We may truncate the last
    322      * pinned range to fit.)  In this way, by choosing to emit certain PIN_RANGE pairs
    323      * before others, file generators can express pins in priority order, making most
    324      * effective use of the pinned-page quota.
    325      *
    326      * N.B. Each PIN_RANGE is clamped to the actual bounds of the file; all inputs have a
    327      * meaningful interpretation. Also, a range locking a single byte of a page locks the
    328      * whole page. Any truncated PIN_RANGE at EOF is ignored. Overlapping pinned entries
    329      * are legal, but each pin of a byte counts toward the pin quota regardless of whether
    330      * that byte has already been pinned, so the generator of PINLIST_META ought to ensure
    331      * that ranges are non-overlapping.
    332      *
    333      * @param fileToPin Path to file to pin
    334      * @param maxBytesToPin Maximum number of bytes to pin
    335      * @param attemptPinIntrospection If true, try to open file as a
    336      *   zip in order to extract the
    337      * @return Pinned memory resource owner thing or null on error
    338      */
    339     private static PinnedFile pinFile(String fileToPin,
    340                                       int maxBytesToPin,
    341                                       boolean attemptPinIntrospection) {
    342         ZipFile fileAsZip = null;
    343         InputStream pinRangeStream = null;
    344         try {
    345             if (attemptPinIntrospection) {
    346                 fileAsZip = maybeOpenZip(fileToPin);
    347             }
    348 
    349             if (fileAsZip != null) {
    350                 pinRangeStream = maybeOpenPinMetaInZip(fileAsZip, fileToPin);
    351             }
    352 
    353             Slog.d(TAG, "pinRangeStream: " + pinRangeStream);
    354 
    355             PinRangeSource pinRangeSource = (pinRangeStream != null)
    356                 ? new PinRangeSourceStream(pinRangeStream)
    357                 : new PinRangeSourceStatic(0, Integer.MAX_VALUE /* will be clipped */);
    358             return pinFileRanges(fileToPin, maxBytesToPin, pinRangeSource);
    359         } finally {
    360             safeClose(pinRangeStream);
    361             safeClose(fileAsZip);  // Also closes any streams we've opened
    362         }
    363     }
    364 
    365     /**
    366      * Attempt to open a file as a zip file. On any sort of corruption, log, swallow the
    367      * error, and return null.
    368      */
    369     private static ZipFile maybeOpenZip(String fileName) {
    370         ZipFile zip = null;
    371         try {
    372             zip = new ZipFile(fileName);
    373         } catch (IOException ex) {
    374             Slog.w(TAG,
    375                    String.format(
    376                        "could not open \"%s\" as zip: pinning as blob",
    377                                  fileName),
    378                    ex);
    379         }
    380         return zip;
    381     }
    382 
    383     /**
    384      * Open a pin metadata file in the zip if one is present.
    385      *
    386      * @param zipFile Zip file to search
    387      * @return Open input stream or null on any error
    388      */
    389     private static InputStream maybeOpenPinMetaInZip(ZipFile zipFile, String fileName) {
    390         ZipEntry pinMetaEntry = zipFile.getEntry(PIN_META_FILENAME);
    391         InputStream pinMetaStream = null;
    392         if (pinMetaEntry != null) {
    393             try {
    394                 pinMetaStream = zipFile.getInputStream(pinMetaEntry);
    395             } catch (IOException ex) {
    396                 Slog.w(TAG,
    397                        String.format("error reading pin metadata \"%s\": pinning as blob",
    398                                      fileName),
    399                        ex);
    400             }
    401         }
    402         return pinMetaStream;
    403     }
    404 
    405     private static abstract class PinRangeSource {
    406         /** Retrive a range to pin.
    407          *
    408          * @param outPinRange Receives the pin region
    409          * @return True if we filled in outPinRange or false if we're out of pin entries
    410          */
    411         abstract boolean read(PinRange outPinRange);
    412     }
    413 
    414     private static final class PinRangeSourceStatic extends PinRangeSource {
    415         private final int mPinStart;
    416         private final int mPinLength;
    417         private boolean mDone = false;
    418 
    419         PinRangeSourceStatic(int pinStart, int pinLength) {
    420             mPinStart = pinStart;
    421             mPinLength = pinLength;
    422         }
    423 
    424         @Override
    425         boolean read(PinRange outPinRange) {
    426             outPinRange.start = mPinStart;
    427             outPinRange.length = mPinLength;
    428             boolean done = mDone;
    429             mDone = true;
    430             return !done;
    431         }
    432     }
    433 
    434     private static final class PinRangeSourceStream extends PinRangeSource {
    435         private final DataInputStream mStream;
    436         private boolean mDone = false;
    437 
    438         PinRangeSourceStream(InputStream stream) {
    439             mStream = new DataInputStream(stream);
    440         }
    441 
    442         @Override
    443         boolean read(PinRange outPinRange) {
    444             if (!mDone) {
    445                 try {
    446                     outPinRange.start = mStream.readInt();
    447                     outPinRange.length = mStream.readInt();
    448                 } catch (IOException ex) {
    449                     mDone = true;
    450                 }
    451             }
    452             return !mDone;
    453         }
    454     }
    455 
    456     /**
    457      * Helper for pinFile.
    458      *
    459      * @param fileToPin Name of file to pin
    460      * @param maxBytesToPin Maximum number of bytes to pin
    461      * @param pinRangeSource Read PIN_RANGE entries from this stream to tell us what bytes
    462      *   to pin.
    463      * @return PinnedFile or null on error
    464      */
    465     private static PinnedFile pinFileRanges(
    466         String fileToPin,
    467         int maxBytesToPin,
    468         PinRangeSource pinRangeSource)
    469     {
    470         FileDescriptor fd = new FileDescriptor();
    471         long address = -1;
    472         int mapSize = 0;
    473 
    474         try {
    475             int openFlags = (OsConstants.O_RDONLY |
    476                              OsConstants.O_CLOEXEC |
    477                              OsConstants.O_NOFOLLOW);
    478             fd = Os.open(fileToPin, openFlags, 0);
    479             mapSize = (int) Math.min(Os.fstat(fd).st_size, Integer.MAX_VALUE);
    480             address = Os.mmap(0, mapSize,
    481                               OsConstants.PROT_READ,
    482                               OsConstants.MAP_SHARED,
    483                               fd, /*offset=*/0);
    484 
    485             PinRange pinRange = new PinRange();
    486             int bytesPinned = 0;
    487 
    488             // We pin at page granularity, so make sure the limit is page-aligned
    489             if (maxBytesToPin % PAGE_SIZE != 0) {
    490                 maxBytesToPin -= maxBytesToPin % PAGE_SIZE;
    491             }
    492 
    493             while (bytesPinned < maxBytesToPin && pinRangeSource.read(pinRange)) {
    494                 int pinStart = pinRange.start;
    495                 int pinLength = pinRange.length;
    496                 pinStart = clamp(0, pinStart, mapSize);
    497                 pinLength = clamp(0, pinLength, mapSize - pinStart);
    498                 pinLength = Math.min(maxBytesToPin - bytesPinned, pinLength);
    499 
    500                 // mlock doesn't require the region to be page-aligned, but we snap the
    501                 // lock region to page boundaries anyway so that we don't under-count
    502                 // locking a single byte of a page as a charge of one byte even though the
    503                 // OS will retain the whole page. Thanks to this adjustment, we slightly
    504                 // over-count the pin charge of back-to-back pins touching the same page,
    505                 // but better that than undercounting. Besides: nothing stops pin metafile
    506                 // creators from making the actual regions page-aligned.
    507                 pinLength += pinStart % PAGE_SIZE;
    508                 pinStart -= pinStart % PAGE_SIZE;
    509                 if (pinLength % PAGE_SIZE != 0) {
    510                     pinLength += PAGE_SIZE - pinLength % PAGE_SIZE;
    511                 }
    512                 pinLength = clamp(0, pinLength, maxBytesToPin - bytesPinned);
    513 
    514                 if (pinLength > 0) {
    515                     if (DEBUG) {
    516                         Slog.d(TAG,
    517                                String.format(
    518                                    "pinning at %s %s bytes of %s",
    519                                    pinStart, pinLength, fileToPin));
    520                     }
    521                     Os.mlock(address + pinStart, pinLength);
    522                 }
    523                 bytesPinned += pinLength;
    524             }
    525 
    526             PinnedFile pinnedFile = new PinnedFile(address, mapSize, fileToPin, bytesPinned);
    527             address = -1;  // Ownership transferred
    528             return pinnedFile;
    529         } catch (ErrnoException ex) {
    530             Slog.e(TAG, "Could not pin file " + fileToPin, ex);
    531             return null;
    532         } finally {
    533             safeClose(fd);
    534             if (address >= 0) {
    535                 safeMunmap(address, mapSize);
    536             }
    537         }
    538     }
    539 
    540     private static int clamp(int min, int value, int max) {
    541         return Math.max(min, Math.min(value, max));
    542     }
    543 
    544     private static void safeMunmap(long address, long mapSize) {
    545         try {
    546             Os.munmap(address, mapSize);
    547         } catch (ErrnoException ex) {
    548             Slog.w(TAG, "ignoring error in unmap", ex);
    549         }
    550     }
    551 
    552     /**
    553      * Close FD, swallowing irrelevant errors.
    554      */
    555     private static void safeClose(@Nullable FileDescriptor fd) {
    556         if (fd != null && fd.valid()) {
    557             try {
    558                 Os.close(fd);
    559             } catch (ErrnoException ex) {
    560                 // Swallow the exception: non-EBADF errors in close(2)
    561                 // indicate deferred paging write errors, which we
    562                 // don't care about here. The underlying file
    563                 // descriptor is always closed.
    564                 if (ex.errno == OsConstants.EBADF) {
    565                     throw new AssertionError(ex);
    566                 }
    567             }
    568         }
    569     }
    570 
    571     /**
    572      * Close closeable thing, swallowing errors.
    573      */
    574     private static void safeClose(@Nullable Closeable thing) {
    575         if (thing != null) {
    576             try {
    577                 thing.close();
    578             } catch (IOException ex) {
    579                 Slog.w(TAG, "ignoring error closing resource: " + thing, ex);
    580             }
    581         }
    582     }
    583 
    584     private synchronized ArrayList<PinnedFile> snapshotPinnedFiles() {
    585         int nrPinnedFiles = mPinnedFiles.size() + mPinnedCameraFiles.size();
    586         ArrayList<PinnedFile> pinnedFiles = new ArrayList<>(nrPinnedFiles);
    587         pinnedFiles.addAll(mPinnedFiles);
    588         pinnedFiles.addAll(mPinnedCameraFiles);
    589         return pinnedFiles;
    590     }
    591 
    592     private final class BinderService extends Binder {
    593         @Override
    594         protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    595             if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
    596             long totalSize = 0;
    597             for (PinnedFile pinnedFile : snapshotPinnedFiles()) {
    598                 pw.format("%s %s\n", pinnedFile.fileName, pinnedFile.bytesPinned);
    599                 totalSize += pinnedFile.bytesPinned;
    600             }
    601             pw.format("Total size: %s\n", totalSize);
    602         }
    603     }
    604 
    605     private static final class PinnedFile implements AutoCloseable {
    606         private long mAddress;
    607         final int mapSize;
    608         final String fileName;
    609         final int bytesPinned;
    610 
    611         PinnedFile(long address, int mapSize, String fileName, int bytesPinned) {
    612              mAddress = address;
    613              this.mapSize = mapSize;
    614              this.fileName = fileName;
    615              this.bytesPinned = bytesPinned;
    616         }
    617 
    618         @Override
    619         public void close() {
    620             if (mAddress >= 0) {
    621                 safeMunmap(mAddress, mapSize);
    622                 mAddress = -1;
    623             }
    624         }
    625 
    626         @Override
    627         public void finalize() {
    628             close();
    629         }
    630     }
    631 
    632     final static class PinRange {
    633         int start;
    634         int length;
    635     }
    636 
    637     final class PinnerHandler extends Handler {
    638         static final int PIN_CAMERA_MSG  = 4000;
    639         static final int PIN_ONSTART_MSG = 4001;
    640 
    641         public PinnerHandler(Looper looper) {
    642             super(looper, null, true);
    643         }
    644 
    645         @Override
    646         public void handleMessage(Message msg) {
    647             switch (msg.what) {
    648 
    649                 case PIN_CAMERA_MSG:
    650                 {
    651                     handlePinCamera(msg.arg1);
    652                 }
    653                 break;
    654 
    655                 case PIN_ONSTART_MSG:
    656                 {
    657                     handlePinOnStart();
    658                 }
    659                 break;
    660 
    661                 default:
    662                     super.handleMessage(msg);
    663             }
    664         }
    665     }
    666 
    667 }
    668