1/*
2 * Copyright (C) 2015 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
17package com.android.printspooler.model;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.graphics.drawable.Icon;
22import android.print.PrinterId;
23import android.util.Log;
24
25import java.io.File;
26import java.io.FileInputStream;
27import java.io.FileOutputStream;
28import java.io.IOException;
29import java.io.UnsupportedEncodingException;
30import java.security.MessageDigest;
31import java.security.NoSuchAlgorithmException;
32import java.util.SortedMap;
33import java.util.TreeMap;
34
35/**
36 * A fixed size cache for custom printer icons. Old icons get removed with a last recently used
37 * policy.
38 */
39public class CustomPrinterIconCache {
40
41    private final static String LOG_TAG = "CustomPrinterIconCache";
42
43    /** Maximum number of icons in the cache */
44    private final static int MAX_SIZE = 1024;
45
46    /** Directory used to persist state and icons */
47    private final File mCacheDirectory;
48
49    /**
50     * Create a new icon cache.
51     */
52    public CustomPrinterIconCache(@NonNull File cacheDirectory) {
53        mCacheDirectory = new File(cacheDirectory, "icons");
54        if (!mCacheDirectory.exists()) {
55            mCacheDirectory.mkdir();
56        }
57    }
58
59    /**
60     * Return the file name to be used for the icon of a printer
61     *
62     * @param printerId the id of the printer
63     *
64     * @return The file to be used for the icon of the printer
65     */
66    private @Nullable File getIconFileName(@NonNull PrinterId printerId) {
67        StringBuffer sb = new StringBuffer(printerId.getServiceName().getPackageName());
68        sb.append("-");
69
70        try {
71            MessageDigest md = MessageDigest.getInstance("SHA-1");
72            md.update(
73                    (printerId.getServiceName().getClassName() + ":" + printerId.getLocalId())
74                            .getBytes("UTF-16"));
75            sb.append(String.format("%#040x", new java.math.BigInteger(1, md.digest())));
76        } catch (UnsupportedEncodingException|NoSuchAlgorithmException e) {
77            Log.e(LOG_TAG, "Could not compute custom printer icon file name", e);
78            return null;
79        }
80
81        return new File(mCacheDirectory, sb.toString());
82    }
83
84    /**
85     * Get the {@link Icon} to be used as a custom icon for the printer. If not available request
86     * the icon to be loaded.
87     *
88     * @param printerId the printer the icon belongs to
89     * @return the {@link Icon} if already available or null if icon is not loaded yet
90     */
91    public synchronized @Nullable Icon getIcon(@NonNull PrinterId printerId) {
92        Icon icon;
93
94        File iconFile = getIconFileName(printerId);
95        if (iconFile != null && iconFile.exists()) {
96            try (FileInputStream is = new FileInputStream(iconFile)) {
97                icon = Icon.createFromStream(is);
98            } catch (IOException e) {
99                icon = null;
100                Log.e(LOG_TAG, "Could not read icon from " + iconFile, e);
101            }
102
103            // Touch file so that it is the not likely to be removed
104            iconFile.setLastModified(System.currentTimeMillis());
105        } else {
106            icon = null;
107        }
108
109        return icon;
110    }
111
112    /**
113     * Remove old icons so that only between numFilesToKeep and twice as many icons are left.
114     *
115     * @param numFilesToKeep the number of icons to keep
116     */
117    public void removeOldFiles(int numFilesToKeep) {
118        File files[] = mCacheDirectory.listFiles();
119
120        // To reduce the number of shrink operations, let the cache grow to twice the max size
121        if (files.length > numFilesToKeep * 2) {
122            SortedMap<Long, File> sortedFiles = new TreeMap<>();
123
124            for (File f : files) {
125                sortedFiles.put(f.lastModified(), f);
126            }
127
128            while (sortedFiles.size() > numFilesToKeep) {
129                sortedFiles.remove(sortedFiles.firstKey());
130            }
131        }
132    }
133
134    /**
135     * Handle that a custom icon for a printer was loaded
136     *
137     * @param printerId the id of the printer the icon belongs to
138     * @param icon the icon that was loaded
139     */
140    public synchronized void onCustomPrinterIconLoaded(@NonNull PrinterId printerId,
141            @Nullable Icon icon) {
142        File iconFile = getIconFileName(printerId);
143
144        if (iconFile == null) {
145            return;
146        }
147
148        try (FileOutputStream os = new FileOutputStream(iconFile)) {
149            icon.writeToStream(os);
150        } catch (IOException e) {
151            Log.e(LOG_TAG, "Could not write icon for " + printerId + " to storage", e);
152        }
153
154        removeOldFiles(MAX_SIZE);
155    }
156
157    /**
158     * Clear all persisted and non-persisted state from this cache.
159     */
160    public synchronized void clear() {
161        for (File f : mCacheDirectory.listFiles()) {
162            f.delete();
163        }
164    }
165}
166