mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-01-10 08:09:26 +01:00
Merge pull request #7234 from zackhow/master
Android: Support for AndroidTV Oreo Homescreen channels
This commit is contained in:
commit
be9437450e
@ -86,6 +86,7 @@ dependencies {
|
|||||||
|
|
||||||
// Android TV UI libraries.
|
// Android TV UI libraries.
|
||||||
implementation "com.android.support:leanback-v17:$androidSupportVersion"
|
implementation "com.android.support:leanback-v17:$androidSupportVersion"
|
||||||
|
implementation "com.android.support:support-tv-provider:$androidSupportVersion"
|
||||||
|
|
||||||
// For showing the banner as a circle a-la Material Design Guidelines
|
// For showing the banner as a circle a-la Material Design Guidelines
|
||||||
implementation 'de.hdodenhof:circleimageview:2.1.0'
|
implementation 'de.hdodenhof:circleimageview:2.1.0'
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
|
||||||
|
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".DolphinApplication"
|
android:name=".DolphinApplication"
|
||||||
@ -69,9 +71,27 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity android:name=".activities.AppLinkActivity" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data
|
||||||
|
android:host="@string/host"
|
||||||
|
android:scheme="@string/scheme" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
|
||||||
<service android:name=".services.DirectoryInitializationService"/>
|
<service android:name=".services.DirectoryInitializationService"/>
|
||||||
<service android:name=".services.GameFileCacheService"/>
|
<service android:name=".services.GameFileCacheService"/>
|
||||||
|
<service
|
||||||
|
android:name=".services.SyncChannelJobService"
|
||||||
|
android:exported="false"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
|
<service
|
||||||
|
android:name=".services.SyncProgramsJobService"
|
||||||
|
android:exported="false"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="android.support.v4.content.FileProvider"
|
android:name="android.support.v4.content.FileProvider"
|
||||||
|
@ -0,0 +1,134 @@
|
|||||||
|
package org.dolphinemu.dolphinemu.activities;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v4.app.FragmentActivity;
|
||||||
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.dolphinemu.dolphinemu.R;
|
||||||
|
import org.dolphinemu.dolphinemu.model.GameFile;
|
||||||
|
import org.dolphinemu.dolphinemu.services.DirectoryInitializationService;
|
||||||
|
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
|
||||||
|
import org.dolphinemu.dolphinemu.ui.main.TvMainActivity;
|
||||||
|
import org.dolphinemu.dolphinemu.utils.AppLinkHelper;
|
||||||
|
import org.dolphinemu.dolphinemu.utils.DirectoryStateReceiver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linker between leanback homescreen and app
|
||||||
|
*/
|
||||||
|
public class AppLinkActivity extends FragmentActivity
|
||||||
|
{
|
||||||
|
private static final String TAG = "AppLinkActivity";
|
||||||
|
|
||||||
|
private AppLinkHelper.PlayAction playAction;
|
||||||
|
private DirectoryStateReceiver directoryStateReceiver;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState)
|
||||||
|
{
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
Intent intent = getIntent();
|
||||||
|
Uri uri = intent.getData();
|
||||||
|
|
||||||
|
Log.v(TAG, uri.toString());
|
||||||
|
|
||||||
|
if (uri.getPathSegments().isEmpty())
|
||||||
|
{
|
||||||
|
Log.e(TAG, "Invalid uri " + uri);
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLinkHelper.AppLinkAction action = AppLinkHelper.extractAction(uri);
|
||||||
|
switch (action.getAction())
|
||||||
|
{
|
||||||
|
case AppLinkHelper.PLAY:
|
||||||
|
playAction = (AppLinkHelper.PlayAction) action;
|
||||||
|
initResources();
|
||||||
|
break;
|
||||||
|
case AppLinkHelper.BROWSE:
|
||||||
|
browse();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid Action " + action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Need to init these since they usually occur in the main activity.
|
||||||
|
*/
|
||||||
|
private void initResources()
|
||||||
|
{
|
||||||
|
IntentFilter statusIntentFilter = new IntentFilter(
|
||||||
|
DirectoryInitializationService.BROADCAST_ACTION);
|
||||||
|
|
||||||
|
directoryStateReceiver =
|
||||||
|
new DirectoryStateReceiver(directoryInitializationState ->
|
||||||
|
{
|
||||||
|
if (directoryInitializationState == DirectoryInitializationService.DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED)
|
||||||
|
{
|
||||||
|
play(playAction);
|
||||||
|
}
|
||||||
|
else if (directoryInitializationState == DirectoryInitializationService.DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED)
|
||||||
|
{
|
||||||
|
Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
else if (directoryInitializationState == DirectoryInitializationService.DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE)
|
||||||
|
{
|
||||||
|
Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Registers the DirectoryStateReceiver and its intent filters
|
||||||
|
LocalBroadcastManager.getInstance(this).registerReceiver(
|
||||||
|
directoryStateReceiver,
|
||||||
|
statusIntentFilter);
|
||||||
|
DirectoryInitializationService.startService(this);
|
||||||
|
GameFileCacheService.startLoad(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action if channel icon is selected
|
||||||
|
*/
|
||||||
|
private void browse()
|
||||||
|
{
|
||||||
|
Intent openApp = new Intent(this, TvMainActivity.class);
|
||||||
|
startActivity(openApp);
|
||||||
|
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action if program(game) is selected
|
||||||
|
*/
|
||||||
|
private void play(AppLinkHelper.PlayAction action)
|
||||||
|
{
|
||||||
|
Log.d(TAG, "Playing game "
|
||||||
|
+ action.getGameId()
|
||||||
|
+ " from channel "
|
||||||
|
+ action.getChannelId());
|
||||||
|
|
||||||
|
GameFile game = GameFileCacheService.getGameFileByGameId(action.getGameId());
|
||||||
|
if (game == null)
|
||||||
|
Log.e(TAG, "Invalid Game: " + action.getGameId());
|
||||||
|
else
|
||||||
|
startGame(game);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startGame(GameFile game)
|
||||||
|
{
|
||||||
|
if (directoryStateReceiver != null)
|
||||||
|
{
|
||||||
|
LocalBroadcastManager.getInstance(this).unregisterReceiver(directoryStateReceiver);
|
||||||
|
directoryStateReceiver = null;
|
||||||
|
}
|
||||||
|
EmulationActivity.launch(this, game, -1, null);
|
||||||
|
}
|
||||||
|
}
|
@ -160,15 +160,21 @@ public final class EmulationActivity extends AppCompatActivity
|
|||||||
launcher.putExtra(EXTRA_PLATFORM, gameFile.getPlatform());
|
launcher.putExtra(EXTRA_PLATFORM, gameFile.getPlatform());
|
||||||
launcher.putExtra(EXTRA_SCREEN_PATH, gameFile.getScreenshotPath());
|
launcher.putExtra(EXTRA_SCREEN_PATH, gameFile.getScreenshotPath());
|
||||||
launcher.putExtra(EXTRA_GRID_POSITION, position);
|
launcher.putExtra(EXTRA_GRID_POSITION, position);
|
||||||
|
Bundle options = new Bundle();
|
||||||
|
|
||||||
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
// Will be null if launched from homescreen
|
||||||
|
if (sharedView != null)
|
||||||
|
{
|
||||||
|
ActivityOptionsCompat transition = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||||
activity,
|
activity,
|
||||||
sharedView,
|
sharedView,
|
||||||
"image_game_screenshot");
|
"image_game_screenshot");
|
||||||
|
options = transition.toBundle();
|
||||||
|
}
|
||||||
|
|
||||||
// I believe this warning is a bug. Activities are FragmentActivity from the support lib
|
// I believe this warning is a bug. Activities are FragmentActivity from the support lib
|
||||||
//noinspection RestrictedApi
|
//noinspection RestrictedApi
|
||||||
activity.startActivityForResult(launcher, MainPresenter.REQUEST_EMULATE_GAME, options.toBundle());
|
activity.startActivityForResult(launcher, MainPresenter.REQUEST_EMULATE_GAME, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
package org.dolphinemu.dolphinemu.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a home screen channel for Android TV api 26+
|
||||||
|
*/
|
||||||
|
public class HomeScreenChannel
|
||||||
|
{
|
||||||
|
|
||||||
|
private long channelId;
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private String appLinkIntentUri;
|
||||||
|
|
||||||
|
public HomeScreenChannel()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public HomeScreenChannel(String name, String description, String appLinkIntentUri)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
this.description = description;
|
||||||
|
this.appLinkIntentUri = appLinkIntentUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getChannelId()
|
||||||
|
{
|
||||||
|
return channelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChannelId(long channelId)
|
||||||
|
{
|
||||||
|
this.channelId = channelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription()
|
||||||
|
{
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description)
|
||||||
|
{
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAppLinkIntentUri()
|
||||||
|
{
|
||||||
|
return appLinkIntentUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAppLinkIntentUri(String appLinkIntentUri)
|
||||||
|
{
|
||||||
|
this.appLinkIntentUri = appLinkIntentUri;
|
||||||
|
}
|
||||||
|
}
|
@ -50,6 +50,19 @@ public final class GameFileCacheService extends IntentService
|
|||||||
return platformGames;
|
return platformGames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static GameFile getGameFileByGameId(String gameId)
|
||||||
|
{
|
||||||
|
GameFile[] allGames = gameFiles.get();
|
||||||
|
for (GameFile game : allGames)
|
||||||
|
{
|
||||||
|
if (game.getGameId().equals(gameId))
|
||||||
|
{
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static void startService(Context context, String action)
|
private static void startService(Context context, String action)
|
||||||
{
|
{
|
||||||
Intent intent = new Intent(context, GameFileCacheService.class);
|
Intent intent = new Intent(context, GameFileCacheService.class);
|
||||||
|
@ -0,0 +1,166 @@
|
|||||||
|
package org.dolphinemu.dolphinemu.services;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.job.JobParameters;
|
||||||
|
import android.app.job.JobService;
|
||||||
|
import android.content.ContentUris;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.media.tv.TvContract;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.support.media.tv.Channel;
|
||||||
|
import android.support.media.tv.ChannelLogoUtils;
|
||||||
|
import android.support.media.tv.TvContractCompat;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.dolphinemu.dolphinemu.R;
|
||||||
|
import org.dolphinemu.dolphinemu.model.HomeScreenChannel;
|
||||||
|
import org.dolphinemu.dolphinemu.utils.TvUtil;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SyncChannelJobService extends JobService
|
||||||
|
{
|
||||||
|
private static final String TAG = "ChannelJobSvc";
|
||||||
|
|
||||||
|
private SyncChannelTask mSyncChannelTask;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onStartJob(final JobParameters jobParameters)
|
||||||
|
{
|
||||||
|
Log.d(TAG, "Starting channel creation job");
|
||||||
|
mSyncChannelTask =
|
||||||
|
new SyncChannelTask(getApplicationContext())
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean success)
|
||||||
|
{
|
||||||
|
super.onPostExecute(success);
|
||||||
|
jobFinished(jobParameters, !success);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mSyncChannelTask.execute();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onStopJob(JobParameters jobParameters)
|
||||||
|
{
|
||||||
|
if (mSyncChannelTask != null)
|
||||||
|
{
|
||||||
|
mSyncChannelTask.cancel(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SyncChannelTask extends AsyncTask<Void, Void, Boolean>
|
||||||
|
{
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
SyncChannelTask(Context context)
|
||||||
|
{
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup channels
|
||||||
|
*/
|
||||||
|
@TargetApi(Build.VERSION_CODES.O)
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... voids)
|
||||||
|
{
|
||||||
|
List<HomeScreenChannel> subscriptions;
|
||||||
|
List<Channel> channels = TvUtil.getAllChannels(context);
|
||||||
|
List<Long> channelIds = new ArrayList<>();
|
||||||
|
// Checks if the default channels are added.
|
||||||
|
// If not, create the channels
|
||||||
|
if (!channels.isEmpty())
|
||||||
|
{
|
||||||
|
channels.forEach(channel -> channelIds.add(channel.getId()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
subscriptions = TvUtil.createUniversalSubscriptions();
|
||||||
|
for (HomeScreenChannel subscription : subscriptions)
|
||||||
|
{
|
||||||
|
long channelId = createChannel(subscription);
|
||||||
|
channelIds.add(channelId);
|
||||||
|
subscription.setChannelId(channelId);
|
||||||
|
// Only the first channel added can be browsable without user intervention.
|
||||||
|
TvContractCompat.requestChannelBrowsable(context, channelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Schedule triggers to update programs
|
||||||
|
channelIds.forEach(channel -> TvUtil.scheduleSyncingProgramsForChannel(context, channel));
|
||||||
|
// Update all channels
|
||||||
|
TvUtil.updateAllChannels(context);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long createChannel(HomeScreenChannel subscription)
|
||||||
|
{
|
||||||
|
long channelId = getChannelIdFromTvProvider(context, subscription);
|
||||||
|
if (channelId != -1L)
|
||||||
|
{
|
||||||
|
return channelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the channel since it has not been added to the TV Provider.
|
||||||
|
Uri appLinkIntentUri = Uri.parse(subscription.getAppLinkIntentUri());
|
||||||
|
|
||||||
|
Channel.Builder builder = new Channel.Builder();
|
||||||
|
builder.setType(TvContractCompat.Channels.TYPE_PREVIEW)
|
||||||
|
.setDisplayName(subscription.getName())
|
||||||
|
.setDescription(subscription.getDescription())
|
||||||
|
.setAppLinkIntentUri(appLinkIntentUri);
|
||||||
|
|
||||||
|
Log.d(TAG, "Creating channel: " + subscription.getName());
|
||||||
|
Uri channelUrl =
|
||||||
|
context.getContentResolver()
|
||||||
|
.insert(
|
||||||
|
TvContractCompat.Channels.CONTENT_URI,
|
||||||
|
builder.build().toContentValues());
|
||||||
|
|
||||||
|
channelId = ContentUris.parseId(channelUrl);
|
||||||
|
Bitmap bitmap = TvUtil.convertToBitmap(context, R.drawable.ic_launcher);
|
||||||
|
ChannelLogoUtils.storeChannelLogo(context, channelId, bitmap);
|
||||||
|
|
||||||
|
return channelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getChannelIdFromTvProvider(Context context, HomeScreenChannel subscription)
|
||||||
|
{
|
||||||
|
Cursor cursor =
|
||||||
|
context.getContentResolver().query(
|
||||||
|
TvContractCompat.Channels.CONTENT_URI,
|
||||||
|
new String[]{
|
||||||
|
TvContractCompat.Channels._ID,
|
||||||
|
TvContract.Channels.COLUMN_DISPLAY_NAME
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
if (cursor != null && cursor.moveToFirst())
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Channel channel = Channel.fromCursor(cursor);
|
||||||
|
if (subscription.getName().equals(channel.getDisplayName()))
|
||||||
|
{
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Channel already exists. Returning channel "
|
||||||
|
+ channel.getId()
|
||||||
|
+ " from TV Provider.");
|
||||||
|
return channel.getId();
|
||||||
|
}
|
||||||
|
} while (cursor.moveToNext());
|
||||||
|
}
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,161 @@
|
|||||||
|
package org.dolphinemu.dolphinemu.services;
|
||||||
|
|
||||||
|
import android.app.job.JobParameters;
|
||||||
|
import android.app.job.JobService;
|
||||||
|
import android.content.ContentUris;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.PersistableBundle;
|
||||||
|
import android.support.media.tv.Channel;
|
||||||
|
import android.support.media.tv.PreviewProgram;
|
||||||
|
import android.support.media.tv.TvContractCompat;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.dolphinemu.dolphinemu.R;
|
||||||
|
import org.dolphinemu.dolphinemu.model.GameFile;
|
||||||
|
import org.dolphinemu.dolphinemu.ui.platform.Platform;
|
||||||
|
import org.dolphinemu.dolphinemu.utils.AppLinkHelper;
|
||||||
|
import org.dolphinemu.dolphinemu.utils.TvUtil;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SyncProgramsJobService extends JobService
|
||||||
|
{
|
||||||
|
private static final String TAG = "SyncProgramsJobService";
|
||||||
|
|
||||||
|
private SyncProgramsTask mSyncProgramsTask;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onStartJob(final JobParameters jobParameters)
|
||||||
|
{
|
||||||
|
Log.d(TAG, "onStartJob(): " + jobParameters);
|
||||||
|
final long channelId = getChannelId(jobParameters);
|
||||||
|
if (channelId == -1L)
|
||||||
|
{
|
||||||
|
Log.d(TAG, "Failed to find channel");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mSyncProgramsTask =
|
||||||
|
new SyncProgramsTask(getApplicationContext())
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean finished)
|
||||||
|
{
|
||||||
|
super.onPostExecute(finished);
|
||||||
|
mSyncProgramsTask = null;
|
||||||
|
jobFinished(jobParameters, !finished);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mSyncProgramsTask.execute(channelId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onStopJob(JobParameters jobParameters)
|
||||||
|
{
|
||||||
|
if (mSyncProgramsTask != null)
|
||||||
|
{
|
||||||
|
mSyncProgramsTask.cancel(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getChannelId(JobParameters jobParameters)
|
||||||
|
{
|
||||||
|
PersistableBundle extras = jobParameters.getExtras();
|
||||||
|
return extras.getLong(TvContractCompat.EXTRA_CHANNEL_ID, -1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SyncProgramsTask extends AsyncTask<Long, Void, Boolean>
|
||||||
|
{
|
||||||
|
private Context context;
|
||||||
|
private List<GameFile> updatePrograms;
|
||||||
|
|
||||||
|
private SyncProgramsTask(Context context)
|
||||||
|
{
|
||||||
|
this.context = context;
|
||||||
|
updatePrograms = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines which channel to update, get the game files for the channel,
|
||||||
|
* then updates the list
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Long... channelIds)
|
||||||
|
{
|
||||||
|
List<Long> params = Arrays.asList(channelIds);
|
||||||
|
if (!params.isEmpty())
|
||||||
|
{
|
||||||
|
for (Long channelId : params)
|
||||||
|
{
|
||||||
|
Channel channel = TvUtil.getChannelById(context, channelId);
|
||||||
|
for (Platform platform : Platform.values())
|
||||||
|
{
|
||||||
|
if (channel != null && channel.getDisplayName().equals(platform.getHeaderName()))
|
||||||
|
{
|
||||||
|
getGamesByPlatform(platform);
|
||||||
|
syncPrograms(channelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getGamesByPlatform(Platform platform)
|
||||||
|
{
|
||||||
|
updatePrograms = GameFileCacheService.getGameFilesForPlatform(platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncPrograms(long channelId)
|
||||||
|
{
|
||||||
|
Log.d(TAG, "Sync programs for channel: " + channelId);
|
||||||
|
deletePrograms(channelId);
|
||||||
|
createPrograms(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createPrograms(long channelId)
|
||||||
|
{
|
||||||
|
for (GameFile game : updatePrograms)
|
||||||
|
{
|
||||||
|
PreviewProgram previewProgram = buildProgram(channelId, game);
|
||||||
|
|
||||||
|
context.getContentResolver()
|
||||||
|
.insert(
|
||||||
|
TvContractCompat.PreviewPrograms.CONTENT_URI,
|
||||||
|
previewProgram.toContentValues());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deletePrograms(long channelId)
|
||||||
|
{
|
||||||
|
context.getContentResolver().delete(
|
||||||
|
TvContractCompat.buildPreviewProgramsUriForChannel(channelId),
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PreviewProgram buildProgram(long channelId, GameFile game)
|
||||||
|
{
|
||||||
|
Uri appLinkUri = AppLinkHelper.buildGameUri(channelId, game.getGameId());
|
||||||
|
Uri banner = TvUtil.buildBanner(game, context);
|
||||||
|
if (banner == null)
|
||||||
|
banner = TvUtil.getUriToResource(context, R.drawable.banner_tv);
|
||||||
|
|
||||||
|
PreviewProgram.Builder builder = new PreviewProgram.Builder();
|
||||||
|
builder.setChannelId(channelId)
|
||||||
|
.setType(TvContractCompat.PreviewProgramColumns.TYPE_GAME)
|
||||||
|
.setTitle(game.getTitle())
|
||||||
|
.setDescription(game.getDescription())
|
||||||
|
.setPosterArtUri(banner)
|
||||||
|
.setIntentUri(appLinkUri);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu.ui.main;
|
|||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v17.leanback.app.BrowseFragment;
|
import android.support.v17.leanback.app.BrowseFragment;
|
||||||
import android.support.v17.leanback.app.BrowseSupportFragment;
|
import android.support.v17.leanback.app.BrowseSupportFragment;
|
||||||
@ -28,6 +29,7 @@ import org.dolphinemu.dolphinemu.ui.settings.SettingsActivity;
|
|||||||
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
|
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
|
||||||
import org.dolphinemu.dolphinemu.utils.PermissionsHandler;
|
import org.dolphinemu.dolphinemu.utils.PermissionsHandler;
|
||||||
import org.dolphinemu.dolphinemu.utils.StartupHandler;
|
import org.dolphinemu.dolphinemu.utils.StartupHandler;
|
||||||
|
import org.dolphinemu.dolphinemu.utils.TvUtil;
|
||||||
import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder;
|
import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@ -53,6 +55,8 @@ public final class TvMainActivity extends FragmentActivity implements MainView
|
|||||||
// Stuff in this block only happens when this activity is newly created (i.e. not a rotation)
|
// Stuff in this block only happens when this activity is newly created (i.e. not a rotation)
|
||||||
if (savedInstanceState == null)
|
if (savedInstanceState == null)
|
||||||
StartupHandler.HandleInit(this);
|
StartupHandler.HandleInit(this);
|
||||||
|
// Setup and/or sync channels
|
||||||
|
TvUtil.scheduleSyncingChannel(getApplicationContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -134,6 +138,9 @@ public final class TvMainActivity extends FragmentActivity implements MainView
|
|||||||
@Override
|
@Override
|
||||||
public void showGames()
|
public void showGames()
|
||||||
{
|
{
|
||||||
|
// Kicks off the program services to update all channels
|
||||||
|
TvUtil.updateAllChannels(getApplicationContext());
|
||||||
|
|
||||||
recreate();
|
recreate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,159 @@
|
|||||||
|
package org.dolphinemu.dolphinemu.utils;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.StringDef;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helps link home screen selection to a game.
|
||||||
|
*/
|
||||||
|
public class AppLinkHelper
|
||||||
|
{
|
||||||
|
public static final String PLAY = "play";
|
||||||
|
public static final String BROWSE = "browse";
|
||||||
|
private static final String SCHEMA_URI_PREFIX = "dolphinemu://app/";
|
||||||
|
private static final String URI_PLAY = SCHEMA_URI_PREFIX + PLAY;
|
||||||
|
private static final String URI_VIEW = SCHEMA_URI_PREFIX + BROWSE;
|
||||||
|
private static final int URI_INDEX_OPTION = 0;
|
||||||
|
private static final int URI_INDEX_CHANNEL = 1;
|
||||||
|
private static final int URI_INDEX_GAME = 2;
|
||||||
|
|
||||||
|
public static Uri buildGameUri(long channelId, String gameId)
|
||||||
|
{
|
||||||
|
return Uri.parse(URI_PLAY)
|
||||||
|
.buildUpon()
|
||||||
|
.appendPath(String.valueOf(channelId))
|
||||||
|
.appendPath(String.valueOf(gameId))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Uri buildBrowseUri(String subscriptionName)
|
||||||
|
{
|
||||||
|
return Uri.parse(URI_VIEW).buildUpon().appendPath(subscriptionName).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AppLinkAction extractAction(Uri uri)
|
||||||
|
{
|
||||||
|
if (isGameUri(uri))
|
||||||
|
return new PlayAction(extractChannelId(uri), extractGameId(uri));
|
||||||
|
else if (isBrowseUri(uri))
|
||||||
|
return new BrowseAction(extractSubscriptionName(uri));
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("No action found for uri " + uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isGameUri(Uri uri)
|
||||||
|
{
|
||||||
|
if (uri.getPathSegments().isEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String option = uri.getPathSegments().get(URI_INDEX_OPTION);
|
||||||
|
return PLAY.equals(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBrowseUri(Uri uri)
|
||||||
|
{
|
||||||
|
if (uri.getPathSegments().isEmpty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
String option = uri.getPathSegments().get(URI_INDEX_OPTION);
|
||||||
|
return BROWSE.equals(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractSubscriptionName(Uri uri)
|
||||||
|
{
|
||||||
|
return extract(uri, URI_INDEX_CHANNEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long extractChannelId(Uri uri)
|
||||||
|
{
|
||||||
|
return extractLong(uri, URI_INDEX_CHANNEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractGameId(Uri uri)
|
||||||
|
{
|
||||||
|
return extract(uri, URI_INDEX_GAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long extractLong(Uri uri, int index)
|
||||||
|
{
|
||||||
|
return Long.valueOf(extract(uri, index));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extract(Uri uri, int index)
|
||||||
|
{
|
||||||
|
List<String> pathSegments = uri.getPathSegments();
|
||||||
|
if (pathSegments.isEmpty() || pathSegments.size() < index)
|
||||||
|
return null;
|
||||||
|
return pathSegments.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@StringDef({BROWSE, PLAY})
|
||||||
|
public @interface ActionFlags
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action for deep linking.
|
||||||
|
*/
|
||||||
|
public interface AppLinkAction
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns an string representation of the action.
|
||||||
|
*/
|
||||||
|
@ActionFlags
|
||||||
|
String getAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action when clicking the channel icon
|
||||||
|
*/
|
||||||
|
public static class BrowseAction implements AppLinkAction
|
||||||
|
{
|
||||||
|
private final String mSubscriptionName;
|
||||||
|
|
||||||
|
private BrowseAction(String subscriptionName)
|
||||||
|
{
|
||||||
|
this.mSubscriptionName = subscriptionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAction()
|
||||||
|
{
|
||||||
|
return BROWSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action when clicking a program(game)
|
||||||
|
*/
|
||||||
|
public static class PlayAction implements AppLinkAction
|
||||||
|
{
|
||||||
|
private final long channelId;
|
||||||
|
private final String gameId;
|
||||||
|
|
||||||
|
private PlayAction(long channelId, String gameId)
|
||||||
|
{
|
||||||
|
this.channelId = channelId;
|
||||||
|
this.gameId = gameId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getChannelId()
|
||||||
|
{
|
||||||
|
return channelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGameId()
|
||||||
|
{
|
||||||
|
return gameId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAction()
|
||||||
|
{
|
||||||
|
return PLAY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,296 @@
|
|||||||
|
package org.dolphinemu.dolphinemu.utils;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.job.JobInfo;
|
||||||
|
import android.app.job.JobScheduler;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.graphics.drawable.VectorDrawable;
|
||||||
|
import android.media.tv.TvContract;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.os.PersistableBundle;
|
||||||
|
import android.support.annotation.AnyRes;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.media.tv.Channel;
|
||||||
|
import android.support.media.tv.TvContractCompat;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.dolphinemu.dolphinemu.R;
|
||||||
|
import org.dolphinemu.dolphinemu.model.GameFile;
|
||||||
|
import org.dolphinemu.dolphinemu.model.HomeScreenChannel;
|
||||||
|
import org.dolphinemu.dolphinemu.services.SyncChannelJobService;
|
||||||
|
import org.dolphinemu.dolphinemu.services.SyncProgramsJobService;
|
||||||
|
import org.dolphinemu.dolphinemu.ui.platform.Platform;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
|
||||||
|
import static android.support.v4.content.FileProvider.getUriForFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assists in TV related services, e.g., home screen channels
|
||||||
|
*/
|
||||||
|
public class TvUtil
|
||||||
|
{
|
||||||
|
private static final String TAG = "TvUtil";
|
||||||
|
private static final long CHANNEL_JOB_ID_OFFSET = 1000;
|
||||||
|
|
||||||
|
private static final String[] CHANNELS_PROJECTION = {
|
||||||
|
TvContractCompat.Channels._ID,
|
||||||
|
TvContract.Channels.COLUMN_DISPLAY_NAME,
|
||||||
|
TvContractCompat.Channels.COLUMN_BROWSABLE
|
||||||
|
};
|
||||||
|
private static final String LEANBACK_PACKAGE = "com.google.android.tvlauncher";
|
||||||
|
|
||||||
|
public static int getNumberOfChannels(Context context)
|
||||||
|
{
|
||||||
|
Cursor cursor =
|
||||||
|
context.getContentResolver()
|
||||||
|
.query(
|
||||||
|
TvContractCompat.Channels.CONTENT_URI,
|
||||||
|
CHANNELS_PROJECTION,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
return cursor != null ? cursor.getCount() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Channel> getAllChannels(Context context)
|
||||||
|
{
|
||||||
|
List<Channel> channels = new ArrayList<>();
|
||||||
|
Cursor cursor =
|
||||||
|
context.getContentResolver()
|
||||||
|
.query(
|
||||||
|
TvContractCompat.Channels.CONTENT_URI,
|
||||||
|
CHANNELS_PROJECTION,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
if (cursor != null && cursor.moveToFirst())
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
channels.add(Channel.fromCursor(cursor));
|
||||||
|
} while (cursor.moveToNext());
|
||||||
|
}
|
||||||
|
return channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Channel getChannelById(Context context, long channelId)
|
||||||
|
{
|
||||||
|
for (Channel channel : getAllChannels(context))
|
||||||
|
{
|
||||||
|
if (channel.getId() == channelId)
|
||||||
|
{
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates all Leanback homescreen channels
|
||||||
|
*/
|
||||||
|
public static void updateAllChannels(Context context)
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SDK_INT < 26)
|
||||||
|
return;
|
||||||
|
for (Channel channel : getAllChannels(context))
|
||||||
|
{
|
||||||
|
context.getContentResolver()
|
||||||
|
.update(
|
||||||
|
TvContractCompat.buildChannelUri(channel.getId()),
|
||||||
|
channel.toContentValues(),
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Uri getUriToResource(Context context, @AnyRes int resId)
|
||||||
|
throws Resources.NotFoundException
|
||||||
|
{
|
||||||
|
Resources res = context.getResources();
|
||||||
|
Uri resUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE +
|
||||||
|
"://" + res.getResourcePackageName(resId)
|
||||||
|
+ '/' + res.getResourceTypeName(resId)
|
||||||
|
+ '/' + res.getResourceEntryName(resId));
|
||||||
|
return resUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a resource into a {@link Bitmap}. If the resource is a vector drawable, it will be
|
||||||
|
* drawn into a new Bitmap. Otherwise the {@link BitmapFactory} will decode the resource.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static Bitmap convertToBitmap(Context context, int resourceId)
|
||||||
|
{
|
||||||
|
Drawable drawable = context.getDrawable(resourceId);
|
||||||
|
if (drawable instanceof VectorDrawable)
|
||||||
|
{
|
||||||
|
Bitmap bitmap =
|
||||||
|
Bitmap.createBitmap(
|
||||||
|
drawable.getIntrinsicWidth(),
|
||||||
|
drawable.getIntrinsicHeight(),
|
||||||
|
Bitmap.Config.ARGB_8888);
|
||||||
|
Canvas canvas = new Canvas(bitmap);
|
||||||
|
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||||
|
drawable.draw(canvas);
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
return BitmapFactory.decodeResource(context.getResources(), resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leanback lanucher requires a uri for poster art, so we take the banner vector,
|
||||||
|
* make a bitmap, save that bitmap, then return the file provider uri.
|
||||||
|
*/
|
||||||
|
public static Uri buildBanner(GameFile game, Context context)
|
||||||
|
{
|
||||||
|
Uri contentUri = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//Substring needed to strip "file:" from the path beginning
|
||||||
|
File screenshotFile = new File(game.getScreenshotPath().substring(5));
|
||||||
|
if (screenshotFile.exists())
|
||||||
|
{
|
||||||
|
contentUri = getUriForFile(context, getFilePrivider(context), screenshotFile);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
File file = new File(buildBannerFilename(game.getGameId()));
|
||||||
|
if (!file.exists())
|
||||||
|
{
|
||||||
|
int[] vector = game.getBanner();
|
||||||
|
int width = game.getBannerWidth();
|
||||||
|
int height = game.getBannerHeight();
|
||||||
|
|
||||||
|
if (vector.length > 0 || width > 0 || height > 0)
|
||||||
|
{
|
||||||
|
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||||
|
bitmap.setPixels(vector, 0, width, 0, 0, width, height);
|
||||||
|
FileOutputStream out = new FileOutputStream(file);
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
contentUri = getUriForFile(context, getFilePrivider(context), file);
|
||||||
|
}
|
||||||
|
context.grantUriPermission(LEANBACK_PACKAGE, contentUri,
|
||||||
|
FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.e(TAG, "Failed to create banner");
|
||||||
|
Log.e(TAG, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildBannerFilename(String gameId)
|
||||||
|
{
|
||||||
|
return Environment.getExternalStorageDirectory().getPath() +
|
||||||
|
"/dolphin-emu/Cache/" + gameId + "_banner.png";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Needed since debug builds append '.debug' to the end of the package
|
||||||
|
*/
|
||||||
|
private static String getFilePrivider(Context context)
|
||||||
|
{
|
||||||
|
return context.getPackageName() + ".filesprovider";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules syncing channels via a {@link JobScheduler}.
|
||||||
|
*
|
||||||
|
* @param context for accessing the {@link JobScheduler}.
|
||||||
|
*/
|
||||||
|
public static void scheduleSyncingChannel(Context context)
|
||||||
|
{
|
||||||
|
ComponentName componentName = new ComponentName(context, SyncChannelJobService.class);
|
||||||
|
JobInfo.Builder builder = new JobInfo.Builder(1, componentName);
|
||||||
|
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
|
||||||
|
|
||||||
|
JobScheduler scheduler =
|
||||||
|
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
|
||||||
|
|
||||||
|
Log.d(TAG, "Scheduled channel creation.");
|
||||||
|
scheduler.schedule(builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedulers syncing programs for a channel. The scheduler will listen to a {@link Uri} for a
|
||||||
|
* particular channel.
|
||||||
|
*
|
||||||
|
* @param context for accessing the {@link JobScheduler}.
|
||||||
|
* @param channelId for the channel to listen for changes.
|
||||||
|
*/
|
||||||
|
@TargetApi(Build.VERSION_CODES.O)
|
||||||
|
public static void scheduleSyncingProgramsForChannel(Context context, long channelId)
|
||||||
|
{
|
||||||
|
Log.d(TAG, "ProgramsRefresh job");
|
||||||
|
ComponentName componentName = new ComponentName(context, SyncProgramsJobService.class);
|
||||||
|
JobInfo.Builder builder =
|
||||||
|
new JobInfo.Builder(getJobIdForChannelId(channelId), componentName);
|
||||||
|
JobInfo.TriggerContentUri triggerContentUri =
|
||||||
|
new JobInfo.TriggerContentUri(
|
||||||
|
TvContractCompat.buildChannelUri(channelId),
|
||||||
|
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS);
|
||||||
|
builder.addTriggerContentUri(triggerContentUri);
|
||||||
|
builder.setTriggerContentMaxDelay(0L);
|
||||||
|
builder.setTriggerContentUpdateDelay(0L);
|
||||||
|
|
||||||
|
PersistableBundle bundle = new PersistableBundle();
|
||||||
|
bundle.putLong(TvContractCompat.EXTRA_CHANNEL_ID, channelId);
|
||||||
|
builder.setExtras(bundle);
|
||||||
|
|
||||||
|
JobScheduler scheduler =
|
||||||
|
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
|
||||||
|
scheduler.cancel(getJobIdForChannelId(channelId));
|
||||||
|
scheduler.schedule(builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getJobIdForChannelId(long channelId)
|
||||||
|
{
|
||||||
|
return (int) (CHANNEL_JOB_ID_OFFSET + channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates all subscriptions for homescreen channels.
|
||||||
|
*/
|
||||||
|
public static List<HomeScreenChannel> createUniversalSubscriptions()
|
||||||
|
{
|
||||||
|
//Leaving the subs local variable in case more channels are created other than platforms.
|
||||||
|
List<HomeScreenChannel> subs = new ArrayList<>(createPlatformSubscriptions());
|
||||||
|
return subs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<HomeScreenChannel> createPlatformSubscriptions()
|
||||||
|
{
|
||||||
|
List<HomeScreenChannel> subs = new ArrayList<>();
|
||||||
|
for (Platform platform : Platform.values())
|
||||||
|
{
|
||||||
|
subs.add(new HomeScreenChannel(
|
||||||
|
platform.getHeaderName(),
|
||||||
|
platform.getHeaderName(),
|
||||||
|
AppLinkHelper.buildBrowseUri(platform.getHeaderName()).toString()));
|
||||||
|
}
|
||||||
|
return subs;
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
<!-- Title of the app -->
|
<!-- Title of the app -->
|
||||||
<string name="app_name">Dolphin Emulator</string>
|
<string name="app_name">Dolphin Emulator</string>
|
||||||
|
<string name="host">app</string>
|
||||||
|
<string name="scheme">dolphinemu</string>
|
||||||
|
|
||||||
<!-- WARNING Do not move these controller entries AT ALL COSTS! They are indexed with ints, and an assumption
|
<!-- WARNING Do not move these controller entries AT ALL COSTS! They are indexed with ints, and an assumption
|
||||||
is made that they are placed together so that we can access them sequentially in a loop. -->
|
is made that they are placed together so that we can access them sequentially in a loop. -->
|
||||||
@ -282,4 +284,7 @@
|
|||||||
<string name="emulation_change_disc">Change Disc</string>
|
<string name="emulation_change_disc">Change Disc</string>
|
||||||
|
|
||||||
<string name="external_storage_not_mounted">The external storage needs to be available in order to use Dolphin</string>
|
<string name="external_storage_not_mounted">The external storage needs to be available in order to use Dolphin</string>
|
||||||
|
|
||||||
|
<!-- Extra Leanback Homescreen Channels-->
|
||||||
|
<string name="homescreen_favorites">Favorites</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user