Merge pull request #1285 from lioncash/android

Android: Very early initial core downloader implementation
This commit is contained in:
Twinaphex 2014-12-13 23:16:43 +01:00
commit aa9b4a9aa3
10 changed files with 575 additions and 66 deletions

View File

@ -5,5 +5,6 @@
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="lib" path="libs/jsoup-1.8.1.jar"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

Binary file not shown.

View File

@ -40,6 +40,11 @@
<string name="core_info_manufacturer">Manufacturer</string>
<string name="core_info_permissions">Permissions</string>
<!-- Core Downloader Fragment -->
<string name="download_core_confirm_title">Confirm</string>
<string name="download_core_confirm_msg">Are you sure you want to download %1$s?</string>
<string name="downloading_msg">Downloading %1$s…</string>
<!-- Display Refresh Rate Test Class -->
<string name="refresh_rate_calibration">Refresh rate calibration</string>
<string name="touch_screen_with_fingers">Touch the screen with your fingers for more accurate measurements.</string>

View File

@ -4,6 +4,7 @@ import java.util.List;
import com.retroarch.R;
import com.retroarch.browser.coremanager.fragments.DownloadableCoresFragment;
import com.retroarch.browser.coremanager.fragments.InstalledCoresFragment;
import com.retroarch.browser.coremanager.fragments.InstalledCoresManagerFragment;
import android.os.Bundle;
@ -17,7 +18,7 @@ import android.support.v7.app.ActionBarActivity;
* Activity which provides the base for viewing installed cores,
* as well as the ability to download other cores.
*/
public final class CoreManagerActivity extends ActionBarActivity
public final class CoreManagerActivity extends ActionBarActivity implements DownloadableCoresFragment.OnCoreDownloadedListener
{
@Override
public void onCreate(Bundle savedInstanceState)
@ -76,6 +77,19 @@ public final class CoreManagerActivity extends ActionBarActivity
return false;
}
// Callback function used to update the installed cores list
@Override
public void onCoreDownloaded()
{
InstalledCoresManagerFragment icmf = (InstalledCoresManagerFragment) getSupportFragmentManager().findFragmentByTag("android:switcher:" + R.id.coreviewer_viewPager + ":" + 0);
if (icmf != null)
{
InstalledCoresFragment icf = (InstalledCoresFragment) icmf.getChildFragmentManager().findFragmentByTag("InstalledCoresList");
if (icf != null)
icf.updateInstalledCoresList();
}
}
// Adapter for the core manager ViewPager.
private final class ViewPagerAdapter extends FragmentPagerAdapter
{

View File

@ -0,0 +1,57 @@
package com.retroarch.browser.coremanager.fragments;
/**
* Represents a core that can be downloaded.
*/
final class DownloadableCore
{
private final String coreName;
private final String coreURL;
private final String shortURL;
/**
* Constructor
*
* @param coreName Name of the core.
* @param coreURL URL to this core.
*/
public DownloadableCore(String coreName, String coreURL)
{
this.coreName = coreName;
this.coreURL = coreURL;
this.shortURL = coreURL.substring(coreURL.lastIndexOf('/') + 1);
}
/**
* Gets the name of this core.
*
* @return The name of this core.
*/
public String getCoreName()
{
return coreName;
}
/**
* Gets the URL to download this core.
*
* @return The URL to download this core.
*/
public String getCoreURL()
{
return coreURL;
}
/**
* Gets the short URL name of this core.
* <p>
* e.g. Consider the url: www.somesite/somecore.zip.
* This would return "somecore.zip"
*
* @return the short URL name of this core.
*/
public String getShortURLName()
{
return shortURL;
}
}

View File

@ -0,0 +1,54 @@
package com.retroarch.browser.coremanager.fragments;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
/**
* {@link ArrayAdapter} that handles the display of downloadable cores.
*/
final class DownloadableCoresAdapter extends ArrayAdapter<DownloadableCore>
{
private final int layoutId;
/**
* Constructor
*
* @param context The current {@link Context}.
* @param layoutID The resource ID for a layout file containing a layout to use when instantiating views
*/
public DownloadableCoresAdapter(Context context, int layoutId)
{
super(context, layoutId);
this.layoutId = layoutId;
}
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
if (convertView == null)
{
final LayoutInflater li = LayoutInflater.from(getContext());
convertView = li.inflate(layoutId, parent, false);
}
final DownloadableCore core = getItem(position);
if (core != null)
{
TextView title = (TextView) convertView.findViewById(android.R.id.text1);
TextView subtitle = (TextView) convertView.findViewById(android.R.id.text2);
if (title != null)
title.setText(core.getCoreName());
if (subtitle != null)
subtitle.setText(core.getCoreURL());
}
return convertView;
}
}

View File

@ -1,27 +1,378 @@
package com.retroarch.browser.coremanager.fragments;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.ListFragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.ListView;
import com.retroarch.R;
/**
* {@link ListFragment} that is responsible for showing
* cores that are able to be downloaded or are not installed.
*/
public final class DownloadableCoresFragment extends Fragment
public final class DownloadableCoresFragment extends ListFragment
{
// TODO: Implement complete functionality.
// List of TODOs.
// - Eventually make the core downloader capable of directory-based browsing from the base URL.
// - Allow for 'repository'-like core downloading.
// - Clean this up a little better. It can likely be way more organized.
// - Don't re-download the info files on orientation changes.
// - Use a loading wheel when core retrieval is being done. User may think something went wrong otherwise.
// - Check the info directory for an info file before downloading it. Can save bandwidth this way (and list load times would be faster).
// - Should probably display a dialog or a toast message when the Internet connection process fails.
/**
* Dictates what actions will occur when a core download completes.
* <p>
* Acts like a callback so that communication between fragments is possible.
*/
public interface OnCoreDownloadedListener
{
/** The action that will occur when a core is successfully downloaded. */
void onCoreDownloaded();
}
private static final String BUILDBOT_BASE_URL = "http://buildbot.libretro.com";
private static final String BUILDBOT_CORE_URL = BUILDBOT_BASE_URL + "/stable/android/armv7/1.0.0.2/cores/";
private static final String BUILDBOT_INFO_URL = BUILDBOT_BASE_URL + "/stable/android/armv7/1.0.0.2/info/";
private OnCoreDownloadedListener coreDownloadedListener = null;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
TextView tv = new TextView(getActivity());
tv.setText("In development, just a little longer!");
super.onCreateView(inflater, container, savedInstanceState);
final ListView coreList = (ListView) inflater.inflate(R.layout.coremanager_listview, container, false);
return tv;
final DownloadableCoresAdapter adapter = new DownloadableCoresAdapter(getActivity(), android.R.layout.simple_list_item_2);
adapter.setNotifyOnChange(true);
coreList.setAdapter(adapter);
coreDownloadedListener = (OnCoreDownloadedListener) getActivity();
new PopulateCoresListOperation(adapter).execute();
return coreList;
}
@Override
public void onListItemClick(final ListView lv, final View v, final int position, final long id)
{
super.onListItemClick(lv, v, position, id);
final DownloadableCore core = (DownloadableCore) lv.getItemAtPosition(position);
// Prompt the user for confirmation on downloading the core.
AlertDialog.Builder notification = new AlertDialog.Builder(getActivity());
notification.setMessage(String.format(getString(R.string.download_core_confirm_msg), core.getCoreName()));
notification.setTitle(R.string.download_core_confirm_title);
notification.setNegativeButton(R.string.no, null);
notification.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which)
{
// Begin downloading the core.
new DownloadCoreOperation(getActivity(), core.getCoreName()).execute(core.getCoreURL(), core.getShortURLName());
}
});
notification.show();
}
// Async event responsible for populating the Downloadable Cores list.
private static final class PopulateCoresListOperation extends AsyncTask<Void, Void, ArrayList<DownloadableCore>>
{
// Acts as an object reference to an adapter in a list view.
private DownloadableCoresAdapter adapter;
/**
* Constructor
*
* @param adapter The adapter to asynchronously update.
*/
public PopulateCoresListOperation(DownloadableCoresAdapter adapter)
{
this.adapter = adapter;
}
@Override
protected ArrayList<DownloadableCore> doInBackground(Void... params)
{
try
{
final Connection core_connection = Jsoup.connect(BUILDBOT_CORE_URL);
final Elements coreElements = core_connection.get().body().getElementsByClass("fb-n").select("a");
final ArrayList<DownloadableCore> downloadableCores = new ArrayList<DownloadableCore>();
// NOTE: Start from 1 to skip the ".." (parent directory element)
// Set this to zero if directory-based browsing becomes a thing.
for (int i = 1; i < coreElements.size(); i++)
{
Element coreElement = coreElements.get(i);
final String coreURL = BUILDBOT_BASE_URL + coreElement.attr("href");
final String coreName = coreURL.substring(coreURL.lastIndexOf("/") + 1);
final String infoURL = BUILDBOT_INFO_URL + coreName.replace("_android.so.zip", ".info");
downloadableCores.add(new DownloadableCore(getCoreName(infoURL), coreURL));
}
return downloadableCores;
}
catch (IOException e)
{
Log.e("PopulateCoresListOperation", e.getMessage());
// Make a dummy entry to notify an error.
final ArrayList<DownloadableCore> errorList = new ArrayList<DownloadableCore>();
errorList.add(new DownloadableCore("Error", e.getMessage()));
return errorList;
}
}
@Override
protected void onPostExecute(ArrayList<DownloadableCore> result)
{
super.onPostExecute(result);
adapter.addAll(result);
}
// Literally downloads the info file, writes it, and parses it for the corename key/value pair.
// AKA an argument for having a manifest file on the server.
//
// This makes list loading take way longer than it should.
//
// One way this can be improved is by checking the info directory for
// existing info files that match the core. Eliminating the download retrieval.
private String getCoreName(String urlPath) throws IOException
{
final URL url = new URL(urlPath);
final BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()));
final StringBuilder sb = new StringBuilder();
String str = "";
while ((str = br.readLine()) != null)
sb.append(str + "\n");
br.close();
// Write the info file.
File outputPath = new File(adapter.getContext().getApplicationInfo().dataDir + "/info/", urlPath.substring(urlPath.lastIndexOf('/') + 1));
BufferedWriter bw = new BufferedWriter(new FileWriter(outputPath));
bw.append(sb);
bw.close();
// Now read the core name
String[] lines = sb.toString().split("\n");
String name = "";
for (int i = 0; i < lines.length; i++)
{
if (lines[i].contains("corename"))
{
// Gross
name = lines[i].split("=")[1].trim().replace("\"", "");
break;
}
}
return name;
}
}
// Executed when the user confirms a core download.
private final class DownloadCoreOperation extends AsyncTask<String, Integer, Void>
{
private final ProgressDialog dlg;
private final Context ctx;
private final String coreName;
/**
* Constructor
*
* @param ctx The current {@link Context}.
* @param coreName The name of the core being downloaded.
*/
public DownloadCoreOperation(Context ctx, String coreName)
{
this.dlg = new ProgressDialog(ctx);
this.ctx = ctx;
this.coreName = coreName;
}
@Override
protected void onPreExecute()
{
super.onPreExecute();
dlg.setMessage(String.format(ctx.getString(R.string.downloading_msg), coreName));
dlg.setCancelable(false);
dlg.setCanceledOnTouchOutside(false);
dlg.setIndeterminate(false);
dlg.setMax(100);
dlg.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dlg.show();
}
@Override
protected Void doInBackground(String... params)
{
InputStream input = null;
OutputStream output = null;
HttpURLConnection connection = null;
try
{
URL url = new URL(params[0]);
connection = (HttpURLConnection) url.openConnection();
connection.connect();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
{
Log.i("DownloadCoreOperation", "HTTP response code not OK. Response code: " + connection.getResponseCode());
return null;
}
// Set up the streams
final int fileLen = connection.getContentLength();
final File zipPath = new File(ctx.getApplicationInfo().dataDir + "/cores/", params[1]);
input = new BufferedInputStream(connection.getInputStream(), 8192);
output = new FileOutputStream(zipPath);
// Download and write to storage.
long totalDownloaded = 0;
byte[] buffer = new byte[4096];
int countBytes = 0;
while ((countBytes = input.read(buffer)) != -1)
{
totalDownloaded += countBytes;
if (fileLen > 0)
publishProgress((int) (totalDownloaded * 100 / fileLen));
output.write(buffer, 0, countBytes);
}
unzipCore(zipPath);
}
catch (IOException ignored)
{
// Can't really do anything to recover.
}
finally
{
try
{
if (output != null)
output.close();
if (input != null)
input.close();
}
catch (IOException ignored)
{
}
if (connection != null)
connection.disconnect();
}
return null;
}
@Override
protected void onProgressUpdate(Integer... progress)
{
super.onProgressUpdate(progress);
dlg.setProgress(progress[0]);
}
@Override
protected void onPostExecute(Void result)
{
super.onPostExecute(result);
if (dlg.isShowing())
dlg.dismiss();
// Invoke callback to update the installed cores list.
coreDownloadedListener.onCoreDownloaded();
}
}
// Java 6 ladies and gentlemen.
private static void unzipCore(File zipFile)
{
ZipInputStream zis = null;
try
{
zis = new ZipInputStream(new FileInputStream(zipFile));
ZipEntry entry = zis.getNextEntry();
while (entry != null)
{
File file = new File(zipFile.getParent(), entry.getName());
FileOutputStream fos = new FileOutputStream(file);
int len = 0;
byte[] buffer = new byte[4096];
while ((len = zis.read(buffer)) != -1)
{
fos.write(buffer, 0, len);
}
fos.close();
entry = zis.getNextEntry();
}
}
catch (IOException ignored)
{
// Can't do anything.
}
finally
{
try
{
if (zis != null)
{
zis.closeEntry();
zis.close();
}
}
catch (IOException ignored)
{
// Can't do anything
}
zipFile.delete();
}
}
}

View File

@ -56,70 +56,23 @@ public final class InstalledCoresFragment extends ListFragment
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
public void onActivityCreated(Bundle savedInstanceState)
{
// Inflate the layout for this ListFragment.
ListView parentView = (ListView) inflater.inflate(R.layout.coremanager_listview, container, false);
super.onActivityCreated(savedInstanceState);
// Set the long click listener.
parentView.setOnItemLongClickListener(itemLongClickListener);
adapter = new InstalledCoresAdapter(getActivity(), android.R.layout.simple_list_item_2, getInstalledCoresList());
setListAdapter(adapter);
// Get the callback. (implemented within InstalledCoresManagerFragment).
callback = (OnCoreItemClickedListener) getParentFragment();
}
// The list of items that will be added to the adapter backing this ListFragment.
final List<ModuleWrapper> items = new ArrayList<ModuleWrapper>();
@Override
public void onViewCreated(View view, Bundle savedInstanceState)
{
super.onViewCreated(view, savedInstanceState);
// Check if the device supports NEON.
final String cpuInfo = UserPreferences.readCPUInfo();
final boolean supportsNeon = cpuInfo.contains("neon");
// Populate the list
final File[] libs = new File(getActivity().getApplicationInfo().dataDir, "/cores").listFiles();
if (libs != null)
{
for (File lib : libs)
{
String libName = lib.getName();
// Never append a NEON lib if we don't have NEON.
if (libName.contains("neon") && !supportsNeon)
continue;
// If we have a NEON version with NEON capable CPU,
// never append a non-NEON version.
if (supportsNeon && !libName.contains("neon"))
{
boolean hasNeonVersion = false;
for (File lib_ : libs)
{
String otherName = lib_.getName();
String baseName = libName.replace(".so", "");
if (otherName.contains("neon") && otherName.startsWith(baseName))
{
hasNeonVersion = true;
break;
}
}
if (hasNeonVersion)
continue;
}
// Add it to the list.
items.add(new ModuleWrapper(getActivity(), lib));
}
}
// Sort the list alphabetically
Collections.sort(items);
// Initialize and set the backing adapter for this ListFragment.
adapter = new InstalledCoresAdapter(getActivity(), android.R.layout.simple_list_item_2, items);
parentView.setAdapter(adapter);
return parentView;
getListView().setOnItemLongClickListener(itemLongClickListener);
}
@Override
@ -131,6 +84,68 @@ public final class InstalledCoresFragment extends ListFragment
getListView().setItemChecked(position, true);
}
/**
* Refreshes the list of installed cores.
*/
public void updateInstalledCoresList()
{
adapter.clear();
adapter.addAll(getInstalledCoresList());
adapter.notifyDataSetChanged();
}
private List<ModuleWrapper> getInstalledCoresList()
{
// The list of items that will be added to the adapter backing this ListFragment.
final List<ModuleWrapper> items = new ArrayList<ModuleWrapper>();
// Check if the device supports NEON.
final String cpuInfo = UserPreferences.readCPUInfo();
final boolean supportsNeon = cpuInfo.contains("neon");
// Populate the list
final File[] libs = new File(getActivity().getApplicationInfo().dataDir, "/cores").listFiles();
if (libs != null)
{
for (File lib : libs)
{
String libName = lib.getName();
// Never append a NEON lib if we don't have NEON.
if (libName.contains("neon") && !supportsNeon)
continue;
// If we have a NEON version with NEON capable CPU,
// never append a non-NEON version.
if (supportsNeon && !libName.contains("neon"))
{
boolean hasNeonVersion = false;
for (File lib_ : libs)
{
String otherName = lib_.getName();
String baseName = libName.replace(".so", "");
if (otherName.contains("neon") && otherName.startsWith(baseName))
{
hasNeonVersion = true;
break;
}
}
if (hasNeonVersion)
continue;
}
// Add it to the list.
items.add(new ModuleWrapper(getActivity(), lib));
}
}
// Sort the list alphabetically
Collections.sort(items);
return items;
}
// This will be the handler for long clicks on individual list items in this ListFragment.
private final OnItemLongClickListener itemLongClickListener = new OnItemLongClickListener()
{

View File

@ -23,7 +23,7 @@ public class InstalledCoresManagerFragment extends Fragment implements Installed
final Fragment installedCores = new InstalledCoresFragment();
final FragmentTransaction ft = getChildFragmentManager().beginTransaction();
ft.replace(R.id.installed_cores_fragment_container1, installedCores);
ft.replace(R.id.installed_cores_fragment_container1, installedCores, "InstalledCoresList");
ft.commit();
return v;

View File

@ -1,9 +1,12 @@
package com.retroarch.browser.mainmenu;
import java.io.File;
import com.retroarch.R;
import com.retroarch.browser.preferences.util.UserPreferences;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.media.AudioManager;
import android.os.Bundle;
import android.preference.PreferenceActivity;
@ -21,6 +24,15 @@ public final class MainMenuActivity extends FragmentActivity
{
super.onCreate(savedInstanceState);
// Ensure resource directories are created.
final ApplicationInfo info = getApplicationInfo();
final File coresDir = new File(info.dataDir, "cores");
final File infoDir = new File(info.dataDir, "info");
if (!coresDir.exists())
coresDir.mkdir();
if (!infoDir.exists())
infoDir.mkdir();
// Load the main menu layout
setContentView(R.layout.mainmenu_activity_layout);
if (savedInstanceState == null)