mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-18 10:32:43 +00:00
Merge branch 'tv-channels' of https://github.com/GinVavilon/moonlight-android into GinVavilon-tv-channels
This commit is contained in:
commit
d9c0830198
@ -8,6 +8,8 @@
|
|||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA"/>
|
||||||
|
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA"/>
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.touchscreen"
|
android:name="android.hardware.touchscreen"
|
||||||
@ -40,7 +42,12 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher"
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
<provider
|
||||||
|
android:name=".PosterContentProvider"
|
||||||
|
android:authorities="poster.${applicationId}"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true">
|
||||||
|
</provider>
|
||||||
<!-- Samsung multi-window support -->
|
<!-- Samsung multi-window support -->
|
||||||
<uses-library
|
<uses-library
|
||||||
android:name="com.sec.android.app.multiwindow"
|
android:name="com.sec.android.app.multiwindow"
|
||||||
|
@ -19,6 +19,7 @@ import com.limelight.utils.Dialog;
|
|||||||
import com.limelight.utils.ServerHelper;
|
import com.limelight.utils.ServerHelper;
|
||||||
import com.limelight.utils.ShortcutHelper;
|
import com.limelight.utils.ShortcutHelper;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
|
import com.limelight.utils.TvChannelHelper;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@ -50,6 +51,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private AppGridAdapter appGridAdapter;
|
private AppGridAdapter appGridAdapter;
|
||||||
private String uuidString;
|
private String uuidString;
|
||||||
private ShortcutHelper shortcutHelper;
|
private ShortcutHelper shortcutHelper;
|
||||||
|
private TvChannelHelper tvChannelHelper;
|
||||||
|
|
||||||
private ComputerDetails computer;
|
private ComputerDetails computer;
|
||||||
private ComputerManagerService.ApplistPoller poller;
|
private ComputerManagerService.ApplistPoller poller;
|
||||||
@ -65,6 +67,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private final static int START_WITH_QUIT = 4;
|
private final static int START_WITH_QUIT = 4;
|
||||||
private final static int VIEW_DETAILS_ID = 5;
|
private final static int VIEW_DETAILS_ID = 5;
|
||||||
private final static int CREATE_SHORTCUT_ID = 6;
|
private final static int CREATE_SHORTCUT_ID = 6;
|
||||||
|
private final static int ADD_TO_TV_CHANNEL = 7;
|
||||||
|
|
||||||
public final static String NAME_EXTRA = "Name";
|
public final static String NAME_EXTRA = "Name";
|
||||||
public final static String UUID_EXTRA = "UUID";
|
public final static String UUID_EXTRA = "UUID";
|
||||||
@ -184,6 +187,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
shortcutHelper.disableShortcut(details.uuid,
|
shortcutHelper.disableShortcut(details.uuid,
|
||||||
getResources().getString(R.string.scut_not_paired));
|
getResources().getString(R.string.scut_not_paired));
|
||||||
|
|
||||||
|
//Delete channel of PC for now
|
||||||
|
tvChannelHelper.deleteChannel(details.uuid);
|
||||||
|
|
||||||
// Display a toast to the user and quit the activity
|
// Display a toast to the user and quit the activity
|
||||||
Toast.makeText(AppView.this, getResources().getText(R.string.scut_not_paired), Toast.LENGTH_SHORT).show();
|
Toast.makeText(AppView.this, getResources().getText(R.string.scut_not_paired), Toast.LENGTH_SHORT).show();
|
||||||
finish();
|
finish();
|
||||||
@ -252,6 +258,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
inForeground = true;
|
inForeground = true;
|
||||||
|
|
||||||
shortcutHelper = new ShortcutHelper(this);
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
|
tvChannelHelper = new TvChannelHelper(this);
|
||||||
|
|
||||||
UiHelper.setLocale(this);
|
UiHelper.setLocale(this);
|
||||||
|
|
||||||
@ -270,6 +277,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||||
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
|
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
|
||||||
shortcutHelper.reportShortcutUsed(uuidString);
|
shortcutHelper.reportShortcutUsed(uuidString);
|
||||||
|
tvChannelHelper.createTvChannel(uuidString, computerName);
|
||||||
|
|
||||||
// Bind to the computer manager service
|
// Bind to the computer manager service
|
||||||
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||||
@ -348,7 +356,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
menu.add(Menu.NONE, VIEW_DETAILS_ID, 3, getResources().getString(R.string.applist_menu_details));
|
menu.add(Menu.NONE, VIEW_DETAILS_ID, 3, getResources().getString(R.string.applist_menu_details));
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (tvChannelHelper.isSupport()) {
|
||||||
|
menu.add(Menu.NONE, ADD_TO_TV_CHANNEL, 4, getResources().getString(R.string.applist_menu_tv_channel));
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
// Only add an option to create shortcut if box art is loaded
|
// Only add an option to create shortcut if box art is loaded
|
||||||
// and when we're in grid-mode (not list-mode).
|
// and when we're in grid-mode (not list-mode).
|
||||||
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
|
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
|
||||||
@ -424,6 +434,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case ADD_TO_TV_CHANNEL:
|
||||||
|
tvChannelHelper.addGameToChannel(computer, app.app);
|
||||||
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return super.onContextItemSelected(item);
|
return super.onContextItemSelected(item);
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import com.limelight.utils.Dialog;
|
|||||||
import com.limelight.utils.HelpLauncher;
|
import com.limelight.utils.HelpLauncher;
|
||||||
import com.limelight.utils.ServerHelper;
|
import com.limelight.utils.ServerHelper;
|
||||||
import com.limelight.utils.ShortcutHelper;
|
import com.limelight.utils.ShortcutHelper;
|
||||||
|
import com.limelight.utils.TvChannelHelper;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@ -62,6 +63,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private RelativeLayout noPcFoundLayout;
|
private RelativeLayout noPcFoundLayout;
|
||||||
private PcGridAdapter pcGridAdapter;
|
private PcGridAdapter pcGridAdapter;
|
||||||
private ShortcutHelper shortcutHelper;
|
private ShortcutHelper shortcutHelper;
|
||||||
|
private TvChannelHelper tvChannelHelper;
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private boolean freezeUpdates, runningPolling, inForeground, completeOnCreateCalled;
|
private boolean freezeUpdates, runningPolling, inForeground, completeOnCreateCalled;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
@ -215,6 +217,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
completeOnCreateCalled = true;
|
completeOnCreateCalled = true;
|
||||||
|
|
||||||
shortcutHelper = new ShortcutHelper(this);
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
|
tvChannelHelper = new TvChannelHelper(this);
|
||||||
|
|
||||||
UiHelper.setLocale(this);
|
UiHelper.setLocale(this);
|
||||||
|
|
||||||
@ -637,6 +640,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
shortcutHelper.disableShortcut(details.uuid,
|
shortcutHelper.disableShortcut(details.uuid,
|
||||||
getResources().getString(R.string.scut_deleted_pc));
|
getResources().getString(R.string.scut_deleted_pc));
|
||||||
|
|
||||||
|
//Delete channel of PC
|
||||||
|
tvChannelHelper.deleteChannel(details.uuid);
|
||||||
|
|
||||||
pcGridAdapter.removeComputer(computer);
|
pcGridAdapter.removeComputer(computer);
|
||||||
pcGridAdapter.notifyDataSetChanged();
|
pcGridAdapter.notifyDataSetChanged();
|
||||||
|
|
||||||
|
107
app/src/main/java/com/limelight/PosterContentProvider.java
Normal file
107
app/src/main/java/com/limelight/PosterContentProvider.java
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
import android.content.ContentProvider;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.UriMatcher;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
|
||||||
|
import com.limelight.grid.assets.DiskAssetLoader;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PosterContentProvider extends ContentProvider {
|
||||||
|
|
||||||
|
|
||||||
|
public static final String AUTHORITY = "poster." + BuildConfig.APPLICATION_ID;
|
||||||
|
public static final String PNG_MIME_TYPE = "image/png";
|
||||||
|
public static final int APP_ID_PATH_INDEX = 2;
|
||||||
|
public static final int COMPUTER_UUID_PATH_INDEX = 1;
|
||||||
|
private DiskAssetLoader mDiskAssetLoader;
|
||||||
|
|
||||||
|
private static final UriMatcher sUriMatcher;
|
||||||
|
private static final String BOXART_PATH = "boxart";
|
||||||
|
private static final int BOXART_URI_ID = 1;
|
||||||
|
|
||||||
|
static {
|
||||||
|
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||||
|
sUriMatcher.addURI(AUTHORITY, BOXART_PATH, BOXART_URI_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
|
||||||
|
int match = sUriMatcher.match(uri);
|
||||||
|
if (match == BOXART_URI_ID) {
|
||||||
|
return openBoxArtFile(uri, mode);
|
||||||
|
}
|
||||||
|
return openBoxArtFile(uri, mode);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParcelFileDescriptor openBoxArtFile(Uri uri, String mode) throws FileNotFoundException {
|
||||||
|
if (!"r".equals(mode)) {
|
||||||
|
throw new UnsupportedOperationException("This provider is only for read mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> segments = uri.getPathSegments();
|
||||||
|
if (segments.size() != 3) {
|
||||||
|
throw new FileNotFoundException();
|
||||||
|
}
|
||||||
|
String appId = segments.get(APP_ID_PATH_INDEX);
|
||||||
|
String uuid = segments.get(COMPUTER_UUID_PATH_INDEX);
|
||||||
|
File file = mDiskAssetLoader.getFile(uuid, Integer.parseInt(appId));
|
||||||
|
if (file.exists()) {
|
||||||
|
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||||
|
}
|
||||||
|
throw new FileNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||||
|
throw new UnsupportedOperationException("This provider is only for read mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getType(Uri uri) {
|
||||||
|
return PNG_MIME_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri insert(Uri uri, ContentValues values) {
|
||||||
|
throw new UnsupportedOperationException("This provider is only for read mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreate() {
|
||||||
|
mDiskAssetLoader = new DiskAssetLoader(getContext());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cursor query(Uri uri, String[] projection, String selection,
|
||||||
|
String[] selectionArgs, String sortOrder) {
|
||||||
|
throw new UnsupportedOperationException("This provider doesn't support query");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int update(Uri uri, ContentValues values, String selection,
|
||||||
|
String[] selectionArgs) {
|
||||||
|
throw new UnsupportedOperationException("This provider is support read only");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Uri createBoxArtUri(String uuid, String appId) {
|
||||||
|
return new Uri.Builder()
|
||||||
|
.scheme(ContentResolver.SCHEME_CONTENT)
|
||||||
|
.authority(AUTHORITY)
|
||||||
|
.appendPath(BOXART_PATH)
|
||||||
|
.appendPath(uuid)
|
||||||
|
.appendPath(appId)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -65,7 +65,7 @@ public class DiskAssetLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||||
File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
File file = getFile(tuple.computer.uuid, tuple.app.getAppId());
|
||||||
|
|
||||||
// Don't bother with anything if it doesn't exist
|
// Don't bother with anything if it doesn't exist
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
@ -133,6 +133,10 @@ public class DiskAssetLoader {
|
|||||||
return bmp;
|
return bmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public File getFile(String computerUuid, int appId) {
|
||||||
|
return CacheHelper.openPath(false, cacheDir, "boxart", computerUuid, appId + ".png");
|
||||||
|
}
|
||||||
|
|
||||||
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
|
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
|
||||||
OutputStream out = null;
|
OutputStream out = null;
|
||||||
boolean success = false;
|
boolean success = false;
|
||||||
|
307
app/src/main/java/com/limelight/utils/TvChannelHelper.java
Normal file
307
app/src/main/java/com/limelight/utils/TvChannelHelper.java
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.UiModeManager;
|
||||||
|
import android.content.ContentUris;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteException;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.media.tv.TvContract;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import com.limelight.AppView;
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.PosterContentProvider;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.ShortcutTrampoline;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.O)
|
||||||
|
public class TvChannelHelper {
|
||||||
|
|
||||||
|
private static final int ASPECT_RATIO_MOVIE_POSTER = 5;
|
||||||
|
private static final int TYPE_GAME = 12;
|
||||||
|
public static final String[] CHANNEL_PROJECTION = {TvContract.Channels._ID, TvContract.Channels.COLUMN_INTERNAL_PROVIDER_ID};
|
||||||
|
public static final int INTERNAL_PROVIDER_ID_INDEX = 1;
|
||||||
|
public static final int ID_INDEX = 0;
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
public TvChannelHelper(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean createTvChannel(String computerUuid, String computerName) {
|
||||||
|
if (!isSupport()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent i = new Intent(context, ShortcutTrampoline.class);
|
||||||
|
i.putExtra(AppView.NAME_EXTRA, computerName);
|
||||||
|
i.putExtra(AppView.UUID_EXTRA, computerUuid);
|
||||||
|
i.setAction(Intent.ACTION_DEFAULT);
|
||||||
|
ChannelBuilder builder = new ChannelBuilder()
|
||||||
|
.setType(TvContract.Channels.TYPE_PREVIEW)
|
||||||
|
.setDisplayName(computerName)
|
||||||
|
.setInternalProviderId(computerUuid)
|
||||||
|
.setAppLinkIntent(i);
|
||||||
|
|
||||||
|
Long channelId = getChannelId(computerUuid);
|
||||||
|
|
||||||
|
|
||||||
|
if (channelId != null) {
|
||||||
|
context.getContentResolver().update(TvContract.buildChannelUri(channelId),
|
||||||
|
builder.toContentValues(), null, null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Uri channelUri = context.getContentResolver().insert(
|
||||||
|
TvContract.Channels.CONTENT_URI, builder.toContentValues());
|
||||||
|
|
||||||
|
|
||||||
|
if (channelUri != null) {
|
||||||
|
long id = ContentUris.parseId(channelUri);
|
||||||
|
updateChannelIcon(id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateChannelIcon(long id) {
|
||||||
|
Bitmap bitmap = drawableToBitmap(context.getResources().getDrawable(R.drawable.ic_channel));
|
||||||
|
try {
|
||||||
|
storeChannelLogo(id, bitmap);
|
||||||
|
} finally {
|
||||||
|
bitmap.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the given channel logo {@link Bitmap} in the system content provider and associate
|
||||||
|
* it with the given channel ID.
|
||||||
|
*
|
||||||
|
* @param channelId the ID of the target channel with which the given logo should be associated
|
||||||
|
* @param logo the logo image to be stored
|
||||||
|
* @return {@code true} if successfully stored the logo in the system content provider,
|
||||||
|
* otherwise {@code false}.
|
||||||
|
*/
|
||||||
|
private boolean storeChannelLogo(long channelId,
|
||||||
|
Bitmap logo) {
|
||||||
|
if (!isSupport()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean result = false;
|
||||||
|
Uri localUri = TvContract.buildChannelLogoUri(channelId);
|
||||||
|
try (OutputStream outputStream = context.getContentResolver().openOutputStream(localUri)) {
|
||||||
|
result = logo.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
|
||||||
|
outputStream.flush();
|
||||||
|
} catch (SQLiteException | IOException e) {
|
||||||
|
LimeLog.warning("Failed to store the logo to the system content provider.");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bitmap drawableToBitmap(Drawable drawable) {
|
||||||
|
int width = context.getResources().getDimensionPixelSize(R.dimen.tv_channel_logo_width);
|
||||||
|
int height = context.getResources().getDimensionPixelSize(R.dimen.tv_channel_logo_width);
|
||||||
|
|
||||||
|
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||||
|
Canvas canvas = new Canvas(bitmap);
|
||||||
|
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||||
|
drawable.draw(canvas);
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean addGameToChannel(String computerUuid, String computerName, String appId, String appName) {
|
||||||
|
if (!isSupport()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PreviewProgramBuilder builder = new PreviewProgramBuilder();
|
||||||
|
Intent i = new Intent(context, ShortcutTrampoline.class);
|
||||||
|
|
||||||
|
i.putExtra(AppView.NAME_EXTRA, computerName);
|
||||||
|
i.putExtra(AppView.UUID_EXTRA, computerUuid);
|
||||||
|
i.putExtra(ShortcutTrampoline.APP_ID_EXTRA, appId);
|
||||||
|
i.setAction(Intent.ACTION_DEFAULT);
|
||||||
|
|
||||||
|
Uri resourceURI = PosterContentProvider.createBoxArtUri(computerUuid, appId);
|
||||||
|
|
||||||
|
Long channelId = getChannelId(computerUuid);
|
||||||
|
|
||||||
|
|
||||||
|
if (channelId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
builder.setChannelId(channelId)
|
||||||
|
.setType(TYPE_GAME)
|
||||||
|
.setTitle(appName)
|
||||||
|
.setPosterArtAspectRatio(ASPECT_RATIO_MOVIE_POSTER)
|
||||||
|
.setPosterArtUri(resourceURI)
|
||||||
|
.setIntent(i)
|
||||||
|
.setInternalProviderId(appId);
|
||||||
|
Uri programUri = context.getContentResolver().insert(TvContract.PreviewPrograms.CONTENT_URI,
|
||||||
|
builder.toContentValues());
|
||||||
|
|
||||||
|
|
||||||
|
TvContract.requestChannelBrowsable(context, channelId);
|
||||||
|
return programUri != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean deleteChannel(String computerUuid) {
|
||||||
|
if (!isSupport()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Long channelId = getChannelId(computerUuid);
|
||||||
|
if (channelId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Uri uri = TvContract.buildChannelUri(channelId);
|
||||||
|
return context.getContentResolver().delete(uri, null, null) > 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
private Long getChannelId(String computerUuid) {
|
||||||
|
Uri uri = TvContract.Channels.CONTENT_URI;
|
||||||
|
Cursor cursor = context.getContentResolver().query(uri,
|
||||||
|
CHANNEL_PROJECTION,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
try {
|
||||||
|
if (cursor == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
String internalProviderId = cursor.getString(INTERNAL_PROVIDER_ID_INDEX);
|
||||||
|
if (computerUuid.equals(internalProviderId)) {
|
||||||
|
return cursor.getLong(ID_INDEX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (cursor != null) {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void addGameToChannel(ComputerDetails computer, NvApp app) {
|
||||||
|
addGameToChannel(computer.uuid, computer.name, String.valueOf(app.getAppId()), app.getAppName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> String toValueString(T value) {
|
||||||
|
return value == null ? null : value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toUriString(Intent intent) {
|
||||||
|
return intent == null ? null : intent.toUri(Intent.URI_INTENT_SCHEME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSupport() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
|
||||||
|
return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PreviewProgramBuilder {
|
||||||
|
|
||||||
|
private ContentValues mValues = new ContentValues();
|
||||||
|
|
||||||
|
|
||||||
|
public PreviewProgramBuilder setChannelId(Long channelId) {
|
||||||
|
mValues.put(TvContract.PreviewPrograms.COLUMN_CHANNEL_ID, channelId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreviewProgramBuilder setType(int type) {
|
||||||
|
mValues.put(TvContract.PreviewPrograms.COLUMN_TYPE, type);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreviewProgramBuilder setTitle(String title) {
|
||||||
|
mValues.put(TvContract.PreviewPrograms.COLUMN_TITLE, title);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreviewProgramBuilder setPosterArtAspectRatio(int aspectRatio) {
|
||||||
|
mValues.put(TvContract.PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO, aspectRatio);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreviewProgramBuilder setIntent(Intent intent) {
|
||||||
|
mValues.put(TvContract.PreviewPrograms.COLUMN_INTENT_URI, toUriString(intent));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreviewProgramBuilder setIntentUri(Uri uri) {
|
||||||
|
mValues.put(TvContract.PreviewPrograms.COLUMN_INTENT_URI, toValueString(uri));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreviewProgramBuilder setInternalProviderId(String id) {
|
||||||
|
mValues.put(TvContract.PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID, id);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreviewProgramBuilder setPosterArtUri(Uri uri) {
|
||||||
|
mValues.put(TvContract.PreviewPrograms.COLUMN_POSTER_ART_URI, toValueString(uri));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContentValues toContentValues() {
|
||||||
|
return new ContentValues(mValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ChannelBuilder {
|
||||||
|
|
||||||
|
private ContentValues mValues = new ContentValues();
|
||||||
|
|
||||||
|
public ChannelBuilder setType(String type) {
|
||||||
|
mValues.put(TvContract.Channels.COLUMN_TYPE, type);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelBuilder setDisplayName(String displayName) {
|
||||||
|
mValues.put(TvContract.Channels.COLUMN_DISPLAY_NAME, displayName);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelBuilder setInternalProviderId(String internalProviderId) {
|
||||||
|
mValues.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_ID, internalProviderId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelBuilder setAppLinkIntent(Intent intent) {
|
||||||
|
mValues.put(TvContract.Channels.COLUMN_APP_LINK_INTENT_URI, toUriString(intent));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContentValues toContentValues() {
|
||||||
|
return new ContentValues(mValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
10
app/src/main/res/drawable/ic_channel.xml
Normal file
10
app/src/main/res/drawable/ic_channel.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<item android:drawable="@drawable/ic_lime_layer"
|
||||||
|
android:bottom="10dp"
|
||||||
|
android:left="10dp"
|
||||||
|
android:right="10dp"
|
||||||
|
android:top="10dp"
|
||||||
|
/>
|
||||||
|
</layer-list>
|
19
app/src/main/res/drawable/ic_lime_layer.xml
Normal file
19
app/src/main/res/drawable/ic_lime_layer.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<vector android:height="24dp" android:viewportHeight="546.15576"
|
||||||
|
android:viewportWidth="546.08374" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#ffffff"
|
||||||
|
android:pathData="M252.584,0.356c-58.2,4.8 -112.3,27.1 -156.8,64.6l-8.4,7 86.9,86.9c47.7,47.7 87.1,86.8 87.6,86.8 0.4,-0 0.6,-55.2 0.5,-122.8l-0.3,-122.7 -2.5,-0.1c-1.4,-0 -4.5,0.1 -7,0.3z" android:strokeColor="#00000000"/>
|
||||||
|
<path android:fillColor="#ffffff"
|
||||||
|
android:pathData="M284.284,0.356c-1,0.9 -0.9,245.3 0.1,245.3 0.4,-0 39.8,-39.1 87.5,-86.8l86.9,-86.9 -8.4,-7c-34.4,-29 -74.9,-49.2 -117.8,-58.7 -16.6,-3.6 -46.9,-7.4 -48.3,-5.9z" android:strokeColor="#00000000"/>
|
||||||
|
<path android:fillColor="#ffffff"
|
||||||
|
android:pathData="M64.884,95.856c-24.1,28.5 -41.2,59.5 -52.6,95.3 -6.3,19.6 -11.1,45.8 -11.9,64l-0.3,7 122.8,0.3c67.5,0.1 122.7,-0.1 122.7,-0.5 0,-0.5 -39.1,-39.9 -86.8,-87.6l-86.9,-86.9 -7,8.4z" android:strokeColor="#00000000"/>
|
||||||
|
<path android:fillColor="#ffffff"
|
||||||
|
android:pathData="M387.384,174.356c-47.8,47.7 -86.8,87.1 -86.8,87.6 0,0.4 55.2,0.6 122.8,0.5l122.7,-0.3 -0.3,-7c-1.4,-32.6 -12.4,-72.9 -28.7,-105.5 -9.1,-18.2 -25.9,-43 -38.7,-57.3l-4.3,-4.8 -86.7,86.8z" android:strokeColor="#00000000"/>
|
||||||
|
<path android:fillColor="#ffffff"
|
||||||
|
android:pathData="M0.184,284.556c-0.7,1.1 1,19.8 3,32.1 7.6,48.6 29.1,95.2 61.7,133.8l7,8.4 86.9,-86.9c47.7,-47.7 86.8,-87.1 86.8,-87.5 0,-1.1 -244.8,-1 -245.4,0.1z" android:strokeColor="#00000000"/>
|
||||||
|
<path android:fillColor="#ffffff"
|
||||||
|
android:pathData="M300.584,284.356c0,0.5 39.1,39.9 86.8,87.6l86.9,86.9 7,-8.4c24.1,-28.5 41.2,-59.5 52.6,-95.3 6.3,-19.6 11.1,-45.8 11.9,-64l0.3,-7 -122.7,-0.3c-67.6,-0.1 -122.8,0.1 -122.8,0.5z" android:strokeColor="#00000000"/>
|
||||||
|
<path android:fillColor="#ffffff"
|
||||||
|
android:pathData="M174.284,387.456l-86.9,86.9 8.4,7c28.5,24.1 59.5,41.2 95.3,52.6 19.6,6.3 45.8,11.1 64,11.9l7,0.3 0.3,-122.8c0.1,-67.5 -0.1,-122.7 -0.5,-122.7 -0.5,-0 -39.9,39.1 -87.6,86.8z" android:strokeColor="#00000000"/>
|
||||||
|
<path android:fillColor="#ffffff"
|
||||||
|
android:pathData="M283.784,423.356l0.3,122.8 7,-0.3c18.2,-0.8 44.4,-5.6 64,-11.9 35.8,-11.4 66.8,-28.5 95.3,-52.6l8.4,-7 -86.9,-86.9c-47.7,-47.7 -87.1,-86.8 -87.6,-86.8 -0.4,-0 -0.6,55.2 -0.5,122.7z" android:strokeColor="#00000000"/>
|
||||||
|
</vector>
|
@ -66,6 +66,7 @@
|
|||||||
<string name="applist_menu_quit">Выйти из сессии</string>
|
<string name="applist_menu_quit">Выйти из сессии</string>
|
||||||
<string name="applist_menu_quit_and_start">Выйти из текущей игры и запустить</string>
|
<string name="applist_menu_quit_and_start">Выйти из текущей игры и запустить</string>
|
||||||
<string name="applist_menu_cancel">Отмена</string>
|
<string name="applist_menu_cancel">Отмена</string>
|
||||||
|
<string name="applist_menu_tv_channel">Добавать на канал</string>
|
||||||
<string name="applist_refresh_title">Список приложений</string>
|
<string name="applist_refresh_title">Список приложений</string>
|
||||||
<string name="applist_refresh_msg">Обновление приложений…</string>
|
<string name="applist_refresh_msg">Обновление приложений…</string>
|
||||||
<string name="applist_refresh_error_title">Ошибка</string>
|
<string name="applist_refresh_error_title">Ошибка</string>
|
||||||
|
@ -3,5 +3,7 @@
|
|||||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||||
|
<dimen name="tv_channel_logo_width">80dp</dimen>
|
||||||
|
<dimen name="tv_channel_logo_height">80dp</dimen>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -96,6 +96,7 @@
|
|||||||
<string name="applist_menu_cancel">Cancel</string>
|
<string name="applist_menu_cancel">Cancel</string>
|
||||||
<string name="applist_menu_details">View Details</string>
|
<string name="applist_menu_details">View Details</string>
|
||||||
<string name="applist_menu_scut">Create Shortcut</string>
|
<string name="applist_menu_scut">Create Shortcut</string>
|
||||||
|
<string name="applist_menu_tv_channel">Add to Channel</string>
|
||||||
<string name="applist_refresh_title">App List</string>
|
<string name="applist_refresh_title">App List</string>
|
||||||
<string name="applist_refresh_msg">Refreshing apps…</string>
|
<string name="applist_refresh_msg">Refreshing apps…</string>
|
||||||
<string name="applist_refresh_error_title">Error</string>
|
<string name="applist_refresh_error_title">Error</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user