Android Open Source - CCSocialNetwork File Lru Cache






From Project

Back to project page CCSocialNetwork.

License

The source code is released under:

GNU Lesser General Public License

If you think the Android project CCSocialNetwork listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/**
 * Copyright 2010-present Facebook./*from w w  w .j  a v a2  s  .  com*/
 *
 * 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.facebook.internal;

import android.content.Context;
import android.util.Log;
import com.facebook.LoggingBehavior;
import com.facebook.Settings;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import java.io.*;
import java.security.InvalidParameterException;
import java.util.Date;
import java.util.PriorityQueue;
import java.util.concurrent.atomic.AtomicLong;

// This class is intended to be thread-safe.
//
// There are two classes of files:  buffer files and cache files:
// - A buffer file is in the process of being written, and there is an open stream on the file.  These files are
//   named as "bufferN" where N is an incrementing integer.  On startup, we delete all existing files of this form.
//   Once the stream is closed, we rename the buffer file to a cache file or attempt to delete if this fails.  We
//   do not otherwise ever attempt to delete these files.
// - A cache file is a non-changing file that is named by the md5 hash of the cache key.  We monitor the size of
//   these files in aggregate and remove the oldest one(s) to stay under quota.  This process does not block threads
//   calling into this class, so theoretically we could go arbitrarily over quota but in practice this should not
//   happen because deleting files should be much cheaper than downloading new file content.
//
// Since there can only ever be one thread accessing a particular buffer file, we do not synchronize access to these.
// We do assume that file rename is atomic when converting a buffer file to a cache file, and that if multiple files
// are renamed to a single target that exactly one of them continues to exist.
//
// Standard POSIX file semantics guarantee being able to continue to use a file handle even after the
// corresponding file has been deleted.  Given this and that cache files never change other than deleting in trim(),
// we only have to ensure that there is at most one trim() process deleting files at any given time.

/**
 * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of
 * any of the classes in this package is unsupported, and they may be modified or removed without warning at
 * any time.
 */
public final class FileLruCache {
    static final String TAG = FileLruCache.class.getSimpleName();
    private static final String HEADER_CACHEKEY_KEY = "key";
    private static final String HEADER_CACHE_CONTENT_TAG_KEY = "tag";

    private static final AtomicLong bufferIndex = new AtomicLong();

    private final String tag;
    private final Limits limits;
    private final File directory;
    private boolean isTrimPending;
    private final Object lock;

    // The value of tag should be a final String that works as a directory name.
    public FileLruCache(Context context, String tag, Limits limits) {
        this.tag = tag;
        this.limits = limits;
        this.directory = new File(context.getCacheDir(), tag);
        this.lock = new Object();

        // Ensure the cache dir exists
        this.directory.mkdirs();

        // Remove any stale partially-written files from a previous run
        BufferFile.deleteAll(this.directory);
    }

    // Other code in this class is not necessarily robust to having buffer files deleted concurrently.
    // If this is ever used for non-test code, we should make sure the synchronization is correct.  See
    // the threading notes at the top of this class.
    public void clearForTest() throws IOException {
        for (File file : this.directory.listFiles()) {
            file.delete();
        }
    }

    // This is not robust to files changing dynamically underneath it and should therefore only be used
    // for test code.  If we ever need this for product code we need to think through synchronization.
    // See the threading notes at the top of this class.
    //
    // Also, since trim() runs asynchronously now, this blocks until any pending trim has completed.
    long sizeInBytesForTest() {
        synchronized (lock) {
            while (isTrimPending) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    // intentional no-op
                }
            }
        }

        File[] files = this.directory.listFiles();
        long total = 0;
        for (File file : files) {
            total += file.length();
        }
        return total;
    }

    public InputStream get(String key) throws IOException {
        return get(key, null);
    }

    public InputStream get(String key, String contentTag) throws IOException {
        File file = new File(this.directory, Utility.md5hash(key));

        FileInputStream input = null;
        try {
            input = new FileInputStream(file);
        } catch (IOException e) {
            return null;
        }

        BufferedInputStream buffered = new BufferedInputStream(input, Utility.DEFAULT_STREAM_BUFFER_SIZE);
        boolean success = false;

        try {
            JSONObject header = StreamHeader.readHeader(buffered);
            if (header == null) {
                return null;
            }

            String foundKey = header.optString(HEADER_CACHEKEY_KEY);
            if ((foundKey == null) || !foundKey.equals(key)) {
                return null;
            }

            String headerContentTag = header.optString(HEADER_CACHE_CONTENT_TAG_KEY, null);

            if ((contentTag == null && headerContentTag != null) ||
                    (contentTag != null && !contentTag.equals(headerContentTag))) {
                return null;
            }

            long accessTime = new Date().getTime();
            Logger.log(LoggingBehavior.CACHE, TAG, "Setting lastModified to " + Long.valueOf(accessTime) + " for "
                    + file.getName());
            file.setLastModified(accessTime);

            success = true;
            return buffered;
        } finally {
            if (!success) {
                buffered.close();
            }
        }
    }

    OutputStream openPutStream(final String key) throws IOException {
        return openPutStream(key, null);
    }

    public OutputStream openPutStream(final String key, String contentTag) throws IOException {
        final File buffer = BufferFile.newFile(this.directory);
        buffer.delete();
        if (!buffer.createNewFile()) {
            throw new IOException("Could not create file at " + buffer.getAbsolutePath());
        }

        FileOutputStream file = null;
        try {
            file = new FileOutputStream(buffer);
        } catch (FileNotFoundException e) {
            Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, "Error creating buffer output stream: " + e);
            throw new IOException(e.getMessage());
        }

        StreamCloseCallback renameToTargetCallback = new StreamCloseCallback() {
            @Override
            public void onClose() {
                renameToTargetAndTrim(key, buffer);
            }
        };

        CloseCallbackOutputStream cleanup = new CloseCallbackOutputStream(file, renameToTargetCallback);
        BufferedOutputStream buffered = new BufferedOutputStream(cleanup, Utility.DEFAULT_STREAM_BUFFER_SIZE);
        boolean success = false;

        try {
            // Prefix the stream with the actual key, since there could be collisions
            JSONObject header = new JSONObject();
            header.put(HEADER_CACHEKEY_KEY, key);
            if (!Utility.isNullOrEmpty(contentTag)) {
                header.put(HEADER_CACHE_CONTENT_TAG_KEY, contentTag);
            }

            StreamHeader.writeHeader(buffered, header);

            success = true;
            return buffered;
        } catch (JSONException e) {
            // JSON is an implementation detail of the cache, so don't let JSON exceptions out.
            Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, "Error creating JSON header for cache file: " + e);
            throw new IOException(e.getMessage());
        } finally {
            if (!success) {
                buffered.close();
            }
        }
    }

    private void renameToTargetAndTrim(String key, File buffer) {
        final File target = new File(directory, Utility.md5hash(key));

        // This is triggered by close().  By the time close() returns, the file should be cached, so this needs to
        // happen synchronously on this thread.
        //
        // However, it does not need to be synchronized, since in the race we will just start an unnecesary trim
        // operation.  Avoiding the cost of holding the lock across the file operation seems worth this cost.
        if (!buffer.renameTo(target)) {
            buffer.delete();
        }

        postTrim();
    }

    // Opens an output stream for the key, and creates an input stream wrapper to copy
    // the contents of input into the new output stream.  The effect is to store a
    // copy of input, and associate that data with key.
    public InputStream interceptAndPut(String key, InputStream input) throws IOException {
        OutputStream output = openPutStream(key);
        return new CopyingInputStream(input, output);
    }

    public String toString() {
        return "{FileLruCache:" + " tag:" + this.tag + " file:" + this.directory.getName() + "}";
    }

    private void postTrim() {
        synchronized (lock) {
            if (!isTrimPending) {
                isTrimPending = true;
                Settings.getExecutor().execute(new Runnable() {
                    @Override
                    public void run() {
                        trim();
                    }
                });
            }
        }
    }

    private void trim() {
        try {
            Logger.log(LoggingBehavior.CACHE, TAG, "trim started");
            PriorityQueue<ModifiedFile> heap = new PriorityQueue<ModifiedFile>();
            long size = 0;
            long count = 0;
            for (File file : this.directory.listFiles(BufferFile.excludeBufferFiles())) {
                ModifiedFile modified = new ModifiedFile(file);
                heap.add(modified);
                Logger.log(LoggingBehavior.CACHE, TAG, "  trim considering time=" + Long.valueOf(modified.getModified())
                        + " name=" + modified.getFile().getName());

                size += file.length();
                count++;
            }

            while ((size > limits.getByteCount()) || (count > limits.getFileCount())) {
                File file = heap.remove().getFile();
                Logger.log(LoggingBehavior.CACHE, TAG, "  trim removing " + file.getName());
                size -= file.length();
                count--;
                file.delete();
            }
        } finally {
            synchronized (lock) {
                isTrimPending = false;
                lock.notifyAll();
            }
        }
    }

    private static class BufferFile {
        private static final String FILE_NAME_PREFIX = "buffer";
        private static final FilenameFilter filterExcludeBufferFiles = new FilenameFilter() {
            @Override
            public boolean accept(File dir, String filename) {
                return !filename.startsWith(FILE_NAME_PREFIX);
            }
        };
        private static final FilenameFilter filterExcludeNonBufferFiles = new FilenameFilter() {
            @Override
            public boolean accept(File dir, String filename) {
                return filename.startsWith(FILE_NAME_PREFIX);
            }
        };

        static void deleteAll(final File root) {
            for (File file : root.listFiles(excludeNonBufferFiles())) {
                file.delete();
            }
        }

        static FilenameFilter excludeBufferFiles() {
            return filterExcludeBufferFiles;
        }

        static FilenameFilter excludeNonBufferFiles() {
            return filterExcludeNonBufferFiles;
        }

        static File newFile(final File root) {
            String name = FILE_NAME_PREFIX + Long.valueOf(bufferIndex.incrementAndGet()).toString();
            return new File(root, name);
        }
    }

    // Treats the first part of a stream as a header, reads/writes it as a JSON blob, and
    // leaves the stream positioned exactly after the header.
    //
    // The format is as follows:
    //     byte: meaning
    // ---------------------------------
    //        0: version number
    //      1-3: big-endian JSON header blob size
    // 4-size+4: UTF-8 JSON header blob
    //      ...: stream data
    private static final class StreamHeader {
        private static final int HEADER_VERSION = 0;

        static void writeHeader(OutputStream stream, JSONObject header) throws IOException {
            String headerString = header.toString();
            byte[] headerBytes = headerString.getBytes();

            // Write version number and big-endian header size
            stream.write(HEADER_VERSION);
            stream.write((headerBytes.length >> 16) & 0xff);
            stream.write((headerBytes.length >> 8) & 0xff);
            stream.write((headerBytes.length >> 0) & 0xff);

            stream.write(headerBytes);
        }

        static JSONObject readHeader(InputStream stream) throws IOException {
            int version = stream.read();
            if (version != HEADER_VERSION) {
                return null;
            }

            int headerSize = 0;
            for (int i = 0; i < 3; i++) {
                int b = stream.read();
                if (b == -1) {
                    Logger.log(LoggingBehavior.CACHE, TAG,
                            "readHeader: stream.read returned -1 while reading header size");
                    return null;
                }
                headerSize <<= 8;
                headerSize += b & 0xff;
            }

            byte[] headerBytes = new byte[headerSize];
            int count = 0;
            while (count < headerBytes.length) {
                int readCount = stream.read(headerBytes, count, headerBytes.length - count);
                if (readCount < 1) {
                    Logger.log(LoggingBehavior.CACHE, TAG,
                            "readHeader: stream.read stopped at " + Integer.valueOf(count) + " when expected "
                                    + headerBytes.length);
                    return null;
                }
                count += readCount;
            }

            String headerString = new String(headerBytes);
            JSONObject header = null;
            JSONTokener tokener = new JSONTokener(headerString);
            try {
                Object parsed = tokener.nextValue();
                if (!(parsed instanceof JSONObject)) {
                    Logger.log(LoggingBehavior.CACHE, TAG, "readHeader: expected JSONObject, got " + parsed.getClass().getCanonicalName());
                    return null;
                }
                header = (JSONObject) parsed;
            } catch (JSONException e) {
                throw new IOException(e.getMessage());
            }

            return header;
        }
    }

    private static class CloseCallbackOutputStream extends OutputStream {
        final OutputStream innerStream;
        final StreamCloseCallback callback;

        CloseCallbackOutputStream(OutputStream innerStream, StreamCloseCallback callback) {
            this.innerStream = innerStream;
            this.callback = callback;
        }

        @Override
        public void close() throws IOException {
            try {
                this.innerStream.close();
            } finally {
                this.callback.onClose();
            }
        }

        @Override
        public void flush() throws IOException {
            this.innerStream.flush();
        }

        @Override
        public void write(byte[] buffer, int offset, int count) throws IOException {
            this.innerStream.write(buffer, offset, count);
        }

        @Override
        public void write(byte[] buffer) throws IOException {
            this.innerStream.write(buffer);
        }

        @Override
        public void write(int oneByte) throws IOException {
            this.innerStream.write(oneByte);
        }
    }

    private static final class CopyingInputStream extends InputStream {
        final InputStream input;
        final OutputStream output;

        CopyingInputStream(final InputStream input, final OutputStream output) {
            this.input = input;
            this.output = output;
        }

        @Override
        public int available() throws IOException {
            return input.available();
        }

        @Override
        public void close() throws IOException {
            // According to http://www.cs.cornell.edu/andru/javaspec/11.doc.html:
            //  "If a finally clause is executed because of abrupt completion of a try block and the finally clause
            //   itself completes abruptly, then the reason for the abrupt completion of the try block is discarded
            //   and the new reason for abrupt completion is propagated from there."
            //
            // Android does appear to behave like this.
            try {
                this.input.close();
            } finally {
                this.output.close();
            }
        }

        @Override
        public void mark(int readlimit) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean markSupported() {
            return false;
        }

        @Override
        public int read(byte[] buffer) throws IOException {
            int count = input.read(buffer);
            if (count > 0) {
                output.write(buffer, 0, count);
            }
            return count;
        }

        @Override
        public int read() throws IOException {
            int b = input.read();
            if (b >= 0) {
                output.write(b);
            }
            return b;
        }

        @Override
        public int read(byte[] buffer, int offset, int length) throws IOException {
            int count = input.read(buffer, offset, length);
            if (count > 0) {
                output.write(buffer, offset, count);
            }
            return count;
        }

        @Override
        public synchronized void reset() {
            throw new UnsupportedOperationException();
        }

        @Override
        public long skip(long byteCount) throws IOException {
            byte[] buffer = new byte[1024];
            long total = 0;
            while (total < byteCount) {
                int count = read(buffer, 0, (int)Math.min(byteCount - total, buffer.length));
                if (count < 0) {
                    return total;
                }
                total += count;
            }
            return total;
        }
    }

    public static final class Limits {
        private int byteCount;
        private int fileCount;

        public Limits() {
            // A Samsung Galaxy Nexus can create 1k files in half a second.  By the time
            // it gets to 5k files it takes 5 seconds.  10k files took 15 seconds.  This
            // continues to slow down as files are added.  This assumes all files are in
            // a single directory.
            //
            // Following a git-like strategy where we partition MD5-named files based on
            // the first 2 characters is slower across the board.
            this.fileCount = 1024;
            this.byteCount = 1024 * 1024;
        }

        int getByteCount() {
            return byteCount;
        }

        int getFileCount() {
            return fileCount;
        }

        void setByteCount(int n) {
            if (n < 0) {
                throw new InvalidParameterException("Cache byte-count limit must be >= 0");
            }
            byteCount = n;
        }

        void setFileCount(int n) {
            if (n < 0) {
                throw new InvalidParameterException("Cache file count limit must be >= 0");
            }
            fileCount = n;
        }
    }

    // Caches the result of lastModified during sort/heap operations
    private final static class ModifiedFile implements Comparable<ModifiedFile> {
        private final File file;
        private final long modified;

        ModifiedFile(File file) {
            this.file = file;
            this.modified = file.lastModified();
        }

        File getFile() {
            return file;
        }

        long getModified() {
            return modified;
        }

        @Override
        public int compareTo(ModifiedFile another) {
            if (getModified() < another.getModified()) {
                return -1;
            } else if (getModified() > another.getModified()) {
                return 1;
            } else {
                return getFile().compareTo(another.getFile());
            }
        }

        @Override
        public boolean equals(Object another) {
            return
                    (another instanceof ModifiedFile) &&
                    (compareTo((ModifiedFile)another) == 0);
        }
    }

    private interface StreamCloseCallback {
        void onClose();
    }
}




Java Source Code List

com.facebook.AccessTokenSource.java
com.facebook.AccessToken.java
com.facebook.AuthorizationClient.java
com.facebook.FacebookAuthorizationException.java
com.facebook.FacebookDialogException.java
com.facebook.FacebookException.java
com.facebook.FacebookGraphObjectException.java
com.facebook.FacebookOperationCanceledException.java
com.facebook.FacebookRequestError.java
com.facebook.FacebookSdkVersion.java
com.facebook.FacebookServiceException.java
com.facebook.GetTokenClient.java
com.facebook.HttpMethod.java
com.facebook.InsightsLogger.java
com.facebook.LegacyHelper.java
com.facebook.LoggingBehavior.java
com.facebook.LoginActivity.java
com.facebook.NativeProtocol.java
com.facebook.NonCachingTokenCachingStrategy.java
com.facebook.RequestAsyncTask.java
com.facebook.RequestBatch.java
com.facebook.Request.java
com.facebook.Response.java
com.facebook.SessionDefaultAudience.java
com.facebook.SessionLoginBehavior.java
com.facebook.SessionState.java
com.facebook.Session.java
com.facebook.Settings.java
com.facebook.SharedPreferencesTokenCachingStrategy.java
com.facebook.TestSession.java
com.facebook.TokenCachingStrategy.java
com.facebook.UiLifecycleHelper.java
com.facebook.android.AsyncFacebookRunner.java
com.facebook.android.DialogError.java
com.facebook.android.FacebookError.java
com.facebook.android.Facebook.java
com.facebook.android.FbDialog.java
com.facebook.android.Util.java
com.facebook.internal.CacheableRequestBatch.java
com.facebook.internal.FileLruCache.java
com.facebook.internal.Logger.java
com.facebook.internal.ServerProtocol.java
com.facebook.internal.SessionAuthorizationType.java
com.facebook.internal.SessionTracker.java
com.facebook.internal.Utility.java
com.facebook.internal.Validate.java
com.facebook.internal.package-info.java
com.facebook.model.GraphLocation.java
com.facebook.model.GraphMultiResult.java
com.facebook.model.GraphObjectList.java
com.facebook.model.GraphObject.java
com.facebook.model.GraphPlace.java
com.facebook.model.GraphUser.java
com.facebook.model.JsonUtil.java
com.facebook.model.OpenGraphAction.java
com.facebook.model.PropertyName.java
com.facebook.widget.FacebookFragment.java
com.facebook.widget.FriendPickerFragment.java
com.facebook.widget.GraphObjectAdapter.java
com.facebook.widget.GraphObjectCursor.java
com.facebook.widget.GraphObjectPagingLoader.java
com.facebook.widget.ImageDownloader.java
com.facebook.widget.ImageRequest.java
com.facebook.widget.ImageResponseCache.java
com.facebook.widget.ImageResponse.java
com.facebook.widget.LoginButton.java
com.facebook.widget.PickerFragment.java
com.facebook.widget.PlacePickerFragment.java
com.facebook.widget.ProfilePictureView.java
com.facebook.widget.SimpleGraphObjectCursor.java
com.facebook.widget.UrlRedirectCache.java
com.facebook.widget.UserSettingsFragment.java
com.facebook.widget.WebDialog.java
com.facebook.widget.WorkQueue.java
org.example.SocialNetwork.CCSocialNetwork.java
org.example.SocialNetwork.EmailSender.java
org.example.SocialNetwork.InternetConnection.java