• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 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.Manifest;
20 import android.annotation.Nullable;
21 import android.app.ActivityManager;
22 import android.app.AppOpsManager;
23 import android.app.BroadcastOptions;
24 import android.app.compat.CompatChanges;
25 import android.compat.annotation.ChangeId;
26 import android.compat.annotation.EnabledAfter;
27 import android.content.BroadcastReceiver;
28 import android.content.ContentResolver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.IntentFilter;
32 import android.content.pm.PackageManager;
33 import android.content.res.Resources;
34 import android.database.ContentObserver;
35 import android.net.Uri;
36 import android.os.Binder;
37 import android.os.Build;
38 import android.os.BundleMerger;
39 import android.os.Debug;
40 import android.os.DropBoxManager;
41 import android.os.FileUtils;
42 import android.os.Handler;
43 import android.os.Looper;
44 import android.os.Message;
45 import android.os.ParcelFileDescriptor;
46 import android.os.ResultReceiver;
47 import android.os.ShellCallback;
48 import android.os.ShellCommand;
49 import android.os.StatFs;
50 import android.os.SystemClock;
51 import android.os.UserHandle;
52 import android.provider.Settings;
53 import android.service.dropbox.DropBoxManagerServiceDumpProto;
54 import android.system.ErrnoException;
55 import android.system.Os;
56 import android.system.OsConstants;
57 import android.system.StructStat;
58 import android.text.TextUtils;
59 import android.text.format.TimeMigrationUtils;
60 import android.util.ArrayMap;
61 import android.util.ArraySet;
62 import android.util.Slog;
63 import android.util.proto.ProtoOutputStream;
64 
65 import com.android.internal.R;
66 import com.android.internal.annotations.GuardedBy;
67 import com.android.internal.annotations.VisibleForTesting;
68 import com.android.internal.os.IDropBoxManagerService;
69 import com.android.internal.util.DumpUtils;
70 import com.android.internal.util.FrameworkStatsLog;
71 import com.android.internal.util.ObjectUtils;
72 import com.android.server.DropBoxManagerInternal.EntrySource;
73 import com.android.server.feature.flags.Flags;
74 
75 import libcore.io.IoUtils;
76 
77 import java.io.ByteArrayInputStream;
78 import java.io.File;
79 import java.io.FileDescriptor;
80 import java.io.FileOutputStream;
81 import java.io.IOException;
82 import java.io.InputStream;
83 import java.io.InputStreamReader;
84 import java.io.PrintWriter;
85 import java.util.ArrayList;
86 import java.util.Arrays;
87 import java.util.List;
88 import java.util.SortedSet;
89 import java.util.TreeSet;
90 import java.util.zip.GZIPOutputStream;
91 
92 /**
93  * Implementation of {@link IDropBoxManagerService} using the filesystem.
94  * Clients use {@link DropBoxManager} to access this service.
95  */
96 public final class DropBoxManagerService extends SystemService {
97     /**
98      * For Android U and earlier versions, apps can continue to use the READ_LOGS permission,
99      * but for all subsequent versions, the READ_DROPBOX_DATA permission must be used.
100      */
101     @ChangeId
102     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
103     private static final long ENFORCE_READ_DROPBOX_DATA = 296060945L;
104     private static final String TAG = "DropBoxManagerService";
105     private static final int DEFAULT_AGE_SECONDS = 3 * 86400;
106     private static final int DEFAULT_MAX_FILES = 1000;
107     private static final int DEFAULT_MAX_FILES_LOWRAM = 300;
108     public static final int DEFAULT_QUOTA_KB = Build.IS_USERDEBUG ? 20 * 1024 : 10 * 1024;
109     private static final int DEFAULT_QUOTA_PERCENT = 10;
110     private static final int DEFAULT_RESERVE_PERCENT = 0;
111     private static final int QUOTA_RESCAN_MILLIS = 5000;
112 
113     private static final boolean PROFILE_DUMP = false;
114 
115     // Max number of bytes of a dropbox entry to write into protobuf.
116     private static final int PROTO_MAX_DATA_BYTES = 256 * 1024;
117 
118     // Size beyond which to force-compress newly added entries.
119     private static final long COMPRESS_THRESHOLD_BYTES = 16_384;
120 
121     // Tags that we should drop by default.
122     private static final List<String> DISABLED_BY_DEFAULT_TAGS =
123             List.of("data_app_wtf", "system_app_wtf", "system_server_wtf");
124     // TODO: This implementation currently uses one file per entry, which is
125     // inefficient for smallish entries -- consider using a single queue file
126     // per tag (or even globally) instead.
127 
128     // The cached context and derived objects
129 
130     private final ContentResolver mContentResolver;
131     private final File mDropBoxDir;
132 
133     // Accounting of all currently written log files (set in init()).
134 
135     private FileList mAllFiles = null;
136     private ArrayMap<String, FileList> mFilesByTag = null;
137 
138     private long mLowPriorityRateLimitPeriod = 0;
139     private ArraySet<String> mLowPriorityTags = null;
140 
141     // Various bits of disk information
142 
143     private StatFs mStatFs = null;
144     private int mBlockSize = 0;
145     private int mCachedQuotaBlocks = 0;  // Space we can use: computed from free space, etc.
146     private long mCachedQuotaUptimeMillis = 0;
147 
148     private volatile boolean mBooted = false;
149 
150     // Provide a way to perform sendBroadcast asynchronously to avoid deadlocks.
151     private final DropBoxManagerBroadcastHandler mHandler;
152 
153     private int mMaxFiles = -1; // -1 means uninitialized.
154 
155     /** Receives events that might indicate a need to clean up files. */
156     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
157         @Override
158         public void onReceive(Context context, Intent intent) {
159             // For ACTION_DEVICE_STORAGE_LOW:
160             mCachedQuotaUptimeMillis = 0;  // Force a re-check of quota size
161 
162             // Run the initialization in the background (not this main thread).
163             // The init() and trimToFit() methods are synchronized, so they still
164             // block other users -- but at least the onReceive() call can finish.
165             new Thread() {
166                 public void run() {
167                     try {
168                         init();
169                         trimToFit();
170                     } catch (IOException e) {
171                         Slog.e(TAG, "Can't init", e);
172                     }
173                 }
174             }.start();
175         }
176     };
177 
178     private static final BundleMerger sDropboxEntryAddedExtrasMerger;
179     static {
180         sDropboxEntryAddedExtrasMerger = new BundleMerger();
181         sDropboxEntryAddedExtrasMerger.setDefaultMergeStrategy(BundleMerger.STRATEGY_FIRST);
sDropboxEntryAddedExtrasMerger.setMergeStrategy(DropBoxManager.EXTRA_TIME, BundleMerger.STRATEGY_COMPARABLE_MAX)182         sDropboxEntryAddedExtrasMerger.setMergeStrategy(DropBoxManager.EXTRA_TIME,
183                 BundleMerger.STRATEGY_COMPARABLE_MAX);
sDropboxEntryAddedExtrasMerger.setMergeStrategy(DropBoxManager.EXTRA_DROPPED_COUNT, BundleMerger.STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD)184         sDropboxEntryAddedExtrasMerger.setMergeStrategy(DropBoxManager.EXTRA_DROPPED_COUNT,
185                 BundleMerger.STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD);
186     }
187 
188     private final IDropBoxManagerService.Stub mStub = new IDropBoxManagerService.Stub() {
189         @Override
190         public void addData(String tag, byte[] data, int flags) {
191             DropBoxManagerService.this.addData(tag, data, flags);
192         }
193 
194         @Override
195         public void addFile(String tag, ParcelFileDescriptor fd, int flags) {
196             DropBoxManagerService.this.addFile(tag, fd, flags);
197         }
198 
199         @Override
200         public boolean isTagEnabled(String tag) {
201             return DropBoxManagerService.this.isTagEnabled(tag);
202         }
203 
204         @Override
205         public DropBoxManager.Entry getNextEntry(String tag, long millis, String callingPackage) {
206             return getNextEntryWithAttribution(tag, millis, callingPackage, null);
207         }
208 
209         @Override
210         public DropBoxManager.Entry getNextEntryWithAttribution(String tag, long millis,
211                 String callingPackage, String callingAttributionTag) {
212             return DropBoxManagerService.this.getNextEntry(tag, millis, callingPackage,
213                     callingAttributionTag);
214         }
215 
216         @Override
217         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
218             DropBoxManagerService.this.dump(fd, pw, args);
219         }
220 
221         @Override
222         public void onShellCommand(FileDescriptor in, FileDescriptor out,
223                                    FileDescriptor err, String[] args, ShellCallback callback,
224                                    ResultReceiver resultReceiver) {
225             (new ShellCmd()).exec(this, in, out, err, args, callback, resultReceiver);
226         }
227     };
228 
229     private class ShellCmd extends ShellCommand {
230         @Override
onCommand(String cmd)231         public int onCommand(String cmd) {
232             if (cmd == null) {
233                 return handleDefaultCommands(cmd);
234             }
235             final PrintWriter pw = getOutPrintWriter();
236             try {
237                 switch (cmd) {
238                     case "set-rate-limit":
239                         final long period = Long.parseLong(getNextArgRequired());
240                         DropBoxManagerService.this.setLowPriorityRateLimit(period);
241                         break;
242                     case "add-low-priority":
243                         final String addedTag = getNextArgRequired();
244                         DropBoxManagerService.this.addLowPriorityTag(addedTag);
245                         break;
246                     case "remove-low-priority":
247                         final String removeTag = getNextArgRequired();
248                         DropBoxManagerService.this.removeLowPriorityTag(removeTag);
249                         break;
250                     case "restore-defaults":
251                         DropBoxManagerService.this.restoreDefaults();
252                         break;
253                     default:
254                         return handleDefaultCommands(cmd);
255                 }
256             } catch (Exception e) {
257                 pw.println(e);
258             }
259             return 0;
260         }
261 
262         @Override
onHelp()263         public void onHelp() {
264             PrintWriter pw = getOutPrintWriter();
265             pw.println("Dropbox manager service commands:");
266             pw.println("  help");
267             pw.println("    Print this help text.");
268             pw.println("  set-rate-limit PERIOD");
269             pw.println("    Sets low priority broadcast rate limit period to PERIOD ms");
270             pw.println("  add-low-priority TAG");
271             pw.println("    Add TAG to dropbox low priority list");
272             pw.println("  remove-low-priority TAG");
273             pw.println("    Remove TAG from dropbox low priority list");
274             pw.println("  restore-defaults");
275             pw.println("    restore dropbox settings to defaults");
276         }
277     }
278 
279     private class DropBoxManagerBroadcastHandler extends Handler {
280         private final Object mLock = new Object();
281 
282         static final int MSG_SEND_BROADCAST = 1;
283         static final int MSG_SEND_DEFERRED_BROADCAST = 2;
284 
285         @GuardedBy("mLock")
286         private final ArrayMap<String, Intent> mDeferredMap = new ArrayMap();
287 
DropBoxManagerBroadcastHandler(Looper looper)288         DropBoxManagerBroadcastHandler(Looper looper) {
289             super(looper);
290         }
291 
292         @Override
handleMessage(Message msg)293         public void handleMessage(Message msg) {
294             switch (msg.what) {
295                 case MSG_SEND_BROADCAST:
296                     prepareAndSendBroadcast((Intent) msg.obj, false);
297                     break;
298                 case MSG_SEND_DEFERRED_BROADCAST:
299                     Intent deferredIntent;
300                     synchronized (mLock) {
301                         deferredIntent = mDeferredMap.remove((String) msg.obj);
302                     }
303                     if (deferredIntent != null) {
304                         prepareAndSendBroadcast(deferredIntent, true);
305                     }
306                     break;
307             }
308         }
309 
prepareAndSendBroadcast(Intent intent, boolean deferrable)310         private void prepareAndSendBroadcast(Intent intent, boolean deferrable) {
311             if (!DropBoxManagerService.this.mBooted) {
312                 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
313             }
314             final BroadcastOptions options = BroadcastOptions.makeBasic();
315             if (Flags.enableReadDropboxPermission()) {
316                 options.setRequireCompatChange(ENFORCE_READ_DROPBOX_DATA, true);
317                 if (deferrable) {
318                     final String matchingKey = intent.getStringExtra(DropBoxManager.EXTRA_TAG)
319                             + "-READ_DROPBOX_DATA";
320                     setBroadcastOptionsForDeferral(options, matchingKey);
321                 }
322                 getContext().sendBroadcastAsUser(intent, UserHandle.ALL,
323                         Manifest.permission.READ_DROPBOX_DATA, options.toBundle());
324 
325                 options.setRequireCompatChange(ENFORCE_READ_DROPBOX_DATA, false);
326                 if (deferrable) {
327                     final String matchingKey = intent.getStringExtra(DropBoxManager.EXTRA_TAG)
328                             + "-READ_LOGS";
329                     setBroadcastOptionsForDeferral(options, matchingKey);
330                 }
331                 getContext().sendBroadcastAsUser(intent, UserHandle.ALL,
332                         Manifest.permission.READ_LOGS, options.toBundle());
333             } else {
334                 if (deferrable) {
335                     final String matchingKey = intent.getStringExtra(DropBoxManager.EXTRA_TAG);
336                     setBroadcastOptionsForDeferral(options, matchingKey);
337                 }
338                 getContext().sendBroadcastAsUser(intent, UserHandle.ALL,
339                         android.Manifest.permission.READ_LOGS, options.toBundle());
340             }
341         }
342 
createIntent(String tag, long time)343         private Intent createIntent(String tag, long time) {
344             final Intent dropboxIntent = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED);
345             dropboxIntent.putExtra(DropBoxManager.EXTRA_TAG, tag);
346             dropboxIntent.putExtra(DropBoxManager.EXTRA_TIME, time);
347             dropboxIntent.putExtra(DropBoxManager.EXTRA_DROPPED_COUNT, 0);
348             return dropboxIntent;
349         }
350 
setBroadcastOptionsForDeferral(BroadcastOptions options, String matchingKey)351         private void setBroadcastOptionsForDeferral(BroadcastOptions options, String matchingKey) {
352             options.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED)
353                     .setDeliveryGroupMatchingKey(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED,
354                             matchingKey)
355                     .setDeliveryGroupExtrasMerger(sDropboxEntryAddedExtrasMerger)
356                     .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE);
357         }
358 
359         /**
360          * Schedule a dropbox broadcast to be sent asynchronously.
361          */
sendBroadcast(String tag, long time)362         public void sendBroadcast(String tag, long time) {
363             sendMessage(obtainMessage(MSG_SEND_BROADCAST, createIntent(tag, time)));
364         }
365 
366         /**
367          * Possibly schedule a delayed dropbox broadcast. The broadcast will only be scheduled if
368          * no broadcast is currently scheduled. Otherwise updated the scheduled broadcast with the
369          * new intent information, effectively dropping the previous broadcast.
370          */
maybeDeferBroadcast(String tag, long time)371         public void maybeDeferBroadcast(String tag, long time) {
372             synchronized (mLock) {
373                 final Intent intent = mDeferredMap.get(tag);
374                 if (intent == null) {
375                     // Schedule new delayed broadcast.
376                     mDeferredMap.put(tag, createIntent(tag, time));
377                     sendMessageDelayed(obtainMessage(MSG_SEND_DEFERRED_BROADCAST, tag),
378                             mLowPriorityRateLimitPeriod);
379                 } else {
380                     // Broadcast is already scheduled. Update intent with new data.
381                     intent.putExtra(DropBoxManager.EXTRA_TIME, time);
382                     final int dropped = intent.getIntExtra(DropBoxManager.EXTRA_DROPPED_COUNT, 0);
383                     intent.putExtra(DropBoxManager.EXTRA_DROPPED_COUNT, dropped + 1);
384                     return;
385                 }
386             }
387         }
388     }
389 
390     /**
391      * Creates an instance of managed drop box storage using the default dropbox
392      * directory.
393      *
394      * @param context to use for receiving free space & gservices intents
395      */
DropBoxManagerService(final Context context)396     public DropBoxManagerService(final Context context) {
397         this(context, new File("/data/system/dropbox"), FgThread.get().getLooper());
398     }
399 
400     /**
401      * Creates an instance of managed drop box storage.  Normally there is one of these
402      * run by the system, but others can be created for testing and other purposes.
403      *
404      * @param context to use for receiving free space & gservices intents
405      * @param path to store drop box entries in
406      */
407     @VisibleForTesting
DropBoxManagerService(final Context context, File path, Looper looper)408     public DropBoxManagerService(final Context context, File path, Looper looper) {
409         super(context);
410         mDropBoxDir = path;
411         mContentResolver = getContext().getContentResolver();
412         mHandler = new DropBoxManagerBroadcastHandler(looper);
413         LocalServices.addService(DropBoxManagerInternal.class, new DropBoxManagerInternalImpl());
414     }
415 
416     @Override
onStart()417     public void onStart() {
418         publishBinderService(Context.DROPBOX_SERVICE, mStub);
419 
420         // The real work gets done lazily in init() -- that way service creation always
421         // succeeds, and things like disk problems cause individual method failures.
422     }
423 
424     @Override
onBootPhase(int phase)425     public void onBootPhase(int phase) {
426         switch (phase) {
427             case PHASE_SYSTEM_SERVICES_READY:
428                 IntentFilter filter = new IntentFilter();
429                 filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
430                 getContext().registerReceiver(mReceiver, filter);
431 
432                 mContentResolver.registerContentObserver(
433                     Settings.Global.CONTENT_URI, true,
434                     new ContentObserver(new Handler()) {
435                         @Override
436                         public void onChange(boolean selfChange) {
437                             mReceiver.onReceive(getContext(), (Intent) null);
438                         }
439                     });
440 
441                 getLowPriorityResourceConfigs();
442                 break;
443 
444             case PHASE_BOOT_COMPLETED:
445                 mBooted = true;
446                 break;
447         }
448     }
449 
450     /** Retrieves the binder stub -- for test instances */
getServiceStub()451     public IDropBoxManagerService getServiceStub() {
452         return mStub;
453     }
454 
addData(String tag, byte[] data, int flags)455     public void addData(String tag, byte[] data, int flags) {
456         addEntry(tag, new ByteArrayInputStream(data), data.length, flags);
457     }
458 
addFile(String tag, ParcelFileDescriptor fd, int flags)459     public void addFile(String tag, ParcelFileDescriptor fd, int flags) {
460         final StructStat stat;
461         try {
462             stat = Os.fstat(fd.getFileDescriptor());
463 
464             // Verify caller isn't playing games with pipes or sockets
465             if (!OsConstants.S_ISREG(stat.st_mode)) {
466                 throw new IllegalArgumentException(tag + " entry must be real file");
467             }
468         } catch (ErrnoException e) {
469             throw new IllegalArgumentException(e);
470         }
471 
472         addEntry(tag, new ParcelFileDescriptor.AutoCloseInputStream(fd), stat.st_size, flags);
473     }
474 
addEntry(String tag, InputStream in, long length, int flags)475     public void addEntry(String tag, InputStream in, long length, int flags) {
476         // If entry being added is large, and if it's not already compressed,
477         // then we'll force compress it during write
478         boolean forceCompress = false;
479         if ((flags & DropBoxManager.IS_GZIPPED) == 0
480                 && length > COMPRESS_THRESHOLD_BYTES) {
481             forceCompress = true;
482             flags |= DropBoxManager.IS_GZIPPED;
483         }
484 
485         addEntry(tag, new SimpleEntrySource(in, length, forceCompress), flags);
486     }
487 
488     /**
489      * Simple entry which contains data ready to be written.
490      */
491     public static class SimpleEntrySource implements EntrySource {
492         private final InputStream in;
493         private final long length;
494         private final boolean forceCompress;
495 
SimpleEntrySource(InputStream in, long length, boolean forceCompress)496         public SimpleEntrySource(InputStream in, long length, boolean forceCompress) {
497             this.in = in;
498             this.length = length;
499             this.forceCompress = forceCompress;
500         }
501 
length()502         public long length() {
503             return length;
504         }
505 
506         @Override
writeTo(FileDescriptor fd)507         public void writeTo(FileDescriptor fd) throws IOException {
508             // No need to buffer the output here, since data is either coming
509             // from an in-memory buffer, or another file on disk; if we buffered
510             // we'd lose out on sendfile() optimizations
511             if (forceCompress) {
512                 final GZIPOutputStream gzipOutputStream =
513                         new GZIPOutputStream(new FileOutputStream(fd));
514                 FileUtils.copy(in, gzipOutputStream);
515                 gzipOutputStream.close();
516             } else {
517                 FileUtils.copy(in, new FileOutputStream(fd));
518             }
519         }
520 
521         @Override
close()522         public void close() throws IOException {
523             FileUtils.closeQuietly(in);
524         }
525     }
526 
addEntry(String tag, EntrySource entry, int flags)527     public void addEntry(String tag, EntrySource entry, int flags) {
528         File temp = null;
529         try {
530             Slog.i(TAG, "add tag=" + tag + " isTagEnabled=" + isTagEnabled(tag)
531                     + " flags=0x" + Integer.toHexString(flags));
532             if ((flags & DropBoxManager.IS_EMPTY) != 0) throw new IllegalArgumentException();
533 
534             init();
535 
536             // Bail early if we know tag is disabled
537             if (!isTagEnabled(tag)) return;
538 
539             // Drop entries which are too large for our quota
540             final long length = entry.length();
541             final long max = trimToFit();
542             if (length > max) {
543                 // Log and fall through to create empty tombstone below
544                 Slog.w(TAG, "Dropping: " + tag + " (" + length + " > " + max + " bytes)");
545                 logDropboxDropped(
546                         FrameworkStatsLog.DROPBOX_ENTRY_DROPPED__DROP_REASON__ENTRY_TOO_LARGE,
547                         tag,
548                         0);
549             } else {
550                 temp = new File(mDropBoxDir, "drop" + Thread.currentThread().getId() + ".tmp");
551                 try (FileOutputStream out = new FileOutputStream(temp)) {
552                     entry.writeTo(out.getFD());
553                 }
554             }
555 
556             // Writing above succeeded, so create the finalized entry
557             long time = createEntry(temp, tag, flags);
558             temp = null;
559 
560             // Call sendBroadcast after returning from this call to avoid deadlock. In particular
561             // the caller may be holding the WindowManagerService lock but sendBroadcast requires a
562             // lock in ActivityManagerService. ActivityManagerService has been caught holding that
563             // very lock while waiting for the WindowManagerService lock.
564             if (mLowPriorityTags != null && mLowPriorityTags.contains(tag)) {
565                 // Rate limit low priority Dropbox entries
566                 mHandler.maybeDeferBroadcast(tag, time);
567             } else {
568                 mHandler.sendBroadcast(tag, time);
569             }
570         } catch (IOException e) {
571             Slog.e(TAG, "Can't write: " + tag, e);
572             logDropboxDropped(
573                     FrameworkStatsLog.DROPBOX_ENTRY_DROPPED__DROP_REASON__WRITE_FAILURE,
574                     tag,
575                     0);
576         } finally {
577             IoUtils.closeQuietly(entry);
578             if (temp != null) temp.delete();
579         }
580     }
581 
logDropboxDropped(int reason, String tag, long entryAge)582     private void logDropboxDropped(int reason, String tag, long entryAge) {
583         FrameworkStatsLog.write(FrameworkStatsLog.DROPBOX_ENTRY_DROPPED, reason, tag, entryAge);
584     }
585 
isTagEnabled(String tag)586     public boolean isTagEnabled(String tag) {
587         final long token = Binder.clearCallingIdentity();
588         try {
589             if (DISABLED_BY_DEFAULT_TAGS.contains(tag)) {
590                 return "enabled".equals(Settings.Global.getString(
591                     mContentResolver, Settings.Global.DROPBOX_TAG_PREFIX + tag));
592             } else {
593                 return !"disabled".equals(Settings.Global.getString(
594                     mContentResolver, Settings.Global.DROPBOX_TAG_PREFIX + tag));
595             }
596         } finally {
597             Binder.restoreCallingIdentity(token);
598         }
599     }
600 
checkPermission(int callingUid, String callingPackage, @Nullable String callingAttributionTag)601     private boolean checkPermission(int callingUid, String callingPackage,
602             @Nullable String callingAttributionTag) {
603         // If callers have this permission, then we don't need to check
604         // USAGE_STATS, because they are part of the system and have agreed to
605         // check USAGE_STATS before passing the data along.
606         if (getContext().checkCallingPermission(android.Manifest.permission.PEEK_DROPBOX_DATA)
607                 == PackageManager.PERMISSION_GRANTED) {
608             return true;
609         }
610 
611 
612         String permission = Manifest.permission.READ_LOGS;
613         if (Flags.enableReadDropboxPermission()
614                 && CompatChanges.isChangeEnabled(ENFORCE_READ_DROPBOX_DATA, callingUid)) {
615             permission = Manifest.permission.READ_DROPBOX_DATA;
616         }
617 
618         // Callers always need this permission
619         getContext().enforceCallingOrSelfPermission(permission, TAG);
620 
621 
622         // Callers also need the ability to read usage statistics
623         switch (getContext().getSystemService(AppOpsManager.class).noteOp(
624                 AppOpsManager.OP_GET_USAGE_STATS, callingUid, callingPackage, callingAttributionTag,
625                 null)) {
626             case AppOpsManager.MODE_ALLOWED:
627                 return true;
628             case AppOpsManager.MODE_DEFAULT:
629                 getContext().enforceCallingOrSelfPermission(
630                         android.Manifest.permission.PACKAGE_USAGE_STATS, TAG);
631                 return true;
632             default:
633                 return false;
634         }
635     }
636 
getNextEntry(String tag, long millis, String callingPackage, @Nullable String callingAttributionTag)637     public synchronized DropBoxManager.Entry getNextEntry(String tag, long millis,
638             String callingPackage, @Nullable String callingAttributionTag) {
639         if (!checkPermission(Binder.getCallingUid(), callingPackage, callingAttributionTag)) {
640             return null;
641         }
642 
643         try {
644             init();
645         } catch (IOException e) {
646             Slog.e(TAG, "Can't init", e);
647             return null;
648         }
649 
650         FileList list = tag == null ? mAllFiles : mFilesByTag.get(tag);
651         if (list == null) return null;
652 
653         for (EntryFile entry : list.contents.tailSet(new EntryFile(millis + 1))) {
654             if (entry.tag == null) continue;
655             if ((entry.flags & DropBoxManager.IS_EMPTY) != 0) {
656                 return new DropBoxManager.Entry(entry.tag, entry.timestampMillis);
657             }
658             final File file = entry.getFile(mDropBoxDir);
659             try {
660                 return new DropBoxManager.Entry(
661                         entry.tag, entry.timestampMillis, file, entry.flags);
662             } catch (IOException e) {
663                 Slog.wtf(TAG, "Can't read: " + file, e);
664                 // Continue to next file
665             }
666         }
667 
668         return null;
669     }
670 
setLowPriorityRateLimit(long period)671     private synchronized void setLowPriorityRateLimit(long period) {
672         mLowPriorityRateLimitPeriod = period;
673     }
674 
addLowPriorityTag(String tag)675     private synchronized void addLowPriorityTag(String tag) {
676         mLowPriorityTags.add(tag);
677     }
678 
removeLowPriorityTag(String tag)679     private synchronized void removeLowPriorityTag(String tag) {
680         mLowPriorityTags.remove(tag);
681     }
682 
restoreDefaults()683     private synchronized void restoreDefaults() {
684         getLowPriorityResourceConfigs();
685     }
686 
dump(FileDescriptor fd, PrintWriter pw, String[] args)687     public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
688         if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), TAG, pw)) return;
689 
690         try {
691             init();
692         } catch (IOException e) {
693             pw.println("Can't initialize: " + e);
694             Slog.e(TAG, "Can't init", e);
695             return;
696         }
697 
698         if (PROFILE_DUMP) Debug.startMethodTracing("/data/trace/dropbox.dump");
699 
700         StringBuilder out = new StringBuilder();
701         boolean doPrint = false, doFile = false;
702         boolean dumpProto = false;
703         ArrayList<String> searchArgs = new ArrayList<String>();
704         for (int i = 0; args != null && i < args.length; i++) {
705             if (args[i].equals("-p") || args[i].equals("--print")) {
706                 doPrint = true;
707             } else if (args[i].equals("-f") || args[i].equals("--file")) {
708                 doFile = true;
709             } else if (args[i].equals("--proto")) {
710                 dumpProto = true;
711             } else if (args[i].equals("-h") || args[i].equals("--help")) {
712                 pw.println("Dropbox (dropbox) dump options:");
713                 pw.println("  [-h|--help] [-p|--print] [-f|--file] [timestamp]");
714                 pw.println("    -h|--help: print this help");
715                 pw.println("    -p|--print: print full contents of each entry");
716                 pw.println("    -f|--file: print path of each entry's file");
717                 pw.println("    --proto: dump data to proto");
718                 pw.println("  [timestamp] optionally filters to only those entries.");
719                 return;
720             } else if (args[i].startsWith("-")) {
721                 out.append("Unknown argument: ").append(args[i]).append("\n");
722             } else {
723                 searchArgs.add(args[i]);
724             }
725         }
726 
727         if (dumpProto) {
728             dumpProtoLocked(fd, searchArgs);
729             return;
730         }
731 
732         out.append("Drop box contents: ").append(mAllFiles.contents.size()).append(" entries\n");
733         out.append("Max entries: ").append(mMaxFiles).append("\n");
734 
735         out.append("Low priority rate limit period: ");
736         out.append(mLowPriorityRateLimitPeriod).append(" ms\n");
737         out.append("Low priority tags: ").append(mLowPriorityTags).append("\n");
738 
739         if (!searchArgs.isEmpty()) {
740             out.append("Searching for:");
741             for (String a : searchArgs) out.append(" ").append(a);
742             out.append("\n");
743         }
744 
745         int numFound = 0;
746         out.append("\n");
747         for (EntryFile entry : mAllFiles.contents) {
748             if (!matchEntry(entry, searchArgs)) continue;
749 
750             numFound++;
751             if (doPrint) out.append("========================================\n");
752 
753             String date = TimeMigrationUtils.formatMillisWithFixedFormat(entry.timestampMillis);
754             out.append(date).append(" ").append(entry.tag == null ? "(no tag)" : entry.tag);
755 
756             final File file = entry.getFile(mDropBoxDir);
757             if (file == null) {
758                 out.append(" (no file)\n");
759                 continue;
760             } else if ((entry.flags & DropBoxManager.IS_EMPTY) != 0) {
761                 out.append(" (contents lost)\n");
762                 continue;
763             } else {
764                 out.append(" (");
765                 if ((entry.flags & DropBoxManager.IS_GZIPPED) != 0) out.append("compressed ");
766                 out.append((entry.flags & DropBoxManager.IS_TEXT) != 0 ? "text" : "data");
767                 out.append(", ").append(file.length()).append(" bytes)\n");
768             }
769 
770             if (doFile || (doPrint && (entry.flags & DropBoxManager.IS_TEXT) == 0)) {
771                 if (!doPrint) out.append("    ");
772                 out.append(file.getPath()).append("\n");
773             }
774 
775             if ((entry.flags & DropBoxManager.IS_TEXT) != 0 && doPrint) {
776                 DropBoxManager.Entry dbe = null;
777                 InputStreamReader isr = null;
778                 try {
779                     dbe = new DropBoxManager.Entry(
780                              entry.tag, entry.timestampMillis, file, entry.flags);
781 
782                     if (doPrint) {
783                         isr = new InputStreamReader(dbe.getInputStream());
784                         char[] buf = new char[4096];
785                         boolean newline = false;
786                         for (;;) {
787                             int n = isr.read(buf);
788                             if (n <= 0) break;
789                             out.append(buf, 0, n);
790                             newline = (buf[n - 1] == '\n');
791 
792                             // Flush periodically when printing to avoid out-of-memory.
793                             if (out.length() > 65536) {
794                                 pw.write(out.toString());
795                                 out.setLength(0);
796                             }
797                         }
798                         if (!newline) out.append("\n");
799                     }
800                 } catch (IOException e) {
801                     out.append("*** ").append(e.toString()).append("\n");
802                     Slog.e(TAG, "Can't read: " + file, e);
803                 } finally {
804                     if (dbe != null) dbe.close();
805                     if (isr != null) {
806                         try {
807                             isr.close();
808                         } catch (IOException unused) {
809                         }
810                     }
811                 }
812             }
813 
814             if (doPrint) out.append("\n");
815         }
816 
817         if (numFound == 0) out.append("(No entries found.)\n");
818 
819         if (args == null || args.length == 0) {
820             if (!doPrint) out.append("\n");
821             out.append("Usage: dumpsys dropbox [--print|--file] [YYYY-mm-dd] [HH:MM:SS] [tag]\n");
822         }
823 
824         pw.write(out.toString());
825         if (PROFILE_DUMP) Debug.stopMethodTracing();
826     }
827 
matchEntry(EntryFile entry, ArrayList<String> searchArgs)828     private boolean matchEntry(EntryFile entry, ArrayList<String> searchArgs) {
829         String date = TimeMigrationUtils.formatMillisWithFixedFormat(entry.timestampMillis);
830         boolean match = true;
831         int numArgs = searchArgs.size();
832         for (int i = 0; i < numArgs && match; i++) {
833             String arg = searchArgs.get(i);
834             match = (date.contains(arg) || arg.equals(entry.tag));
835         }
836         return match;
837     }
838 
dumpProtoLocked(FileDescriptor fd, ArrayList<String> searchArgs)839     private void dumpProtoLocked(FileDescriptor fd, ArrayList<String> searchArgs) {
840         final ProtoOutputStream proto = new ProtoOutputStream(fd);
841 
842         for (EntryFile entry : mAllFiles.contents) {
843             if (!matchEntry(entry, searchArgs)) continue;
844 
845             final File file = entry.getFile(mDropBoxDir);
846             if ((file == null) || ((entry.flags & DropBoxManager.IS_EMPTY) != 0)) {
847                 continue;
848             }
849 
850             final long bToken = proto.start(DropBoxManagerServiceDumpProto.ENTRIES);
851             proto.write(DropBoxManagerServiceDumpProto.Entry.TIME_MS, entry.timestampMillis);
852             try (
853                 DropBoxManager.Entry dbe = new DropBoxManager.Entry(
854                         entry.tag, entry.timestampMillis, file, entry.flags);
855                 InputStream is = dbe.getInputStream();
856             ) {
857                 if (is != null) {
858                     byte[] buf = new byte[PROTO_MAX_DATA_BYTES];
859                     int readBytes = 0;
860                     int n = 0;
861                     while (n >= 0 && (readBytes += n) < PROTO_MAX_DATA_BYTES) {
862                         n = is.read(buf, readBytes, PROTO_MAX_DATA_BYTES - readBytes);
863                     }
864                     proto.write(DropBoxManagerServiceDumpProto.Entry.DATA,
865                             Arrays.copyOf(buf, readBytes));
866                 }
867             } catch (IOException e) {
868                 Slog.e(TAG, "Can't read: " + file, e);
869             }
870 
871             proto.end(bToken);
872         }
873 
874         proto.flush();
875     }
876 
877     ///////////////////////////////////////////////////////////////////////////
878 
879     /** Chronologically sorted list of {@link EntryFile} */
880     private static final class FileList implements Comparable<FileList> {
881         public int blocks = 0;
882         public final TreeSet<EntryFile> contents = new TreeSet<EntryFile>();
883 
884         /** Sorts bigger FileList instances before smaller ones. */
compareTo(FileList o)885         public final int compareTo(FileList o) {
886             if (blocks != o.blocks) return o.blocks - blocks;
887             if (this == o) return 0;
888             if (hashCode() < o.hashCode()) return -1;
889             if (hashCode() > o.hashCode()) return 1;
890             return 0;
891         }
892     }
893 
894     /**
895      * Metadata describing an on-disk log file.
896      *
897      * Note its instances do no have knowledge on what directory they're stored, just to save
898      * 4/8 bytes per instance.  Instead, {@link #getFile} takes a directory so it can build a
899      * fullpath.
900      */
901     @VisibleForTesting
902     static final class EntryFile implements Comparable<EntryFile> {
903         public final String tag;
904         public final long timestampMillis;
905         public final int flags;
906         public final int blocks;
907 
908         /** Sorts earlier EntryFile instances before later ones. */
compareTo(EntryFile o)909         public final int compareTo(EntryFile o) {
910             int comp = Long.compare(timestampMillis, o.timestampMillis);
911             if (comp != 0) return comp;
912 
913             comp = ObjectUtils.compare(tag, o.tag);
914             if (comp != 0) return comp;
915 
916             comp = Integer.compare(flags, o.flags);
917             if (comp != 0) return comp;
918 
919             return Integer.compare(hashCode(), o.hashCode());
920         }
921 
922         /**
923          * Moves an existing temporary file to a new log filename.
924          *
925          * @param temp file to rename
926          * @param dir to store file in
927          * @param tag to use for new log file name
928          * @param timestampMillis of log entry
929          * @param flags for the entry data
930          * @param blockSize to use for space accounting
931          * @throws IOException if the file can't be moved
932          */
EntryFile(File temp, File dir, String tag,long timestampMillis, int flags, int blockSize)933         public EntryFile(File temp, File dir, String tag,long timestampMillis,
934                          int flags, int blockSize) throws IOException {
935             if ((flags & DropBoxManager.IS_EMPTY) != 0) throw new IllegalArgumentException();
936 
937             this.tag = TextUtils.safeIntern(tag);
938             this.timestampMillis = timestampMillis;
939             this.flags = flags;
940 
941             final File file = this.getFile(dir);
942             if (!temp.renameTo(file)) {
943                 throw new IOException("Can't rename " + temp + " to " + file);
944             }
945             this.blocks = (int) ((file.length() + blockSize - 1) / blockSize);
946         }
947 
948         /**
949          * Creates a zero-length tombstone for a file whose contents were lost.
950          *
951          * @param dir to store file in
952          * @param tag to use for new log file name
953          * @param timestampMillis of log entry
954          * @throws IOException if the file can't be created.
955          */
EntryFile(File dir, String tag, long timestampMillis)956         public EntryFile(File dir, String tag, long timestampMillis) throws IOException {
957             this.tag = TextUtils.safeIntern(tag);
958             this.timestampMillis = timestampMillis;
959             this.flags = DropBoxManager.IS_EMPTY;
960             this.blocks = 0;
961             new FileOutputStream(getFile(dir)).close();
962         }
963 
964         /**
965          * Extracts metadata from an existing on-disk log filename.
966          *
967          * Note when a filename is not recognizable, it will create an instance that
968          * {@link #hasFile()} would return false on, and also remove the file.
969          *
970          * @param file name of existing log file
971          * @param blockSize to use for space accounting
972          */
EntryFile(File file, int blockSize)973         public EntryFile(File file, int blockSize) {
974 
975             boolean parseFailure = false;
976 
977             String name = file.getName();
978             int flags = 0;
979             String tag = null;
980             long millis = 0;
981 
982             final int at = name.lastIndexOf('@');
983             if (at < 0) {
984                 parseFailure = true;
985             } else {
986                 tag = Uri.decode(name.substring(0, at));
987                 if (name.endsWith(".gz")) {
988                     flags |= DropBoxManager.IS_GZIPPED;
989                     name = name.substring(0, name.length() - 3);
990                 }
991                 if (name.endsWith(".lost")) {
992                     flags |= DropBoxManager.IS_EMPTY;
993                     name = name.substring(at + 1, name.length() - 5);
994                 } else if (name.endsWith(".txt")) {
995                     flags |= DropBoxManager.IS_TEXT;
996                     name = name.substring(at + 1, name.length() - 4);
997                 } else if (name.endsWith(".dat")) {
998                     name = name.substring(at + 1, name.length() - 4);
999                 } else {
1000                     parseFailure = true;
1001                 }
1002                 if (!parseFailure) {
1003                     try {
1004                         millis = Long.parseLong(name);
1005                     } catch (NumberFormatException e) {
1006                         parseFailure = true;
1007                     }
1008                 }
1009             }
1010             if (parseFailure) {
1011                 Slog.wtf(TAG, "Invalid filename: " + file);
1012 
1013                 // Remove the file and return an empty instance.
1014                 file.delete();
1015                 this.tag = null;
1016                 this.flags = DropBoxManager.IS_EMPTY;
1017                 this.timestampMillis = 0;
1018                 this.blocks = 0;
1019                 return;
1020             }
1021 
1022             this.blocks = (int) ((file.length() + blockSize - 1) / blockSize);
1023             this.tag = TextUtils.safeIntern(tag);
1024             this.flags = flags;
1025             this.timestampMillis = millis;
1026         }
1027 
1028         /**
1029          * Creates a EntryFile object with only a timestamp for comparison purposes.
1030          * @param millis to compare with.
1031          */
EntryFile(long millis)1032         public EntryFile(long millis) {
1033             this.tag = null;
1034             this.timestampMillis = millis;
1035             this.flags = DropBoxManager.IS_EMPTY;
1036             this.blocks = 0;
1037         }
1038 
1039         /**
1040          * @return whether an entry actually has a backing file, or it's an empty "tombstone"
1041          * entry.
1042          */
hasFile()1043         public boolean hasFile() {
1044             return tag != null;
1045         }
1046 
1047         /** @return File extension for the flags. */
getExtension()1048         private String getExtension() {
1049             if ((flags &  DropBoxManager.IS_EMPTY) != 0) {
1050                 return ".lost";
1051             }
1052             return ((flags & DropBoxManager.IS_TEXT) != 0 ? ".txt" : ".dat") +
1053                     ((flags & DropBoxManager.IS_GZIPPED) != 0 ? ".gz" : "");
1054         }
1055 
1056         /**
1057          * @return filename for this entry without the pathname.
1058          */
getFilename()1059         public String getFilename() {
1060             return hasFile() ? Uri.encode(tag) + "@" + timestampMillis + getExtension() : null;
1061         }
1062 
1063         /**
1064          * Get a full-path {@link File} representing this entry.
1065          * @param dir Parent directly.  The caller needs to pass it because {@link EntryFile}s don't
1066          *            know in which directory they're stored.
1067          */
getFile(File dir)1068         public File getFile(File dir) {
1069             return hasFile() ? new File(dir, getFilename()) : null;
1070         }
1071 
1072         /**
1073          * If an entry has a backing file, remove it.
1074          */
deleteFile(File dir)1075         public void deleteFile(File dir) {
1076             if (hasFile()) {
1077                 getFile(dir).delete();
1078             }
1079         }
1080     }
1081 
1082     ///////////////////////////////////////////////////////////////////////////
1083 
1084     /** If never run before, scans disk contents to build in-memory tracking data. */
init()1085     private synchronized void init() throws IOException {
1086         if (mStatFs == null) {
1087             if (!mDropBoxDir.isDirectory() && !mDropBoxDir.mkdirs()) {
1088                 throw new IOException("Can't mkdir: " + mDropBoxDir);
1089             }
1090             try {
1091                 mStatFs = new StatFs(mDropBoxDir.getPath());
1092                 mBlockSize = mStatFs.getBlockSize();
1093             } catch (IllegalArgumentException e) {  // StatFs throws this on error
1094                 throw new IOException("Can't statfs: " + mDropBoxDir);
1095             }
1096         }
1097 
1098         if (mAllFiles == null) {
1099             File[] files = mDropBoxDir.listFiles();
1100             if (files == null) throw new IOException("Can't list files: " + mDropBoxDir);
1101 
1102             mAllFiles = new FileList();
1103             mFilesByTag = new ArrayMap<>();
1104 
1105             // Scan pre-existing files.
1106             for (File file : files) {
1107                 if (file.getName().endsWith(".tmp")) {
1108                     Slog.i(TAG, "Cleaning temp file: " + file);
1109                     file.delete();
1110                     continue;
1111                 }
1112 
1113                 EntryFile entry = new EntryFile(file, mBlockSize);
1114 
1115                 if (entry.hasFile()) {
1116                     // Enroll only when the filename is valid.  Otherwise the above constructor
1117                     // has removed the file already.
1118                     enrollEntry(entry);
1119                 }
1120             }
1121         }
1122     }
1123 
1124     /** Adds a disk log file to in-memory tracking for accounting and enumeration. */
enrollEntry(EntryFile entry)1125     private synchronized void enrollEntry(EntryFile entry) {
1126         mAllFiles.contents.add(entry);
1127         mAllFiles.blocks += entry.blocks;
1128 
1129         // mFilesByTag is used for trimming, so don't list empty files.
1130         // (Zero-length/lost files are trimmed by date from mAllFiles.)
1131 
1132         if (entry.hasFile() && entry.blocks > 0) {
1133             FileList tagFiles = mFilesByTag.get(entry.tag);
1134             if (tagFiles == null) {
1135                 tagFiles = new FileList();
1136                 mFilesByTag.put(TextUtils.safeIntern(entry.tag), tagFiles);
1137             }
1138             tagFiles.contents.add(entry);
1139             tagFiles.blocks += entry.blocks;
1140         }
1141     }
1142 
1143     /** Moves a temporary file to a final log filename and enrolls it. */
createEntry(File temp, String tag, int flags)1144     private synchronized long createEntry(File temp, String tag, int flags) throws IOException {
1145         long t = System.currentTimeMillis();
1146 
1147         // Require each entry to have a unique timestamp; if there are entries
1148         // >10sec in the future (due to clock skew), drag them back to avoid
1149         // keeping them around forever.
1150 
1151         SortedSet<EntryFile> tail = mAllFiles.contents.tailSet(new EntryFile(t + 10000));
1152         EntryFile[] future = null;
1153         if (!tail.isEmpty()) {
1154             future = tail.toArray(new EntryFile[tail.size()]);
1155             tail.clear();  // Remove from mAllFiles
1156         }
1157 
1158         if (!mAllFiles.contents.isEmpty()) {
1159             t = Math.max(t, mAllFiles.contents.last().timestampMillis + 1);
1160         }
1161 
1162         if (future != null) {
1163             for (EntryFile late : future) {
1164                 mAllFiles.blocks -= late.blocks;
1165                 FileList tagFiles = mFilesByTag.get(late.tag);
1166                 if (tagFiles != null && tagFiles.contents.remove(late)) {
1167                     tagFiles.blocks -= late.blocks;
1168                 }
1169                 if ((late.flags & DropBoxManager.IS_EMPTY) == 0) {
1170                     enrollEntry(new EntryFile(late.getFile(mDropBoxDir), mDropBoxDir,
1171                             late.tag, t++, late.flags, mBlockSize));
1172                 } else {
1173                     enrollEntry(new EntryFile(mDropBoxDir, late.tag, t++));
1174                 }
1175             }
1176         }
1177 
1178         if (temp == null) {
1179             enrollEntry(new EntryFile(mDropBoxDir, tag, t));
1180         } else {
1181             enrollEntry(new EntryFile(temp, mDropBoxDir, tag, t, flags, mBlockSize));
1182         }
1183         return t;
1184     }
1185 
1186     /**
1187      * Trims the files on disk to make sure they aren't using too much space.
1188      * @return the overall quota for storage (in bytes)
1189      */
trimToFit()1190     private synchronized long trimToFit() throws IOException {
1191         // Expunge aged items (including tombstones marking deleted data).
1192 
1193         int ageSeconds = Settings.Global.getInt(mContentResolver,
1194                 Settings.Global.DROPBOX_AGE_SECONDS, DEFAULT_AGE_SECONDS);
1195         mMaxFiles = Settings.Global.getInt(mContentResolver,
1196                 Settings.Global.DROPBOX_MAX_FILES,
1197                 (ActivityManager.isLowRamDeviceStatic()
1198                         ?  DEFAULT_MAX_FILES_LOWRAM : DEFAULT_MAX_FILES));
1199         long curTimeMillis = System.currentTimeMillis();
1200         long cutoffMillis = curTimeMillis - ageSeconds * 1000;
1201         while (!mAllFiles.contents.isEmpty()) {
1202             EntryFile entry = mAllFiles.contents.first();
1203             if (entry.timestampMillis > cutoffMillis && mAllFiles.contents.size() < mMaxFiles) {
1204                 break;
1205             }
1206 
1207             logDropboxDropped(
1208                     FrameworkStatsLog.DROPBOX_ENTRY_DROPPED__DROP_REASON__AGED,
1209                     entry.tag,
1210                     curTimeMillis - entry.timestampMillis);
1211 
1212             FileList tag = mFilesByTag.get(entry.tag);
1213             if (tag != null && tag.contents.remove(entry)) tag.blocks -= entry.blocks;
1214             if (mAllFiles.contents.remove(entry)) mAllFiles.blocks -= entry.blocks;
1215             entry.deleteFile(mDropBoxDir);
1216         }
1217 
1218         // Compute overall quota (a fraction of available free space) in blocks.
1219         // The quota changes dynamically based on the amount of free space;
1220         // that way when lots of data is available we can use it, but we'll get
1221         // out of the way if storage starts getting tight.
1222 
1223         long uptimeMillis = SystemClock.uptimeMillis();
1224         if (uptimeMillis > mCachedQuotaUptimeMillis + QUOTA_RESCAN_MILLIS) {
1225             int quotaPercent = Settings.Global.getInt(mContentResolver,
1226                     Settings.Global.DROPBOX_QUOTA_PERCENT, DEFAULT_QUOTA_PERCENT);
1227             int reservePercent = Settings.Global.getInt(mContentResolver,
1228                     Settings.Global.DROPBOX_RESERVE_PERCENT, DEFAULT_RESERVE_PERCENT);
1229             int quotaKb = Settings.Global.getInt(mContentResolver,
1230                     Settings.Global.DROPBOX_QUOTA_KB, DEFAULT_QUOTA_KB);
1231 
1232             String dirPath = mDropBoxDir.getPath();
1233             try {
1234                 mStatFs.restat(dirPath);
1235             } catch (IllegalArgumentException e) {  // restat throws this on error
1236                 throw new IOException("Can't restat: " + mDropBoxDir);
1237             }
1238             long available = mStatFs.getAvailableBlocksLong();
1239             long nonreserved = available - mStatFs.getBlockCountLong() * reservePercent / 100;
1240             long maxAvailableLong = nonreserved * quotaPercent / 100;
1241             int maxAvailable = Math.toIntExact(Math.max(0,
1242                     Math.min(maxAvailableLong, Integer.MAX_VALUE)));
1243             int maximum = quotaKb * 1024 / mBlockSize;
1244             mCachedQuotaBlocks = Math.min(maximum, maxAvailable);
1245             mCachedQuotaUptimeMillis = uptimeMillis;
1246         }
1247 
1248         // If we're using too much space, delete old items to make room.
1249         //
1250         // We trim each tag independently (this is why we keep per-tag lists).
1251         // Space is "fairly" shared between tags -- they are all squeezed
1252         // equally until enough space is reclaimed.
1253         //
1254         // A single circular buffer (a la logcat) would be simpler, but this
1255         // way we can handle fat/bursty data (like 1MB+ bugreports, 300KB+
1256         // kernel crash dumps, and 100KB+ ANR reports) without swamping small,
1257         // well-behaved data streams (event statistics, profile data, etc).
1258         //
1259         // Deleted files are replaced with zero-length tombstones to mark what
1260         // was lost.  Tombstones are expunged by age (see above).
1261 
1262         if (mAllFiles.blocks > mCachedQuotaBlocks) {
1263             // Find a fair share amount of space to limit each tag
1264             int unsqueezed = mAllFiles.blocks, squeezed = 0;
1265             TreeSet<FileList> tags = new TreeSet<FileList>(mFilesByTag.values());
1266             for (FileList tag : tags) {
1267                 if (squeezed > 0 && tag.blocks <= (mCachedQuotaBlocks - unsqueezed) / squeezed) {
1268                     break;
1269                 }
1270                 unsqueezed -= tag.blocks;
1271                 squeezed++;
1272             }
1273             int tagQuota = (mCachedQuotaBlocks - unsqueezed) / squeezed;
1274 
1275             // Remove old items from each tag until it meets the per-tag quota.
1276             for (FileList tag : tags) {
1277                 if (mAllFiles.blocks < mCachedQuotaBlocks) break;
1278                 while (tag.blocks > tagQuota && !tag.contents.isEmpty()) {
1279                     EntryFile entry = tag.contents.first();
1280                     logDropboxDropped(
1281                             FrameworkStatsLog.DROPBOX_ENTRY_DROPPED__DROP_REASON__CLEARING_DATA,
1282                             entry.tag,
1283                             curTimeMillis - entry.timestampMillis);
1284 
1285                     if (tag.contents.remove(entry)) tag.blocks -= entry.blocks;
1286                     if (mAllFiles.contents.remove(entry)) mAllFiles.blocks -= entry.blocks;
1287 
1288                     try {
1289                         entry.deleteFile(mDropBoxDir);
1290                         enrollEntry(new EntryFile(mDropBoxDir, entry.tag, entry.timestampMillis));
1291                     } catch (IOException e) {
1292                         Slog.e(TAG, "Can't write tombstone file", e);
1293                     }
1294                 }
1295             }
1296         }
1297 
1298         return mCachedQuotaBlocks * mBlockSize;
1299     }
1300 
getLowPriorityResourceConfigs()1301     private void getLowPriorityResourceConfigs() {
1302         mLowPriorityRateLimitPeriod = Resources.getSystem().getInteger(
1303                 R.integer.config_dropboxLowPriorityBroadcastRateLimitPeriod);
1304 
1305         final String[] lowPrioritytags = Resources.getSystem().getStringArray(
1306                 R.array.config_dropboxLowPriorityTags);
1307         final int size = lowPrioritytags.length;
1308         if (size == 0) {
1309             mLowPriorityTags = null;
1310             return;
1311         }
1312         mLowPriorityTags = new ArraySet(size);
1313         for (int i = 0; i < size; i++) {
1314             mLowPriorityTags.add(lowPrioritytags[i]);
1315         }
1316     }
1317 
1318     private final class DropBoxManagerInternalImpl extends DropBoxManagerInternal {
1319         @Override
addEntry(String tag, EntrySource entry, int flags)1320         public void addEntry(String tag, EntrySource entry, int flags) {
1321             DropBoxManagerService.this.addEntry(tag, entry, flags);
1322         }
1323     }
1324 }
1325