1 /* 2 * Copyright (C) 2006-2007 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.am; 18 19 import android.content.ComponentName; 20 import android.content.Context; 21 import android.content.pm.PackageInfo; 22 import android.content.pm.PackageManager; 23 import android.os.Binder; 24 import android.os.IBinder; 25 import android.os.FileUtils; 26 import android.os.Parcel; 27 import android.os.Process; 28 import android.os.ServiceManager; 29 import android.os.SystemClock; 30 import android.util.AtomicFile; 31 import android.util.Slog; 32 import android.util.Xml; 33 34 import com.android.internal.app.IUsageStats; 35 import com.android.internal.content.PackageMonitor; 36 import com.android.internal.os.PkgUsageStats; 37 import com.android.internal.util.FastXmlSerializer; 38 39 import org.xmlpull.v1.XmlPullParser; 40 import org.xmlpull.v1.XmlPullParserException; 41 import org.xmlpull.v1.XmlSerializer; 42 43 import java.io.File; 44 import java.io.FileDescriptor; 45 import java.io.FileInputStream; 46 import java.io.FileNotFoundException; 47 import java.io.FileOutputStream; 48 import java.io.IOException; 49 import java.io.PrintWriter; 50 import java.util.ArrayList; 51 import java.util.Calendar; 52 import java.util.Collections; 53 import java.util.HashMap; 54 import java.util.HashSet; 55 import java.util.List; 56 import java.util.Map; 57 import java.util.Set; 58 import java.util.TimeZone; 59 import java.util.concurrent.atomic.AtomicBoolean; 60 import java.util.concurrent.atomic.AtomicInteger; 61 import java.util.concurrent.atomic.AtomicLong; 62 63 /** 64 * This service collects the statistics associated with usage 65 * of various components, like when a particular package is launched or 66 * paused and aggregates events like number of time a component is launched 67 * total duration of a component launch. 68 */ 69 public final class UsageStatsService extends IUsageStats.Stub { 70 public static final String SERVICE_NAME = "usagestats"; 71 private static final boolean localLOGV = false; 72 private static final boolean REPORT_UNEXPECTED = false; 73 private static final String TAG = "UsageStats"; 74 75 // Current on-disk Parcel version 76 private static final int VERSION = 1007; 77 78 private static final int CHECKIN_VERSION = 4; 79 80 private static final String FILE_PREFIX = "usage-"; 81 82 private static final String FILE_HISTORY = FILE_PREFIX + "history.xml"; 83 84 private static final int FILE_WRITE_INTERVAL = 30*60*1000; //ms 85 86 private static final int MAX_NUM_FILES = 5; 87 88 private static final int NUM_LAUNCH_TIME_BINS = 10; 89 private static final int[] LAUNCH_TIME_BINS = { 90 250, 500, 750, 1000, 1500, 2000, 3000, 4000, 5000 91 }; 92 93 static IUsageStats sService; 94 private Context mContext; 95 // structure used to maintain statistics since the last checkin. 96 final private Map<String, PkgUsageStatsExtended> mStats; 97 98 // Maintains the last time any component was resumed, for all time. 99 final private Map<String, Map<String, Long>> mLastResumeTimes; 100 101 // To remove last-resume time stats when a pacakge is removed. 102 private PackageMonitor mPackageMonitor; 103 104 // Lock to update package stats. Methods suffixed by SLOCK should invoked with 105 // this lock held 106 final Object mStatsLock; 107 // Lock to write to file. Methods suffixed by FLOCK should invoked with 108 // this lock held. 109 final Object mFileLock; 110 // Order of locks is mFileLock followed by mStatsLock to avoid deadlocks 111 private String mLastResumedPkg; 112 private String mLastResumedComp; 113 private boolean mIsResumed; 114 private File mFile; 115 private AtomicFile mHistoryFile; 116 private String mFileLeaf; 117 private File mDir; 118 119 private Calendar mCal; // guarded by itself 120 121 private final AtomicInteger mLastWriteDay = new AtomicInteger(-1); 122 private final AtomicLong mLastWriteElapsedTime = new AtomicLong(0); 123 private final AtomicBoolean mUnforcedDiskWriteRunning = new AtomicBoolean(false); 124 125 static class TimeStats { 126 int count; 127 int[] times = new int[NUM_LAUNCH_TIME_BINS]; 128 129 TimeStats() { 130 } 131 132 void incCount() { 133 count++; 134 } 135 136 void add(int val) { 137 final int[] bins = LAUNCH_TIME_BINS; 138 for (int i=0; i<NUM_LAUNCH_TIME_BINS-1; i++) { 139 if (val < bins[i]) { 140 times[i]++; 141 return; 142 } 143 } 144 times[NUM_LAUNCH_TIME_BINS-1]++; 145 } 146 147 TimeStats(Parcel in) { 148 count = in.readInt(); 149 final int[] localTimes = times; 150 for (int i=0; i<NUM_LAUNCH_TIME_BINS; i++) { 151 localTimes[i] = in.readInt(); 152 } 153 } 154 155 void writeToParcel(Parcel out) { 156 out.writeInt(count); 157 final int[] localTimes = times; 158 for (int i=0; i<NUM_LAUNCH_TIME_BINS; i++) { 159 out.writeInt(localTimes[i]); 160 } 161 } 162 } 163 164 private class PkgUsageStatsExtended { 165 final HashMap<String, TimeStats> mLaunchTimes 166 = new HashMap<String, TimeStats>(); 167 int mLaunchCount; 168 long mUsageTime; 169 long mPausedTime; 170 long mResumedTime; 171 172 PkgUsageStatsExtended() { 173 mLaunchCount = 0; 174 mUsageTime = 0; 175 } 176 177 PkgUsageStatsExtended(Parcel in) { 178 mLaunchCount = in.readInt(); 179 mUsageTime = in.readLong(); 180 if (localLOGV) Slog.v(TAG, "Launch count: " + mLaunchCount 181 + ", Usage time:" + mUsageTime); 182 183 final int numTimeStats = in.readInt(); 184 if (localLOGV) Slog.v(TAG, "Reading comps: " + numTimeStats); 185 for (int i=0; i<numTimeStats; i++) { 186 String comp = in.readString(); 187 if (localLOGV) Slog.v(TAG, "Component: " + comp); 188 TimeStats times = new TimeStats(in); 189 mLaunchTimes.put(comp, times); 190 } 191 } 192 193 void updateResume(String comp, boolean launched) { 194 if (launched) { 195 mLaunchCount ++; 196 } 197 mResumedTime = SystemClock.elapsedRealtime(); 198 } 199 200 void updatePause() { 201 mPausedTime = SystemClock.elapsedRealtime(); 202 mUsageTime += (mPausedTime - mResumedTime); 203 } 204 205 void addLaunchCount(String comp) { 206 TimeStats times = mLaunchTimes.get(comp); 207 if (times == null) { 208 times = new TimeStats(); 209 mLaunchTimes.put(comp, times); 210 } 211 times.incCount(); 212 } 213 214 void addLaunchTime(String comp, int millis) { 215 TimeStats times = mLaunchTimes.get(comp); 216 if (times == null) { 217 times = new TimeStats(); 218 mLaunchTimes.put(comp, times); 219 } 220 times.add(millis); 221 } 222 223 void writeToParcel(Parcel out) { 224 out.writeInt(mLaunchCount); 225 out.writeLong(mUsageTime); 226 final int numTimeStats = mLaunchTimes.size(); 227 out.writeInt(numTimeStats); 228 if (numTimeStats > 0) { 229 for (Map.Entry<String, TimeStats> ent : mLaunchTimes.entrySet()) { 230 out.writeString(ent.getKey()); 231 TimeStats times = ent.getValue(); 232 times.writeToParcel(out); 233 } 234 } 235 } 236 237 void clear() { 238 mLaunchTimes.clear(); 239 mLaunchCount = 0; 240 mUsageTime = 0; 241 } 242 } 243 244 UsageStatsService(String dir) { 245 mStats = new HashMap<String, PkgUsageStatsExtended>(); 246 mLastResumeTimes = new HashMap<String, Map<String, Long>>(); 247 mStatsLock = new Object(); 248 mFileLock = new Object(); 249 mDir = new File(dir); 250 mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0")); 251 252 mDir.mkdir(); 253 254 // Remove any old usage files from previous versions. 255 File parentDir = mDir.getParentFile(); 256 String fList[] = parentDir.list(); 257 if (fList != null) { 258 String prefix = mDir.getName() + "."; 259 int i = fList.length; 260 while (i > 0) { 261 i--; 262 if (fList[i].startsWith(prefix)) { 263 Slog.i(TAG, "Deleting old usage file: " + fList[i]); 264 (new File(parentDir, fList[i])).delete(); 265 } 266 } 267 } 268 269 // Update current stats which are binned by date 270 mFileLeaf = getCurrentDateStr(FILE_PREFIX); 271 mFile = new File(mDir, mFileLeaf); 272 mHistoryFile = new AtomicFile(new File(mDir, FILE_HISTORY)); 273 readStatsFromFile(); 274 readHistoryStatsFromFile(); 275 mLastWriteElapsedTime.set(SystemClock.elapsedRealtime()); 276 // mCal was set by getCurrentDateStr(), want to use that same time. 277 mLastWriteDay.set(mCal.get(Calendar.DAY_OF_YEAR)); 278 } 279 280 /* 281 * Utility method to convert date into string. 282 */ 283 private String getCurrentDateStr(String prefix) { 284 StringBuilder sb = new StringBuilder(); 285 synchronized (mCal) { 286 mCal.setTimeInMillis(System.currentTimeMillis()); 287 if (prefix != null) { 288 sb.append(prefix); 289 } 290 sb.append(mCal.get(Calendar.YEAR)); 291 int mm = mCal.get(Calendar.MONTH) - Calendar.JANUARY +1; 292 if (mm < 10) { 293 sb.append("0"); 294 } 295 sb.append(mm); 296 int dd = mCal.get(Calendar.DAY_OF_MONTH); 297 if (dd < 10) { 298 sb.append("0"); 299 } 300 sb.append(dd); 301 } 302 return sb.toString(); 303 } 304 305 private Parcel getParcelForFile(File file) throws IOException { 306 FileInputStream stream = new FileInputStream(file); 307 byte[] raw = readFully(stream); 308 Parcel in = Parcel.obtain(); 309 in.unmarshall(raw, 0, raw.length); 310 in.setDataPosition(0); 311 stream.close(); 312 return in; 313 } 314 315 private void readStatsFromFile() { 316 File newFile = mFile; 317 synchronized (mFileLock) { 318 try { 319 if (newFile.exists()) { 320 readStatsFLOCK(newFile); 321 } else { 322 // Check for file limit before creating a new file 323 checkFileLimitFLOCK(); 324 newFile.createNewFile(); 325 } 326 } catch (IOException e) { 327 Slog.w(TAG,"Error : " + e + " reading data from file:" + newFile); 328 } 329 } 330 } 331 332 private void readStatsFLOCK(File file) throws IOException { 333 Parcel in = getParcelForFile(file); 334 int vers = in.readInt(); 335 if (vers != VERSION) { 336 Slog.w(TAG, "Usage stats version changed; dropping"); 337 return; 338 } 339 int N = in.readInt(); 340 while (N > 0) { 341 N--; 342 String pkgName = in.readString(); 343 if (pkgName == null) { 344 break; 345 } 346 if (localLOGV) Slog.v(TAG, "Reading package #" + N + ": " + pkgName); 347 PkgUsageStatsExtended pus = new PkgUsageStatsExtended(in); 348 synchronized (mStatsLock) { 349 mStats.put(pkgName, pus); 350 } 351 } 352 } 353 354 private void readHistoryStatsFromFile() { 355 synchronized (mFileLock) { 356 if (mHistoryFile.getBaseFile().exists()) { 357 readHistoryStatsFLOCK(mHistoryFile); 358 } 359 } 360 } 361 362 private void readHistoryStatsFLOCK(AtomicFile file) { 363 FileInputStream fis = null; 364 try { 365 fis = mHistoryFile.openRead(); 366 XmlPullParser parser = Xml.newPullParser(); 367 parser.setInput(fis, null); 368 int eventType = parser.getEventType(); 369 while (eventType != XmlPullParser.START_TAG) { 370 eventType = parser.next(); 371 } 372 String tagName = parser.getName(); 373 if ("usage-history".equals(tagName)) { 374 String pkg = null; 375 do { 376 eventType = parser.next(); 377 if (eventType == XmlPullParser.START_TAG) { 378 tagName = parser.getName(); 379 int depth = parser.getDepth(); 380 if ("pkg".equals(tagName) && depth == 2) { 381 pkg = parser.getAttributeValue(null, "name"); 382 } else if ("comp".equals(tagName) && depth == 3 && pkg != null) { 383 String comp = parser.getAttributeValue(null, "name"); 384 String lastResumeTimeStr = parser.getAttributeValue(null, "lrt"); 385 if (comp != null && lastResumeTimeStr != null) { 386 try { 387 long lastResumeTime = Long.parseLong(lastResumeTimeStr); 388 synchronized (mStatsLock) { 389 Map<String, Long> lrt = mLastResumeTimes.get(pkg); 390 if (lrt == null) { 391 lrt = new HashMap<String, Long>(); 392 mLastResumeTimes.put(pkg, lrt); 393 } 394 lrt.put(comp, lastResumeTime); 395 } 396 } catch (NumberFormatException e) { 397 } 398 } 399 } 400 } else if (eventType == XmlPullParser.END_TAG) { 401 if ("pkg".equals(parser.getName())) { 402 pkg = null; 403 } 404 } 405 } while (eventType != XmlPullParser.END_DOCUMENT); 406 } 407 } catch (XmlPullParserException e) { 408 Slog.w(TAG,"Error reading history stats: " + e); 409 } catch (IOException e) { 410 Slog.w(TAG,"Error reading history stats: " + e); 411 } finally { 412 if (fis != null) { 413 try { 414 fis.close(); 415 } catch (IOException e) { 416 } 417 } 418 } 419 } 420 421 private ArrayList<String> getUsageStatsFileListFLOCK() { 422 // Check if there are too many files in the system and delete older files 423 String fList[] = mDir.list(); 424 if (fList == null) { 425 return null; 426 } 427 ArrayList<String> fileList = new ArrayList<String>(); 428 for (String file : fList) { 429 if (!file.startsWith(FILE_PREFIX)) { 430 continue; 431 } 432 if (file.endsWith(".bak")) { 433 (new File(mDir, file)).delete(); 434 continue; 435 } 436 fileList.add(file); 437 } 438 return fileList; 439 } 440 441 private void checkFileLimitFLOCK() { 442 // Get all usage stats output files 443 ArrayList<String> fileList = getUsageStatsFileListFLOCK(); 444 if (fileList == null) { 445 // Strange but we dont have to delete any thing 446 return; 447 } 448 int count = fileList.size(); 449 if (count <= MAX_NUM_FILES) { 450 return; 451 } 452 // Sort files 453 Collections.sort(fileList); 454 count -= MAX_NUM_FILES; 455 // Delete older files 456 for (int i = 0; i < count; i++) { 457 String fileName = fileList.get(i); 458 File file = new File(mDir, fileName); 459 Slog.i(TAG, "Deleting usage file : " + fileName); 460 file.delete(); 461 } 462 } 463 464 /** 465 * Conditionally start up a disk write if it's been awhile, or the 466 * day has rolled over. 467 * 468 * This is called indirectly from user-facing actions (when 469 * 'force' is false) so it tries to be quick, without writing to 470 * disk directly or acquiring heavy locks. 471 * 472 * @params force do an unconditional, synchronous stats flush 473 * to disk on the current thread. 474 * @params forceWriteHistoryStats Force writing of historical stats. 475 */ 476 private void writeStatsToFile(final boolean force, final boolean forceWriteHistoryStats) { 477 int curDay; 478 synchronized (mCal) { 479 mCal.setTimeInMillis(System.currentTimeMillis()); 480 curDay = mCal.get(Calendar.DAY_OF_YEAR); 481 } 482 final boolean dayChanged = curDay != mLastWriteDay.get(); 483 484 // Determine if the day changed... note that this will be wrong 485 // if the year has changed but we are in the same day of year... 486 // we can probably live with this. 487 final long currElapsedTime = SystemClock.elapsedRealtime(); 488 489 // Fast common path, without taking the often-contentious 490 // mFileLock. 491 if (!force) { 492 if (!dayChanged && 493 (currElapsedTime - mLastWriteElapsedTime.get()) < FILE_WRITE_INTERVAL) { 494 // wait till the next update 495 return; 496 } 497 if (mUnforcedDiskWriteRunning.compareAndSet(false, true)) { 498 new Thread("UsageStatsService_DiskWriter") { 499 public void run() { 500 try { 501 if (localLOGV) Slog.d(TAG, "Disk writer thread starting."); 502 writeStatsToFile(true, false); 503 } finally { 504 mUnforcedDiskWriteRunning.set(false); 505 if (localLOGV) Slog.d(TAG, "Disk writer thread ending."); 506 } 507 } 508 }.start(); 509 } 510 return; 511 } 512 513 synchronized (mFileLock) { 514 // Get the most recent file 515 mFileLeaf = getCurrentDateStr(FILE_PREFIX); 516 // Copy current file to back up 517 File backupFile = null; 518 if (mFile != null && mFile.exists()) { 519 backupFile = new File(mFile.getPath() + ".bak"); 520 if (!backupFile.exists()) { 521 if (!mFile.renameTo(backupFile)) { 522 Slog.w(TAG, "Failed to persist new stats"); 523 return; 524 } 525 } else { 526 mFile.delete(); 527 } 528 } 529 530 try { 531 // Write mStats to file 532 writeStatsFLOCK(mFile); 533 mLastWriteElapsedTime.set(currElapsedTime); 534 if (dayChanged) { 535 mLastWriteDay.set(curDay); 536 // clear stats 537 synchronized (mStats) { 538 mStats.clear(); 539 } 540 mFile = new File(mDir, mFileLeaf); 541 checkFileLimitFLOCK(); 542 } 543 544 if (dayChanged || forceWriteHistoryStats) { 545 // Write history stats daily, or when forced (due to shutdown). 546 writeHistoryStatsFLOCK(mHistoryFile); 547 } 548 549 // Delete the backup file 550 if (backupFile != null) { 551 backupFile.delete(); 552 } 553 } catch (IOException e) { 554 Slog.w(TAG, "Failed writing stats to file:" + mFile); 555 if (backupFile != null) { 556 mFile.delete(); 557 backupFile.renameTo(mFile); 558 } 559 } 560 } 561 if (localLOGV) Slog.d(TAG, "Dumped usage stats."); 562 } 563 564 private void writeStatsFLOCK(File file) throws IOException { 565 FileOutputStream stream = new FileOutputStream(file); 566 try { 567 Parcel out = Parcel.obtain(); 568 writeStatsToParcelFLOCK(out); 569 stream.write(out.marshall()); 570 out.recycle(); 571 stream.flush(); 572 } finally { 573 FileUtils.sync(stream); 574 stream.close(); 575 } 576 } 577 578 private void writeStatsToParcelFLOCK(Parcel out) { 579 synchronized (mStatsLock) { 580 out.writeInt(VERSION); 581 Set<String> keys = mStats.keySet(); 582 out.writeInt(keys.size()); 583 for (String key : keys) { 584 PkgUsageStatsExtended pus = mStats.get(key); 585 out.writeString(key); 586 pus.writeToParcel(out); 587 } 588 } 589 } 590 591 /** Filter out stats for any packages which aren't present anymore. */ 592 private void filterHistoryStats() { 593 synchronized (mStatsLock) { 594 // Copy and clear the last resume times map, then copy back stats 595 // for all installed packages. 596 Map<String, Map<String, Long>> tmpLastResumeTimes = 597 new HashMap<String, Map<String, Long>>(mLastResumeTimes); 598 mLastResumeTimes.clear(); 599 for (PackageInfo info : mContext.getPackageManager().getInstalledPackages(0)) { 600 if (tmpLastResumeTimes.containsKey(info.packageName)) { 601 mLastResumeTimes.put(info.packageName, tmpLastResumeTimes.get(info.packageName)); 602 } 603 } 604 } 605 } 606 607 private void writeHistoryStatsFLOCK(AtomicFile historyFile) { 608 FileOutputStream fos = null; 609 try { 610 fos = historyFile.startWrite(); 611 XmlSerializer out = new FastXmlSerializer(); 612 out.setOutput(fos, "utf-8"); 613 out.startDocument(null, true); 614 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 615 out.startTag(null, "usage-history"); 616 synchronized (mStatsLock) { 617 for (Map.Entry<String, Map<String, Long>> pkgEntry : mLastResumeTimes.entrySet()) { 618 out.startTag(null, "pkg"); 619 out.attribute(null, "name", pkgEntry.getKey()); 620 for (Map.Entry<String, Long> compEntry : pkgEntry.getValue().entrySet()) { 621 out.startTag(null, "comp"); 622 out.attribute(null, "name", compEntry.getKey()); 623 out.attribute(null, "lrt", compEntry.getValue().toString()); 624 out.endTag(null, "comp"); 625 } 626 out.endTag(null, "pkg"); 627 } 628 } 629 out.endTag(null, "usage-history"); 630 out.endDocument(); 631 632 historyFile.finishWrite(fos); 633 } catch (IOException e) { 634 Slog.w(TAG,"Error writing history stats" + e); 635 if (fos != null) { 636 historyFile.failWrite(fos); 637 } 638 } 639 } 640 641 public void publish(Context context) { 642 mContext = context; 643 ServiceManager.addService(SERVICE_NAME, asBinder()); 644 } 645 646 /** 647 * Start watching packages to remove stats when a package is uninstalled. 648 * May only be called when the package manager is ready. 649 */ 650 public void monitorPackages() { 651 mPackageMonitor = new PackageMonitor() { 652 @Override 653 public void onPackageRemovedAllUsers(String packageName, int uid) { 654 synchronized (mStatsLock) { 655 mLastResumeTimes.remove(packageName); 656 } 657 } 658 }; 659 mPackageMonitor.register(mContext, null, true); 660 filterHistoryStats(); 661 } 662 663 public void shutdown() { 664 if (mPackageMonitor != null) { 665 mPackageMonitor.unregister(); 666 } 667 Slog.i(TAG, "Writing usage stats before shutdown..."); 668 writeStatsToFile(true, true); 669 } 670 671 public static IUsageStats getService() { 672 if (sService != null) { 673 return sService; 674 } 675 IBinder b = ServiceManager.getService(SERVICE_NAME); 676 sService = asInterface(b); 677 return sService; 678 } 679 680 public void noteResumeComponent(ComponentName componentName) { 681 enforceCallingPermission(); 682 String pkgName; 683 synchronized (mStatsLock) { 684 if ((componentName == null) || 685 ((pkgName = componentName.getPackageName()) == null)) { 686 return; 687 } 688 689 final boolean samePackage = pkgName.equals(mLastResumedPkg); 690 if (mIsResumed) { 691 if (mLastResumedPkg != null) { 692 // We last resumed some other package... just pause it now 693 // to recover. 694 if (REPORT_UNEXPECTED) Slog.i(TAG, "Unexpected resume of " + pkgName 695 + " while already resumed in " + mLastResumedPkg); 696 PkgUsageStatsExtended pus = mStats.get(mLastResumedPkg); 697 if (pus != null) { 698 pus.updatePause(); 699 } 700 } 701 } 702 703 final boolean sameComp = samePackage 704 && componentName.getClassName().equals(mLastResumedComp); 705 706 mIsResumed = true; 707 mLastResumedPkg = pkgName; 708 mLastResumedComp = componentName.getClassName(); 709 710 if (localLOGV) Slog.i(TAG, "started component:" + pkgName); 711 PkgUsageStatsExtended pus = mStats.get(pkgName); 712 if (pus == null) { 713 pus = new PkgUsageStatsExtended(); 714 mStats.put(pkgName, pus); 715 } 716 pus.updateResume(mLastResumedComp, !samePackage); 717 if (!sameComp) { 718 pus.addLaunchCount(mLastResumedComp); 719 } 720 721 Map<String, Long> componentResumeTimes = mLastResumeTimes.get(pkgName); 722 if (componentResumeTimes == null) { 723 componentResumeTimes = new HashMap<String, Long>(); 724 mLastResumeTimes.put(pkgName, componentResumeTimes); 725 } 726 componentResumeTimes.put(mLastResumedComp, System.currentTimeMillis()); 727 } 728 } 729 730 public void notePauseComponent(ComponentName componentName) { 731 enforceCallingPermission(); 732 733 synchronized (mStatsLock) { 734 String pkgName; 735 if ((componentName == null) || 736 ((pkgName = componentName.getPackageName()) == null)) { 737 return; 738 } 739 if (!mIsResumed) { 740 if (REPORT_UNEXPECTED) Slog.i(TAG, "Something wrong here, didn't expect " 741 + pkgName + " to be paused"); 742 return; 743 } 744 mIsResumed = false; 745 746 if (localLOGV) Slog.i(TAG, "paused component:"+pkgName); 747 748 PkgUsageStatsExtended pus = mStats.get(pkgName); 749 if (pus == null) { 750 // Weird some error here 751 Slog.i(TAG, "No package stats for pkg:"+pkgName); 752 return; 753 } 754 pus.updatePause(); 755 } 756 757 // Persist current data to file if needed. 758 writeStatsToFile(false, false); 759 } 760 761 public void noteLaunchTime(ComponentName componentName, int millis) { 762 enforceCallingPermission(); 763 String pkgName; 764 if ((componentName == null) || 765 ((pkgName = componentName.getPackageName()) == null)) { 766 return; 767 } 768 769 // Persist current data to file if needed. 770 writeStatsToFile(false, false); 771 772 synchronized (mStatsLock) { 773 PkgUsageStatsExtended pus = mStats.get(pkgName); 774 if (pus != null) { 775 pus.addLaunchTime(componentName.getClassName(), millis); 776 } 777 } 778 } 779 780 public void enforceCallingPermission() { 781 if (Binder.getCallingPid() == Process.myPid()) { 782 return; 783 } 784 mContext.enforcePermission(android.Manifest.permission.UPDATE_DEVICE_STATS, 785 Binder.getCallingPid(), Binder.getCallingUid(), null); 786 } 787 788 public PkgUsageStats getPkgUsageStats(ComponentName componentName) { 789 mContext.enforceCallingOrSelfPermission( 790 android.Manifest.permission.PACKAGE_USAGE_STATS, null); 791 String pkgName; 792 if ((componentName == null) || 793 ((pkgName = componentName.getPackageName()) == null)) { 794 return null; 795 } 796 synchronized (mStatsLock) { 797 PkgUsageStatsExtended pus = mStats.get(pkgName); 798 Map<String, Long> lastResumeTimes = mLastResumeTimes.get(pkgName); 799 if (pus == null && lastResumeTimes == null) { 800 return null; 801 } 802 int launchCount = pus != null ? pus.mLaunchCount : 0; 803 long usageTime = pus != null ? pus.mUsageTime : 0; 804 return new PkgUsageStats(pkgName, launchCount, usageTime, lastResumeTimes); 805 } 806 } 807 808 public PkgUsageStats[] getAllPkgUsageStats() { 809 mContext.enforceCallingOrSelfPermission( 810 android.Manifest.permission.PACKAGE_USAGE_STATS, null); 811 synchronized (mStatsLock) { 812 int size = mLastResumeTimes.size(); 813 if (size <= 0) { 814 return null; 815 } 816 PkgUsageStats retArr[] = new PkgUsageStats[size]; 817 int i = 0; 818 for (Map.Entry<String, Map<String, Long>> entry : mLastResumeTimes.entrySet()) { 819 String pkg = entry.getKey(); 820 long usageTime = 0; 821 int launchCount = 0; 822 823 PkgUsageStatsExtended pus = mStats.get(pkg); 824 if (pus != null) { 825 usageTime = pus.mUsageTime; 826 launchCount = pus.mLaunchCount; 827 } 828 retArr[i] = new PkgUsageStats(pkg, launchCount, usageTime, entry.getValue()); 829 i++; 830 } 831 return retArr; 832 } 833 } 834 835 static byte[] readFully(FileInputStream stream) throws java.io.IOException { 836 int pos = 0; 837 int avail = stream.available(); 838 byte[] data = new byte[avail]; 839 while (true) { 840 int amt = stream.read(data, pos, data.length-pos); 841 if (amt <= 0) { 842 return data; 843 } 844 pos += amt; 845 avail = stream.available(); 846 if (avail > data.length-pos) { 847 byte[] newData = new byte[pos+avail]; 848 System.arraycopy(data, 0, newData, 0, pos); 849 data = newData; 850 } 851 } 852 } 853 854 private void collectDumpInfoFLOCK(PrintWriter pw, boolean isCompactOutput, 855 boolean deleteAfterPrint, HashSet<String> packages) { 856 List<String> fileList = getUsageStatsFileListFLOCK(); 857 if (fileList == null) { 858 return; 859 } 860 Collections.sort(fileList); 861 for (String file : fileList) { 862 if (deleteAfterPrint && file.equalsIgnoreCase(mFileLeaf)) { 863 // In this mode we don't print the current day's stats, since 864 // they are incomplete. 865 continue; 866 } 867 File dFile = new File(mDir, file); 868 String dateStr = file.substring(FILE_PREFIX.length()); 869 try { 870 Parcel in = getParcelForFile(dFile); 871 collectDumpInfoFromParcelFLOCK(in, pw, dateStr, isCompactOutput, 872 packages); 873 if (deleteAfterPrint) { 874 // Delete old file after collecting info only for checkin requests 875 dFile.delete(); 876 } 877 } catch (FileNotFoundException e) { 878 Slog.w(TAG, "Failed with "+e+" when collecting dump info from file : " + file); 879 return; 880 } catch (IOException e) { 881 Slog.w(TAG, "Failed with "+e+" when collecting dump info from file : "+file); 882 } 883 } 884 } 885 886 private void collectDumpInfoFromParcelFLOCK(Parcel in, PrintWriter pw, 887 String date, boolean isCompactOutput, HashSet<String> packages) { 888 StringBuilder sb = new StringBuilder(512); 889 if (isCompactOutput) { 890 sb.append("D:"); 891 sb.append(CHECKIN_VERSION); 892 sb.append(','); 893 } else { 894 sb.append("Date: "); 895 } 896 897 sb.append(date); 898 899 int vers = in.readInt(); 900 if (vers != VERSION) { 901 sb.append(" (old data version)"); 902 pw.println(sb.toString()); 903 return; 904 } 905 906 pw.println(sb.toString()); 907 int N = in.readInt(); 908 909 while (N > 0) { 910 N--; 911 String pkgName = in.readString(); 912 if (pkgName == null) { 913 break; 914 } 915 sb.setLength(0); 916 PkgUsageStatsExtended pus = new PkgUsageStatsExtended(in); 917 if (packages != null && !packages.contains(pkgName)) { 918 // This package has not been requested -- don't print 919 // anything for it. 920 } else if (isCompactOutput) { 921 sb.append("P:"); 922 sb.append(pkgName); 923 sb.append(','); 924 sb.append(pus.mLaunchCount); 925 sb.append(','); 926 sb.append(pus.mUsageTime); 927 sb.append('\n'); 928 final int NC = pus.mLaunchTimes.size(); 929 if (NC > 0) { 930 for (Map.Entry<String, TimeStats> ent : pus.mLaunchTimes.entrySet()) { 931 sb.append("A:"); 932 String activity = ent.getKey(); 933 if (activity.startsWith(pkgName)) { 934 sb.append('*'); 935 sb.append(activity.substring( 936 pkgName.length(), activity.length())); 937 } else { 938 sb.append(activity); 939 } 940 TimeStats times = ent.getValue(); 941 sb.append(','); 942 sb.append(times.count); 943 for (int i=0; i<NUM_LAUNCH_TIME_BINS; i++) { 944 sb.append(","); 945 sb.append(times.times[i]); 946 } 947 sb.append('\n'); 948 } 949 } 950 951 } else { 952 sb.append(" "); 953 sb.append(pkgName); 954 sb.append(": "); 955 sb.append(pus.mLaunchCount); 956 sb.append(" times, "); 957 sb.append(pus.mUsageTime); 958 sb.append(" ms"); 959 sb.append('\n'); 960 final int NC = pus.mLaunchTimes.size(); 961 if (NC > 0) { 962 for (Map.Entry<String, TimeStats> ent : pus.mLaunchTimes.entrySet()) { 963 sb.append(" "); 964 sb.append(ent.getKey()); 965 TimeStats times = ent.getValue(); 966 sb.append(": "); 967 sb.append(times.count); 968 sb.append(" starts"); 969 int lastBin = 0; 970 for (int i=0; i<NUM_LAUNCH_TIME_BINS-1; i++) { 971 if (times.times[i] != 0) { 972 sb.append(", "); 973 sb.append(lastBin); 974 sb.append('-'); 975 sb.append(LAUNCH_TIME_BINS[i]); 976 sb.append("ms="); 977 sb.append(times.times[i]); 978 } 979 lastBin = LAUNCH_TIME_BINS[i]; 980 } 981 if (times.times[NUM_LAUNCH_TIME_BINS-1] != 0) { 982 sb.append(", "); 983 sb.append(">="); 984 sb.append(lastBin); 985 sb.append("ms="); 986 sb.append(times.times[NUM_LAUNCH_TIME_BINS-1]); 987 } 988 sb.append('\n'); 989 } 990 } 991 } 992 993 pw.write(sb.toString()); 994 } 995 } 996 997 /** 998 * Searches array of arguments for the specified string 999 * @param args array of argument strings 1000 * @param value value to search for 1001 * @return true if the value is contained in the array 1002 */ 1003 private static boolean scanArgs(String[] args, String value) { 1004 if (args != null) { 1005 for (String arg : args) { 1006 if (value.equals(arg)) { 1007 return true; 1008 } 1009 } 1010 } 1011 return false; 1012 } 1013 1014 /** 1015 * Searches array of arguments for the specified string's data 1016 * @param args array of argument strings 1017 * @param value value to search for 1018 * @return the string of data after the arg, or null if there is none 1019 */ 1020 private static String scanArgsData(String[] args, String value) { 1021 if (args != null) { 1022 final int N = args.length; 1023 for (int i=0; i<N; i++) { 1024 if (value.equals(args[i])) { 1025 i++; 1026 return i < N ? args[i] : null; 1027 } 1028 } 1029 } 1030 return null; 1031 } 1032 1033 @Override 1034 /* 1035 * The data persisted to file is parsed and the stats are computed. 1036 */ 1037 protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 1038 if (mContext.checkCallingPermission(android.Manifest.permission.DUMP) 1039 != PackageManager.PERMISSION_GRANTED) { 1040 pw.println("Permission Denial: can't dump UsageStats from from pid=" 1041 + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() 1042 + " without permission " + android.Manifest.permission.DUMP); 1043 return; 1044 } 1045 1046 final boolean isCheckinRequest = scanArgs(args, "--checkin"); 1047 final boolean isCompactOutput = isCheckinRequest || scanArgs(args, "-c"); 1048 final boolean deleteAfterPrint = isCheckinRequest || scanArgs(args, "-d"); 1049 final String rawPackages = scanArgsData(args, "--packages"); 1050 1051 // Make sure the current stats are written to the file. This 1052 // doesn't need to be done if we are deleting files after printing, 1053 // since it that case we won't print the current stats. 1054 if (!deleteAfterPrint) { 1055 writeStatsToFile(true, false); 1056 } 1057 1058 HashSet<String> packages = null; 1059 if (rawPackages != null) { 1060 if (!"*".equals(rawPackages)) { 1061 // A * is a wildcard to show all packages. 1062 String[] names = rawPackages.split(","); 1063 for (String n : names) { 1064 if (packages == null) { 1065 packages = new HashSet<String>(); 1066 } 1067 packages.add(n); 1068 } 1069 } 1070 } else if (isCheckinRequest) { 1071 // If checkin doesn't specify any packages, then we simply won't 1072 // show anything. 1073 Slog.w(TAG, "Checkin without packages"); 1074 return; 1075 } 1076 1077 synchronized (mFileLock) { 1078 collectDumpInfoFLOCK(pw, isCompactOutput, deleteAfterPrint, packages); 1079 } 1080 } 1081 1082 } 1083