/*
* Copyright 2016 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.google.android.media.tv.companionlibrary;
import android.animation.LayoutTransition;
import android.app.Fragment;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.annotation.ColorInt;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.google.android.media.tv.companionlibrary.model.Channel;
import java.util.ArrayList;
/**
* The ChannelSetupFragment class provides a simple extendable class to create a user interface
* for scanning channels. This fragment will be displayed to the user when they are setting up
* your app's channels for the first time in the setup activity.
* </p>
* There are a handful of methods which can be used to customize and theme the user interface.
* Methods like {@link #setBackgroundColor(int)}, {@link #setTitle(CharSequence)}, and
* {@link #setBadge(Drawable)} can be called when your fragment first is initialized to change their
* values.
* </p>
* Additionally, your fragment can override certain methods to provide custom functionality to your
* setup activity. When the user clicks on the button to start scanning for your channels, the
* {@link #onScanStarted()} method will be called. Here your {@link EpgSyncJobService} should run
* the {@link EpgSyncJobService#requestImmediateSync(Context, String, ComponentName)} and start
* syncing.
* </p>
* When your service is done scanning, the method {@link #onScanFinished()} will called. Here should
* be the code to exit the setup activity and return to the system TV app.
* </p>
* While channels are being scanned, the methods {@link #onChannelScanCompleted(int, int)} and
* {@link #onScannedChannel(CharSequence, CharSequence)} will be called to provide status updates
* during scans.
* This information can be provided to the user by calling
* {@link #setDescription(CharSequence)}. Additionally, a progress bar will automatically
* increment.
* </p>
* Users should be able to manually start and stop the scanning process and the button text should
* be updated by calling {@link #setButtonText(CharSequence)}. If
* {@link #setChannelListVisibility(boolean)} is {@code true}, the channels will appear on the
* screen as they are scanned.
*/
public abstract class ChannelSetupFragment extends Fragment {
private static final String TAG = "ScanFragment";
private static final boolean DEBUG = false;
private ProgressBar mProgressBar;
private View mChannelHolder;
private TextView mScanningMessage;
private ChannelAdapter mAdapter;
private Button mCancelButton;
private boolean mFinishedScan;
private View mChannelScanLayout;
private TextView mTitle;
private ImageView mBadge;
private final BroadcastReceiver mSyncStatusChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, final Intent intent) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (mFinishedScan) {
return;
}
String syncStatusChangedInputId = intent.getStringExtra(
EpgSyncJobService.BUNDLE_KEY_INPUT_ID);
if (syncStatusChangedInputId != null
&& syncStatusChangedInputId.equals(getInputId())) {
String syncStatus = intent.getStringExtra(EpgSyncJobService.SYNC_STATUS);
if (syncStatus.equals(EpgSyncJobService.SYNC_STARTED)) {
if (DEBUG) {
Log.d(TAG, "Sync status: Started");
}
} else if (syncStatus.equals(EpgSyncJobService.SYNC_SCANNED)) {
int channelsScanned = intent.
getIntExtra(EpgSyncJobService.BUNDLE_KEY_CHANNELS_SCANNED, 0);
int channelCount = intent.
getIntExtra(EpgSyncJobService.BUNDLE_KEY_CHANNEL_COUNT, 0);
updateScanProgress(++channelsScanned, channelCount);
String channelDisplayName = intent.getStringExtra(
EpgSyncJobService.BUNDLE_KEY_SCANNED_CHANNEL_DISPLAY_NAME);
String channelDisplayNumber = intent.getStringExtra(
EpgSyncJobService.BUNDLE_KEY_SCANNED_CHANNEL_DISPLAY_NUMBER);
if (DEBUG) {
Log.d(TAG, "Sync status: Channel Scanned");
Log.d(TAG, "Scanned " + channelsScanned + " out of " + channelCount);
}
onScannedChannel(channelDisplayName, channelDisplayNumber);
mAdapter.add(new Pair<>(channelDisplayName, channelDisplayNumber));
} else if (syncStatus.equals(EpgSyncJobService.SYNC_FINISHED)) {
if (DEBUG) {
Log.d(TAG, "Sync status: Finished");
}
finishScan(true);
} else if (syncStatus.equals(EpgSyncJobService.SYNC_ERROR)) {
int errorCode =
intent.getIntExtra(EpgSyncJobService.BUNDLE_KEY_ERROR_REASON,
0);
if (DEBUG) {
Log.d(TAG, "Error occurred: " + errorCode);
}
onScanError(errorCode);
}
}
}
});
}
};
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(getLayoutResourceId(), container, false);
// Make sure this view is focused
view.requestFocus();
mProgressBar = (ProgressBar) view.findViewById(R.id.tune_progress);
mScanningMessage = (TextView) view.findViewById(R.id.tune_description);
mTitle = (TextView) view.findViewById(R.id.tune_title);
mBadge = (ImageView) view.findViewById(R.id.tune_icon);
mChannelHolder = view.findViewById(R.id.channel_holder);
mCancelButton = (Button) view.findViewById(R.id.tune_cancel);
ListView channelList = (ListView) view.findViewById(R.id.channel_list);
mAdapter = new ChannelAdapter();
channelList.setAdapter(mAdapter);
channelList.setOnItemClickListener(null);
ViewGroup progressHolder = (ViewGroup) view.findViewById(R.id.progress_holder);
LayoutTransition transition = new LayoutTransition();
transition.enableTransitionType(LayoutTransition.CHANGING);
progressHolder.setLayoutTransition(transition);
mCancelButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finishScan(false);
}
});
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
mSyncStatusChangedReceiver,
new IntentFilter(EpgSyncJobService.ACTION_SYNC_STATUS_CHANGED));
mChannelScanLayout = view;
setChannelListVisibility(false);
setBackgroundColor(getResources().getColor(android.R.color.holo_blue_dark));
return view;
}
@Override
public void onStart() {
super.onStart();
onScanStarted();
}
@Override
public void onDestroy() {
super.onDestroy();
LocalBroadcastManager.getInstance(getActivity())
.unregisterReceiver(mSyncStatusChangedReceiver);
}
/**
* This method returns the layout for this fragment.
*
* @return Resource id for the fragment layout.
*/
public int getLayoutResourceId() {
return R.layout.tif_channel_setup;
}
/**
* Sets the background color for the layout. This allows the setup fragment to be themed.
*
* @param backgroundColor The color for the background.
*/
public void setBackgroundColor(@ColorInt int backgroundColor) {
mChannelScanLayout.findViewById(R.id.channel_setup_layout)
.setBackgroundColor(backgroundColor);
}
private void updateScanProgress(int channelsScanned, int channelCount) {
if (channelCount > 0) {
mProgressBar.setMax(channelCount);
mProgressBar.setProgress(channelsScanned);
}
onChannelScanCompleted(channelsScanned, channelCount);
}
/**
* Finishes the current scan thread. This fragment will be popped after the scan thread ends.
*
* @param scanCompleted a flag which indicates the scan was completed successfully or canceled.
*/
private void finishScan(boolean scanCompleted) {
// Hides the cancel button.
mFinishedScan = true;
mCancelButton.setEnabled(false);
onScanFinished();
}
/**
* This method will be called when scanning should begin. Developers should request a new
* immediate sync by calling
* {@link EpgSyncJobService#requestImmediateSync(Context, String, ComponentName)}.
*/
public abstract void onScanStarted();
/**
* @return The input id for your Tv service.
*/
public abstract String getInputId();
/**
* This method will be called when scanning ends. Developers may want to notify the user that
* scanning has completed and allow them to exit the activity.
*/
public abstract void onScanFinished();
/**
* This method will be called when an error occurs in scanning. Developers may want to notify
* the user that an error has happened or resolve the error.
*
* @param reason A constant indicating the type of error that has happened. Possible values are
* {@link EpgSyncJobService#ERROR_EPG_SYNC_CANCELED},
* {@link EpgSyncJobService#ERROR_INPUT_ID_NULL},
* {@link EpgSyncJobService#ERROR_NO_PROGRAMS},
* {@link EpgSyncJobService#ERROR_NO_CHANNELS}, or
* {@link EpgSyncJobService#ERROR_DATABASE_INSERT},
*/
public void onScanError(int reason) {
}
/**
* Update the description that will be displayed underneath the progress bar. This could be used
* to state the current progress of the scan.
* @param message The message to be displayed.
*/
public void setDescription(CharSequence message) {
mScanningMessage.setText(message);
}
/**
* Update the description that will be displayed underneath the progress bar. This could be used
* to state the current progress of the scan.
* @param resId The string resource to be displayed.
*/
public void setDescription(int resId) {
mScanningMessage.setText(resId);
}
/**
* Sets the title that will be displayed above the progress bar. This could be used to display
* your app's title.
* @param title The title to be displayed.
*/
public void setTitle(CharSequence title) {
mTitle.setText(title);
}
/**
* Sets the title that will be displayed above the progress bar. This could be used to display
* your app's title.
* @param resId The string resource to be displayed.
*/
public void setTitle(int resId) {
mTitle.setText(resId);
}
/**
* Sets the image that will be displayed to the left of the progress bar. This could be used to
* display your app's icon.
* @param drawable The drawable to be displayed.
*/
public void setBadge(Drawable drawable) {
mBadge.setImageDrawable(drawable);
}
/**
* Sets the image that will be displayed to the left of the progress bar. This could be used to
* display your app's icon.
* @param bitmap The bitmap image to be displayed.
*/
public void setBadge(Bitmap bitmap) {
mBadge.setImageBitmap(bitmap);
}
/**
* Sets the text that will appear on the button on the screen.
* @param message The button text.
*/
public void setButtonText(CharSequence message) {
mCancelButton.setText(message);
}
/**
* Sets the text that will appear on the button on the screen.
* @param resId The string resource to be displayed.
*/
public void setButtonText(int resId) {
mCancelButton.setText(resId);
}
/**
* Sets whether the channel list will be displayed to the right of the screen, displaying
* each channel as it is scanned.
* @param visible If true, the list will be displayed. Otherwise it will be hidden.
*/
public void setChannelListVisibility(boolean visible) {
mChannelHolder.setVisibility(visible ? View.VISIBLE : View.GONE);
}
/**
* This method will be called when a channel has been completely scanned. It can be overriden
* to display custom information about this channel to the user.
*
* @param displayName {@link Channel#getDisplayName()} for the scanned channel.
* @param displayNumber {@link Channel#getDisplayNumber()} ()} for the scanned channel.
*/
public void onScannedChannel(CharSequence displayName, CharSequence displayNumber) {
if (DEBUG) {
Log.d(TAG, "Scanned channel data: " + displayName + ", " + displayNumber);
}
}
/**
* This method will be called when another channel has been scanned. It can be overriden to
* display custom information about the current progress of the scan.
*
* @param channelsScanned The number of channels that have been scanned so far.
* @param channelCount The total number of channels that need to be scanned.
*/
public void onChannelScanCompleted(int channelsScanned, int channelCount) {
}
private class ChannelAdapter extends BaseAdapter {
private final ArrayList<Pair<String, String>> mChannels;
public ChannelAdapter() {
mChannels = new ArrayList<>();
}
@Override
public boolean areAllItemsEnabled() {
return false;
}
@Override
public boolean isEnabled(int pos) {
return false;
}
@Override
public int getCount() {
return mChannels.size();
}
@Override
public Object getItem(int pos) {
return pos;
}
@Override
public long getItemId(int pos) {
return pos;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final Context context = parent.getContext();
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.tif_channel_list, parent, false);
}
TextView channelName = (TextView) convertView.findViewById(R.id.channel_name);
channelName.setText(mChannels.get(position).first);
TextView channelNum = (TextView) convertView.findViewById(R.id.channel_num);
channelNum.setText(mChannels.get(position).second);
return convertView;
}
public void add(Pair<String, String> channelData) {
mChannels.add(channelData);
notifyDataSetChanged();
}
}
}