Initial commit.

This is (mostly) functional.  Still needs some cleanup.
This commit is contained in:
Tom Marshall 2012-06-01 15:20:26 -07:00
parent 2cf21fbb17
commit 9f9fb637b8
48 changed files with 2600 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
local.properties
bin/*
gen/*

33
AndroidManifest.xml Normal file
View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="tdm.romkeeper"
android:versionCode="1"
android:versionName="1.0">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- uses-permission android:name="android.permission.REBOOT" / -->
<application android:label="@string/app_name" android:icon="@drawable/icon">
<activity android:name="RomKeeperActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="RomKeeperPreferenceActivity"
android:label="@string/app_name">
</activity>
<service android:name=".FileDownloadService"
android:label="@string/file_download_service_name">
</service>
<service android:name=".ManifestCheckerService"
android:label="@string/manifest_checker_service_name">
</service>
<receiver android:name=".StartupReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>

17
ant.properties Normal file
View File

@ -0,0 +1,17 @@
# This file is used to override default values used by the Ant build system.
#
# This file must be checked into Version Control Systems, as it is
# integral to the build system of your project.
# This file is only used by the Ant script.
# You can use this to override default values such as
# 'source.dir' for the location of your java source folder and
# 'out.dir' for the location of your output folder.
# You can also use it define how the release builds are signed by declaring
# the following properties:
# 'key.store' for the location of your keystore and
# 'key.alias' for the name of the key to use.
# The password will be asked during the build when you use the 'release' target.

83
build.xml Normal file
View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="RomKeeper" default="help">
<!-- The local.properties file is created and updated by the 'android' tool.
It contains the path to the SDK. It should *NOT* be checked into
Version Control Systems. -->
<property file="local.properties" />
<!-- The ant.properties file can be created by you. It is only edited by the
'android' tool to add properties to it.
This is the place to change some Ant specific build properties.
Here are some properties you may want to change/update:
source.dir
The name of the source directory. Default is 'src'.
out.dir
The name of the output directory. Default is 'bin'.
For other overridable properties, look at the beginning of the rules
files in the SDK, at tools/ant/build.xml
Properties related to the SDK location or the project target should
be updated using the 'android' tool with the 'update' action.
This file is an integral part of the build system for your
application and should be checked into Version Control Systems.
-->
<property file="ant.properties" />
<!-- The project.properties file is created and updated by the 'android'
tool, as well as ADT.
This contains project specific properties such as project target, and library
dependencies. Lower level build properties are stored in ant.properties
(or in .classpath for Eclipse projects).
This file is an integral part of the build system for your
application and should be checked into Version Control Systems. -->
<loadproperties srcFile="project.properties" />
<!-- quick check on sdk.dir -->
<fail
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through an env var"
unless="sdk.dir"
/>
<!--
Import per project custom build rules if present at the root of the project.
This is the place to put custom intermediary targets such as:
-pre-build
-pre-compile
-post-compile (This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override ${out.dex.input.absolute.dir})
-post-package
-post-build
-pre-clean
-->
<import file="custom_rules.xml" optional="true" />
<!-- Import the actual build file.
To customize existing targets, there are two options:
- Customize only one target:
- copy/paste the target into this file, *before* the
<import> task.
- customize it to your needs.
- Customize the whole content of build.xml
- copy/paste the content of the rules files (minus the top node)
into this file, replacing the <import> task.
- customize to your needs.
***********************
****** IMPORTANT ******
***********************
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
in order to avoid having your file be overridden by tools such as "android update project"
-->
<!-- version-tag: 1 -->
<import file="${sdk.dir}/tools/ant/build.xml" />
</project>

7
main.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<AbsoluteLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
</AbsoluteLayout>

20
proguard-project.txt Normal file
View File

@ -0,0 +1,20 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

36
proguard.cfg Normal file
View File

@ -0,0 +1,36 @@
-optimizationpasses 5
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontpreverify
-verbose
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class com.android.vending.licensing.ILicensingService
-keepclasseswithmembernames class * {
native <methods>;
}
-keepclasseswithmembernames class * {
public <init>(android.content.Context, android.util.AttributeSet);
}
-keepclasseswithmembernames class * {
public <init>(android.content.Context, android.util.AttributeSet, int);
}
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}

14
project.properties Normal file
View File

@ -0,0 +1,14 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-10

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
res/drawable-hdpi/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
res/drawable-ldpi/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
res/drawable-mdpi/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

7
res/layout/list_item.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:padding="10dp"
android:textSize="16sp" >
</TextView>

10
res/layout/romlist.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ListView
android:id="@+id/romlist_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<TextView android:id="@+id/name_entry"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/menu_fetch"
android:icon="@drawable/ic_menu_fetch"
android:alphabeticShortcut='f'
android:title="@string/menu_fetch" />
<item android:id="@+id/menu_install"
android:icon="@drawable/ic_menu_install"
android:alphabeticShortcut='i'
android:title="@string/menu_install" />
</menu>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/menu_refresh"
android:icon="@drawable/ic_menu_refresh"
android:alphabeticShortcut='r'
android:title="@string/menu_refresh" />
<item android:id="@+id/menu_settings"
android:icon="@drawable/ic_menu_settings"
android:alphabeticShortcut='s'
android:title="@string/menu_settings" />
</menu>

12
res/values/arrays.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="updateInterval">
<item name="hourly">Hourly</item>
<item name="daily">Daily</item>
</string-array>
<string-array name="updateIntervalValues">
<item name="hourly">3600</item>
<item name="daily">86400</item>
</string-array>
</resources>

23
res/values/strings.xml Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">RomKeeper</string>
<string name="file_download_service_name">File Download Service</string>
<string name="manifest_checker_service_name">Manifest Checker Service</string>
<string name="menu_refresh">Refresh</string>
<string name="menu_settings">Settings</string>
<string name="menu_fetch">Fetch</string>
<string name="menu_install">Install</string>
<!-- <string name="menu_delete">Delete</string> -->
<string name="empty">Empty</string>
<string name="repeating_scheduled">Repeating Scheduled</string>
<string name="repeating_unscheduled">Repeating Unscheduled</string>
<string name="alarm_service_started">Alarm Service Started</string>
<string name="alarm_service_finished">Alarm Service Finished</string>
<string name="alarm_service_label">Alarm Service Label</string>
</resources>

25
res/xml/preferences.xml Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="Updates"
android:key="updates">
<CheckBoxPreference
android:key="auto_updates"
android:summary="Control automatic updates"
android:title="Automatic updates"
android:defaultValue="false"
/>
</PreferenceCategory>
<PreferenceCategory
android:title="Data Source"
android:key="data_source">
<EditTextPreference
android:key="manifest_url"
android:title="Manifest URL"
android:summary="Where to find the manifest"
android:dialogTitle="Manifest URL"
android:dialogMessage="Provide an URL"
android:defaultValue="http://vmroms.com/~vmstorage/romkeeper" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -0,0 +1,93 @@
package tdm.romkeeper;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
/* A base/helper class for implementing async HTTP file fetches. */
class BrowserFileDownloader extends FileDownloader
{
private Context mContext;
BrowserFileDownloader(Context ctx, String url, String pathname, long size) {
super(url, pathname, size);
mContext = ctx;
}
BrowserFileDownloader(Context ctx, Handler handler, String url, String pathname, long size) {
super(handler, url, pathname, size);
mContext = ctx;
}
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
onStart();
Log.i("BrowserFileDownloader", "downloading "+getUrl());
Intent i = new Intent(Intent.ACTION_VIEW);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // XXX: is this what we want?
i.setData(Uri.parse(getUrl()));
mContext.startActivity(i);
String filename = getPathname();
int idx = filename.lastIndexOf('/');
if (idx > 0) {
filename = filename.substring(idx+1);
}
String dlpath = "/sdcard/download/" + filename;
long startTime = System.currentTimeMillis();
File f = new File(dlpath);
long lastTime = startTime;
long lastSize = 0;
while (lastSize < getSize()) {
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
// Ignore
}
long currentTime = System.currentTimeMillis();
long currentSize = 0;
if (f.exists()) {
currentSize = f.length();
}
if (currentSize > lastSize) {
updateBytesWritten((int)(currentSize - lastSize));
}
else {
if (currentTime - lastTime > 60*1000) {
Log.e("FileDownloader", "Download failed or complete");
onFinish(FAILURE);
return;
}
}
lastTime = currentTime;
lastSize = currentSize;
}
File dest = new File("/sdcard/" + filename);
f.renameTo(dest);
Log.i("FileDownloader", "Download complete");
onFinish(SUCCESS);
}
}

View File

@ -0,0 +1,109 @@
package tdm.romkeeper;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
class BrowserFilePatcher extends FileDownloader
{
private Context mContext;
private String mDeltaPathname;
private long mDeltaSize;
private String mBasisPathname;
BrowserFilePatcher(Context ctx, Handler handler,
String deltaUrl, String deltaPathname, long deltaSize,
String outPathname, long outSize, String basisPathname) {
super(handler, deltaUrl, outPathname, outSize);
mContext = ctx;
mDeltaPathname = deltaPathname;
mDeltaSize = deltaSize;
mBasisPathname = basisPathname;
}
private String basename(String pathname) {
int idx = pathname.lastIndexOf('/');
if (idx > 0) {
return pathname.substring(idx+1);
}
return pathname;
}
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
onStart();
Log.i("BrowserFilePatcher", "downloading "+getUrl());
Intent i = new Intent(Intent.ACTION_VIEW);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // XXX: is this what we want?
i.setData(Uri.parse(getUrl()));
mContext.startActivity(i);
String dlpath = "/sdcard/download/" + basename(mDeltaPathname);
long lastTime = System.currentTimeMillis();
long lastSize = 0;
File f = new File(dlpath);
while (lastSize < mDeltaSize) {
long currentTime = System.currentTimeMillis();
long currentSize = 0;
if (f.exists()) {
currentSize = f.length();
}
if (currentSize <= lastSize) {
if (currentTime - lastTime > 60*1000) {
Log.e("BrowserFilePatcher", "Download failed or stalled");
onFinish(FAILURE);
return;
}
}
lastTime = currentTime;
lastSize = currentSize;
// XXX: update progress...???
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
// Ignore
}
}
try {
InputStream is = new FileInputStream(f);
OutputStream os = new FileOutputStream(getPathname());
FilePatcher patcher = new FilePatcher(this, mBasisPathname, is, os);
patcher.patch();
f.delete();
Log.i("BrowserFilePatcher", "Download complete");
onFinish(SUCCESS);
}
catch (Exception e) {
Log.e("BrowserFilePatcher", "Download failed or incomplete");
onFinish(FAILURE);
}
}
}

View File

@ -0,0 +1,193 @@
package tdm.romkeeper;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase;
import java.io.File;
import java.util.ArrayList;
interface DbObserver
{
void onContentInsert(String name);
void onContentUpdate(String name);
void onContentDelete(String name);
}
class DbAdapter
{
private static class DbHelper extends SQLiteOpenHelper
{
private static final String DB_NAME = "roms";
private static final int DB_VERSION = 1;
private static final String DB_CREATE =
"CREATE TABLE romlist (" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"name VARCHAR(64) NOT NULL UNIQUE, " +
"url VARCHAR(256) NOT NULL UNIQUE, " +
"filename VARCHAR(64) NOT NULL UNIQUE, " +
"size INTEGER, " +
"digest CHAR(32), " +
"basis VARCHAR(64), " +
"deltaurl VARCHAR(256), " +
"deltafilename VARCHAR(64), " +
"deltasize INTEGER, " +
"mtime INTEGER, " +
"verified INTEGER" +
")";
DbHelper(Context ctx) {
super(ctx, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DB_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS romlist");
onCreate(db);
}
}
private final Context mCtx;
private DbHelper mHelper;
private SQLiteDatabase mDb;
ArrayList<DbObserver> mObservers;
void registerObserver(DbObserver o) {
mObservers.add(o);
}
void removeObserver(DbObserver o) {
mObservers.remove(o);
}
private static DbAdapter sInstance;
static DbAdapter getInstance(Context ctx) {
if (sInstance == null) {
sInstance = new DbAdapter(ctx);
}
return sInstance;
}
private DbAdapter(Context ctx) {
mCtx = ctx;
mHelper = new DbHelper(ctx);
mDb = mHelper.getWritableDatabase();
mObservers = new ArrayList<DbObserver>();
}
Cursor getRomCursor() {
Cursor c = mDb.query("romlist",
new String[] { "_id", "name" },
null, null, null, null, null);
return c;
}
void insert(Rom r) {
ContentValues values = new ContentValues();
values.put("name", r.getName());
values.put("url", r.getUrl());
values.put("filename", r.getFilename());
values.put("size", r.getSize());
values.put("digest", r.getDigest());
values.put("basis", r.getBasis());
values.put("deltaurl", r.getDeltaUrl());
values.put("deltafilename", r.getDeltaFilename());
values.put("deltasize", r.getDeltaSize());
values.put("mtime", r.getModTime());
values.put("verified", (r.getVerified() ? 1 : 0));
mDb.insert("romlist", null, values);
for (DbObserver o : mObservers) {
o.onContentInsert(r.getName());
}
}
void update(Rom r) {
ContentValues values = new ContentValues();
values.put("url", r.getUrl());
values.put("filename", r.getFilename());
values.put("size", r.getSize());
values.put("digest", r.getDigest());
values.put("basis", r.getBasis());
values.put("deltaurl", r.getDeltaUrl());
values.put("deltafilename", r.getDeltaFilename());
values.put("deltasize", r.getDeltaSize());
mDb.update("romlist",
values,
"name=?",
new String[] { r.getName() });
for (DbObserver o : mObservers) {
o.onContentUpdate(r.getName());
}
}
void delete(Rom r) {
mDb.delete("romlist",
"name=?",
new String[] { r.getName() });
for (DbObserver o : mObservers) {
o.onContentDelete(r.getName());
}
}
void deleteAll() {
mDb.delete("romlist", null, null);
//XXX: no observer callback...?
}
Rom get(String name) {
Rom r = null;
Cursor c = mDb.query("romlist",
new String[] { "url", "filename", "size", "digest", "basis",
"deltaurl", "deltafilename", "deltasize",
"mtime", "verified" },
"name=?",
new String[] { name },
null, null, null);
if (c != null) {
c.moveToFirst();
int idx = 0;
r = new Rom(name);
r.setUrl(c.getString(idx++));
r.setFilename(c.getString(idx++));
r.setSize(c.getInt(idx++));
r.setDigest(c.getString(idx++));
r.setBasis(c.getString(idx++));
r.setDeltaUrl(c.getString(idx++));
r.setDeltaFilename(c.getString(idx++));
r.setDeltaSize(c.getInt(idx++));
long mtime = c.getLong(idx++);
boolean verified = (c.getInt(idx++) != 0);
r.setVerified(verified, mtime);
}
return r;
}
void setVerified(String name, boolean val, long mtime) {
ContentValues values = new ContentValues();
values.put("mtime", mtime);
values.put("verified", (val ? 1 : 0));
mDb.update("romlist",
values,
"name=?",
new String[] { name });
for (DbObserver o : mObservers) {
o.onContentUpdate(name);
}
}
}

View File

@ -0,0 +1,72 @@
package tdm.romkeeper;
import android.content.Context;
import android.util.Log;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
class FileDownloadProgressUpdater implements StreamListener
{
static final int MSG_START = 100;
static final int MSG_PROGRESS = 101;
static final int MSG_FINISH = 102;
static final int SUCCESS = 0;
static final int FAILURE = 1;
private Handler mHandler;
private long mSize;
private long mTotalWritten;
FileDownloadProgressUpdater(Handler handler, long size) {
mHandler = handler;
mSize = size;
}
private void sendStatus(int what, int arg1, int arg2) {
mHandler.sendMessage(Message.obtain(mHandler, what, arg1, arg2));
}
private void sendStatus(int what, int arg1) { sendStatus(what, arg1, 0); }
private void sendStatus(int what) { sendStatus(what, 0, 0); }
private int calcPercent(long n, long d) {
int pct = (100*((int)(n>>8)))/((int)(d>>8));
return pct;
}
long getSize() { return mSize; }
public void updateBytesRead(int len) {}
public void updateBytesWritten(int len) {
if (mSize > 0) {
int oldPct = calcPercent(mTotalWritten, mSize);
int newPct = calcPercent(mTotalWritten+len, mSize);
if (newPct > oldPct) {
sendStatus(MSG_PROGRESS, newPct);
}
}
mTotalWritten += len;
}
void onStart() {
sendStatus(MSG_START);
}
void onFinish(int status) {
sendStatus(MSG_FINISH, status);
}
}

View File

@ -0,0 +1,348 @@
package tdm.romkeeper;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.widget.Toast;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
/*
* XXX:
* Currently we update the database directly with the download/verify
* status. This is not really good practice, and it is probably the
* only thing preventing this from being a general purpose class.
*/
public class FileDownloadService extends Service
{
private static final int DS_NONE = 0;
private static final int DS_VERIFY_EXISTING = 1;
private static final int DS_DOWNLOAD = 2;
private static final int DS_VERIFY_DOWNLOAD = 3;
private static final int DS_COMPLETE = 4;
static final String EXTRA_NAME = "EXTRA_NAME";
static final String EXTRA_FILENAME = "EXTRA_FILENAME";
static final String EXTRA_SIZE = "EXTRA_SIZE";
static final String EXTRA_DIGEST = "EXTRA_DIGEST";
static final String EXTRA_BASIS = "EXTRA_BASIS";
static final String EXTRA_DELTAURL = "EXTRA_DELTAURL";
static final String EXTRA_DELTAFILENAME = "EXTRA_DELTAFILENAME";
static final String EXTRA_DELTASIZE = "EXTRA_DELTASIZE";
static final int NOTIFICATION_ID = 1; /* XXX: need one id per file download */
class DownloadEntry
{
int mState;
int mProgress; /* Download: 0..79, Verify: 80..99 */
String mUrl;
String mName;
String mPathname;
long mSize;
String mDigest;
String mBasis;
String mDeltaUrl;
String mDeltaPathname;
long mDeltaSize;
}
class DownloadStatusHandler extends Handler
{
private FileDownloadService mOwner;
private DownloadEntry mEntry;
DownloadStatusHandler(FileDownloadService owner, DownloadEntry e) {
mOwner = owner;
mEntry = e;
}
public void handleMessage(Message msg) {
switch (msg.what) {
case FileDownloader.MSG_START:
break;
case FileDownloader.MSG_PROGRESS:
mOwner.onDownloadProgress(mEntry, msg.arg1);
break;
case FileDownloader.MSG_FINISH:
mOwner.onDownloadFinish(mEntry, msg.arg1);
break;
default:
Log.e("FileDownloadService", "Unknown msg.what=" + msg.what);
}
}
}
class VerifyStatusHandler extends Handler
{
private FileDownloadService mOwner;
private DownloadEntry mEntry;
VerifyStatusHandler(FileDownloadService owner, DownloadEntry e) {
mOwner = owner;
mEntry = e;
}
public void handleMessage(Message msg) {
switch (msg.what) {
case FileVerifier.MSG_START:
break;
case FileVerifier.MSG_PROGRESS:
mOwner.onVerifyProgress(mEntry, msg.arg1);
break;
case FileVerifier.MSG_FINISH:
mOwner.onVerifyFinish(mEntry, msg.arg1);
break;
default:
Log.e("FileDownloadService", "Unknown msg.what=" + msg.what);
}
}
}
private DbAdapter mDbAdapter;
private NotificationManager mNM;
private Notification mNotification;
private PendingIntent mPendingIntent;
private List<DownloadEntry> mDownloads;
private void updateProgress() {
String status;
int count = mDownloads.size();
if (count == 0) {
status = "Complete";
}
else if (count == 1) {
DownloadEntry entry = mDownloads.get(0);
if (entry.mState == DS_VERIFY_EXISTING || entry.mState == DS_VERIFY_DOWNLOAD) {
status = "Verifying: " + entry.mProgress + "% complete";
}
else {
status = "Downloading: " + entry.mProgress + "% complete";
}
}
else {
int total = 0;
for (DownloadEntry entry : mDownloads) {
total += entry.mProgress;
}
int pct = total/count;
status = "Downloading " + count + " files: " + pct + "% complete";
}
mNotification.setLatestEventInfo(this, "Download", status, mPendingIntent);
mNM.notify(NOTIFICATION_ID, mNotification);
}
private boolean browserDownloadRequired(String location) {
try {
URL url = new URL(location);
String host = url.getHost().toLowerCase();
if (host.indexOf("mediafire.com") != -1) {
return true;
}
}
catch (Exception e) {
// Ignore
}
return false;
}
private void startFetch(DownloadEntry e) {
File f = new File(e.mPathname);
e.mState = DS_NONE;
e.mProgress = 0;
updateProgress();
if (f.exists()) {
Log.i("FileDownloadService", "verify existing");
e.mState = DS_VERIFY_EXISTING;
VerifyStatusHandler h = new VerifyStatusHandler(this, e);
FileVerifier fv = new FileVerifier(h, e.mPathname, e.mSize, e.mDigest);
fv.start();
}
else {
e.mState = DS_DOWNLOAD;
DownloadStatusHandler h = new DownloadStatusHandler(this, e);
/*
* TODO: figure out if mediafire is in the url.
* If so, launch a BrowserDownloader or BrowserPatcher.
* If not, launch a FileFetcher or FilePatcher.
*
* This should all be hidden behind the scenes, especially the
* patching. Or at least the FilePatcher could extend a new
* class StreamPatcher, or something.
*/
FileDownloader downloader;
if (e.mBasis != null && e.mDeltaUrl != null) {
Log.i("FileDownloadService", "patch");
if (browserDownloadRequired(e.mDeltaUrl)) {
downloader = new BrowserFilePatcher(this, h,
e.mDeltaUrl, e.mDeltaPathname, e.mDeltaSize,
e.mPathname, e.mSize, e.mBasis);
}
else {
downloader = new HttpFilePatcher(h,
e.mDeltaUrl, e.mDeltaPathname, e.mDeltaSize,
e.mPathname, e.mSize, e.mBasis);
}
}
else {
Log.i("FileDownloadService", "download");
if (browserDownloadRequired(e.mUrl)) {
downloader = new BrowserFileDownloader(this, h,
e.mUrl, e.mPathname, e.mSize);
}
else {
downloader = new HttpFileDownloader(h,
e.mUrl, e.mPathname, e.mSize);
}
}
downloader.start();
}
}
private void onDownloadProgress(DownloadEntry e, int pct) {
e.mProgress = (pct*80)/100;
updateProgress();
}
private void onDownloadFinish(DownloadEntry e, int status) {
Log.i("FileDownloadService", "onDownloadFinish: status="+status);
if (status == FileDownloader.SUCCESS) {
e.mState = DS_VERIFY_DOWNLOAD;
e.mProgress = 80;
updateProgress();
VerifyStatusHandler h = new VerifyStatusHandler(this, e);
FileVerifier fv = new FileVerifier(h, e.mPathname, e.mSize, e.mDigest);
fv.start();
return;
}
mDownloads.remove(e);
updateProgress();
mDbAdapter.setVerified(e.mName, false, 0);
if (mDownloads.isEmpty()) {
stopSelf();
}
}
private void onVerifyProgress(DownloadEntry e, int pct) {
e.mProgress = 80 + (pct*20)/100;
updateProgress();
}
private void onVerifyFinish(DownloadEntry e, int status) {
Log.i("FileDownloadService", "onVerifyFinish: status="+status);
if (e.mState == DS_VERIFY_EXISTING && status != FileVerifier.SUCCESS) {
Log.i("FileDownloadService", "Verify failed, downloading...");
File f = new File(e.mPathname);
f.delete();
startFetch(e);
return;
}
mDownloads.remove(e);
long mtime = 0;
File f = new File(e.mPathname);
if (f.exists()) {
mtime = f.lastModified();
}
mDbAdapter.setVerified(e.mName, (status == 0), mtime);
if (mDownloads.isEmpty()) {
stopSelf();
}
}
@Override
public void onCreate() {
Log.i("FileDownloadService", "onCreate");
mDbAdapter = DbAdapter.getInstance(this);
mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
mNotification = new Notification(R.drawable.icon, "Download", System.currentTimeMillis());
mNotification.flags |= Notification.FLAG_AUTO_CANCEL;
mPendingIntent = PendingIntent.getActivity(this, 0,
new Intent(this, RomKeeperActivity.class), 0);
mDownloads = new ArrayList<DownloadEntry>();
super.onCreate();
}
@Override
public void onDestroy() {
Log.i("FileDownloadService", "onDestroy");
}
@Override
public IBinder onBind(Intent i) {
return null;
}
@Override
public int onStartCommand(Intent i, int flags, int startId) {
Bundle extras = i.getExtras();
if (extras == null) {
Log.e("FileDownloadService", "no extras in intent");
return START_NOT_STICKY;
}
DownloadEntry e = new DownloadEntry();
e.mState = DS_NONE;
e.mProgress = 0;
e.mUrl = i.getData().toString();
e.mName = extras.getString("EXTRA_NAME");
e.mPathname = "/sdcard/" + extras.getString(EXTRA_FILENAME);
e.mSize = extras.getLong(EXTRA_SIZE);
e.mDigest = extras.getString(EXTRA_DIGEST);
Log.i("FileDownloadService", "onStartCommand: url="+e.mUrl+", pathname="+e.mPathname);
String val;
val = extras.getString(EXTRA_BASIS);
if (val != null && val.length() > 0) {
e.mBasis = "/sdcard/" + val;
Log.i("FileDownloadService", "onStartCommand: basis=" + e.mBasis);
}
val = extras.getString(EXTRA_DELTAURL);
if (val != null && val.length() > 0) {
e.mDeltaUrl = val;
Log.i("FileDownloadService", "onStartCommand: deltaurl=" + e.mDeltaUrl);
e.mDeltaPathname = "/sdcard/" + extras.getString(EXTRA_DELTAFILENAME);
e.mDeltaSize = extras.getLong(EXTRA_DELTASIZE);
}
mDownloads.add(e);
startFetch(e);
return START_STICKY;
}
}

View File

@ -0,0 +1,92 @@
package tdm.romkeeper;
import android.content.Context;
import android.util.Log;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
/*
* Base class for all file download operations. File downloads may be
* initiated in a variety of ways: directly (eg. by URLConnection),
* indirectly (eg. by opening a browser), or whatever.
*/
abstract class FileDownloader extends Thread
implements StreamListener
{
static final int MSG_START = 100;
static final int MSG_PROGRESS = 101;
static final int MSG_FINISH = 102;
static final int SUCCESS = 0;
static final int FAILURE = 1;
private Handler mHandler;
private String mUrl;
private String mPathname;
private long mSize;
private long mTotalWritten;
FileDownloader(String url, String pathname, long size) {
mHandler = null;
mUrl = url;
mPathname = pathname;
mSize = size;
}
FileDownloader(Handler handler, String url, String pathname, long size) {
mHandler = handler;
mUrl = url;
mPathname = pathname;
mSize = size;
}
private void sendStatus(int what, int arg1, int arg2) {
if (mHandler != null) {
mHandler.sendMessage(Message.obtain(mHandler, what, arg1, arg2));
}
}
private void sendStatus(int what, int arg1) { sendStatus(what, arg1, 0); }
private void sendStatus(int what) { sendStatus(what, 0, 0); }
private int calcPercent(long n, long d) {
int pct = (100*((int)(n>>8)))/((int)(d>>8));
return pct;
}
String getUrl() { return mUrl; }
String getPathname() { return mPathname; }
long getSize() { return mSize; }
public void updateBytesRead(int len) {}
public void updateBytesWritten(int len) {
if (mSize > 0) {
int oldPct = calcPercent(mTotalWritten, mSize);
int newPct = calcPercent(mTotalWritten+len, mSize);
if (newPct > oldPct) {
sendStatus(MSG_PROGRESS, newPct);
}
}
mTotalWritten += len;
}
void onStart() {
sendStatus(MSG_START);
}
void onFinish(int status) {
sendStatus(MSG_FINISH, status);
}
}

View File

@ -0,0 +1,122 @@
package tdm.romkeeper;
import android.content.Context;
import android.util.Log;
import android.os.Handler;
import android.os.Message;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.EOFException;
import java.io.IOException;
class FilePatcher
{
private static final int MAX_BUFLEN = 64*1024;
private static final int DELTA_MAGIC = 0x72730236;
private static final byte OP_END = 0x00;
private static final byte OP_LITERAL_N1 = 0x41;
private static final byte OP_LITERAL_N2 = 0x42;
private static final byte OP_LITERAL_N4 = 0x43;
private static final byte OP_COPY_N2_N4 = 0x4b;
private static final byte OP_COPY_N4_N2 = 0x4e;
private static final byte OP_COPY_N4_N4 = 0x4f;
private StreamListener mListener;
private String mBasisPathname;
private InputStream mDeltaStream;
private OutputStream mOutputStream;
private RandomAccessFile mBasis;
FilePatcher(StreamListener listener, String basisPathname, InputStream deltas, OutputStream out) {
mListener = listener;
mBasisPathname = basisPathname;
mDeltaStream = deltas;
mOutputStream = out;
}
private int readInt(int len) throws IOException {
int val = 0;
int b;
while (len-- != 0) {
b = mDeltaStream.read();
if (b == -1) {
throw new EOFException();
}
val = (val << 8) | b;
}
return val;
}
private void readHeader() throws Exception {
if (readInt(4) != DELTA_MAGIC) {
throw new IOException("Bad delta header");
}
}
private void applyLiteral(int n) throws Exception {
byte[] buf = new byte[MAX_BUFLEN];
int bufsz;
int total = readInt(n);
int len;
while (total > 0) {
len = mDeltaStream.read(buf, 0, Math.min(MAX_BUFLEN, total));
if (len == -1) {
throw new IOException("Failed to read from deltas");
}
mOutputStream.write(buf, 0, len);
mListener.updateBytesWritten(len);
total -= len;
}
}
private void applyCopy(int n1, int n2) throws Exception {
byte[] buf = new byte[MAX_BUFLEN];
int bufsz;
int oldoff = readInt(n1);
int total = readInt(n2);
int len;
mBasis.seek(oldoff);
while (total > 0) {
len = mBasis.read(buf, 0, Math.min(MAX_BUFLEN, total));
if (len == -1) {
throw new IOException("Failed to read from basis");
}
mOutputStream.write(buf, 0, len);
mListener.updateBytesWritten(len);
total -= len;
}
}
void patch() throws Exception {
mBasis = new RandomAccessFile(mBasisPathname, "rw");
readHeader();
boolean end = false;
while (!end) {
int cmd = mDeltaStream.read();
switch (cmd) {
case OP_END: end = true; break;
case OP_LITERAL_N1: applyLiteral(1); break;
case OP_LITERAL_N2: applyLiteral(2); break;
case OP_LITERAL_N4: applyLiteral(4); break;
case OP_COPY_N2_N4: applyCopy(2, 4); break;
case OP_COPY_N4_N2: applyCopy(4, 2); break;
case OP_COPY_N4_N4: applyCopy(4, 4); break;
default:
throw new IOException("Bad delta command");
}
}
mBasis.close();
}
}

View File

@ -0,0 +1,93 @@
package tdm.romkeeper;
import android.content.Context;
import android.util.Log;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.security.MessageDigest;
class FileVerifier extends Thread
{
static final int MSG_START = 100;
static final int MSG_PROGRESS = 101;
static final int MSG_FINISH = 102;
static final int SUCCESS = 0;
static final int FAILURE = 1;
private Handler mHandler;
private String mPathname;
private long mSize;
private String mDigest;
private void sendStatus(int what, int arg1, int arg2) {
mHandler.sendMessage(Message.obtain(mHandler, what, arg1, arg2));
}
private void sendStatus(int what, int arg1) { sendStatus(what, arg1, 0); }
private void sendStatus(int what) { sendStatus(what, 0, 0); }
FileVerifier(Handler handler, String pathname, long size, String digest) {
mHandler = handler;
mPathname = pathname;
mSize = size;
mDigest = digest;
}
public void run() {
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
sendStatus(MSG_START);
File f = new File(mPathname);
if (f.length() != mSize) {
throw new Exception("Incorrect length");
}
Log.i("FileVerifier", "verify " + mPathname);
FileInputStream is = new FileInputStream(f);
MessageDigest digester = MessageDigest.getInstance("MD5");
byte[] buf = new byte[4096];
int progress = 0;
long total = 0;
int len;
while ((len = is.read(buf)) > 0) {
digester.update(buf, 0, len);
total += len;
int p = (100*((int)(total>>8)))/((int)(mSize>>8));
if (p > progress) {
progress = p;
sendStatus(MSG_PROGRESS, progress);
}
}
is.close();
byte[] calculatedDigest = digester.digest();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < calculatedDigest.length; ++i) {
sb.append(String.format("%02x", calculatedDigest[i]));
}
Log.i("FileVerifier", "Expected digest=" + mDigest);
Log.i("FileVerifier", "Calculated digest=" + sb.toString());
if (!sb.toString().equalsIgnoreCase(mDigest)) {
throw new Exception("Incorrect digest");
}
sendStatus(MSG_FINISH, SUCCESS);
}
catch (Exception e) {
Log.e("FileVerifier", "exception: " + e.getMessage());
e.printStackTrace();
sendStatus(MSG_FINISH, FAILURE);
}
}
}

View File

@ -0,0 +1,57 @@
package tdm.romkeeper;
import android.os.Handler;
import android.os.Process;
import android.util.Log;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
class HttpFileDownloader extends FileDownloader
{
private static final int MAX_BUFLEN = 64*1024;
HttpFileDownloader(Handler handler, String url, String pathname, long size) {
super(handler, url, pathname, size);
}
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
try {
onStart();
URL url = new URL(getUrl());
URLConnection conn = url.openConnection();
InputStream is = conn.getInputStream();
int len;
len = conn.getContentLength();
if (len > 0 && len != getSize()) {
throw new Exception("Bad content length");
}
Log.i("FileDownloader", "length "+len+" bytes");
OutputStream os = new FileOutputStream(getPathname());
byte[] buf = new byte[MAX_BUFLEN];
while ((len = is.read(buf)) > 0) {
os.write(buf, 0, len);
updateBytesWritten(len);
}
os.close();
is.close();
onFinish(SUCCESS);
}
catch (Exception e) {
Log.e("FileDownloader", "exception: " + e.getMessage());
e.printStackTrace();
onFinish(FAILURE);
}
}
}

View File

@ -0,0 +1,68 @@
package tdm.romkeeper;
import android.content.Context;
import android.util.Log;
import android.os.Handler;
import android.os.Message;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.EOFException;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
class HttpFilePatcher extends FileDownloader
{
private String mDeltaPathname;
private long mDeltaSize;
private String mBasisPathname;
private RandomAccessFile mBasis;
HttpFilePatcher(Handler handler,
String deltaUrl, String deltaPathname, long deltaSize,
String outPathname, long outSize, String basisPathname) {
super(handler, deltaUrl, outPathname, outSize);
mDeltaPathname = deltaPathname;
mDeltaSize = deltaSize;
mBasisPathname = basisPathname;
}
public void run() {
try {
onStart();
URL url = new URL(getUrl());
URLConnection conn = url.openConnection();
InputStream is = conn.getInputStream();
int len = conn.getContentLength();
if (len > 0 && len != mDeltaSize) {
throw new Exception("Bad content length");
}
Log.i("FileDownloader", "length "+len+" bytes");
OutputStream os = new FileOutputStream(getPathname());
FilePatcher patcher = new FilePatcher(this, mBasisPathname, is, os);
patcher.patch();
os.close();
os.close();
onFinish(SUCCESS);
}
catch (Exception e) {
Log.e("FileDownloader", "exception: " + e.getMessage());
e.printStackTrace();
onFinish(FAILURE);
}
}
}

View File

@ -0,0 +1,190 @@
package tdm.romkeeper;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;
import android.widget.Toast;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.MessageDigest;
import java.util.Arrays;
interface ManifestCheckerCallback
{
void onManifestCheckComplete(boolean isUpdated);
}
interface IManifestChecker
{
void doCheck(ManifestCheckerCallback cb);
}
public class ManifestCheckerService extends Service
{
private File mCacheFile;
private ManifestCheckerCallback mCallback;
private ManifestFetcher mFetcher;
public class ManifestCheckerBinder extends Binder
implements IManifestChecker
{
private ManifestCheckerService mService;
ManifestCheckerBinder(ManifestCheckerService svc) {
mService = svc;
}
ManifestCheckerService getService() {
return ManifestCheckerService.this;
}
public void doCheck(ManifestCheckerCallback cb) {
mService.doCheck(cb);
}
}
private final ManifestCheckerBinder mBinder = new ManifestCheckerBinder(this);
class ManifestFetcherHandler extends Handler
{
private ManifestCheckerService mService;
ManifestFetcherHandler(ManifestCheckerService service) {
mService = service;
}
public void handleMessage(Message msg) {
mService.onManifestCheckDone();
}
}
@Override
public void onCreate() {
Log.i("ManifestCheckerService", "onCreate");
String pathname = getCacheDir() + "/manifest.xml";
mCacheFile = new File(pathname);
Intent alarmIntent = new Intent(this, ManifestCheckerService.class);
PendingIntent alarmPending = PendingIntent.getService(this, 0, alarmIntent, 0);
AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE);
am.setInexactRepeating(AlarmManager.RTC,
System.currentTimeMillis(),
AlarmManager.INTERVAL_HOUR,
alarmPending);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i("ManifestCheckerService", "onStartCommand");
startCheck();
// If we get killed, after returning from here, restart
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void onDestroy() {
Log.i("ManifestCheckerService", "onDestroy");
}
private void startCheck() {
if (mFetcher == null) {
Handler handler = new ManifestFetcherHandler(this);
mFetcher = new ManifestFetcher(this, handler);
mFetcher.start();
}
}
void doCheck(ManifestCheckerCallback cb) {
mCallback = cb;
startCheck();
}
boolean isManifestUpdated() {
if (!mCacheFile.exists()) {
Log.i("ManifestCheckerService", "updated: cache file does not exist");
return true;
}
try {
MessageDigest dataDigester = MessageDigest.getInstance("MD5");
dataDigester.update(mFetcher.getData(), 0, mFetcher.getDataLen());
byte[] dataDigest = dataDigester.digest();
MessageDigest fileDigester = MessageDigest.getInstance("MD5");
FileInputStream is = new FileInputStream(mCacheFile);
byte[] buf = new byte[4096];
int len;
while ((len = is.read(buf)) > 0) {
fileDigester.update(buf, 0, len);
}
is.close();
byte[] fileDigest = fileDigester.digest();
if (Arrays.equals(dataDigest, fileDigest)) {
return false;
}
Log.i("ManifestCheckerService", "updated: digests differ");
}
catch (Exception e) {
Log.e("ManifestFetcher", "caught exception: " + e.getMessage());
e.printStackTrace();
}
return true;
}
void writeManifest() {
try {
FileOutputStream os = new FileOutputStream(mCacheFile);
os.write(mFetcher.getData(), 0, mFetcher.getDataLen());
os.close();
}
catch (Exception e) {
Log.e("ManifestFetcher", "caught exception: " + e.getMessage());
e.printStackTrace();
}
}
void onManifestCheckDone() {
boolean isUpdated = isManifestUpdated();
if (isUpdated) {
writeManifest();
NotificationManager nm = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
Notification n = new Notification(R.drawable.icon, "Rom List Updated", System.currentTimeMillis());
n.flags |= Notification.FLAG_AUTO_CANCEL;
PendingIntent i = PendingIntent.getActivity(this, 0,
new Intent(this, RomKeeperActivity.class), 0);
n.setLatestEventInfo(this, "RomKeeper", "Rom List Updated", i);
nm.notify(1, n);
}
if (mCallback != null) {
mCallback.onManifestCheckComplete(isUpdated);
mCallback = null;
}
mFetcher = null;
}
}

View File

@ -0,0 +1,79 @@
package tdm.romkeeper;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import android.util.Log;
import java.io.File;
import java.io.InputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
class ManifestFetcher extends Thread
{
static final String mDefaultManifestBaseUrl = "http://vmroms.com/~vmstorage/romkeeper";
String mManifestBaseUrl;
Context mContext;
Handler mHandler;
byte[] mData;
int mDataLen;
ManifestFetcher(Context ctx, Handler handler) {
mContext = ctx;
mHandler = handler;
SharedPreferences sp = ctx.getSharedPreferences("prefs", Context.MODE_PRIVATE);
mManifestBaseUrl = sp.getString("manifesturl", mDefaultManifestBaseUrl);
}
byte[] getData() { return mData; }
int getDataLen() { return mDataLen; }
public void run() {
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
String manifestUrl = mManifestBaseUrl;
manifestUrl += "/" + android.os.Build.MODEL;
manifestUrl += "/manifest.xml";
Log.i("ManifestFetcher", "manifestUrl=" + manifestUrl);
HttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet(manifestUrl);
HttpResponse response = client.execute(request);
InputStream is = response.getEntity().getContent();
mData = new byte[64*1024];
mDataLen = 0;
int len = 0;
do {
len = is.read(mData, mDataLen, mData.length - mDataLen);
if (len > 0) {
mDataLen += len;
if (mDataLen == mData.length) {
throw new Exception("Manifest too large");
}
}
}
while (len > 0);
is.close();
Log.i("ManifestFetcher", "success");
}
catch (Exception e) {
Log.e("ManifestFetcher", "caught exception: " + e.getMessage());
e.printStackTrace();
}
Message msg = Message.obtain(mHandler, 0, this);
mHandler.sendMessage(msg);
}
}

View File

@ -0,0 +1,77 @@
package tdm.romkeeper;
import java.io.File;
import java.io.InputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import android.util.Log;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
// For testing
import org.xml.sax.InputSource;
import java.io.StringReader;
class ManifestReader
{
ManifestReader() {}
public ArrayList<Rom> readManifest(File cacheDir) {
File cacheFile = new File(cacheDir, "manifest.xml");
ArrayList<Rom> roms = new ArrayList<Rom>();
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document dom = null;
dom = db.parse(cacheFile);
Log.i("ManifestReader", "preparing to traverse");
Element root = dom.getDocumentElement();
Log.i("ManifestReader", "root: " + root.getTagName());
if (!root.getTagName().equals("romlist")) {
throw new Exception("Document not a romlist");
}
NodeList nodes = root.getChildNodes();
for (int i = 0; i < nodes.getLength(); ++i) {
Node node = nodes.item(i);
if (node instanceof Element) {
Element e = (Element)node;
if (e.getTagName().equals("rom")) {
Log.i("ManifestReader", "Found rom");
String name = e.getAttribute("name");
Rom r = new Rom(name);
r.setUrl(e.getAttribute("url"));
r.setFilename(e.getAttribute("filename"));
r.setSize(e.getAttribute("size"));
r.setDigest(e.getAttribute("digest"));
r.setBasis(e.getAttribute("basis"));
r.setDeltaUrl(e.getAttribute("delta-url"));
r.setDeltaFilename(e.getAttribute("delta-filename"));
r.setDeltaSize(e.getAttribute("delta-size"));
roms.add(r);
}
}
}
Log.i("ManifestReader", "success: " + roms.size() + " roms");
}
catch (Exception e) {
Log.e("ManifestReader", "readManifest caught exception: " + e.getMessage());
e.printStackTrace();
}
return roms;
}
}

112
src/tdm/romkeeper/Rom.java Normal file
View File

@ -0,0 +1,112 @@
package tdm.romkeeper;
import android.content.Context;
import android.os.PowerManager;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import android.util.Log;
class Rom
{
private String mName;
private String mUrl;
private String mFilename;
private long mSize;
private String mDigest;
private String mBasis;
private String mDeltaUrl;
private String mDeltaFilename;
private long mDeltaSize;
private boolean mVerified;
private long mModTime;
private int toInt(String s) {
int i = 0;
try {
i = Integer.parseInt(s);
}
catch (Exception e) {
// Ignore it
}
return i;
}
Rom(String name) {
mName = name;
mSize = 0;
mVerified = false;
mModTime = 0;
}
void setUrl(String val) { mUrl = val; }
void setFilename(String val) { mFilename = val; }
void setSize(long val) { mSize = val; }
void setDigest(String val) { mDigest = val; }
void setBasis(String val) { mBasis = val; }
void setDeltaUrl(String val) { mDeltaUrl = val; }
void setDeltaFilename(String val) { mDeltaFilename = val; }
void setDeltaSize(long val) { mDeltaSize = val; }
void setVerified(boolean val, long mtime) { mVerified = val; mModTime = mtime; }
void setSize(String val) { setSize(toInt(val)); }
void setDeltaSize(String val) { setDeltaSize(toInt(val)); }
String getName() { return mName; }
String getUrl() { return mUrl; }
String getFilename() { return mFilename; }
long getSize() { return mSize; }
String getDigest() { return mDigest; }
String getBasis() { return mBasis; }
String getDeltaUrl() { return mDeltaUrl; }
String getDeltaFilename() { return mDeltaFilename; }
long getDeltaSize() { return mDeltaSize; }
boolean getVerified() { return mVerified; }
long getModTime() { return mModTime; }
boolean exists() {
File f = new File("/sdcard/" + mFilename);
return f.exists();
}
public String toString() {
return mName;
}
void install(Context ctx) {
if (!mVerified) {
Log.e("RomKeeper", "not verified");
return;
}
String cmd = "mkdir -p /cache/recovery " +
"&& echo '--update_package=" +
"/sdcard/" + mFilename + "' > /cache/recovery/command " +
"&& reboot recovery";
String[] argv = { "su", "-c", cmd };
String res;
BufferedReader br = null;
try {
Process proc = Runtime.getRuntime().exec(argv);
br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
res = br.readLine();
PowerManager pm = (PowerManager)ctx.getSystemService(Context.POWER_SERVICE);
pm.reboot("recovery");
}
catch (Exception e) {
Log.e("Rom", "install failed: " + e.getMessage());
e.printStackTrace();
}
finally {
try { br.close(); } catch (Exception e) {}
br = null;
}
}
}

View File

@ -0,0 +1,210 @@
package tdm.romkeeper;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase;
import java.io.File;
public class RomDataProvider
{
private static final string DB_NAME = "roms.db";
private static final int DB_VERSION = 1;
private DbHelper mHelper;
private SQLiteDatabase mDb;
private static class DbHelper extends SQLiteOpenHelper
{
DbHelper(Context ctx) {
super(ctx, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(
"CREATE TABLE romlist (" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"name VARCHAR(64) NOT NULL UNIQUE, " +
"url VARCHAR(256) NOT NULL UNIQUE, " +
"filename VARCHAR(64) NOT NULL UNIQUE, " +
"size INTEGER, " +
"digest CHAR(32), " +
"basis VARCHAR(64), " +
"delta VARCHAR(256), " +
"mtime INTEGER, " +
"verified INTEGER" +
")");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS romlist");
onCreate(db);
}
}
DbAdapter(Context ctx) {
mHelper = new DbHelper(ctx);
}
@Override
public boolean onCreate() {
mDb = mHelper.openDatabase(getContext(), DB_NAME, null, DB_VERSION);
return (mDb != null);
}
public Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs, String sort) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables("romlist");
switch (URL_MATCHER.match(url)) {
case NAME:
qb.setProjectionMap(NAMES_LIST_PROJECTION_MAP);
break;
case NAME_ID:
qb.appendWhere("_id=" + uri.getPathSegments().get(1));
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
String orderBy;
if (TextUtils.isEmpty(sort)) {
orderBy = "_id DESC";
}
else {
orderBy = sort;
}
Cursor c = qb.query(mDb, projection, selection, selectionArgs, null, null, orderBy);
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
}
@Override
public String getType(Uri uri) {
switch (URL_MATCHER.match(uri)) {
case NAME:
return "vnd.android.cursor.dir/tdm.romkeeper.string";
case NAME_ID:
return "vnd.android.cursor.item/tdm.romkeeper.string";
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
@Override
public Uri insert(Uri uri, ContentValues initialValues) {
long rowid;
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
}
else {
values = new ContentValues();
}
if (URI_MATCHER.match(uri) != NAMES) {
throw new IllegalArgumentException("Unknown URI " + uri);
}
Resources r = Resources.getSystem();
if (!values.containsKey(roms)) {
values.put("roms", "");
}
values.put(SimpleString.Strings._SYNC_VERSION,
Long.toString(System.currentTimeMillis()));
rowid = mDb.insert("roms", "romlist", values);
if (rowid > 0) {
Uri uri = Uri.withAppendedPath(SimpleString.Strings.CONTENT_URI,
Long.toString(rowid));
getContext().getContentResolver().notifyChange(uri, null);
}
}
@Override
public int delete(Uri uri, String where, String[] whereArgs) {
int count;
int rowid = 0;
switch (URI_MATCHER.match(uri)) {
case NAMES:
count = mDb.delete("roms", where, whereArgs);
break;
case NAME_ID:
String segment = uri.getPathSegments().get(1);
rowid = Long.parseLong(segment);
count = mDb.delete("roms", "_id=" +
segment +
(!TextUtils.isEmpty(where) ? " AND (" + where
+ ')' : ""), whereArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
@Override
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
int count;
values.put(SimpleString.Strings._SYNC_VERSION,
Long.toString(System.currentTimeMillis()));
switch (URL_MATCHER.match(uri)) {
case NAMES:
count = mDb.update("roms", values, where, whereArgs);
break;
case NAME_ID:
String segment = uri.getPathSegments().get(1);
count = mDb.update("roms", values, "_id="
+ segment
+ (!TextUtils.isEmpty(where) ? " AND (" + where
+ ')' : ""), whereArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
getContext().getContentResolver().notifyChange(url, null);
return count;
}
/* Rom extensions */
Rom get(String name) {
Rom r = null;
Cursor c = mDb.query("romlist",
new String[] { "url", "filename", "size", "digest", "basis", "delta" },
"name="+name, null, null, null, null);
if (c != null) {
c.moveToFirst();
int idx = 0;
r = new Rom(name);
r.setUrl(c.getString(idx++));
r.setFilename(c.getString(idx++));
r.setSize(c.getInt(idx++));
r.setDigest(c.getString(idx++));
r.setBasis(c.getString(idx++));
r.setDelta(c.getString(idx++));
}
return r;
}
void setVerified(String name, boolean val) {
File f = new File(name);
ContentValues values = new ContentValues();
if (val) {
values.put("mtime", f.lastModified());
values.put("verified", 1);
}
else {
values.put("mtime", 0);
values.put("verified", 0);
}
mDb.update("romlist", values, "name="+name, null);
}
}

View File

@ -0,0 +1,316 @@
package tdm.romkeeper;
import android.app.Activity;
import android.app.AlarmManager;
import android.app.ListActivity;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.sqlite.SQLiteDatabase;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.RelativeLayout;
import java.util.ArrayList;
import android.content.BroadcastReceiver;
public class RomKeeperActivity extends Activity
implements AdapterView.OnItemClickListener,
ManifestCheckerCallback,
DbObserver
{
static final String TAG = "RomKeeper";
Rom mCurrentRom;
private boolean mShowingDetail;
private ManifestCheckerService mManifestCheckerService;
private ServiceConnection mManifestCheckerConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
mManifestCheckerService = ((ManifestCheckerService.ManifestCheckerBinder)service).getService();
}
public void onServiceDisconnected(ComponentName className) {
mManifestCheckerService = null;
}
};
private ListView mRomListView;
private ArrayAdapter<Rom> mRomListAdapter;
private ListView mRomDetailView;
private ArrayAdapter<String> mRomDetailAdapter;
private DbAdapter mDb;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
bindService(new Intent(RomKeeperActivity.this, ManifestCheckerService.class),
mManifestCheckerConnection, Context.BIND_AUTO_CREATE);
/*
* This does not work, returns NULL. WTF?!?
* mRomListView = (ListView)findViewById(R.id.romlist_view);
*/
mRomListView = new ListView(this);
mRomListAdapter = new ArrayAdapter<Rom>(this,
R.layout.list_item, new ArrayList<Rom>());
mRomListView.setAdapter(mRomListAdapter);
mRomListView.setOnItemClickListener(this);
mRomDetailView = new ListView(this);
mRomDetailAdapter = new ArrayAdapter<String>(this,
R.layout.list_item, new ArrayList<String>());
mRomDetailView.setAdapter(mRomDetailAdapter);
/* XXX: should not do this in the UI thread */
mDb = DbAdapter.getInstance(this);
Cursor c = mDb.getRomCursor();
if (c.moveToFirst()) {
while (!c.isAfterLast()) {
String name = c.getString(1);
Log.i("RomKeeperActivity", "onCreate: found rom " + name);
Rom r = mDb.get(name);
mRomListAdapter.add(r);
c.moveToNext();
}
}
c.close();
mDb.registerObserver(this);
showRomList();
}
protected void onResume() {
Log.e(TAG, "onResume");
super.onResume();
}
protected void onSaveInstanceState(Bundle state) {
Log.i(TAG, "onSaveInstanceState");
}
protected void onPause() {
Log.e(TAG, "onPause");
super.onPause();
// ...?
}
protected void onStop() {
Log.e(TAG, "onStop");
super.onStop();
}
protected void onDestroy() {
unbindService(mManifestCheckerConnection);
super.onDestroy();
}
void showRomList() {
mShowingDetail = false;
mCurrentRom = null;
setContentView(mRomListView);
}
void showRomDetail(Rom r) {
mShowingDetail = true;
mCurrentRom = r;
refreshRomDetail();
setContentView(mRomDetailView);
}
public boolean onCreateOptionsMenu(Menu menu) {
Log.i(TAG, "onCreateOptionsMenu");
return super.onCreateOptionsMenu(menu);
}
public boolean onPrepareOptionsMenu(Menu menu) {
Log.i(TAG, "onPrepareOptionsMenu");
menu.clear();
MenuInflater inflater = getMenuInflater();
if (mShowingDetail) {
inflater.inflate(R.menu.romdetail_options_menu, menu);
}
else {
inflater.inflate(R.menu.romlist_options_menu, menu);
}
return super.onPrepareOptionsMenu(menu);
}
private void doCheckManifest() {
mManifestCheckerService.doCheck(this);
}
public void onManifestCheckComplete(boolean isUpdated) {
Log.i("RomKeeperActivity", "onManifestCheckComplete");
if (isUpdated) {
ManifestReader reader = new ManifestReader();
ArrayList<Rom> newList = reader.readManifest(getCacheDir());
/* XXX: should not do this in the UI thread */
mDb.deleteAll();
mRomListAdapter.clear();
mRomListAdapter.notifyDataSetChanged();
for (Rom r : newList) {
mDb.insert(r);
}
}
}
private void refreshRomDetail() {
mRomDetailAdapter.clear();
Rom r = mCurrentRom;
String status = "not present";
if (r.exists()) {
status = "present";
}
if (r.getVerified()) {
status = "verified";
}
mRomDetailAdapter.add("name: " + r.getName());
mRomDetailAdapter.add("filename: " + r.getFilename());
mRomDetailAdapter.add("size: " + r.getSize());
mRomDetailAdapter.add("digest: " + r.getDigest());
mRomDetailAdapter.add("status: " + status);
mRomDetailAdapter.notifyDataSetChanged();
}
private void fetchRom() {
if (mCurrentRom == null) {
Log.e(TAG, "fetchRom: no current rom");
return;
}
Intent i = new Intent(this, FileDownloadService.class);
i.setData(Uri.parse(mCurrentRom.getUrl()));
i.putExtra(FileDownloadService.EXTRA_NAME, mCurrentRom.getName());
i.putExtra(FileDownloadService.EXTRA_FILENAME, mCurrentRom.getFilename());
i.putExtra(FileDownloadService.EXTRA_SIZE, mCurrentRom.getSize());
i.putExtra(FileDownloadService.EXTRA_DIGEST, mCurrentRom.getDigest());
i.putExtra(FileDownloadService.EXTRA_BASIS, mCurrentRom.getBasis());
i.putExtra(FileDownloadService.EXTRA_DELTAURL, mCurrentRom.getDeltaUrl());
i.putExtra(FileDownloadService.EXTRA_DELTAFILENAME, mCurrentRom.getDeltaFilename());
i.putExtra(FileDownloadService.EXTRA_DELTASIZE, mCurrentRom.getDeltaSize());
startService(i);
}
void onFetchRomUpdate() {
refreshRomDetail(); //XXX: inefficient
}
void onFetchRomDone() {
Toast.makeText(this, "Fetch complete", Toast.LENGTH_SHORT).show();
}
private void installRom() {
if (mCurrentRom == null) {
Log.e(TAG, "installRom: no current rom");
return;
}
mCurrentRom.install(this);
}
private void showSettings() {
startActivity(new Intent(this, RomKeeperPreferenceActivity.class));
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_refresh:
doCheckManifest();
break;
case R.id.menu_fetch:
fetchRom();
break;
case R.id.menu_install:
installRom();
break;
case R.id.menu_settings:
showSettings();
break;
}
return super.onOptionsItemSelected(item);
}
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Log.i(TAG, "onItemClick: pos=" + position + ", id=" + id);
if (mShowingDetail) {
// XXX: do anything useful here?
return;
}
Rom r = mRomListAdapter.getItem(position);
showRomDetail(r);
}
public void onBackPressed() {
Log.i("RomKeeperActivity", "onBackPressed");
if (mShowingDetail) {
Log.i("RomKeeperActivity", ".. showing rom list");
showRomList();
return;
}
Log.i("RomKeeperActivity", ".. calling super");
super.onBackPressed();
}
public void onContentInsert(String name) {
Rom r = mDb.get(name);
mRomListAdapter.add(r);
mRomListAdapter.notifyDataSetChanged();
}
public void onContentUpdate(String name) {
if (mShowingDetail && mCurrentRom != null && mCurrentRom.getName().equals(name)) {
mCurrentRom = mDb.get(name);
refreshRomDetail();
}
}
public void onContentDelete(String name) {
Rom r = mDb.get(name);
mRomListAdapter.remove(r);
if (mShowingDetail && mCurrentRom != null && mCurrentRom.getName().equals(name)) {
showRomList();
}
}
}

View File

@ -0,0 +1,14 @@
package tdm.romkeeper;
import android.preference.PreferenceActivity;
import android.os.Bundle;
public class RomKeeperPreferenceActivity extends PreferenceActivity
{
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
}
}

View File

@ -0,0 +1,25 @@
package tdm.romkeeper;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.util.Log;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
public class StartupReceiver extends BroadcastReceiver
{
@Override
public void onReceive(Context ctx, Intent intent) {
Log.i("StartupReceiver", "onReceive called");
SharedPreferences sp = ctx.getSharedPreferences("prefs", Context.MODE_PRIVATE);
boolean autocheck = sp.getBoolean("autocheck", false);
if (autocheck) {
Intent i = new Intent(ctx, ManifestCheckerService.class);
ctx.startService(i);
}
}
}

View File

@ -0,0 +1,7 @@
package tdm.romkeeper;
interface StreamListener
{
void updateBytesRead(int len);
void updateBytesWritten(int len);
}