001package com.intentsoftware.addapptr; 002 003import android.app.Application; 004import android.content.pm.ApplicationInfo; 005import android.content.pm.PackageManager; 006import android.os.Bundle; 007import android.os.Handler; 008import android.util.Log; 009import android.util.Pair; 010 011import com.intentsoftware.addapptr.module.Logger; 012 013import java.util.ArrayList; 014import java.util.LinkedList; 015import java.util.List; 016import java.util.Queue; 017 018/** 019 * A cache of automatically preloaded banner ads. The cache will always try to have a defined amount of banners available for immediate handout to the app whenever they are needed. 020 * <b>The BannerCache needs to be destroyed when no longer needed.</b> 021 */ 022public class BannerCache { 023 024 private static final String BANNER_CACHE_RELOAD_INTERVAL_METADATA_TAG = "com.intentsoftware.addapptr.banner_cache_reload_interval"; 025 private static final int ON_FAILED_BANNER_RELOAD_INTERVAL_DEFAULT = 10000; 026 private static int onFailedBannerReloadInterval = ON_FAILED_BANNER_RELOAD_INTERVAL_DEFAULT; 027 028 private final BannerCacheConfiguration configuration; 029 private final BannerPlacement bannerPlacement; 030 private final Handler handler = new Handler(); 031 032 private final Queue<BannerPlacementLayout> loadedBanners = new LinkedList<>(); 033 private final List<BannerRequest> runningRequests = new ArrayList<>(); 034 private final List<Runnable> bannerReloadRunnables = new ArrayList<>(); //list for runnables for retrying failed banner requests 035 036 private long consumptionTimestamp; 037 038 private boolean destroyed; 039 private boolean shouldNotifyDelegate = true; 040 041 042 //called by AdController, as we need an Context instance 043 static void init(Application application) { 044 try { 045 ApplicationInfo ai = application.getPackageManager().getApplicationInfo(application.getPackageName(), PackageManager.GET_META_DATA); 046 Bundle bundle = ai.metaData; 047 int reloadInterval = bundle.getInt(BANNER_CACHE_RELOAD_INTERVAL_METADATA_TAG); 048 if (reloadInterval > 0) { 049 if (Logger.isLoggable(Log.VERBOSE)) { 050 Logger.v(BannerCache.class, "Found value:" + reloadInterval + " for BannerCache banner reload interval."); 051 } 052 onFailedBannerReloadInterval = reloadInterval * 1000; 053 } else { 054 if (Logger.isLoggable(Log.VERBOSE)) { 055 Logger.v(BannerCache.class, "No value for BannerCache banner reload interval found."); 056 } 057 } 058 } catch (Exception e) { 059 if (Logger.isLoggable(Log.WARN)) { 060 Logger.w(BannerCache.class, "Exception when looking for BannerCache banner reload interval in manifest", e); 061 } 062 } 063 } 064 065 //region Public API 066 067 /** 068 * Creates a cache of automatically preloaded banner ads. 069 * 070 * @param cacheConfiguration {@link BannerCacheConfiguration} instance used to configure the cache. 071 */ 072 public BannerCache(BannerCacheConfiguration cacheConfiguration) { 073 this.configuration = new BannerCacheConfiguration(cacheConfiguration); 074 075 BannerConfiguration configuration = new BannerConfiguration(); 076 configuration.setManualAdSpaceCounting(true); 077 Pair<BannerPlacement, Boolean> placementBooleanPair = AATKit.createBannerPlacementForCache(cacheConfiguration.getPlacementName(), configuration, this); 078 079 bannerPlacement = placementBooleanPair.first; 080 081 if (bannerPlacement == null) { 082 if (Logger.isLoggable(Log.ERROR)) { 083 Logger.e(this, "Failed to create banner placement with name:" + cacheConfiguration.getPlacementName() + " for banner cache:" + toString() + ", cache will not work."); 084 } 085 } else if (Logger.isLoggable(Log.VERBOSE)) { 086 Logger.v(this, "Created banner cache:" + toString() + " with placement name:" + cacheConfiguration.getPlacementName()); 087 } 088 089 if (placementBooleanPair.second) { 090 onResume(); 091 } 092 } 093 094 /** 095 * Creates a cache of automatically preloaded banner ads. 096 * 097 * @param placementName The name of the banner placement that will be created. <b>The placement will be created by the cache and should not be created manually.</b> 098 * @param delegate Optional cache delegate. Can be null. 099 * @param size Defines how many preloaded banners should be available in the cache. Should be smaller than 5. 100 * @param shouldCacheAdditionalAdAtStart Defines if the cache should load additional ad at the beginning. 101 * @param requestConfiguration The configuration that will be used when requesting new banners. Can not be null. 102 * @deprecated This method is deprecated and will be removed in the future. Use {@link #BannerCache(BannerCacheConfiguration)} instead. 103 */ 104 @Deprecated 105 public BannerCache(String placementName, CacheDelegate delegate, int size, boolean shouldCacheAdditionalAdAtStart, BannerRequest requestConfiguration) { 106 this(prepareConfiguration(placementName, size, delegate, shouldCacheAdditionalAdAtStart, requestConfiguration)); //"this" call must be the first one in constructor body, thus - use helper static method 107 } 108 109 private static BannerCacheConfiguration prepareConfiguration(String placementName, int size, CacheDelegate delegate, boolean shouldCacheAdditionalAdAtStart, BannerRequest requestConfiguration) { 110 BannerCacheConfiguration configuration = new BannerCacheConfiguration(placementName, size); 111 configuration.setDelegate(delegate); 112 configuration.setShouldCacheAdditionalAdAtStart(shouldCacheAdditionalAdAtStart); 113 configuration.setRequestConfiguration(requestConfiguration); 114 configuration.setMinimumDelay(0); 115 return configuration; 116 } 117 118 /** 119 * Updates the configuration that will be used when requesting new banners. 120 * 121 * @param requestConfiguration {@link BannerRequest} instance. Can not be null. 122 * @param shouldRefresh True if the whole cache should be re-loaded with new banner request configuration, false if new configuration should only be used for new requests. 123 */ 124 @SuppressWarnings("unused") 125 public void updateRequestConfiguration(BannerRequest requestConfiguration, boolean shouldRefresh) { 126 if (checkDestroyedOrFailedToInitialize("updateRequestConfiguration")) return; 127 if (Logger.isLoggable(Log.VERBOSE)) { 128 Logger.v(this, "Updating request configuration with configuration:" + requestConfiguration + ", shouldRefresh:" + shouldRefresh + " for cache:" + toString()); 129 } 130 131 this.configuration.setRequestConfiguration(requestConfiguration); 132 if (shouldRefresh) { 133 shouldNotifyDelegate = true; //we want to inform the delegate again 134 clearCache(); 135 cancelRunningRequests(); 136 fillCache(); 137 } 138 } 139 140 /** 141 * Sets the optional cache delegate. 142 * 143 * @param delegate {@link CacheDelegate} instance. 144 * @deprecated This method is deprecated and will be removed in the future. Use {@link BannerCacheConfiguration#setDelegate(CacheDelegate)} instead. 145 */ 146 @SuppressWarnings("unused") 147 @Deprecated 148 public void setCacheDelegate(CacheDelegate delegate) { 149 if (checkDestroyedOrFailedToInitialize("setCacheDelegate")) return; 150 if (Logger.isLoggable(Log.VERBOSE)) { 151 Logger.v(this, "Setting delegate:" + delegate + " for cache:" + toString()); 152 } 153 154 configuration.setDelegate(delegate); 155 } 156 157 /** 158 * Returns an instance of {@link BannerPlacementLayout} to be used within the app. Also automatically counts an ad space. 159 * <p> 160 * <b>BannerCache will no longer hold any references to returned banners, and they need to be destroyed manually by the app.</b> 161 * <p> 162 * This method respects the frequency capping, set by {@link BannerCacheConfiguration#setMinimumDelay(int)} 163 * 164 * @return {@link BannerPlacementLayout} instance. 165 */ 166 @SuppressWarnings("unused") 167 public synchronized BannerPlacementLayout consume() { 168 return consume(false); 169 } 170 171 /** 172 * Returns an instance of {@link BannerPlacementLayout} to be used within the app. Also automatically counts an ad space. 173 * <b>BannerCache will no longer hold any references to returned banners, and they need to be destroyed manually by the app.</b> 174 * 175 * @param force true if cache should try to return banner ignoring the frequency capping set by {@link BannerCacheConfiguration#setMinimumDelay(int)} 176 * @return {@link BannerPlacementLayout} instance. 177 */ 178 public synchronized BannerPlacementLayout consume(boolean force) { 179 if (checkDestroyedOrFailedToInitialize("consume")) return null; 180 if (Logger.isLoggable(Log.VERBOSE)) { 181 Logger.v(this, "Called consume method with param force=" + force); 182 } 183 184 long currentTime = System.currentTimeMillis(); 185 if ((currentTime - this.consumptionTimestamp >= configuration.getMinDelayMillis()) || force) { 186 bannerPlacement.countAdSpace(); 187 BannerPlacementLayout banner = loadedBanners.poll(); 188 if (Logger.isLoggable(Log.VERBOSE)) { 189 Logger.v(this, "Consuming banner from cache:" + toString() + ", returning:" + banner); 190 } 191 fillCache(); 192 if (banner != null) { 193 this.consumptionTimestamp = currentTime; 194 } 195 return banner; 196 } else { 197 if (Logger.isLoggable(Log.VERBOSE)) { 198 Logger.v(this, "Minimum delay between \"consume\" calls not reached, returning null"); 199 } 200 return null; 201 } 202 } 203 204 /** 205 * Destroys the BannerCache, clearing all preloaded banner ads and canceling pending reload requests. For proper memory management, it needs to be called when the BannerCache is no longer needed. 206 * <b>Destroyed BannerCache can no longer be used.</b> 207 */ 208 @SuppressWarnings("unused") 209 public void destroy() { 210 if (checkDestroyedOrFailedToInitialize("destroy")) return; 211 if (Logger.isLoggable(Log.VERBOSE)) { 212 Logger.v(this, "Destroying cache:" + toString()); 213 } 214 215 AATKit.destroyBannerCache(this); 216 cancelRunningRequests(); 217 clearCache(); 218 configuration.setDelegate(null); 219 configuration.setRequestConfiguration(null); 220 destroyed = true; 221 } 222 223 //endregion 224 225 //region lifecycle handling 226 227 void onResume() { 228 fillCache(); 229 } 230 231 void onPause() { 232 cancelRunningRequests(); 233 } 234 235 //endregion 236 237 //region private methods 238 239 private boolean checkDestroyedOrFailedToInitialize(String methodName) { 240 if (bannerPlacement == null) { 241 if (Logger.isLoggable(Log.ERROR)) { 242 Logger.e(this, "Cannot execute method " + methodName + ", BannerCache was not created properly."); 243 } 244 return true; 245 } else if (destroyed) { 246 if (Logger.isLoggable(Log.WARN)) { 247 Logger.w(this, "Cannot execute method " + methodName + ", BannerCache is already destroyed!"); 248 } 249 } 250 return destroyed; 251 } 252 253 private synchronized void fillCache() { 254 if (!((Placement) bannerPlacement).isActivityResumed()) { 255 if (Logger.isLoggable(Log.DEBUG)) { 256 Logger.d(this, "Cannot fill cache, activity is paused"); 257 } 258 return; 259 } 260 261 int adsToLoad = configuration.getSize() - loadedBanners.size() - runningRequests.size() - bannerReloadRunnables.size(); 262 263 if (adsToLoad > 0) { 264 if (configuration.shouldCacheAdditionalAdAtStart()) { 265 adsToLoad++; 266 configuration.setShouldCacheAdditionalAdAtStart(false); 267 } 268 if (Logger.isLoggable(Log.VERBOSE)) { 269 Logger.v(this, "Banner cache " + toString() + " will request:" + adsToLoad + " banners."); 270 } 271 for (int i = 0; i < adsToLoad; i++) { 272 requestAd(); 273 } 274 } 275 } 276 277 private synchronized void clearCache() { 278 for (BannerPlacementLayout banner : loadedBanners) { 279 banner.destroy(); 280 } 281 loadedBanners.clear(); 282 } 283 284 private synchronized void cancelRunningRequests() { 285 if (Logger.isLoggable(Log.VERBOSE)) { 286 Logger.v(this, "Canceling running requests"); 287 } 288 289 List<BannerRequest> requestsToBeCanceled = new ArrayList<>(runningRequests); //needs a copy, as request.cancel ends up in modifying the list and thus ConcurrentModificationException 290 runningRequests.clear(); 291 for (BannerRequest request : requestsToBeCanceled) { 292 bannerPlacement.cancel(request); 293 } 294 295 for (Runnable runnable : bannerReloadRunnables) { 296 handler.removeCallbacks(runnable); 297 } 298 bannerReloadRunnables.clear(); 299 } 300 301 private synchronized void requestAd() { 302 BannerRequest newRequest = new BannerRequest(configuration.getRequestConfiguration().getDelegate()); 303 newRequest.setBannerSizes(configuration.getRequestConfiguration().getBannerSizes()); 304 newRequest.setTargetingInformation(configuration.getRequestConfiguration().getTargetingInformation()); 305 newRequest.setContentTargetingUrl(configuration.getRequestConfiguration().getContentTargetingUrl()); 306 307 runningRequests.add(newRequest); 308 bannerPlacement.requestAd(newRequest, createRequestCompletionListener(newRequest)); 309 } 310 311 private BannerRequestCompletionListener createRequestCompletionListener(final BannerRequest request) { 312 return new BannerRequestCompletionListener() { 313 @Override 314 public void onRequestCompleted(BannerPlacementLayout layout, BannerRequestError error) { 315 synchronized (BannerCache.this) { 316 runningRequests.remove(request); 317 if (layout != null) { 318 if (Logger.isLoggable(Log.VERBOSE)) { 319 Logger.v(BannerCache.this, "Banner:" + layout + " added to cache:" + BannerCache.this.toString()); 320 } 321 loadedBanners.add(layout); 322 if (shouldNotifyDelegate && configuration.getDelegate() != null) { 323 if (Logger.isLoggable(Log.VERBOSE)) { 324 Logger.v(BannerCache.this, "Banner cache:" + BannerCache.this.toString() + " notifying delegate:" + configuration.getDelegate()); 325 } 326 configuration.getDelegate().firstBannerLoaded(); 327 } 328 shouldNotifyDelegate = false; 329 } else { 330 if (Logger.isLoggable(Log.VERBOSE)) { 331 Logger.v(BannerCache.this, "Failed to load banner for queue, error:" + error.getMessage()); 332 } 333 Runnable runnable = new Runnable() { 334 @Override 335 public void run() { 336 synchronized (BannerCache.this) { 337 bannerReloadRunnables.remove(this); 338 requestAd(); 339 } 340 } 341 }; 342 bannerReloadRunnables.add(runnable); 343 handler.postDelayed(runnable, onFailedBannerReloadInterval); //retry banner reload after given time 344 } 345 } 346 } 347 }; 348 } 349 350 351 //endregion 352 353 @SuppressWarnings("NullableProblems") 354 @Override 355 public String toString() { 356 return "BannerCache{" + 357 "configuration=" + configuration + 358 '}'; 359 } 360 361 /** 362 * Optional delegate informing about events in BannerCache. 363 */ 364 public interface CacheDelegate { 365 /** 366 * Called when the first banner gets loaded for the cache. Only called once. 367 */ 368 void firstBannerLoaded(); 369 } 370}