blob: 44664dcf9dcb14ab7fa8dd467803a5a73cde7aee [file] [log] [blame]
/*
* Copyright (C) 2017 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 com.android.tv.data;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.tv.TvContract;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.IntDef;
import android.support.annotation.MainThread;
import android.support.media.tv.ChannelLogoUtils;
import android.support.media.tv.PreviewProgram;
import android.util.Log;
import android.util.Pair;
import com.android.tv.R;
import com.android.tv.common.util.PermissionUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
/** Class to manage the preview data. */
@TargetApi(Build.VERSION_CODES.O)
@MainThread
public class PreviewDataManager {
private static final String TAG = "PreviewDataManager";
private static final boolean DEBUG = false;
/** Invalid preview channel ID. */
public static final long INVALID_PREVIEW_CHANNEL_ID = -1;
@IntDef({TYPE_DEFAULT_PREVIEW_CHANNEL, TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL})
@Retention(RetentionPolicy.SOURCE)
public @interface PreviewChannelType {}
/** Type of default preview channel */
public static final int TYPE_DEFAULT_PREVIEW_CHANNEL = 1;
/** Type of recorded program channel */
public static final int TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL = 2;
private final Context mContext;
private final ContentResolver mContentResolver;
private boolean mLoadFinished;
private PreviewData mPreviewData = new PreviewData();
private final Set<PreviewDataListener> mPreviewDataListeners = new CopyOnWriteArraySet<>();
private QueryPreviewDataTask mQueryPreviewTask;
private final Map<Long, CreatePreviewChannelTask> mCreatePreviewChannelTasks = new HashMap<>();
private final Map<Long, UpdatePreviewProgramTask> mUpdatePreviewProgramTasks = new HashMap<>();
private final int mPreviewChannelLogoWidth;
private final int mPreviewChannelLogoHeight;
public PreviewDataManager(Context context) {
mContext = context.getApplicationContext();
mContentResolver = context.getContentResolver();
mPreviewChannelLogoWidth =
mContext.getResources().getDimensionPixelSize(R.dimen.preview_channel_logo_width);
mPreviewChannelLogoHeight =
mContext.getResources().getDimensionPixelSize(R.dimen.preview_channel_logo_height);
}
/** Starts the preview data manager. */
public void start() {
if (mQueryPreviewTask == null) {
mQueryPreviewTask = new QueryPreviewDataTask();
mQueryPreviewTask.execute();
}
}
/** Stops the preview data manager. */
public void stop() {
if (mQueryPreviewTask != null) {
mQueryPreviewTask.cancel(true);
}
for (CreatePreviewChannelTask createPreviewChannelTask :
mCreatePreviewChannelTasks.values()) {
createPreviewChannelTask.cancel(true);
}
for (UpdatePreviewProgramTask updatePreviewProgramTask :
mUpdatePreviewProgramTasks.values()) {
updatePreviewProgramTask.cancel(true);
}
mQueryPreviewTask = null;
mCreatePreviewChannelTasks.clear();
mUpdatePreviewProgramTasks.clear();
}
/** Gets preview channel ID from the preview channel type. */
public @PreviewChannelType long getPreviewChannelId(long previewChannelType) {
return mPreviewData.getPreviewChannelId(previewChannelType);
}
/** Creates default preview channel. */
public void createDefaultPreviewChannel(
OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) {
createPreviewChannel(TYPE_DEFAULT_PREVIEW_CHANNEL, onPreviewChannelCreationResultListener);
}
/** Creates a preview channel for specific channel type. */
public void createPreviewChannel(
@PreviewChannelType long previewChannelType,
OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) {
CreatePreviewChannelTask currentRunningCreateTask =
mCreatePreviewChannelTasks.get(previewChannelType);
if (currentRunningCreateTask == null) {
CreatePreviewChannelTask createPreviewChannelTask =
new CreatePreviewChannelTask(previewChannelType);
createPreviewChannelTask.addOnPreviewChannelCreationResultListener(
onPreviewChannelCreationResultListener);
createPreviewChannelTask.execute();
mCreatePreviewChannelTasks.put(previewChannelType, createPreviewChannelTask);
} else {
currentRunningCreateTask.addOnPreviewChannelCreationResultListener(
onPreviewChannelCreationResultListener);
}
}
/** Returns {@code true} if the preview data is loaded. */
public boolean isLoadFinished() {
return mLoadFinished;
}
/** Adds listener. */
public void addListener(PreviewDataListener previewDataListener) {
mPreviewDataListeners.add(previewDataListener);
}
/** Removes listener. */
public void removeListener(PreviewDataListener previewDataListener) {
mPreviewDataListeners.remove(previewDataListener);
}
/** Updates the preview programs table for a specific preview channel. */
public void updatePreviewProgramsForChannel(
long previewChannelId,
Set<PreviewProgramContent> programs,
PreviewDataListener previewDataListener) {
UpdatePreviewProgramTask currentRunningUpdateTask =
mUpdatePreviewProgramTasks.get(previewChannelId);
if (currentRunningUpdateTask != null
&& currentRunningUpdateTask.getPrograms().equals(programs)) {
currentRunningUpdateTask.addPreviewDataListener(previewDataListener);
return;
}
UpdatePreviewProgramTask updatePreviewProgramTask =
new UpdatePreviewProgramTask(previewChannelId, programs);
updatePreviewProgramTask.addPreviewDataListener(previewDataListener);
if (currentRunningUpdateTask != null) {
currentRunningUpdateTask.cancel(true);
currentRunningUpdateTask.saveStatus();
updatePreviewProgramTask.addPreviewDataListeners(
currentRunningUpdateTask.getPreviewDataListeners());
}
updatePreviewProgramTask.execute();
mUpdatePreviewProgramTasks.put(previewChannelId, updatePreviewProgramTask);
}
private void notifyPreviewDataLoadFinished() {
for (PreviewDataListener l : mPreviewDataListeners) {
l.onPreviewDataLoadFinished();
}
}
public interface PreviewDataListener {
/** Called when the preview data is loaded. */
void onPreviewDataLoadFinished();
/** Called when the preview data is updated. */
void onPreviewDataUpdateFinished();
}
public interface OnPreviewChannelCreationResultListener {
/**
* Called when the creation of preview channel is finished.
*
* @param createdPreviewChannelId The preview channel ID if created successfully, otherwise
* it's {@value #INVALID_PREVIEW_CHANNEL_ID}.
*/
void onPreviewChannelCreationResult(long createdPreviewChannelId);
}
private final class QueryPreviewDataTask extends AsyncTask<Void, Void, PreviewData> {
private final String PARAM_PREVIEW = "preview";
private final String mChannelSelection = TvContract.Channels.COLUMN_PACKAGE_NAME + "=?";
@Override
protected PreviewData doInBackground(Void... voids) {
// Query preview channels and programs.
if (DEBUG) Log.d(TAG, "QueryPreviewDataTask.doInBackground");
PreviewData previewData = new PreviewData();
try {
Uri previewChannelsUri =
PreviewDataUtils.addQueryParamToUri(
TvContract.Channels.CONTENT_URI,
new Pair<>(PARAM_PREVIEW, String.valueOf(true)));
String packageName = mContext.getPackageName();
if (PermissionUtils.hasAccessAllEpg(mContext)) {
try (Cursor cursor =
mContentResolver.query(
previewChannelsUri,
android.support.media.tv.Channel.PROJECTION,
mChannelSelection,
new String[] {packageName},
null)) {
if (cursor != null) {
while (cursor.moveToNext()) {
android.support.media.tv.Channel previewChannel =
android.support.media.tv.Channel.fromCursor(cursor);
Long previewChannelType = previewChannel.getInternalProviderFlag1();
if (previewChannelType != null) {
previewData.addPreviewChannelId(
previewChannelType, previewChannel.getId());
}
}
}
}
} else {
try (Cursor cursor =
mContentResolver.query(
previewChannelsUri,
android.support.media.tv.Channel.PROJECTION,
null,
null,
null)) {
if (cursor != null) {
while (cursor.moveToNext()) {
android.support.media.tv.Channel previewChannel =
android.support.media.tv.Channel.fromCursor(cursor);
Long previewChannelType = previewChannel.getInternalProviderFlag1();
if (packageName.equals(previewChannel.getPackageName())
&& previewChannelType != null) {
previewData.addPreviewChannelId(
previewChannelType, previewChannel.getId());
}
}
}
}
}
for (long previewChannelId : previewData.getAllPreviewChannelIds().values()) {
Uri previewProgramsUriForPreviewChannel =
TvContract.buildPreviewProgramsUriForChannel(previewChannelId);
try (Cursor previewProgramCursor =
mContentResolver.query(
previewProgramsUriForPreviewChannel,
PreviewProgram.PROJECTION,
null,
null,
null)) {
if (previewProgramCursor != null) {
while (previewProgramCursor.moveToNext()) {
PreviewProgram previewProgram =
PreviewProgram.fromCursor(previewProgramCursor);
previewData.addPreviewProgram(previewProgram);
}
}
}
}
} catch (SQLException e) {
Log.w(TAG, "Unable to get preview data", e);
}
return previewData;
}
@Override
protected void onPostExecute(PreviewData result) {
super.onPostExecute(result);
if (mQueryPreviewTask == this) {
mQueryPreviewTask = null;
mPreviewData = new PreviewData(result);
mLoadFinished = true;
notifyPreviewDataLoadFinished();
}
}
}
private final class CreatePreviewChannelTask extends AsyncTask<Void, Void, Long> {
private final long mPreviewChannelType;
private Set<OnPreviewChannelCreationResultListener>
mOnPreviewChannelCreationResultListeners = new CopyOnWriteArraySet<>();
public CreatePreviewChannelTask(long previewChannelType) {
mPreviewChannelType = previewChannelType;
}
public void addOnPreviewChannelCreationResultListener(
OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) {
if (onPreviewChannelCreationResultListener != null) {
mOnPreviewChannelCreationResultListeners.add(
onPreviewChannelCreationResultListener);
}
}
@Override
protected Long doInBackground(Void... params) {
if (DEBUG) Log.d(TAG, "CreatePreviewChannelTask.doInBackground");
long previewChannelId;
try {
Uri channelUri =
mContentResolver.insert(
TvContract.Channels.CONTENT_URI,
PreviewDataUtils.createPreviewChannel(mContext, mPreviewChannelType)
.toContentValues());
if (channelUri != null) {
previewChannelId = ContentUris.parseId(channelUri);
} else {
Log.e(TAG, "Fail to insert preview channel");
return INVALID_PREVIEW_CHANNEL_ID;
}
} catch (UnsupportedOperationException | NumberFormatException e) {
Log.e(TAG, "Fail to get channel ID");
return INVALID_PREVIEW_CHANNEL_ID;
}
Drawable appIcon = mContext.getApplicationInfo().loadIcon(mContext.getPackageManager());
if (appIcon != null && appIcon instanceof BitmapDrawable) {
ChannelLogoUtils.storeChannelLogo(
mContext,
previewChannelId,
Bitmap.createScaledBitmap(
((BitmapDrawable) appIcon).getBitmap(),
mPreviewChannelLogoWidth,
mPreviewChannelLogoHeight,
false));
}
return previewChannelId;
}
@Override
protected void onPostExecute(Long result) {
super.onPostExecute(result);
if (result != INVALID_PREVIEW_CHANNEL_ID) {
mPreviewData.addPreviewChannelId(mPreviewChannelType, result);
}
for (OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener :
mOnPreviewChannelCreationResultListeners) {
onPreviewChannelCreationResultListener.onPreviewChannelCreationResult(result);
}
mCreatePreviewChannelTasks.remove(mPreviewChannelType);
}
}
/**
* Updates the whole data which belongs to the package in preview programs table for a specific
* preview channel with a set of {@link PreviewProgramContent}.
*/
private final class UpdatePreviewProgramTask extends AsyncTask<Void, Void, Void> {
private long mPreviewChannelId;
private Set<PreviewProgramContent> mPrograms;
private Map<Long, Long> mCurrentProgramId2PreviewProgramId;
private Set<PreviewDataListener> mPreviewDataListeners = new CopyOnWriteArraySet<>();
public UpdatePreviewProgramTask(
long previewChannelId, Set<PreviewProgramContent> programs) {
mPreviewChannelId = previewChannelId;
mPrograms = programs;
if (mPreviewData.getPreviewProgramIds(previewChannelId) == null) {
mCurrentProgramId2PreviewProgramId = new HashMap<>();
} else {
mCurrentProgramId2PreviewProgramId =
new HashMap<>(mPreviewData.getPreviewProgramIds(previewChannelId));
}
}
public void addPreviewDataListener(PreviewDataListener previewDataListener) {
if (previewDataListener != null) {
mPreviewDataListeners.add(previewDataListener);
}
}
public void addPreviewDataListeners(Set<PreviewDataListener> previewDataListeners) {
if (previewDataListeners != null) {
mPreviewDataListeners.addAll(previewDataListeners);
}
}
public Set<PreviewProgramContent> getPrograms() {
return mPrograms;
}
public Set<PreviewDataListener> getPreviewDataListeners() {
return mPreviewDataListeners;
}
@Override
protected Void doInBackground(Void... params) {
if (DEBUG) Log.d(TAG, "UpdatePreviewProgamTask.doInBackground");
Map<Long, Long> uncheckedPrograms = new HashMap<>(mCurrentProgramId2PreviewProgramId);
for (PreviewProgramContent program : mPrograms) {
if (isCancelled()) {
return null;
}
Long existingPreviewProgramId = uncheckedPrograms.remove(program.getId());
if (existingPreviewProgramId != null) {
if (DEBUG)
Log.d(
TAG,
"Preview program "
+ existingPreviewProgramId
+ " "
+ "already exists for program "
+ program.getId());
continue;
}
try {
Uri programUri =
mContentResolver.insert(
TvContract.PreviewPrograms.CONTENT_URI,
PreviewDataUtils.createPreviewProgramFromContent(program)
.toContentValues());
if (programUri != null) {
long previewProgramId = ContentUris.parseId(programUri);
mCurrentProgramId2PreviewProgramId.put(program.getId(), previewProgramId);
if (DEBUG) Log.d(TAG, "Add new preview program " + previewProgramId);
} else {
Log.e(TAG, "Fail to insert preview program");
}
} catch (Exception e) {
Log.e(TAG, "Fail to get preview program ID");
}
}
for (Long key : uncheckedPrograms.keySet()) {
if (isCancelled()) {
return null;
}
try {
if (DEBUG) Log.d(TAG, "Remove preview program " + uncheckedPrograms.get(key));
mContentResolver.delete(
TvContract.buildPreviewProgramUri(uncheckedPrograms.get(key)),
null,
null);
mCurrentProgramId2PreviewProgramId.remove(key);
} catch (Exception e) {
Log.e(TAG, "Fail to remove preview program " + uncheckedPrograms.get(key));
}
}
return null;
}
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
mPreviewData.setPreviewProgramIds(
mPreviewChannelId, mCurrentProgramId2PreviewProgramId);
mUpdatePreviewProgramTasks.remove(mPreviewChannelId);
for (PreviewDataListener previewDataListener : mPreviewDataListeners) {
previewDataListener.onPreviewDataUpdateFinished();
}
}
public void saveStatus() {
mPreviewData.setPreviewProgramIds(
mPreviewChannelId, mCurrentProgramId2PreviewProgramId);
}
}
/** Class to store the query result of preview data. */
private static final class PreviewData {
private Map<Long, Long> mPreviewChannelType2Id = new HashMap<>();
private Map<Long, Map<Long, Long>> mProgramId2PreviewProgramId = new HashMap<>();
PreviewData() {
mPreviewChannelType2Id = new HashMap<>();
mProgramId2PreviewProgramId = new HashMap<>();
}
PreviewData(PreviewData previewData) {
mPreviewChannelType2Id = new HashMap<>(previewData.mPreviewChannelType2Id);
mProgramId2PreviewProgramId = new HashMap<>(previewData.mProgramId2PreviewProgramId);
}
public void addPreviewProgram(PreviewProgram previewProgram) {
long previewChannelId = previewProgram.getChannelId();
Map<Long, Long> programId2PreviewProgram =
mProgramId2PreviewProgramId.get(previewChannelId);
if (programId2PreviewProgram == null) {
programId2PreviewProgram = new HashMap<>();
}
mProgramId2PreviewProgramId.put(previewChannelId, programId2PreviewProgram);
if (previewProgram.getInternalProviderId() != null) {
programId2PreviewProgram.put(
Long.parseLong(previewProgram.getInternalProviderId()),
previewProgram.getId());
}
}
public @PreviewChannelType long getPreviewChannelId(long previewChannelType) {
Long result = mPreviewChannelType2Id.get(previewChannelType);
return result == null ? INVALID_PREVIEW_CHANNEL_ID : result;
}
public Map<Long, Long> getAllPreviewChannelIds() {
return mPreviewChannelType2Id;
}
public void addPreviewChannelId(long previewChannelType, long previewChannelId) {
mPreviewChannelType2Id.put(previewChannelType, previewChannelId);
}
public void removePreviewChannelId(long previewChannelType) {
mPreviewChannelType2Id.remove(previewChannelType);
}
public void removePreviewChannel(long previewChannelId) {
removePreviewChannelId(previewChannelId);
removePreviewProgramIds(previewChannelId);
}
public Map<Long, Long> getPreviewProgramIds(long previewChannelId) {
return mProgramId2PreviewProgramId.get(previewChannelId);
}
public Map<Long, Map<Long, Long>> getAllPreviewProgramIds() {
return mProgramId2PreviewProgramId;
}
public void setPreviewProgramIds(
long previewChannelId, Map<Long, Long> programId2PreviewProgramId) {
mProgramId2PreviewProgramId.put(previewChannelId, programId2PreviewProgramId);
}
public void removePreviewProgramIds(long previewChannelId) {
mProgramId2PreviewProgramId.remove(previewChannelId);
}
}
/** A utils class for preview data. */
public static final class PreviewDataUtils {
/** Creates a preview channel. */
public static android.support.media.tv.Channel createPreviewChannel(
Context context, @PreviewChannelType long previewChannelType) {
if (previewChannelType == TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL) {
return createRecordedProgramPreviewChannel(context, previewChannelType);
}
return createDefaultPreviewChannel(context, previewChannelType);
}
private static android.support.media.tv.Channel createDefaultPreviewChannel(
Context context, @PreviewChannelType long previewChannelType) {
android.support.media.tv.Channel.Builder builder =
new android.support.media.tv.Channel.Builder();
CharSequence appLabel =
context.getApplicationInfo().loadLabel(context.getPackageManager());
CharSequence appDescription =
context.getApplicationInfo().loadDescription(context.getPackageManager());
builder.setType(TvContract.Channels.TYPE_PREVIEW)
.setDisplayName(appLabel == null ? null : appLabel.toString())
.setDescription(appDescription == null ? null : appDescription.toString())
.setAppLinkIntentUri(TvContract.Channels.CONTENT_URI)
.setInternalProviderFlag1(previewChannelType);
return builder.build();
}
private static android.support.media.tv.Channel createRecordedProgramPreviewChannel(
Context context, @PreviewChannelType long previewChannelType) {
android.support.media.tv.Channel.Builder builder =
new android.support.media.tv.Channel.Builder();
builder.setType(TvContract.Channels.TYPE_PREVIEW)
.setDisplayName(
context.getResources()
.getString(R.string.recorded_programs_preview_channel))
.setAppLinkIntentUri(TvContract.Channels.CONTENT_URI)
.setInternalProviderFlag1(previewChannelType);
return builder.build();
}
/** Creates a preview program. */
public static PreviewProgram createPreviewProgramFromContent(
PreviewProgramContent program) {
PreviewProgram.Builder builder = new PreviewProgram.Builder();
builder.setChannelId(program.getPreviewChannelId())
.setType(program.getType())
.setLive(program.getLive())
.setTitle(program.getTitle())
.setDescription(program.getDescription())
.setPosterArtUri(program.getPosterArtUri())
.setIntentUri(program.getIntentUri())
.setPreviewVideoUri(program.getPreviewVideoUri())
.setInternalProviderId(Long.toString(program.getId()))
.setContentId(program.getIntentUri().toString());
return builder.build();
}
/** Appends query parameters to a Uri. */
public static Uri addQueryParamToUri(Uri uri, Pair<String, String> param) {
return uri.buildUpon().appendQueryParameter(param.first, param.second).build();
}
}
}