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