Load bitmaps for MediaMetadataCompat and handle the metadata updates.

* Add `Listener` in `MediaSession` with method `onNotificationRefreshRequired(MediaSession)`.
* Add `MediaSessionService` as the listener of the `MediaSession` when `MediaSession` is added to `MediaSessionService`
* Load bitmap when update metadata in `MediaSessionLegacyStub` and call `onNotificationRefreshRequired` when bitmap asynchronously arrives.

PiperOrigin-RevId: 485376145
This commit is contained in:
tianyifeng 2022-11-01 18:43:26 +00:00 committed by microkatz
parent a65ff85a98
commit 77fedd8d7d
11 changed files with 234 additions and 18 deletions

View File

@ -859,6 +859,11 @@ public class MediaSession {
impl.setSessionPositionUpdateDelayMsOnHandler(updateDelayMs);
}
/** Sets the {@linkplain Listener listener}. */
/* package */ void setListener(@Nullable Listener listener) {
impl.setMediaSessionListener(listener);
}
private Uri getUri() {
return impl.getUri();
}
@ -1240,6 +1245,17 @@ public class MediaSession {
default void onRenderedFirstFrame(int seq) throws RemoteException {}
}
/** Listener for media session events */
/* package */ interface Listener {
/**
* Called when the notification requires to be refreshed.
*
* @param session The media session for which the notification requires to be refreshed.
*/
void onNotificationRefreshRequired(MediaSession session);
}
/**
* A base class for {@link MediaSession.Builder} and {@link
* MediaLibraryService.MediaLibrarySession.Builder}. Any changes to this class should be also

View File

@ -119,6 +119,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
private final BitmapLoader bitmapLoader;
@Nullable private PlayerListener playerListener;
@Nullable private MediaSession.Listener mediaSessionListener;
private PlayerInfo playerInfo;
private PlayerWrapper playerWrapper;
@ -573,6 +574,16 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
}
/* package */ void setMediaSessionListener(@Nullable MediaSession.Listener listener) {
this.mediaSessionListener = listener;
}
/* package */ void onNotificationRefreshRequired() {
if (this.mediaSessionListener != null) {
this.mediaSessionListener.onNotificationRefreshRequired(instance);
}
}
private void dispatchRemoteControllerTaskToLegacyStub(RemoteControllerTask task) {
try {
task.run(sessionLegacyStub.getControllerLegacyCbForBroadcast(), /* seq= */ 0);

View File

@ -44,6 +44,7 @@ import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@ -114,8 +115,10 @@ import org.checkerframework.checker.initialization.qual.Initialized;
private final MediaPlayPauseKeyHandler mediaPlayPauseKeyHandler;
private final MediaSessionCompat sessionCompat;
@Nullable private VolumeProviderCompat volumeProviderCompat;
private final Handler mainHandler;
private volatile long connectionTimeoutMs;
@Nullable private FutureCallback<Bitmap> pendingBitmapLoadCallback;
public MediaSessionLegacyStub(
MediaSessionImpl session,
@ -156,6 +159,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
@Initialized
MediaSessionLegacyStub thisRef = this;
sessionCompat.setCallback(thisRef, handler);
mainHandler = new Handler(Looper.getMainLooper());
}
/** Starts to receive commands. */
@ -1110,11 +1114,52 @@ import org.checkerframework.checker.initialization.qual.Initialized;
currentMediaItemForMetadataUpdate = currentMediaItem;
durationMsForMetadataUpdate = durationMs;
if (currentMediaItem == null) {
setMetadata(sessionCompat, /* metadataCompat= */ null);
return;
}
@Nullable Bitmap artworkBitmap = null;
ListenableFuture<Bitmap> bitmapFuture =
sessionImpl.getBitmapLoader().loadBitmapFromMetadata(currentMediaItem.mediaMetadata);
if (bitmapFuture != null) {
pendingBitmapLoadCallback = null;
if (bitmapFuture.isDone()) {
try {
artworkBitmap = Futures.getDone(bitmapFuture);
} catch (ExecutionException e) {
Log.w(TAG, "Failed to load bitmap", e);
}
} else {
pendingBitmapLoadCallback =
new FutureCallback<Bitmap>() {
@Override
public void onSuccess(Bitmap result) {
if (this != pendingBitmapLoadCallback) {
return;
}
setMetadata(
sessionCompat,
MediaUtils.convertToMediaMetadataCompat(
currentMediaItem, durationMs, result));
sessionImpl.onNotificationRefreshRequired();
}
@Override
public void onFailure(Throwable t) {
if (this != pendingBitmapLoadCallback) {
return;
}
Log.d(TAG, "Failed to load bitmap", t);
}
};
Futures.addCallback(
bitmapFuture, pendingBitmapLoadCallback, /* executor= */ mainHandler::post);
}
}
setMetadata(
sessionCompat,
currentMediaItem != null
? MediaUtils.convertToMediaMetadataCompat(currentMediaItem, durationMs)
: null);
MediaUtils.convertToMediaMetadataCompat(currentMediaItem, durationMs, artworkBitmap));
}
}

View File

@ -239,6 +239,7 @@ public abstract class MediaSessionService extends Service {
// TODO(b/191644474): Check whether the session is registered to multiple services.
MediaNotificationManager notificationManager = getMediaNotificationManager();
postOrRun(mainHandler, () -> notificationManager.addSession(session));
session.setListener(this::onUpdateNotification);
}
}
@ -258,6 +259,7 @@ public abstract class MediaSessionService extends Service {
}
MediaNotificationManager notificationManager = getMediaNotificationManager();
postOrRun(mainHandler, () -> notificationManager.removeSession(session));
session.setListener(null);
}
/**

View File

@ -525,7 +525,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Converts a {@link MediaItem} to a {@link MediaMetadataCompat}. */
public static MediaMetadataCompat convertToMediaMetadataCompat(
MediaItem mediaItem, long durationMs) {
MediaItem mediaItem, long durationMs, @Nullable Bitmap artworkBitmap) {
MediaMetadataCompat.Builder builder =
new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaItem.mediaId);
@ -574,11 +574,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, metadata.artworkUri.toString());
}
if (metadata.artworkData != null) {
Bitmap artwork =
BitmapFactory.decodeByteArray(metadata.artworkData, 0, metadata.artworkData.length);
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, artwork);
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, artwork);
if (artworkBitmap != null) {
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, artworkBitmap);
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, artworkBitmap);
}
if (metadata.folderType != null && metadata.folderType != MediaMetadata.FOLDER_TYPE_NONE) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -16,9 +16,13 @@
package androidx.media3.test.session.common;
import static android.content.Context.KEYGUARD_SERVICE;
import static java.lang.Math.min;
import android.app.Activity;
import android.app.KeyguardManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.WindowManager;
@ -28,6 +32,9 @@ import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
/** Provides utility methods for testing purpose. */
@ -40,6 +47,9 @@ public class TestUtils {
public static final long VOLUME_CHANGE_TIMEOUT_MS = 5_000;
public static final long LONG_TIMEOUT_MS = 20_000;
private static final int MAX_BITMAP_WIDTH = 500;
private static final int MAX_BITMAP_HEIGHT = 500;
/**
* Compares contents of two throwables for both message and class.
*
@ -143,5 +153,32 @@ public class TestUtils {
return list.build();
}
/** Returns the bytes of a scaled asset file. */
public static byte[] getByteArrayForScaledBitmap(Context context, String fileName)
throws IOException {
Bitmap bitmap = getBitmap(context, fileName);
int width = min(bitmap.getWidth(), MAX_BITMAP_WIDTH);
int height = min(bitmap.getHeight(), MAX_BITMAP_HEIGHT);
return convertToByteArray(Bitmap.createScaledBitmap(bitmap, width, height, true));
}
/** Returns an {@link InputStream} for reading from an asset file. */
public static InputStream getInputStream(Context context, String fileName) throws IOException {
return context.getResources().getAssets().open(fileName);
}
/** Returns a {@link Bitmap} read from an asset file. */
public static Bitmap getBitmap(Context context, String fileName) throws IOException {
return BitmapFactory.decodeStream(getInputStream(context, fileName));
}
/** Converts the given {@link Bitmap} to an array of bytes. */
public static byte[] convertToByteArray(Bitmap bitmap) throws IOException {
try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
bitmap.compress(Bitmap.CompressFormat.PNG, /* ignored */ 0, stream);
return stream.toByteArray();
}
}
private TestUtils() {}
}

View File

@ -32,12 +32,16 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets.main.assets.srcDir '../test_data/src/test/assets/'
}
dependencies {
implementation project(modulePrefix + 'lib-session')
implementation project(modulePrefix + 'test-session-common')
implementation 'androidx.media:media:' + androidxMediaVersion
implementation 'androidx.test:core:' + androidxTestCoreVersion
implementation project(path: ':test-data')
androidTestImplementation project(modulePrefix + 'lib-exoplayer')
androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
androidTestImplementation 'androidx.test.ext:truth:' + androidxTestTruthVersion

View File

@ -39,10 +39,12 @@ import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PA
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
@ -113,11 +115,13 @@ public class MediaControllerWithMediaSessionCompatTest {
private Context context;
private RemoteMediaSessionCompat session;
private BitmapLoader bitmapLoader;
@Before
public void setUp() throws Exception {
context = ApplicationProvider.getApplicationContext();
session = new RemoteMediaSessionCompat(DEFAULT_TEST_NAME, context);
bitmapLoader = new CacheBitmapLoader(new SimpleBitmapLoader());
}
@After
@ -697,12 +701,14 @@ public class MediaControllerWithMediaSessionCompatTest {
List<QueueItem> testQueue = MediaUtils.convertToQueueItemList(testList);
MediaItem testRemoveMediaItem = MediaTestUtils.createMediaItem("removed");
MediaMetadataCompat testMetadataCompat =
MediaUtils.convertToMediaMetadataCompat(testRemoveMediaItem, /* durationMs= */ 100L);
MediaUtils.convertToMediaMetadataCompat(
testRemoveMediaItem, /* durationMs= */ 100L, /* artworkBitmap= */ null);
session.setQueue(testQueue);
session.setMetadata(testMetadataCompat);
MediaController controller = controllerTestRule.createController(session.getSessionToken());
int mediaItemCount = threadTestRule.getHandler().postAndSync(controller::getMediaItemCount);
assertThat(mediaItemCount).isEqualTo(testList.size() + 1);
}
@ -713,7 +719,8 @@ public class MediaControllerWithMediaSessionCompatTest {
List<QueueItem> testQueue = MediaUtils.convertToQueueItemList(testList);
MediaItem testRemoveMediaItem = MediaTestUtils.createMediaItem("removed");
MediaMetadataCompat testMetadataCompat =
MediaUtils.convertToMediaMetadataCompat(testRemoveMediaItem, /* durationMs= */ 100L);
MediaUtils.convertToMediaMetadataCompat(
testRemoveMediaItem, /* durationMs= */ 100L, /* artworkBitmap= */ null);
session.setQueue(testQueue);
session.setMetadata(testMetadataCompat);
MediaController controller = controllerTestRule.createController(session.getSessionToken());
@ -732,9 +739,11 @@ public class MediaControllerWithMediaSessionCompatTest {
new PlaybackStateCompat.Builder()
.setActiveQueueItemId(testQueue.get(0).getQueueId())
.build());
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
int mediaItemCount = threadTestRule.getHandler().postAndSync(controller::getMediaItemCount);
assertThat(mediaItemCount).isEqualTo(testList.size());
}
@ -745,13 +754,15 @@ public class MediaControllerWithMediaSessionCompatTest {
List<QueueItem> testQueue = MediaUtils.convertToQueueItemList(testList);
MediaItem testRemoveMediaItem = MediaTestUtils.createMediaItem("removed");
MediaMetadataCompat testMetadataCompat =
MediaUtils.convertToMediaMetadataCompat(testRemoveMediaItem, /* durationMs= */ 100L);
MediaUtils.convertToMediaMetadataCompat(
testRemoveMediaItem, /* durationMs= */ 100L, /* artworkBitmap= */ null);
session.setQueue(testQueue);
session.setMetadata(testMetadataCompat);
MediaController controller = controllerTestRule.createController(session.getSessionToken());
int mediaItemIndex =
threadTestRule.getHandler().postAndSync(controller::getCurrentMediaItemIndex);
assertThat(mediaItemIndex).isEqualTo(testList.size());
}
@ -761,12 +772,46 @@ public class MediaControllerWithMediaSessionCompatTest {
MediaItem testMediaItem = MediaTestUtils.createMediaItem("test");
MediaMetadata testMediaMetadata = testMediaItem.mediaMetadata;
MediaMetadataCompat testMediaMetadataCompat =
MediaUtils.convertToMediaMetadataCompat(testMediaItem, /* durationMs= */ 100L);
MediaUtils.convertToMediaMetadataCompat(
testMediaItem, /* durationMs= */ 100L, /* artworkBitmap= */ null);
session.setMetadata(testMediaMetadataCompat);
MediaController controller = controllerTestRule.createController(session.getSessionToken());
MediaMetadata mediaMetadata =
threadTestRule.getHandler().postAndSync(controller::getMediaMetadata);
assertThat(mediaMetadata).isEqualTo(testMediaMetadata);
}
@Test
public void getMediaMetadata_withMediaMetadataCompatAndArtworkData_returnsConvertedMediaMetadata()
throws Exception {
MediaItem testMediaItem = MediaTestUtils.createMediaItemWithArtworkData("test");
MediaMetadata testMediaMetadata = testMediaItem.mediaMetadata;
@Nullable Bitmap artworkBitmap = getBitmapFromMetadata(testMediaMetadata);
MediaMetadataCompat testMediaMetadataCompat =
MediaUtils.convertToMediaMetadataCompat(
testMediaItem, /* durationMs= */ 100L, artworkBitmap);
session.setMetadata(testMediaMetadataCompat);
MediaController controller = controllerTestRule.createController(session.getSessionToken());
MediaMetadata mediaMetadata =
threadTestRule.getHandler().postAndSync(controller::getMediaMetadata);
assertThat(mediaMetadata.artworkData).isNotNull();
if (Util.SDK_INT < 21) {
// Bitmap conversion and back gives not exactly the same byte array below API 21
mediaMetadata =
mediaMetadata
.buildUpon()
.setArtworkData(/* artworkData= */ null, /* artworkDataType= */ null)
.build();
testMediaMetadata =
testMediaMetadata
.buildUpon()
.setArtworkData(/* artworkData= */ null, /* artworkDataType= */ null)
.build();
}
assertThat(mediaMetadata).isEqualTo(testMediaMetadata);
}
@ -1085,7 +1130,8 @@ public class MediaControllerWithMediaSessionCompatTest {
public void setPlaybackState_fromStateBufferingToPlaying_notifiesReadyState() throws Exception {
List<MediaItem> testPlaylist = MediaTestUtils.createMediaItems(/* size= */ 1);
MediaMetadataCompat metadata =
MediaUtils.convertToMediaMetadataCompat(testPlaylist.get(0), /* durationMs= */ 50_000);
MediaUtils.convertToMediaMetadataCompat(
testPlaylist.get(0), /* durationMs= */ 50_000, /* artworkBitmap= */ null);
long testBufferedPosition = 5_000;
session.setMetadata(metadata);
session.setPlaybackState(
@ -1129,7 +1175,8 @@ public class MediaControllerWithMediaSessionCompatTest {
throws Exception {
List<MediaItem> testPlaylist = MediaTestUtils.createMediaItems(1);
MediaMetadataCompat metadata =
MediaUtils.convertToMediaMetadataCompat(testPlaylist.get(0), /* durationMs= */ 1_000);
MediaUtils.convertToMediaMetadataCompat(
testPlaylist.get(0), /* durationMs= */ 1_000, /* artworkBitmap= */ null);
long testBufferingPosition = 0;
session.setMetadata(metadata);
session.setPlaybackState(
@ -1689,4 +1736,14 @@ public class MediaControllerWithMediaSessionCompatTest {
threadTestRule.getHandler().postAndSync(controller::getTotalBufferedDuration);
assertThat(totalBufferedDurationMs).isEqualTo(testTotalBufferedDurationMs);
}
@Nullable
private Bitmap getBitmapFromMetadata(MediaMetadata metadata) throws Exception {
@Nullable Bitmap bitmap = null;
@Nullable ListenableFuture<Bitmap> bitmapFuture = bitmapLoader.loadBitmapFromMetadata(metadata);
if (bitmapFuture != null) {
bitmap = bitmapFuture.get(10, SECONDS);
}
return bitmap;
}
}

View File

@ -18,8 +18,10 @@ package androidx.media3.session;
import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION;
import static android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Parcel;
import android.service.media.MediaBrowserService;
@ -31,6 +33,7 @@ import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.media.AudioAttributesCompat;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
@ -46,6 +49,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -59,10 +63,12 @@ import org.junit.runner.RunWith;
public final class MediaUtilsTest {
private Context context;
private BitmapLoader bitmapLoader;
@Before
public void setUp() {
context = ApplicationProvider.getApplicationContext();
bitmapLoader = new CacheBitmapLoader(new SimpleBitmapLoader());
}
@Test
@ -199,14 +205,24 @@ public final class MediaUtilsTest {
}
@Test
public void convertToMediaMetadata_roundTrip_returnsEqualMediaItem() {
MediaItem testMediaItem = MediaTestUtils.createMediaItem("testZZZ");
public void convertToMediaMetadata_roundTrip_returnsEqualMediaItem() throws Exception {
MediaItem testMediaItem = MediaTestUtils.createMediaItemWithArtworkData("testZZZ");
MediaMetadata testMediaMetadata = testMediaItem.mediaMetadata;
@Nullable Bitmap testArtworkBitmap = null;
@Nullable
ListenableFuture<Bitmap> bitmapFuture = bitmapLoader.loadBitmapFromMetadata(testMediaMetadata);
if (bitmapFuture != null) {
testArtworkBitmap = bitmapFuture.get(10, SECONDS);
}
MediaMetadataCompat testMediaMetadataCompat =
MediaUtils.convertToMediaMetadataCompat(testMediaItem, /* durationMs= */ 100L);
MediaUtils.convertToMediaMetadataCompat(
testMediaItem, /* durationMs= */ 100L, testArtworkBitmap);
MediaMetadata mediaMetadata =
MediaUtils.convertToMediaMetadata(testMediaMetadataCompat, RatingCompat.RATING_NONE);
assertThat(mediaMetadata).isEqualTo(testMediaMetadata);
assertThat(mediaMetadata.artworkData).isNotNull();
}
@Test

View File

@ -23,6 +23,7 @@ import static androidx.media3.test.session.common.CommonConstants.METADATA_TITLE
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;
import android.os.Bundle;
import android.support.v4.media.MediaBrowserCompat;
@ -38,6 +39,8 @@ import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaLibraryService.LibraryParams;
import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.test.session.common.TestUtils;
import androidx.test.core.app.ApplicationProvider;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@ -47,6 +50,8 @@ public final class MediaTestUtils {
private static final String TAG = "MediaTestUtils";
private static final String TEST_IMAGE_PATH = "media/png/non-motion-photo-shortened.png";
/** Create a media item with the mediaId for testing purpose. */
public static MediaItem createMediaItem(String mediaId) {
MediaMetadata mediaMetadata =
@ -57,6 +62,23 @@ public final class MediaTestUtils {
return new MediaItem.Builder().setMediaId(mediaId).setMediaMetadata(mediaMetadata).build();
}
public static MediaItem createMediaItemWithArtworkData(String mediaId) {
MediaMetadata.Builder mediaMetadataBuilder =
new MediaMetadata.Builder()
.setFolderType(MediaMetadata.FOLDER_TYPE_NONE)
.setIsPlayable(true);
try {
byte[] artworkData =
TestUtils.getByteArrayForScaledBitmap(
ApplicationProvider.getApplicationContext(), TEST_IMAGE_PATH);
mediaMetadataBuilder.setArtworkData(artworkData, MediaMetadata.PICTURE_TYPE_FRONT_COVER);
} catch (IOException e) {
fail(e.getMessage());
}
MediaMetadata mediaMetadata = mediaMetadataBuilder.build();
return new MediaItem.Builder().setMediaId(mediaId).setMediaMetadata(mediaMetadata).build();
}
public static ArrayList<MediaItem> createMediaItems(int size) {
ArrayList<MediaItem> list = new ArrayList<>();
for (int i = 0; i < size; i++) {
@ -65,6 +87,14 @@ public final class MediaTestUtils {
return list;
}
public static ArrayList<MediaItem> createMediaItemsWithArtworkData(int size) {
ArrayList<MediaItem> list = new ArrayList<>();
for (int i = 0; i < size; i++) {
list.add(createMediaItemWithArtworkData("mediaItem_" + (i + 1)));
}
return list;
}
public static List<MediaItem> createMediaItems(String... mediaIds) {
List<MediaItem> list = new ArrayList<>();
for (int i = 0; i < mediaIds.length; i++) {