package com.mopub.nativeads; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.mopub.common.logging.MoPubLog; import com.mopub.nativeads.MoPubNativeAdPositioning.MoPubClientPositioning; import java.util.List; /** * A data that represents placed ads in a {@link com.mopub.nativeads.MoPubStreamAdPlacer}, * useful for tracking insertion and placed ad positions. * * It maintains four lists of integers * 1) Desired insertion positions - positions to place ads * 2) Desired original positions - original position for each ad to place * 2) Adjusted ad positions - ad positions that were placed * 3) Original ad positions - original position of the item after each placed ad * * For example, consider the following ad positions: * ORIGINAL LIST ADJUSTED LIST * Item 0 Item 0 * Item 1 Ad * Item 2 Item 1 * Item 3 Ad * Item 2 * Ad * Item 3 * * List starts as: * Item 0 * Item 1 * Item 2 * Item 3 * desiredOriginalPositions: {1, 2, 3} * desiredInsertionPositions: {1, 2, 3} * originalPositions: {} * adjustedPositions: {} * * If we place at position 2: * Item 0 * Item 1 * Ad * Item 2 * Item 3 * desiredOriginalPositions: {1, 3} * desiredInsertionPositions: {1, 4} * originalPositions: {2} * adjustedPositions: {2} * * If the developer adds a content item at position 2 * Item 0 * Item 1 * New Item * Ad * Item 3 * Item 4 * desiredOriginalPositions: {1, 4} * desiredInsertionPositions: {1, 5} * originalPositions: {3} * adjustedPositions: {3} * * Now, place at position 1 * Item 0 * Ad * Item 1 * New Item * Ad * Item 3 * Item 4 * desiredOriginalPositions: {4} * desiredInsertionPositions: {6} * originalPositions: {1, 3} * adjustedPositions: {1, 4} * * Place at position 6 * Item 0 * Ad * Item 1 * New Item * Ad * Item 3 * Ad * Item 4 * desiredOriginalPositions: {} * desiredInsertionPositions: {} * originalPositions: {1, 3, 4} * adjustedPositions: {1, 4, 6} * * Clear ad at position 1 * Item 0 * Item 1 * New Item * Ad * Item 3 * Ad * Item 4 * desiredOriginalPositions: {1} * desiredInsertionPositions: {1} * originalPositions: {3, 4} * adjustedPositions: {3, 5} * * Clear ad at position 5 * Item 0 * Item 1 * New Item * Ad * Item 3 * Item 4 * desiredOriginalPositions: {1, 4} * desiredInsertionPositions: {1, 5} * originalPositions: {3} * adjustedPositions: {3} * * Some runtime guarantees in terms of number of insertion ads: * - Finds the next or previous insertion position in O(logN) * - Maps from adjusted to original positions and vice versa in O(logN) * - Places an ad (moves positions from desired to placed) in O(N) */ class PlacementData { /** * Returned when positions are not found. */ public final static int NOT_FOUND = -1; // Cap the number of ads to avoid unrestrained memory usage. 200 allows the 5 positioning // arrays to fit in less than 4K. private final static int MAX_ADS = 200; // Initialize all of these to their max capacity. This prevents garbage collection when // reallocating the list, which causes noticeable stuttering when scrolling on some devices. @NonNull private final int[] mDesiredOriginalPositions = new int[MAX_ADS]; @NonNull private final int[] mDesiredInsertionPositions = new int[MAX_ADS]; private int mDesiredCount = 0; @NonNull private final int[] mOriginalAdPositions = new int[MAX_ADS]; @NonNull private final int[] mAdjustedAdPositions = new int[MAX_ADS]; @NonNull private final NativeAdData[] mAdDataObjects = new NativeAdData[MAX_ADS]; private int mPlacedCount = 0; /** * @param desiredInsertionPositions Insertion positions, expressed as original positions */ private PlacementData(@NonNull final int[] desiredInsertionPositions) { mDesiredCount = Math.min(desiredInsertionPositions.length, MAX_ADS); System.arraycopy(desiredInsertionPositions, 0, mDesiredInsertionPositions, 0, mDesiredCount); System.arraycopy(desiredInsertionPositions, 0, mDesiredOriginalPositions, 0, mDesiredCount); } @NonNull static PlacementData fromAdPositioning(@NonNull final MoPubClientPositioning adPositioning) { final List<Integer> fixed = adPositioning.getFixedPositions(); final int interval = adPositioning.getRepeatingInterval(); final int size = (interval == MoPubClientPositioning.NO_REPEAT ? fixed.size() : MAX_ADS); final int[] desiredInsertionPositions = new int[size]; // Fixed positions are in terms of final positions. Calculate current insertion positions // by decrementing numAds at each index. int numAds = 0; int lastPos = 0; for (final Integer position : fixed) { lastPos = position - numAds; desiredInsertionPositions[numAds++] = lastPos; } // Expand the repeating positions, if there are any while (numAds < size) { lastPos = lastPos + interval - 1; desiredInsertionPositions[numAds++] = lastPos; } return new PlacementData(desiredInsertionPositions); } @NonNull static PlacementData empty() { return new PlacementData(new int[] {}); } /** * Whether the given position should be an ad. */ boolean shouldPlaceAd(final int position) { final int index = binarySearch(mDesiredInsertionPositions, 0, mDesiredCount, position); return index >= 0; } /** * The next position after this position that should be an ad. Returns NOT_FOUND if there are no * more ads. */ int nextInsertionPosition(final int position) { final int index = binarySearchGreaterThan( mDesiredInsertionPositions, mDesiredCount, position); if (index == mDesiredCount) { return NOT_FOUND; } return mDesiredInsertionPositions[index]; } /** * The next position after this position that should be an ad. Returns NOT_FOUND if there * are no more ads. */ int previousInsertionPosition(final int position) { final int index = binarySearchFirstEquals( mDesiredInsertionPositions, mDesiredCount, position); if (index == 0) { return NOT_FOUND; } return mDesiredInsertionPositions[index - 1]; } /** * Sets ad data at the given position. */ void placeAd(final int adjustedPosition, final NativeAdData adData) { // See if this is a insertion ad final int desiredIndex = binarySearchFirstEquals( mDesiredInsertionPositions, mDesiredCount, adjustedPosition); if (desiredIndex == mDesiredCount || mDesiredInsertionPositions[desiredIndex] != adjustedPosition) { MoPubLog.w("Attempted to insert an ad at an invalid position"); return; } // Add to placed array final int originalPosition = mDesiredOriginalPositions[desiredIndex]; int placeIndex = binarySearchGreaterThan( mOriginalAdPositions, mPlacedCount, originalPosition); if (placeIndex < mPlacedCount) { final int num = mPlacedCount - placeIndex; System.arraycopy(mOriginalAdPositions, placeIndex, mOriginalAdPositions, placeIndex + 1, num); System.arraycopy(mAdjustedAdPositions, placeIndex, mAdjustedAdPositions, placeIndex + 1, num); System.arraycopy(mAdDataObjects, placeIndex, mAdDataObjects, placeIndex + 1, num); } mOriginalAdPositions[placeIndex] = originalPosition; mAdjustedAdPositions[placeIndex] = adjustedPosition; mAdDataObjects[placeIndex] = adData; mPlacedCount++; // Remove desired index final int num = mDesiredCount - desiredIndex - 1; System.arraycopy(mDesiredInsertionPositions, desiredIndex + 1, mDesiredInsertionPositions, desiredIndex, num); System.arraycopy(mDesiredOriginalPositions, desiredIndex + 1, mDesiredOriginalPositions, desiredIndex, num); mDesiredCount--; // Increment adjusted positions for (int i = desiredIndex; i < mDesiredCount; ++i) { mDesiredInsertionPositions[i]++; } for (int i = placeIndex + 1; i < mPlacedCount; ++i) { mAdjustedAdPositions[i]++; } } /** * @see {@link com.mopub.nativeads.MoPubStreamAdPlacer#isAd(int)} */ boolean isPlacedAd(final int position) { final int index = binarySearch(mAdjustedAdPositions, 0, mPlacedCount, position); return index >= 0; } /** * Returns the ad data associated with the given ad position, or {@code null} if there is * no ad at this position. */ @Nullable NativeAdData getPlacedAd(final int position) { final int index = binarySearch(mAdjustedAdPositions, 0, mPlacedCount, position); if (index < 0) { return null; } return mAdDataObjects[index]; } /** * Returns all placed ad positions. This method allocates new memory on every invocation. Do * not call it from performance critical code. */ @NonNull int[] getPlacedAdPositions() { int[] positions = new int[mPlacedCount]; System.arraycopy(mAdjustedAdPositions, 0, positions, 0, mPlacedCount); return positions; } /** * @see com.mopub.nativeads.MoPubStreamAdPlacer#getOriginalPosition(int) */ int getOriginalPosition(final int position) { final int index = binarySearch(mAdjustedAdPositions, 0, mPlacedCount, position); // No match, ~index is the number of ads before this pos. if (index < 0) { return position - ~index; } // This is an ad - there is no original position return NOT_FOUND; } /** * @see com.mopub.nativeads.MoPubStreamAdPlacer#getAdjustedPosition(int) */ int getAdjustedPosition(final int originalPosition) { // This is an ad. Since binary search doesn't properly handle dups, find the first non-ad. int index = binarySearchGreaterThan(mOriginalAdPositions, mPlacedCount, originalPosition); return originalPosition + index; } /** * @see com.mopub.nativeads.MoPubStreamAdPlacer#getOriginalCount(int) */ int getOriginalCount(final int count) { if (count == 0) { return 0; } // The last item will never be an ad final int originalPos = getOriginalPosition(count - 1); return (originalPos == NOT_FOUND) ? NOT_FOUND : originalPos + 1; } /** * @see com.mopub.nativeads.MoPubStreamAdPlacer#getAdjustedCount(int) */ int getAdjustedCount(final int originalCount) { if (originalCount == 0) { return 0; } return getAdjustedPosition(originalCount - 1) + 1; } /** * Clears the ads in the given range. After calling this method, the ad positions * will be removed from the placed ad positions and put back into the desired ad insertion * positions. */ int clearAdsInRange(final int adjustedStartRange, final int adjustedEndRange) { // Temporary arrays to store the cleared positions. Using temporary arrays makes it // easy to debug what positions are being cleared. int[] clearOriginalPositions = new int[mPlacedCount]; int[] clearAdjustedPositions = new int[mPlacedCount]; int clearCount = 0; // Add to the clear position arrays any positions that fall inside // [adjustedRangeStart, adjustedRangeEnd). for (int i = 0; i < mPlacedCount; ++i) { int originalPosition = mOriginalAdPositions[i]; int adjustedPosition = mAdjustedAdPositions[i]; if (adjustedStartRange <= adjustedPosition && adjustedPosition < adjustedEndRange) { // When copying adjusted positions, subtract the current clear count because there // is no longer an ad incrementing the desired insertion position. clearOriginalPositions[clearCount] = originalPosition; clearAdjustedPositions[clearCount] = adjustedPosition - clearCount; // Destroying and nulling out the ad objects to avoids a memory leak. mAdDataObjects[i].getAd().destroy(); mAdDataObjects[i] = null; clearCount++; } else if (clearCount > 0) { // The position is not in the range; shift it by the number of cleared ads. int newIndex = i - clearCount; mOriginalAdPositions[newIndex] = originalPosition; mAdjustedAdPositions[newIndex] = adjustedPosition - clearCount; mAdDataObjects[newIndex] = mAdDataObjects[i]; } } // If we have cleared nothing, this method was a no-op. if (clearCount == 0) { return 0; } // Modify the desired positions arrays in order to make space to put back the // cleared ad positions. For example if the desired array was {1, 10, // 15} and we need to insert {3, 7} we'll shift the desired array to be {1, ?, ? , 10, 15}. int firstCleared = clearAdjustedPositions[0]; int desiredIndex = binarySearchFirstEquals( mDesiredInsertionPositions, mDesiredCount, firstCleared); for (int i = mDesiredCount - 1; i >= desiredIndex; --i) { mDesiredOriginalPositions[i + clearCount] = mDesiredOriginalPositions[i]; mDesiredInsertionPositions[i + clearCount] = mDesiredInsertionPositions[i] - clearCount; } // Copy the cleared ad positions into the desired arrays. for (int i = 0; i < clearCount; ++i) { mDesiredOriginalPositions[desiredIndex + i] = clearOriginalPositions[i]; mDesiredInsertionPositions[desiredIndex + i] = clearAdjustedPositions[i]; } // Update the array counts, and we're done. mDesiredCount = mDesiredCount + clearCount; mPlacedCount = mPlacedCount - clearCount; return clearCount; } /** * Clears the ads in the given range. After calling this method the ad's position * will be back to the desired insertion positions. */ void clearAds() { if (mPlacedCount == 0) { return; } clearAdsInRange(0, mAdjustedAdPositions[mPlacedCount - 1] + 1); } /** * @see com.mopub.nativeads.MoPubStreamAdPlacer#insertItem(int) */ void insertItem(final int originalPosition) { // Increment desired arrays. int indexToIncrement = binarySearchFirstEquals( mDesiredOriginalPositions, mDesiredCount, originalPosition); for (int i = indexToIncrement; i < mDesiredCount; ++i) { mDesiredOriginalPositions[i]++; mDesiredInsertionPositions[i]++; } // Increment placed arrays. indexToIncrement = binarySearchFirstEquals( mOriginalAdPositions, mPlacedCount, originalPosition); for (int i = indexToIncrement; i < mPlacedCount; ++i) { mOriginalAdPositions[i]++; mAdjustedAdPositions[i]++; } } /** * @see com.mopub.nativeads.MoPubStreamAdPlacer#removeItem(int) */ void removeItem(final int originalPosition) { // When removing items, we only decrement ad position values *greater* than the original // position we're removing. The original position associated with an ad is the original // position of the first content item after the ad, so we shouldn't change the original // position of an ad that matches the original position removed. int indexToDecrement = binarySearchGreaterThan( mDesiredOriginalPositions, mDesiredCount, originalPosition); // Decrement desired arrays. for (int i = indexToDecrement; i < mDesiredCount; ++i) { mDesiredOriginalPositions[i]--; mDesiredInsertionPositions[i]--; } indexToDecrement = binarySearchGreaterThan( mOriginalAdPositions, mPlacedCount, originalPosition); for (int i = indexToDecrement; i < mPlacedCount; ++i) { mOriginalAdPositions[i]--; mAdjustedAdPositions[i]--; } } /** * @see com.mopub.nativeads.MoPubStreamAdPlacer#moveItem(int, int) */ void moveItem(final int originalPosition, final int newPosition) { removeItem(originalPosition); insertItem(newPosition); } private static int binarySearchFirstEquals(int[] array, int count, int value) { int index = binarySearch(array, 0, count, value); // If not found, binarySearch returns the 2's complement of the index of the nearest // value higher than the target value, which is also the insertion index. if (index < 0) { return ~index; } int duplicateValue = array[index]; while (index >= 0 && array[index] == duplicateValue) { index--; } return index + 1; } private static int binarySearchGreaterThan(int[] array, int count, int value) { int index = binarySearch(array, 0, count, value); // If not found, binarySearch returns the 2's complement of the index of the nearest // value higher than the target value, which is also the insertion index. if (index < 0) { return ~index; } int duplicateValue = array[index]; while (index < count && array[index] == duplicateValue) { index++; } return index; } /** * Copied from Arrays.java, which isn't available until Gingerbread. */ private static int binarySearch(int[] array, int startIndex, int endIndex, int value) { int lo = startIndex; int hi = endIndex - 1; while (lo <= hi) { int mid = (lo + hi) >>> 1; int midVal = array[mid]; if (midVal < value) { lo = mid + 1; } else if (midVal > value) { hi = mid - 1; } else { return mid; // value found } } return ~lo; // value not present } }