| /* |
| * Copyright 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.media2.session; |
| |
| import static androidx.media2.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED; |
| |
| import android.app.PendingIntent; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.media.browse.MediaBrowser; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.text.TextUtils; |
| |
| import androidx.annotation.IntRange; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.core.content.ContextCompat; |
| import androidx.media2.common.MediaMetadata; |
| import androidx.media2.common.SessionPlayer; |
| import androidx.media2.session.LibraryResult.ResultCode; |
| import androidx.media2.session.MediaSession.ControllerInfo; |
| import androidx.versionedparcelable.ParcelField; |
| import androidx.versionedparcelable.VersionedParcelable; |
| import androidx.versionedparcelable.VersionedParcelize; |
| |
| import java.util.concurrent.Executor; |
| |
| /** |
| * Base class for media library services, which is the service containing |
| * {@link MediaLibrarySession}. |
| * <p> |
| * Media library services enable applications to browse media content provided by an application |
| * and ask the application to start playing it. They may also be used to control content that |
| * is already playing by way of a {@link MediaSession}. |
| * <p> |
| * When extending this class, also add the following to your {@code AndroidManifest.xml}. |
| * <pre> |
| * <service android:name="component_name_of_your_implementation" > |
| * <intent-filter> |
| * <action android:name="androidx.media2.session.MediaLibraryService" /> |
| * </intent-filter> |
| * </service></pre> |
| * <p> |
| * You may also declare <pre>android.media.browse.MediaBrowserService</pre> for compatibility with |
| * {@link android.support.v4.media.MediaBrowserCompat}. This service can handle it automatically. |
| * |
| * @see MediaSessionService |
| */ |
| public abstract class MediaLibraryService extends MediaSessionService { |
| /** |
| * The {@link Intent} that must be declared as handled by the service. |
| */ |
| public static final String SERVICE_INTERFACE = "androidx.media2.session.MediaLibraryService"; |
| |
| /** |
| * Session for the {@link MediaLibraryService}. Build this object with |
| * {@link Builder} and return in {@link MediaSessionService#onGetSession(ControllerInfo)}. |
| */ |
| public static final class MediaLibrarySession extends MediaSession { |
| /** |
| * Callback for the {@link MediaLibrarySession}. |
| * <p> |
| * When you return {@link LibraryResult} with media items, |
| * items must have valid {@link MediaMetadata#METADATA_KEY_MEDIA_ID} and |
| * specify {@link MediaMetadata#METADATA_KEY_BROWSABLE} and |
| * {@link MediaMetadata#METADATA_KEY_PLAYABLE}. |
| */ |
| public static class MediaLibrarySessionCallback extends MediaSession.SessionCallback { |
| /** |
| * Called to get the root information for browsing by a {@link MediaBrowser}. |
| * <p> |
| * To allow browsing media information, return the {@link LibraryResult} with the |
| * {@link LibraryResult#RESULT_SUCCESS} and the root media item with the valid |
| * {@link MediaMetadata#METADATA_KEY_MEDIA_ID media id}. The media id must be included |
| * for the browser to get the children under it. |
| * <p> |
| * Interoperability: this callback may be called on the main thread, regardless of the |
| * callback executor. |
| * |
| * @param session the session for this event |
| * @param controller information of the controller requesting access to browse media. |
| * @param params An optional library params of service-specific arguments to send |
| * to the media library service when connecting and retrieving the |
| * root id for browsing, or {@code null} if none. |
| * @return a library result with the root media item with the id. A runtime exception |
| * will be thrown if an invalid result is returned. |
| * @see SessionCommand#COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT |
| * @see MediaMetadata#METADATA_KEY_MEDIA_ID |
| * @see LibraryParams |
| */ |
| @NonNull |
| public LibraryResult onGetLibraryRoot(@NonNull MediaLibrarySession session, |
| @NonNull ControllerInfo controller, @Nullable LibraryParams params) { |
| return new LibraryResult(RESULT_ERROR_NOT_SUPPORTED); |
| } |
| |
| /** |
| * Called to get an item. |
| * <p> |
| * To allow getting the item, return the {@link LibraryResult} with the |
| * {@link LibraryResult#RESULT_SUCCESS} and the media item. |
| * |
| * @param session the session for this event |
| * @param controller controller |
| * @param mediaId non-empty media id of the requested item |
| * @return a library result with a media item with the id. A runtime exception |
| * will be thrown if an invalid result is returned. |
| * @see SessionCommand#COMMAND_CODE_LIBRARY_GET_ITEM |
| */ |
| @NonNull |
| public LibraryResult onGetItem(@NonNull MediaLibrarySession session, |
| @NonNull ControllerInfo controller, @NonNull String mediaId) { |
| return new LibraryResult(RESULT_ERROR_NOT_SUPPORTED); |
| } |
| |
| /** |
| * Called to get children of given parent id. Return the children here for the browser. |
| * <p> |
| * To allow getting the children, return the {@link LibraryResult} with the |
| * {@link LibraryResult#RESULT_SUCCESS} and the list of media item. Return an empty |
| * list for no children rather than using result code for error. |
| * |
| * @param session the session for this event |
| * @param controller controller |
| * @param parentId non-empty parent id to get children |
| * @param page page number. Starts from {@code 0}. |
| * @param pageSize page size. Should be greater or equal to {@code 1}. |
| * @param params library params |
| * @return a library result with a list of media item with the id. A runtime exception |
| * will be thrown if an invalid result is returned. |
| * @see SessionCommand#COMMAND_CODE_LIBRARY_GET_CHILDREN |
| * @see LibraryParams |
| */ |
| @NonNull |
| public LibraryResult onGetChildren(@NonNull MediaLibrarySession session, |
| @NonNull ControllerInfo controller, @NonNull String parentId, |
| @IntRange(from = 0) int page, @IntRange(from = 1) int pageSize, |
| @Nullable LibraryParams params) { |
| return new LibraryResult(RESULT_ERROR_NOT_SUPPORTED); |
| } |
| |
| /** |
| * Called when a controller subscribes to the parent. |
| * <p> |
| * It's your responsibility to keep subscriptions by your own and call |
| * {@link MediaLibrarySession#notifyChildrenChanged( |
| * ControllerInfo, String, int, LibraryParams)} when the parent is changed until it's |
| * unsubscribed. |
| * <p> |
| * Interoperability: This will be called when |
| * {@link android.support.v4.media.MediaBrowserCompat#subscribe} is called. |
| * However, this won't be called when {@link MediaBrowser#subscribe} is called. |
| * |
| * @param session the session for this event |
| * @param controller controller |
| * @param parentId non-empty parent id |
| * @param params library params |
| * @return result code |
| * @see SessionCommand#COMMAND_CODE_LIBRARY_SUBSCRIBE |
| * @see LibraryParams |
| */ |
| @ResultCode |
| public int onSubscribe(@NonNull MediaLibrarySession session, |
| @NonNull ControllerInfo controller, @NonNull String parentId, |
| @Nullable LibraryParams params) { |
| return RESULT_ERROR_NOT_SUPPORTED; |
| } |
| |
| /** |
| * Called when a controller unsubscribes to the parent. |
| * <p> |
| * Interoperability: This wouldn't be called if {@link MediaBrowser#unsubscribe} is |
| * called while works well with |
| * {@link android.support.v4.media.MediaBrowserCompat#unsubscribe}. |
| * |
| * @param session the session for this event |
| * @param controller controller |
| * @param parentId non-empty parent id |
| * @return result code |
| * @see SessionCommand#COMMAND_CODE_LIBRARY_UNSUBSCRIBE |
| */ |
| @ResultCode |
| public int onUnsubscribe(@NonNull MediaLibrarySession session, |
| @NonNull ControllerInfo controller, @NonNull String parentId) { |
| return RESULT_ERROR_NOT_SUPPORTED; |
| } |
| |
| /** |
| * Called when a controller requests search. |
| * <p> |
| * Return immediately with the result of the attempt to search with the query. Notify |
| * the number of search result through |
| * {@link #notifySearchResultChanged(ControllerInfo, String, int, LibraryParams)}. |
| * {@link MediaBrowser} will ask the search result with the pagination later. |
| * |
| * @param session the session for this event |
| * @param controller controller |
| * @param query The non-empty search query sent from the media browser. |
| * It contains keywords separated by space. |
| * @param params library params |
| * @return result code |
| * @see SessionCommand#COMMAND_CODE_LIBRARY_SEARCH |
| * @see #notifySearchResultChanged(ControllerInfo, String, int, LibraryParams) |
| * @see LibraryParams |
| */ |
| @ResultCode |
| public int onSearch(@NonNull MediaLibrarySession session, |
| @NonNull ControllerInfo controller, @NonNull String query, |
| @Nullable LibraryParams params) { |
| return RESULT_ERROR_NOT_SUPPORTED; |
| } |
| |
| /** |
| * Called to get the search result. |
| * <p> |
| * To allow getting the search result, return the {@link LibraryResult} with the |
| * {@link LibraryResult#RESULT_SUCCESS} and the list of media item. Return an empty |
| * list for no search result rather than using result code for error. |
| * <p> |
| * This may be called with a query that hasn't called with {@link #onSearch}, especially |
| * when {@link android.support.v4.media.MediaBrowserCompat#search} is used. |
| * |
| * @param session the session for this event |
| * @param controller controller |
| * @param query The non-empty search query which was previously sent through |
| * {@link #onSearch}. |
| * @param page page number. Starts from {@code 0}. |
| * @param pageSize page size. Should be greater or equal to {@code 1}. |
| * @param params library params |
| * @return a library result with a list of media item with the id. A runtime exception |
| * will be thrown if an invalid result is returned. |
| * @see SessionCommand#COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT |
| * @see LibraryParams |
| */ |
| @NonNull |
| public LibraryResult onGetSearchResult( |
| @NonNull MediaLibrarySession session, @NonNull ControllerInfo controller, |
| @NonNull String query, @IntRange(from = 0) int page, |
| @IntRange(from = 1) int pageSize, @Nullable LibraryParams params) { |
| return new LibraryResult(RESULT_ERROR_NOT_SUPPORTED); |
| } |
| } |
| |
| /** |
| * Builder for {@link MediaLibrarySession}. |
| * <p> |
| * Any incoming event from the {@link MediaController} will be handled on the callback |
| * executor. If it's not set, {@link ContextCompat#getMainExecutor(Context)} will be used by |
| * default. |
| */ |
| // Override all methods just to show them with the type instead of generics in Javadoc. |
| // This workarounds javadoc issue described in the MediaSession.BuilderBase. |
| // Note: Don't override #setSessionCallback() because the callback can be set by the |
| // constructor. |
| public static final class Builder extends MediaSession.BuilderBase<MediaLibrarySession, |
| Builder, MediaLibrarySessionCallback> { |
| // Builder requires MediaLibraryService instead of Context just to ensure that the |
| // builder can be only instantiated within the MediaLibraryService. |
| // Ideally it's better to make it inner class of service to enforce, but it violates API |
| // guideline that Builders should be the inner class of the building target. |
| public Builder(@NonNull MediaLibraryService service, |
| @NonNull SessionPlayer player, |
| @NonNull Executor callbackExecutor, |
| @NonNull MediaLibrarySessionCallback callback) { |
| super(service, player); |
| setSessionCallback(callbackExecutor, callback); |
| } |
| |
| @Override |
| @NonNull |
| public Builder setSessionActivity(@Nullable PendingIntent pi) { |
| return super.setSessionActivity(pi); |
| } |
| |
| @Override |
| @NonNull |
| public Builder setId(@NonNull String id) { |
| return super.setId(id); |
| } |
| |
| @Override |
| @NonNull |
| public Builder setExtras(@NonNull Bundle extras) { |
| return super.setExtras(extras); |
| } |
| |
| @Override |
| @NonNull |
| public MediaLibrarySession build() { |
| if (mCallbackExecutor == null) { |
| mCallbackExecutor = ContextCompat.getMainExecutor(mContext); |
| } |
| if (mCallback == null) { |
| mCallback = new MediaLibrarySession.MediaLibrarySessionCallback() {}; |
| } |
| return new MediaLibrarySession(mContext, mId, mPlayer, mSessionActivity, |
| mCallbackExecutor, mCallback, mExtras); |
| } |
| } |
| |
| MediaLibrarySession(Context context, String id, SessionPlayer player, |
| PendingIntent sessionActivity, Executor callbackExecutor, |
| MediaSession.SessionCallback callback, Bundle tokenExtras) { |
| super(context, id, player, sessionActivity, callbackExecutor, callback, tokenExtras); |
| } |
| |
| @Override |
| MediaLibrarySessionImpl createImpl(Context context, String id, SessionPlayer player, |
| PendingIntent sessionActivity, Executor callbackExecutor, |
| MediaSession.SessionCallback callback, Bundle tokenExtras) { |
| return new MediaLibrarySessionImplBase(this, context, id, player, sessionActivity, |
| callbackExecutor, callback, tokenExtras); |
| } |
| |
| @Override |
| MediaLibrarySessionImpl getImpl() { |
| return (MediaLibrarySessionImpl) super.getImpl(); |
| } |
| |
| /** |
| * Notifies the controller of the change in a parent's children. |
| * <p> |
| * If the controller hasn't subscribed to the parent, the API will do nothing. |
| * <p> |
| * Controllers will use {@link MediaBrowser#getChildren(String, int, int, LibraryParams)} |
| * to get the list of children. |
| * |
| * @param controller controller to notify |
| * @param parentId non-empty parent id with changes in its children |
| * @param itemCount number of children. |
| * @param params library params |
| */ |
| public void notifyChildrenChanged(@NonNull ControllerInfo controller, |
| @NonNull String parentId, @IntRange(from = 0) int itemCount, |
| @Nullable LibraryParams params) { |
| if (controller == null) { |
| throw new NullPointerException("controller shouldn't be null"); |
| } |
| if (parentId == null) { |
| throw new NullPointerException("parentId shouldn't be null"); |
| } else if (TextUtils.isEmpty(parentId)) { |
| throw new IllegalArgumentException("parentId shouldn't be empty"); |
| } |
| if (itemCount < 0) { |
| throw new IllegalArgumentException("itemCount shouldn't be negative"); |
| } |
| getImpl().notifyChildrenChanged(controller, parentId, itemCount, params); |
| } |
| |
| /** |
| * Notifies all controllers that subscribed to the parent about change in the parent's |
| * children, regardless of the library params supplied by |
| * {@link MediaBrowser#subscribe(String, LibraryParams)}. |
| * @param parentId non-empty parent id |
| * @param itemCount number of children |
| * @param params library params |
| */ |
| // This is for the backward compatibility. |
| public void notifyChildrenChanged(@NonNull String parentId, int itemCount, |
| @Nullable LibraryParams params) { |
| if (TextUtils.isEmpty(parentId)) { |
| throw new IllegalArgumentException("parentId shouldn't be empty"); |
| } |
| if (itemCount < 0) { |
| throw new IllegalArgumentException("itemCount shouldn't be negative"); |
| } |
| getImpl().notifyChildrenChanged(parentId, itemCount, params); |
| } |
| |
| /** |
| * Notifies controller about change in the search result. |
| * |
| * @param controller controller to notify |
| * @param query previously sent non-empty search query from the controller. |
| * @param itemCount the number of items that have been found in the search. |
| * @param params library params |
| */ |
| public void notifySearchResultChanged(@NonNull ControllerInfo controller, |
| @NonNull String query, @IntRange(from = 0) int itemCount, |
| @Nullable LibraryParams params) { |
| if (controller == null) { |
| throw new NullPointerException("controller shouldn't be null"); |
| } |
| if (query == null) { |
| throw new NullPointerException("query shouldn't be null"); |
| } else if (TextUtils.isEmpty(query)) { |
| throw new IllegalArgumentException("query shouldn't be empty"); |
| } |
| if (itemCount < 0) { |
| throw new IllegalArgumentException("itemCount shouldn't be negative"); |
| } |
| getImpl().notifySearchResultChanged(controller, query, itemCount, params); |
| } |
| |
| @Override |
| @NonNull |
| MediaLibrarySessionCallback getCallback() { |
| return (MediaLibrarySessionCallback) super.getCallback(); |
| } |
| |
| interface MediaLibrarySessionImpl extends MediaSessionImpl { |
| // LibrarySession methods |
| void notifyChildrenChanged( |
| @NonNull String parentId, int itemCount, @Nullable LibraryParams params); |
| void notifyChildrenChanged(@NonNull ControllerInfo controller, |
| @NonNull String parentId, int itemCount, @Nullable LibraryParams params); |
| void notifySearchResultChanged(@NonNull ControllerInfo controller, |
| @NonNull String query, int itemCount, @Nullable LibraryParams params); |
| |
| // LibrarySession callback implementations called on the executors |
| LibraryResult onGetLibraryRootOnExecutor(@NonNull ControllerInfo controller, |
| @Nullable LibraryParams params); |
| LibraryResult onGetItemOnExecutor(@NonNull ControllerInfo controller, |
| @NonNull String mediaId); |
| LibraryResult onGetChildrenOnExecutor(@NonNull ControllerInfo controller, |
| @NonNull String parentId, int page, int pageSize, |
| @Nullable LibraryParams params); |
| int onSubscribeOnExecutor(@NonNull ControllerInfo controller, |
| @NonNull String parentId, @Nullable LibraryParams params); |
| int onUnsubscribeOnExecutor(@NonNull ControllerInfo controller, |
| @NonNull String parentId); |
| int onSearchOnExecutor(@NonNull ControllerInfo controller, @NonNull String query, |
| @Nullable LibraryParams params); |
| LibraryResult onGetSearchResultOnExecutor(@NonNull ControllerInfo controller, |
| @NonNull String query, int page, int pageSize, @Nullable LibraryParams params); |
| |
| // Internally used methods - only changing return type |
| @Override |
| MediaLibrarySession getInstance(); |
| |
| @Override |
| MediaLibrarySessionCallback getCallback(); |
| } |
| } |
| |
| @Override |
| MediaSessionServiceImpl createImpl() { |
| return new MediaLibraryServiceImplBase(); |
| } |
| |
| @Override |
| public IBinder onBind(@NonNull Intent intent) { |
| return super.onBind(intent); |
| } |
| |
| @Override |
| @Nullable |
| public abstract MediaLibrarySession onGetSession(@NonNull ControllerInfo controllerInfo); |
| |
| /** |
| * Contains information that the library service needs to send to the client. |
| * <p> |
| * When the browser supplies {@link LibraryParams}, it's optional field when getting the media |
| * item(s). The library session is recommended to do the best effort to provide such result. |
| * It's not an error even when the library session didn't return such items. |
| * <p> |
| * The library params returned in the library session callback must include the information |
| * about the returned media item(s). |
| */ |
| @VersionedParcelize |
| public static final class LibraryParams implements VersionedParcelable { |
| @ParcelField(1) |
| Bundle mBundle; |
| |
| // Types are intentionally Integer for future extension of the value with less effort. |
| @ParcelField(2) |
| int mRecent; |
| @ParcelField(3) |
| int mOffline; |
| @ParcelField(4) |
| int mSuggested; |
| |
| // For versioned parcelable. |
| LibraryParams() { |
| // no-op |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| LibraryParams(Bundle bundle, boolean recent, boolean offline, boolean suggested) { |
| // Keeps the booleans in Integer type. |
| // Types are intentionally Integer for future extension of the value with less effort. |
| this(bundle, |
| convertToInteger(recent), |
| convertToInteger(offline), |
| convertToInteger(suggested)); |
| } |
| |
| private LibraryParams(Bundle bundle, int recent, int offline, int suggested) { |
| mBundle = bundle; |
| mRecent = recent; |
| mOffline = offline; |
| mSuggested = suggested; |
| } |
| |
| private static int convertToInteger(boolean a) { |
| return a ? 1 : 0; |
| } |
| |
| private static boolean convertToBoolean(int a) { |
| return a == 0 ? false : true; |
| } |
| |
| /** |
| * Returns {@code true} for recent media items. |
| * <p> |
| * When the browser supplies {@link LibraryParams} with the {@code true}, library |
| * session is recommended to provide such media items. If so, the library session |
| * implementation must return the params with the {@code true} as well. The list of |
| * media items is considered ordered by relevance, first being the top suggestion. |
| * |
| * @return {@code true} for recent items. {@code false} otherwise. |
| */ |
| public boolean isRecent() { |
| return convertToBoolean(mRecent); |
| } |
| |
| /** |
| * Returns {@code true} for offline media items, which can be played without an internet |
| * connection. |
| * <p> |
| * When the browser supplies {@link LibraryParams} with the {@code true}, library |
| * session is recommended to provide such media items. If so, the library session |
| * implementation must return the params with the {@code true} as well. |
| * |
| * @return {@code true} for offline items. {@code false} otherwise. |
| **/ |
| public boolean isOffline() { |
| return convertToBoolean(mOffline); |
| } |
| |
| /** |
| * Returns {@code true} for suggested media items. |
| * <p> |
| * When the browser supplies {@link LibraryParams} with the {@code true}, library |
| * session is recommended to provide such media items. If so, the library session |
| * implementation must return the params with the {@code true} as well. The list of |
| * media items is considered ordered by relevance, first being the top suggestion. |
| * |
| * @return {@code true} for suggested items. {@code false} otherwise |
| **/ |
| public boolean isSuggested() { |
| return convertToBoolean(mSuggested); |
| } |
| |
| /** |
| * Gets the extras. |
| * <p> |
| * Extras are the private contract between browser and library session. |
| */ |
| @Nullable |
| public Bundle getExtras() { |
| return mBundle; |
| } |
| |
| /** |
| * Builds a {@link LibraryParams}. |
| */ |
| public static final class Builder { |
| private boolean mRecent; |
| private boolean mOffline; |
| private boolean mSuggested; |
| |
| private Bundle mBundle; |
| |
| /** |
| * Sets whether recently played media item. |
| * <p> |
| * When the browser supplies the {@link LibraryParams} with the {@code true}, library |
| * session is recommended to provide such media items. If so, the library session |
| * implementation must return the params with the {@code true} as well. |
| * |
| * @param recent {@code true} for recent items. {@code false} otherwise. |
| * @return this builder |
| */ |
| @NonNull |
| public Builder setRecent(boolean recent) { |
| mRecent = recent; |
| return this; |
| } |
| |
| /** |
| * Sets whether offline media items, which can be played without an internet connection. |
| * <p> |
| * When the browser supplies {@link LibraryParams} with the {@code true}, library |
| * session is recommended to provide such media items. If so, the library session |
| * implementation must return the params with the {@code true} as well. |
| * |
| * @param offline {@code true} for offline items. {@code false} otherwise. |
| * @return this builder |
| */ |
| @NonNull |
| public Builder setOffline(boolean offline) { |
| mOffline = offline; |
| return this; |
| } |
| |
| /** |
| * Sets whether suggested media items. |
| * <p> |
| * When the browser supplies {@link LibraryParams} with the {@code true}, library |
| * session is recommended to provide such media items. If so, the library session |
| * implementation must return the params with the {@code true} as well. The list of |
| * media items is considered ordered by relevance, first being the top suggestion. |
| * |
| * @param suggested {@code true} for suggested items. {@code false} otherwise |
| * @return this builder |
| */ |
| @NonNull |
| public Builder setSuggested(boolean suggested) { |
| mSuggested = suggested; |
| return this; |
| } |
| |
| /** |
| * Set a bundle of extras, that browser and library session can understand each other. |
| * |
| * @param extras The extras or null. |
| * @return this builder |
| */ |
| @NonNull |
| public Builder setExtras(@Nullable Bundle extras) { |
| mBundle = extras; |
| return this; |
| } |
| |
| /** |
| * Builds a {@link LibraryParams}. |
| * |
| * @return new LibraryParams |
| */ |
| @NonNull |
| public LibraryParams build() { |
| return new LibraryParams(mBundle, mRecent, mOffline, mSuggested); |
| } |
| } |
| } |
| } |