1/* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.sdkuilib.internal.repository.ui; 18 19import com.android.SdkConstants; 20import com.android.sdklib.internal.repository.DownloadCache; 21import com.android.sdklib.internal.repository.DownloadCache.Strategy; 22import com.android.sdklib.internal.repository.IDescription; 23import com.android.sdklib.internal.repository.archives.Archive; 24import com.android.sdklib.internal.repository.packages.Package; 25import com.android.sdklib.internal.repository.sources.SdkSource; 26import com.android.sdkuilib.internal.repository.UpdaterData; 27import com.android.sdkuilib.internal.repository.core.PackageLoader; 28import com.android.sdkuilib.internal.repository.core.PackageLoader.ISourceLoadedCallback; 29import com.android.sdkuilib.internal.repository.core.PackagesDiffLogic; 30import com.android.sdkuilib.internal.repository.core.PkgCategory; 31import com.android.sdkuilib.internal.repository.core.PkgCategoryApi; 32import com.android.sdkuilib.internal.repository.core.PkgContentProvider; 33import com.android.sdkuilib.internal.repository.core.PkgItem; 34import com.android.sdkuilib.internal.repository.core.PkgItem.PkgState; 35import com.android.sdkuilib.internal.repository.icons.ImageFactory; 36 37import org.eclipse.jface.viewers.ColumnLabelProvider; 38import org.eclipse.jface.viewers.IInputProvider; 39import org.eclipse.jface.viewers.ITableFontProvider; 40import org.eclipse.swt.graphics.Font; 41import org.eclipse.swt.graphics.Image; 42import org.eclipse.swt.graphics.Point; 43 44import java.net.MalformedURLException; 45import java.net.URL; 46import java.util.List; 47 48/** 49 * Base class for {@link PackagesPage} that holds most of the logic to display 50 * the tree/list of packages. This class holds most of the logic and {@link PackagesPage} 51 * holds most of the UI (creating the UI, dealing with menus and buttons and tree 52 * selection.) This makes it easier to test the functionality by mocking only a 53 * subset of the UI. 54 */ 55abstract class PackagesPageImpl { 56 57 final UpdaterData mUpdaterData; 58 final PackagesDiffLogic mDiffLogic; 59 60 private ICheckboxTreeViewer mITreeViewer; 61 private ITreeViewerColumn mIColumnName; 62 private ITreeViewerColumn mIColumnApi; 63 private ITreeViewerColumn mIColumnRevision; 64 private ITreeViewerColumn mIColumnStatus; 65 66 PackagesPageImpl(UpdaterData updaterData) { 67 mUpdaterData = updaterData; 68 mDiffLogic = new PackagesDiffLogic(updaterData); 69 } 70 71 /** 72 * Utility method that derived classes can override to check whether the UI is disposed. 73 * When the UI is disposed, most operations that affect the UI will be bypassed. 74 * @return True if UI is not available and should not be touched. 75 */ 76 abstract protected boolean isUiDisposed(); 77 78 /** 79 * Utility method to execute a runnable on the main UI thread. 80 * Will do nothing if {@link #isUiDisposed()} returns false. 81 * @param runnable The runnable to execute on the main UI thread. 82 */ 83 abstract protected void syncExec(Runnable runnable); 84 85 void performFirstLoad() { 86 // First a package loader is created that only checks 87 // the local cache xml files. It populates the package 88 // list based on what the client got last, essentially. 89 loadPackages(true /*useLocalCache*/, false /*overrideExisting*/); 90 91 // Next a regular package loader is created that will 92 // respect the expiration and refresh parameters of the 93 // download cache. 94 loadPackages(false /*useLocalCache*/, true /*overrideExisting*/); 95 } 96 97 public void setITreeViewer(ICheckboxTreeViewer iTreeViewer) { 98 mITreeViewer = iTreeViewer; 99 } 100 101 public void setIColumns( 102 ITreeViewerColumn columnName, 103 ITreeViewerColumn columnApi, 104 ITreeViewerColumn columnRevision, 105 ITreeViewerColumn columnStatus) { 106 mIColumnName = columnName; 107 mIColumnApi = columnApi; 108 mIColumnRevision = columnRevision; 109 mIColumnStatus = columnStatus; 110 } 111 112 void postCreate() { 113 // Caller needs to call setITreeViewer before this. 114 assert mITreeViewer != null; 115 // Caller needs to call setIColumns before this. 116 assert mIColumnApi != null; 117 assert mIColumnName != null; 118 assert mIColumnStatus != null; 119 assert mIColumnRevision != null; 120 121 mITreeViewer.setContentProvider(new PkgContentProvider(mITreeViewer)); 122 123 mIColumnApi.setLabelProvider( 124 new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnApi))); 125 mIColumnName.setLabelProvider( 126 new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnName))); 127 mIColumnStatus.setLabelProvider( 128 new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnStatus))); 129 mIColumnRevision.setLabelProvider( 130 new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnRevision))); 131 } 132 133 /** 134 * Performs a full reload by removing all cached packages data, including the platforms 135 * and addons from the sdkmanager instance. This will perform a full local parsing 136 * as well as a full reload of the remote data (by fetching all sources again.) 137 */ 138 void fullReload() { 139 // Clear all source information, forcing them to be refreshed. 140 mUpdaterData.getSources().clearAllPackages(); 141 // Clear and reload all local data too. 142 localReload(); 143 } 144 145 /** 146 * Performs a full reload of all the local package information, including the platforms 147 * and addons from the sdkmanager instance. This will perform a full local parsing. 148 * <p/> 149 * This method does NOT force a new fetch of the remote sources. 150 * 151 * @see #fullReload() 152 */ 153 void localReload() { 154 // Clear all source caches, otherwise loading will use the cached data 155 mUpdaterData.getLocalSdkParser().clearPackages(); 156 mUpdaterData.getSdkManager().reloadSdk(mUpdaterData.getSdkLog()); 157 loadPackages(); 158 } 159 160 /** 161 * Performs a "normal" reload of the package information, use the default download 162 * cache and refreshing strategy as needed. 163 */ 164 void loadPackages() { 165 loadPackages(false /*useLocalCache*/, false /*overrideExisting*/); 166 } 167 168 /** 169 * Performs a reload of the package information. 170 * 171 * @param useLocalCache When true, the {@link PackageLoader} is switched to use 172 * a specific {@link DownloadCache} using the {@link Strategy#ONLY_CACHE}, meaning 173 * it will only use data from the local cache. It will not try to fetch or refresh 174 * manifests. This is used once the very first time the sdk manager window opens 175 * and is typically followed by a regular load with refresh. 176 */ 177 abstract protected void loadPackages(boolean useLocalCache, boolean overrideExisting); 178 179 /** 180 * Actual implementation of {@link #loadPackages(boolean, boolean)}. 181 * Derived implementations must call this to do the actual work after setting up the UI. 182 */ 183 void loadPackagesImpl(final boolean useLocalCache, final boolean overrideExisting) { 184 if (mUpdaterData == null) { 185 return; 186 } 187 188 final boolean displaySortByApi = isSortByApi(); 189 190 PackageLoader packageLoader = getPackageLoader(useLocalCache); 191 assert packageLoader != null; 192 193 mDiffLogic.updateStart(); 194 packageLoader.loadPackages(overrideExisting, new ISourceLoadedCallback() { 195 @Override 196 public boolean onUpdateSource(SdkSource source, Package[] newPackages) { 197 // This runs in a thread and must not access UI directly. 198 final boolean changed = mDiffLogic.updateSourcePackages( 199 displaySortByApi, source, newPackages); 200 201 syncExec(new Runnable() { 202 @Override 203 public void run() { 204 if (changed || 205 mITreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) { 206 refreshViewerInput(); 207 } 208 } 209 }); 210 211 // Return true to tell the loader to continue with the next source. 212 // Return false to stop the loader if any UI has been disposed, which can 213 // happen if the user is trying to close the window during the load operation. 214 return !isUiDisposed(); 215 } 216 217 @Override 218 public void onLoadCompleted() { 219 // This runs in a thread and must not access UI directly. 220 final boolean changed = mDiffLogic.updateEnd(displaySortByApi); 221 222 syncExec(new Runnable() { 223 @Override 224 public void run() { 225 if (changed || 226 mITreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) { 227 try { 228 refreshViewerInput(); 229 } catch (Exception ignore) {} 230 } 231 232 if (!useLocalCache && 233 mDiffLogic.isFirstLoadComplete() && 234 !isUiDisposed()) { 235 // At the end of the first load, if nothing is selected then 236 // automatically select all new and update packages. 237 Object[] checked = mITreeViewer.getCheckedElements(); 238 if (checked == null || checked.length == 0) { 239 onSelectNewUpdates( 240 false, //selectNew 241 true, //selectUpdates, 242 true); //selectTop 243 } 244 } 245 } 246 }); 247 } 248 }); 249 } 250 251 /** 252 * Used by {@link #loadPackagesImpl(boolean, boolean)} to get the package 253 * loader for the first or second pass update. When starting the manager 254 * starts with a first pass that reads only from the local cache, with no 255 * extra network access. That's {@code useLocalCache} being true. 256 * <p/> 257 * Leter it does a second pass with {@code useLocalCache} set to false 258 * and actually uses the download cache specified in {@link UpdaterData}. 259 * 260 * This is extracted so that we can control this cache via unit tests. 261 */ 262 protected PackageLoader getPackageLoader(boolean useLocalCache) { 263 if (useLocalCache) { 264 return new PackageLoader(mUpdaterData, new DownloadCache(Strategy.ONLY_CACHE)); 265 } else { 266 return mUpdaterData.getPackageLoader(); 267 } 268 } 269 270 /** 271 * Overridden by the UI to respond to a request to refresh the tree viewer 272 * when the input has changed. 273 * The implementation must call {@link #setViewerInput()} somehow and will 274 * also need to adjust the expand state of the tree items and/or update 275 * some buttons or other state. 276 */ 277 abstract protected void refreshViewerInput(); 278 279 /** 280 * Invoked from {@link #refreshViewerInput()} to actually either set the 281 * input of the tree viewer or refresh it if it's the <em>same</em> input 282 * object. 283 */ 284 protected void setViewerInput() { 285 List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi()); 286 if (mITreeViewer.getInput() != cats) { 287 // set initial input 288 mITreeViewer.setInput(cats); 289 } else { 290 // refresh existing, which preserves the expanded state, the selection 291 // and the checked state. 292 mITreeViewer.refresh(); 293 } 294 } 295 296 /** 297 * Overridden by the UI to determine if the tree should display packages sorted 298 * by API (returns true) or by repository source (returns false.) 299 */ 300 abstract protected boolean isSortByApi(); 301 302 /** 303 * Checks all PkgItems that are either new or have updates or select top platform 304 * for initial run. 305 */ 306 void onSelectNewUpdates(boolean selectNew, boolean selectUpdates, boolean selectTop) { 307 // This does not update the tree itself, syncViewerSelection does it in the caller. 308 mDiffLogic.checkNewUpdateItems( 309 selectNew, 310 selectUpdates, 311 selectTop, 312 SdkConstants.CURRENT_PLATFORM); 313 } 314 315 /** 316 * Deselect all checked PkgItems. 317 */ 318 void onDeselectAll() { 319 // This does not update the tree itself, syncViewerSelection does it in the caller. 320 mDiffLogic.uncheckAllItems(); 321 } 322 323 // ---------------------- 324 325 abstract protected Font getTreeFontItalic(); 326 327 class PkgCellLabelProvider extends ColumnLabelProvider implements ITableFontProvider { 328 329 private final ITreeViewerColumn mColumn; 330 331 public PkgCellLabelProvider(ITreeViewerColumn column) { 332 super(); 333 mColumn = column; 334 } 335 336 @Override 337 public String getText(Object element) { 338 339 if (mColumn == mIColumnName) { 340 if (element instanceof PkgCategory) { 341 return ((PkgCategory) element).getLabel(); 342 } else if (element instanceof PkgItem) { 343 return getPkgItemName((PkgItem) element); 344 } else if (element instanceof IDescription) { 345 return ((IDescription) element).getShortDescription(); 346 } 347 348 } else if (mColumn == mIColumnApi) { 349 int api = -1; 350 if (element instanceof PkgItem) { 351 api = ((PkgItem) element).getApi(); 352 } 353 if (api >= 1) { 354 return Integer.toString(api); 355 } 356 357 } else if (mColumn == mIColumnRevision) { 358 if (element instanceof PkgItem) { 359 PkgItem pkg = (PkgItem) element; 360 return pkg.getRevision().toShortString(); 361 } 362 363 } else if (mColumn == mIColumnStatus) { 364 if (element instanceof PkgItem) { 365 PkgItem pkg = (PkgItem) element; 366 367 switch(pkg.getState()) { 368 case INSTALLED: 369 Package update = pkg.getUpdatePkg(); 370 if (update != null) { 371 return String.format( 372 "Update available: rev. %1$s", 373 update.getRevision().toShortString()); 374 } 375 return "Installed"; 376 377 case NEW: 378 Package p = pkg.getMainPackage(); 379 if (p != null && p.hasCompatibleArchive()) { 380 return "Not installed"; 381 } else { 382 return String.format("Not compatible with %1$s", 383 SdkConstants.currentPlatformName()); 384 } 385 } 386 return pkg.getState().toString(); 387 388 } else if (element instanceof Package) { 389 // This is an update package. 390 return "New revision " + ((Package) element).getRevision().toShortString(); 391 } 392 } 393 394 return ""; //$NON-NLS-1$ 395 } 396 397 private String getPkgItemName(PkgItem item) { 398 String name = item.getName().trim(); 399 400 if (isSortByApi()) { 401 // When sorting by API, the package name might contains the API number 402 // or the platform name at the end. If we find it, cut it out since it's 403 // redundant. 404 405 PkgCategoryApi cat = (PkgCategoryApi) findCategoryForItem(item); 406 String apiLabel = cat.getApiLabel(); 407 String platLabel = cat.getPlatformName(); 408 409 if (platLabel != null && name.endsWith(platLabel)) { 410 return name.substring(0, name.length() - platLabel.length()); 411 412 } else if (apiLabel != null && name.endsWith(apiLabel)) { 413 return name.substring(0, name.length() - apiLabel.length()); 414 415 } else if (platLabel != null && item.isObsolete() && name.indexOf(platLabel) > 0) { 416 // For obsolete items, the format is "<base name> <platform name> (Obsolete)" 417 // so in this case only accept removing a platform name that is not at 418 // the end. 419 name = name.replace(platLabel, ""); //$NON-NLS-1$ 420 } 421 } 422 423 // Collapse potential duplicated spacing 424 name = name.replaceAll(" +", " "); //$NON-NLS-1$ //$NON-NLS-2$ 425 426 return name; 427 } 428 429 private PkgCategory findCategoryForItem(PkgItem item) { 430 List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi()); 431 for (PkgCategory cat : cats) { 432 for (PkgItem i : cat.getItems()) { 433 if (i == item) { 434 return cat; 435 } 436 } 437 } 438 439 return null; 440 } 441 442 @Override 443 public Image getImage(Object element) { 444 ImageFactory imgFactory = mUpdaterData.getImageFactory(); 445 446 if (imgFactory != null) { 447 if (mColumn == mIColumnName) { 448 if (element instanceof PkgCategory) { 449 return imgFactory.getImageForObject(((PkgCategory) element).getIconRef()); 450 } else if (element instanceof PkgItem) { 451 return imgFactory.getImageForObject(((PkgItem) element).getMainPackage()); 452 } 453 return imgFactory.getImageForObject(element); 454 455 } else if (mColumn == mIColumnStatus && element instanceof PkgItem) { 456 PkgItem pi = (PkgItem) element; 457 switch(pi.getState()) { 458 case INSTALLED: 459 if (pi.hasUpdatePkg()) { 460 return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_UPDATE); 461 } else { 462 return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_INSTALLED); 463 } 464 case NEW: 465 Package p = pi.getMainPackage(); 466 if (p != null && p.hasCompatibleArchive()) { 467 return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_NEW); 468 } else { 469 return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_INCOMPAT); 470 } 471 } 472 } 473 } 474 return super.getImage(element); 475 } 476 477 // -- ITableFontProvider 478 479 @Override 480 public Font getFont(Object element, int columnIndex) { 481 if (element instanceof PkgItem) { 482 if (((PkgItem) element).getState() == PkgState.NEW) { 483 return getTreeFontItalic(); 484 } 485 } else if (element instanceof Package) { 486 // update package 487 return getTreeFontItalic(); 488 } 489 return super.getFont(element); 490 } 491 492 // -- Tooltip support 493 494 @Override 495 public String getToolTipText(Object element) { 496 PkgItem pi = element instanceof PkgItem ? (PkgItem) element : null; 497 if (pi != null) { 498 element = pi.getMainPackage(); 499 } 500 if (element instanceof IDescription) { 501 String s = getTooltipDescription((IDescription) element); 502 503 if (pi != null && pi.hasUpdatePkg()) { 504 s += "\n-----------------" + //$NON-NLS-1$ 505 "\nUpdate Available:\n" + //$NON-NLS-1$ 506 getTooltipDescription(pi.getUpdatePkg()); 507 } 508 509 return s; 510 } 511 return super.getToolTipText(element); 512 } 513 514 private String getTooltipDescription(IDescription element) { 515 String s = element.getLongDescription(); 516 if (element instanceof Package) { 517 Package p = (Package) element; 518 519 if (!p.isLocal()) { 520 // For non-installed item, try to find a download size 521 for (Archive a : p.getArchives()) { 522 if (!a.isLocal() && a.isCompatible()) { 523 s += '\n' + a.getSizeDescription(); 524 break; 525 } 526 } 527 } 528 529 // Display info about where this package comes/came from 530 SdkSource src = p.getParentSource(); 531 if (src != null) { 532 try { 533 URL url = new URL(src.getUrl()); 534 String host = url.getHost(); 535 if (p.isLocal()) { 536 s += String.format("\nInstalled from %1$s", host); 537 } else { 538 s += String.format("\nProvided by %1$s", host); 539 } 540 } catch (MalformedURLException ignore) { 541 } 542 } 543 } 544 return s; 545 } 546 547 @Override 548 public Point getToolTipShift(Object object) { 549 return new Point(15, 5); 550 } 551 552 @Override 553 public int getToolTipDisplayDelayTime(Object object) { 554 return 500; 555 } 556 } 557 558 interface ICheckboxTreeViewer extends IInputProvider { 559 void setContentProvider(PkgContentProvider pkgContentProvider); 560 void refresh(); 561 void setInput(List<PkgCategory> cats); 562 Object[] getCheckedElements(); 563 } 564 565 interface ITreeViewerColumn { 566 void setLabelProvider(ColumnLabelProvider labelProvider); 567 } 568} 569