1/*
2 * Copyright (C) 2017 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 */
16package com.android.server.pm;
17
18import android.annotation.NonNull;
19import android.annotation.Nullable;
20import android.content.pm.ShortcutInfo;
21import android.graphics.Bitmap;
22import android.graphics.Bitmap.CompressFormat;
23import android.graphics.drawable.Icon;
24import android.os.StrictMode;
25import android.os.StrictMode.ThreadPolicy;
26import android.os.SystemClock;
27import android.util.Log;
28import android.util.Slog;
29
30import com.android.internal.annotations.GuardedBy;
31import com.android.internal.util.Preconditions;
32import com.android.server.pm.ShortcutService.FileOutputStreamWithPath;
33
34import libcore.io.IoUtils;
35
36import java.io.ByteArrayOutputStream;
37import java.io.File;
38import java.io.IOException;
39import java.io.PrintWriter;
40import java.util.Deque;
41import java.util.concurrent.CountDownLatch;
42import java.util.concurrent.Executor;
43import java.util.concurrent.LinkedBlockingDeque;
44import java.util.concurrent.LinkedBlockingQueue;
45import java.util.concurrent.ThreadPoolExecutor;
46import java.util.concurrent.TimeUnit;
47
48/**
49 * Class to save shortcut bitmaps on a worker thread.
50 *
51 * The methods with the "Locked" prefix must be called with the service lock held.
52 */
53public class ShortcutBitmapSaver {
54    private static final String TAG = ShortcutService.TAG;
55    private static final boolean DEBUG = ShortcutService.DEBUG;
56
57    private static final boolean ADD_DELAY_BEFORE_SAVE_FOR_TEST = false; // DO NOT submit with true.
58    private static final long SAVE_DELAY_MS_FOR_TEST = 1000; // DO NOT submit with true.
59
60    /**
61     * Before saving shortcuts.xml, and returning icons to the launcher, we wait for all pending
62     * saves to finish.  However if it takes more than this long, we just give up and proceed.
63     */
64    private final long SAVE_WAIT_TIMEOUT_MS = 30 * 1000;
65
66    private final ShortcutService mService;
67
68    /**
69     * Bitmaps are saved on this thread.
70     *
71     * Note: Just before saving shortcuts into the XML, we need to wait on all pending saves to
72     * finish, and we need to do it with the service lock held, which would still block incoming
73     * binder calls, meaning saving bitmaps *will* still actually block API calls too, which is
74     * not ideal but fixing it would be tricky, so this is still a known issue on the current
75     * version.
76     *
77     * In order to reduce the conflict, we use an own thread for this purpose, rather than
78     * reusing existing background threads, and also to avoid possible deadlocks.
79     */
80    private final Executor mExecutor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
81            new LinkedBlockingQueue<>());
82
83    /** Represents a bitmap to save. */
84    private static class PendingItem {
85        /** Hosting shortcut. */
86        public final ShortcutInfo shortcut;
87
88        /** Compressed bitmap data. */
89        public final byte[] bytes;
90
91        /** Instantiated time, only for dogfooding. */
92        private final long mInstantiatedUptimeMillis; // Only for dumpsys.
93
94        private PendingItem(ShortcutInfo shortcut, byte[] bytes) {
95            this.shortcut = shortcut;
96            this.bytes = bytes;
97            mInstantiatedUptimeMillis = SystemClock.uptimeMillis();
98        }
99
100        @Override
101        public String toString() {
102            return "PendingItem{size=" + bytes.length
103                    + " age=" + (SystemClock.uptimeMillis() - mInstantiatedUptimeMillis) + "ms"
104                    + " shortcut=" + shortcut.toInsecureString()
105                    + "}";
106        }
107    }
108
109    @GuardedBy("mPendingItems")
110    private final Deque<PendingItem> mPendingItems = new LinkedBlockingDeque<>();
111
112    public ShortcutBitmapSaver(ShortcutService service) {
113        mService = service;
114        // mLock = lock;
115    }
116
117    public boolean waitForAllSavesLocked() {
118        final CountDownLatch latch = new CountDownLatch(1);
119
120        mExecutor.execute(() -> latch.countDown());
121
122        try {
123            if (latch.await(SAVE_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
124                return true;
125            }
126            mService.wtf("Timed out waiting on saving bitmaps.");
127        } catch (InterruptedException e) {
128            Slog.w(TAG, "interrupted");
129        }
130        return false;
131    }
132
133    /**
134     * Wait for all pending saves to finish, and then return the given shortcut's bitmap path.
135     */
136    @Nullable
137    public String getBitmapPathMayWaitLocked(ShortcutInfo shortcut) {
138        final boolean success = waitForAllSavesLocked();
139        if (success && shortcut.hasIconFile()) {
140            return shortcut.getBitmapPath();
141        } else {
142            return null;
143        }
144    }
145
146    public void removeIcon(ShortcutInfo shortcut) {
147        // Do not remove the actual bitmap file yet, because if the device crashes before saving
148        // the XML we'd lose the icon.  We just remove all dangling files after saving the XML.
149        shortcut.setIconResourceId(0);
150        shortcut.setIconResName(null);
151        shortcut.setBitmapPath(null);
152        shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE |
153                ShortcutInfo.FLAG_ADAPTIVE_BITMAP | ShortcutInfo.FLAG_HAS_ICON_RES |
154                ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE);
155    }
156
157    public void saveBitmapLocked(ShortcutInfo shortcut,
158            int maxDimension, CompressFormat format, int quality) {
159        final Icon icon = shortcut.getIcon();
160        Preconditions.checkNotNull(icon);
161
162        final Bitmap original = icon.getBitmap();
163        if (original == null) {
164            Log.e(TAG, "Missing icon: " + shortcut);
165            return;
166        }
167
168        // Compress it and enqueue to the requests.
169        final byte[] bytes;
170        final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
171        try {
172            // compress() triggers a slow call, but in this case it's needed to save RAM and also
173            // the target bitmap is of an icon size, so let's just permit it.
174            StrictMode.setThreadPolicy(new ThreadPolicy.Builder(oldPolicy)
175                    .permitCustomSlowCalls()
176                    .build());
177            final Bitmap shrunk = mService.shrinkBitmap(original, maxDimension);
178            try {
179                try (final ByteArrayOutputStream out = new ByteArrayOutputStream(64 * 1024)) {
180                    if (!shrunk.compress(format, quality, out)) {
181                        Slog.wtf(ShortcutService.TAG, "Unable to compress bitmap");
182                    }
183                    out.flush();
184                    bytes = out.toByteArray();
185                    out.close();
186                }
187            } finally {
188                if (shrunk != original) {
189                    shrunk.recycle();
190                }
191            }
192        } catch (IOException | RuntimeException | OutOfMemoryError e) {
193            Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e);
194            return;
195        } finally {
196            StrictMode.setThreadPolicy(oldPolicy);
197        }
198
199        shortcut.addFlags(
200                ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE);
201
202        if (icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
203            shortcut.addFlags(ShortcutInfo.FLAG_ADAPTIVE_BITMAP);
204        }
205
206        // Enqueue a pending save.
207        final PendingItem item = new PendingItem(shortcut, bytes);
208        synchronized (mPendingItems) {
209            mPendingItems.add(item);
210        }
211
212        if (DEBUG) {
213            Slog.d(TAG, "Scheduling to save: " + item);
214        }
215
216        mExecutor.execute(mRunnable);
217    }
218
219    private final Runnable mRunnable = () -> {
220        // Process all pending items.
221        while (processPendingItems()) {
222        }
223    };
224
225    /**
226     * Takes a {@link PendingItem} from {@link #mPendingItems} and process it.
227     *
228     * Must be called {@link #mExecutor}.
229     *
230     * @return true if it processed an item, false if the queue is empty.
231     */
232    private boolean processPendingItems() {
233        if (ADD_DELAY_BEFORE_SAVE_FOR_TEST) {
234            Slog.w(TAG, "*** ARTIFICIAL SLEEP ***");
235            try {
236                Thread.sleep(SAVE_DELAY_MS_FOR_TEST);
237            } catch (InterruptedException e) {
238            }
239        }
240
241        // NOTE:
242        // Ideally we should be holding the service lock when accessing shortcut instances,
243        // but that could cause a deadlock so we don't do it.
244        //
245        // Instead, waitForAllSavesLocked() uses a latch to make sure changes made on this
246        // thread is visible on the caller thread.
247
248        ShortcutInfo shortcut = null;
249        try {
250            final PendingItem item;
251
252            synchronized (mPendingItems) {
253                if (mPendingItems.size() == 0) {
254                    return false;
255                }
256                item = mPendingItems.pop();
257            }
258
259            shortcut = item.shortcut;
260
261            // See if the shortcut is still relevant. (It might have been removed already.)
262            if (!shortcut.isIconPendingSave()) {
263                return true;
264            }
265
266            if (DEBUG) {
267                Slog.d(TAG, "Saving bitmap: " + item);
268            }
269
270            File file = null;
271            try {
272                final FileOutputStreamWithPath out = mService.openIconFileForWrite(
273                        shortcut.getUserId(), shortcut);
274                file = out.getFile();
275
276                try {
277                    out.write(item.bytes);
278                } finally {
279                    IoUtils.closeQuietly(out);
280                }
281
282                shortcut.setBitmapPath(file.getAbsolutePath());
283
284            } catch (IOException | RuntimeException e) {
285                Slog.e(ShortcutService.TAG, "Unable to write bitmap to file", e);
286
287                if (file != null && file.exists()) {
288                    file.delete();
289                }
290                return true;
291            }
292        } finally {
293            if (DEBUG) {
294                Slog.d(TAG, "Saved bitmap.");
295            }
296            if (shortcut != null) {
297                if (shortcut.getBitmapPath() == null) {
298                    removeIcon(shortcut);
299                }
300
301                // Whatever happened, remove this flag.
302                shortcut.clearFlags(ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE);
303            }
304        }
305        return true;
306    }
307
308    public void dumpLocked(@NonNull PrintWriter pw, @NonNull String prefix) {
309        synchronized (mPendingItems) {
310            final int N = mPendingItems.size();
311            pw.print(prefix);
312            pw.println("Pending saves: Num=" + N + " Executor=" + mExecutor);
313
314            for (PendingItem item : mPendingItems) {
315                pw.print(prefix);
316                pw.print("  ");
317                pw.println(item);
318            }
319        }
320    }
321}
322