Small Issues & Maintenance

-=-=-=-=-=-=-=-=-=-=-=-=-=

-Begin work on Navigation Meshes (simple pathfinding for now, will improve soon)
-More doc on theme overriding
-Upgraded OpenSSL to version without bugs
-Misc bugfixes
This commit is contained in:
Juan Linietsky
2014-08-01 22:10:38 -03:00
parent 9ff6d55822
commit 678948068b
1323 changed files with 11564 additions and 39728 deletions

View File

@@ -10,12 +10,12 @@
android:largeScreens="true"
android:xlargeScreens="true"/>
<application android:label="@string/godot_project_name_string" android:icon="@drawable/icon">
<application android:label="@string/godot_project_name_string" android:icon="@drawable/icon" android:allowBackup="false">
<activity android:name="com.android.godot.Godot"
android:label="@string/godot_project_name_string"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:launchMode="singleTask"
android:screenOrientation="landscape"
android:screenOrientation="portrait"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize">
<intent-filter>
@@ -23,6 +23,7 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name="com.android.godot.GodotDownloaderService" />

View File

@@ -61,8 +61,11 @@ def configure(env):
env.Tool('gcc')
env['SPAWN'] = methods.win32_spawn
env.android_source_modules.append("../libs/apk_expansion")
# env.android_source_modules.append("../libs/apk_expansion")
env.android_source_modules.append("../libs/google_play_services")
env.android_source_modules.append("../libs/downloader_library")
env.android_source_modules.append("../libs/play_licensing")
ndk_platform=""
ndk_platform="android-15"

View File

@@ -426,7 +426,7 @@ static String _parse_string(const uint8_t *p_bytes,bool p_utf8) {
}
offset+=2;
printf("len %i, unicode: %i\n",len,int(p_utf8));
//printf("len %i, unicode: %i\n",len,int(p_utf8));
if (p_utf8) {

View File

@@ -0,0 +1,166 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0"
android:orientation="vertical" >
<TextView
android:id="@+id/statusText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginLeft="5dp"
android:layout_marginTop="10dp"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/downloaderDashboard"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/statusText"
android:orientation="vertical" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1" >
<TextView
android:id="@+id/progressAsFraction"
style="@android:style/TextAppearance.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_marginLeft="5dp"
android:text="0MB / 0MB" >
</TextView>
<TextView
android:id="@+id/progressAsPercentage"
style="@android:style/TextAppearance.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignRight="@+id/progressBar"
android:text="0%" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/progressAsFraction"
android:layout_marginBottom="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp"
android:layout_weight="1" />
<TextView
android:id="@+id/progressAverageSpeed"
style="@android:style/TextAppearance.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@+id/progressBar"
android:layout_marginLeft="5dp" />
<TextView
android:id="@+id/progressTimeRemaining"
style="@android:style/TextAppearance.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignRight="@+id/progressBar"
android:layout_below="@+id/progressBar" />
</RelativeLayout>
<LinearLayout
android:id="@+id/downloaderDashboard"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<Button
android:id="@+id/pauseButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginBottom="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="5dp"
android:layout_marginTop="10dp"
android:layout_weight="0"
android:minHeight="40dp"
android:minWidth="94dp"
android:text="@string/text_button_pause" />
<Button
android:id="@+id/cancelButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginBottom="10dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginTop="10dp"
android:layout_weight="0"
android:minHeight="40dp"
android:minWidth="94dp"
android:text="@string/text_button_cancel"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/approveCellular"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:visibility="gone" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:id="@+id/textPausedParagraph1"
android:text="@string/text_paused_cellular" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:id="@+id/textPausedParagraph2"
android:text="@string/text_paused_cellular_2" />
<LinearLayout
android:id="@+id/buttonRow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<Button
android:id="@+id/resumeOverCellular"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp"
android:text="@string/text_button_resume_cellular" />
<Button
android:id="@+id/wifiSettingsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp"
android:text="@string/text_button_wifi_settings" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -3,5 +3,15 @@
<string name="godot_project_name_string">godot-project-name</string>
<string name="testuf8">元気です</string>
<string name="testuf2">元気です元気です元気です</string>
<string name="text_paused_cellular">Would you like to enable downloading over cellular connections? Depending on your data plan, this may cost you money.</string>
<string name="text_paused_cellular_2">If you choose not to enable downloading over cellular connections, the download will automatically resume when wi-fi is available.</string>
<string name="text_button_resume_cellular">Resume download</string>
<string name="text_button_wifi_settings">Wi-Fi settings</string>
<string name="text_verifying_download">Verifying Download</string>
<string name="text_validation_complete">XAPK File Validation Complete. Select OK to exit.</string>
<string name="text_validation_failed">XAPK File Validation Failed.</string>
<string name="text_button_pause">Pause Download</string>
<string name="text_button_resume">Resume Download</string>
<string name="text_button_cancel">Cancel</string>
<string name="text_button_cancel_verify">Cancel Verification</string>
</resources>

View File

@@ -28,16 +28,20 @@
/*************************************************************************/
package com.android.godot;
import android.R;
import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.view.ViewGroup.LayoutParams;
import android.app.*;
import android.content.*;
import android.content.SharedPreferences.Editor;
import android.view.*;
import android.view.inputmethod.InputMethodManager;
import android.os.*;
@@ -48,33 +52,82 @@ import android.text.*;
import android.media.*;
import android.hardware.*;
import android.content.*;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.media.MediaPlayer;
import java.lang.reflect.Method;
import java.util.List;
import java.util.ArrayList;
import com.android.godot.payments.PaymentsManager;
import java.io.IOException;
import android.provider.Settings.Secure;
import android.widget.FrameLayout;
import com.android.godot.input.*;
import java.io.InputStream;
import com.android.godot.input.*;
import java.io.InputStream;
import javax.microedition.khronos.opengles.GL10;
import java.security.MessageDigest;
import java.io.File;
import java.io.FileInputStream;
import java.util.LinkedList;
public class Godot extends Activity implements SensorEventListener
import com.google.android.vending.expansion.downloader.Constants;
import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller;
import com.google.android.vending.expansion.downloader.Helpers;
import com.google.android.vending.expansion.downloader.IDownloaderClient;
import com.google.android.vending.expansion.downloader.IDownloaderService;
import com.google.android.vending.expansion.downloader.IStub;
import android.os.Bundle;
import android.os.Messenger;
import android.os.SystemClock;
public class Godot extends Activity implements SensorEventListener, IDownloaderClient
{
static final int MAX_SINGLETONS = 64;
private IStub mDownloaderClientStub;
private IDownloaderService mRemoteService;
private TextView mStatusText;
private TextView mProgressFraction;
private TextView mProgressPercent;
private TextView mAverageSpeed;
private TextView mTimeRemaining;
private ProgressBar mPB;
private View mDashboard;
private View mCellMessage;
private Button mPauseButton;
private Button mWiFiSettingsButton;
private boolean mStatePaused;
private int mState;
private void setState(int newState) {
if (mState != newState) {
mState = newState;
mStatusText.setText(Helpers.getDownloaderStringResourceIDFromState(newState));
}
}
private void setButtonPausedState(boolean paused) {
mStatePaused = paused;
int stringResourceID = paused ? com.godot.game.R.string.text_button_resume :
com.godot.game.R.string.text_button_pause;
mPauseButton.setText(stringResourceID);
}
static public class SingletonBase {
protected void registerClass(String p_name, String[] p_methods) {
GodotLib.singleton(p_name,this);
@@ -83,20 +136,20 @@ public class Godot extends Activity implements SensorEventListener
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
boolean found=false;
System.out.printf("METHOD: %s\n",method.getName());
Log.d("XXX","METHOD: %s\n" + method.getName());
for (String s : p_methods) {
System.out.printf("METHOD CMP WITH: %s\n",s);
Log.d("XXX", "METHOD CMP WITH: %s\n" + s);
if (s.equals(method.getName())) {
found=true;
System.out.printf("METHOD CMP VALID");
Log.d("XXX","METHOD CMP VALID");
break;
}
}
if (!found)
continue;
System.out.printf("METHOD FOUND: %s\n",method.getName());
Log.d("XXX","METHOD FOUND: %s\n" + method.getName());
List<String> ptr = new ArrayList<String>();
@@ -230,7 +283,7 @@ public class Godot extends Activity implements SensorEventListener
byte[] len = new byte[4];
int r = is.read(len);
if (r<4) {
System.out.printf("**ERROR** Wrong cmdline length.\n");
Log.d("XXX","**ERROR** Wrong cmdline length.\n");
Log.d("GODOT", "**ERROR** Wrong cmdline length.\n");
return new String[0];
}
@@ -258,7 +311,6 @@ public class Godot extends Activity implements SensorEventListener
return cmdline;
} catch (Exception e) {
e.printStackTrace();
System.out.printf("**ERROR** No commandline.\n");
Log.d("GODOT", "**ERROR** Exception " + e.getClass().getName() + ":" + e.getMessage());
return new String[0];
}
@@ -277,12 +329,14 @@ public class Godot extends Activity implements SensorEventListener
String[] new_cmdline;
int cll=0;
if (command_line!=null) {
Log.d("GODOT", "initializeGodot: command_line: is not null" );
new_cmdline = new String[ command_line.length + 2 ];
cll=command_line.length;
for(int i=0;i<command_line.length;i++) {
new_cmdline[i]=command_line[i];
}
} else {
Log.d("GODOT", "initializeGodot: command_line: is null" );
new_cmdline = new String[ 2 ];
}
@@ -294,6 +348,13 @@ public class Godot extends Activity implements SensorEventListener
io = new GodotIO(this);
io.unique_id = Secure.getString(getContentResolver(), Secure.ANDROID_ID);
GodotLib.io=io;
Log.d("GODOT", "command_line is null? " + ((command_line == null)?"yes":"no"));
if(command_line != null){
Log.d("GODOT", "Command Line:");
for(int w=0;w <command_line.length;w++){
Log.d("GODOT"," " + command_line[w]);
}
}
GodotLib.initialize(this,io.needsReloadHooks(),command_line);
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
@@ -306,10 +367,16 @@ public class Godot extends Activity implements SensorEventListener
}
@Override
public void onServiceConnected(Messenger m) {
mRemoteService = DownloaderServiceMarshaller.CreateProxy(m);
mRemoteService.onClientUpdated(mDownloaderClientStub.getMessenger());
}
@Override protected void onCreate(Bundle icicle) {
@Override
protected void onCreate(Bundle icicle) {
System.out.printf("** GODOT ACTIVITY CREATED HERE ***\n");
Log.d("GODOT", "** GODOT ACTIVITY CREATED HERE ***\n");
super.onCreate(icicle);
_self = this;
@@ -319,7 +386,8 @@ public class Godot extends Activity implements SensorEventListener
//check for apk expansion API
if (!true) {
if (true) {
boolean md5mismatch = false;
command_line = getCommandLine();
boolean use_apk_expansion=false;
String main_pack_md5=null;
@@ -338,17 +406,25 @@ public class Godot extends Activity implements SensorEventListener
i++;
} else if (has_extra && command_line[i].equals("-apk_expansion_key")) {
main_pack_key=command_line[i+1];
SharedPreferences prefs = getSharedPreferences("app_data_keys", MODE_PRIVATE);
Editor editor = prefs.edit();
editor.putString("store_public_key", main_pack_key);
editor.commit();
i++;
} else if (command_line[i].trim().length()!=0){
new_args.add(command_line[i]);
}
}
if (new_args.isEmpty())
if (new_args.isEmpty()){
Log.d("GODOT", "new_args is empty");
command_line=null;
else
}else{
Log.d("GODOT", "new_args is not empty");
command_line = new_args.toArray(new String[new_args.size()]);
Log.d("GODOT", "command line is null? " + ( (command_line == null) ? "yes":"no"));
}
if (use_apk_expansion && main_pack_md5!=null && main_pack_key!=null) {
//check that environment is ok!
if (!Environment.getExternalStorageState().equals( Environment.MEDIA_MOUNTED )) {
@@ -374,49 +450,62 @@ public class Godot extends Activity implements SensorEventListener
pack_valid=false;
Log.d("GODOT","**PACK** - File does not exist");
} else {
try {
InputStream fis = new FileInputStream(expansion_pack_path);
// Create MD5 Hash
byte[] buffer = new byte[16384];
MessageDigest complete = MessageDigest.getInstance("MD5");
int numRead;
do {
numRead = fis.read(buffer);
if (numRead > 0) {
complete.update(buffer, 0, numRead);
}
} while (numRead != -1);
fis.close();
byte[] messageDigest = complete.digest();
// Create Hex String
StringBuffer hexString = new StringBuffer();
for (int i=0; i<messageDigest.length; i++)
hexString.append(Integer.toHexString(0xFF & messageDigest[i]));
String md5str = hexString.toString();
Log.d("GODOT","**PACK** - My MD5: "+hexString+" - APK md5: "+main_pack_md5);
if (!hexString.equals(main_pack_md5)) {
pack_valid=false;
}
} catch (Exception e) {
e.printStackTrace();
Log.d("GODOT","**PACK FAIL**");
pack_valid=false;
} else if( obbIsCorrupted(expansion_pack_path, main_pack_md5)){
Log.d("GODOT", "**PACK** - Expansion pack (obb) is corrupted");
pack_valid = false;
try{
f.delete();
}catch(Exception e){
Log.d("GODOT", "**PACK** - Error deleting corrupted expansion pack (obb)");
}
}
if (!pack_valid) {
Log.d("GODOT", "Tengo que bajarme el apk");
Intent notifierIntent = new Intent(this, this.getClass());
notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT);
int startResult;
try {
Log.d("GODOT", "INICIANDO DOWNLOAD SERVICE");
startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(
getApplicationContext(),
pendingIntent,
GodotDownloaderService.class);
Log.d("GODOT", "DOWNLOAD SERVICE FINISHED:" + startResult);
if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
Log.d("GODOT", "DOWNLOAD REQUIRED");
// This is where you do set up to display the download
// progress (next step)
mDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this,
GodotDownloaderService.class);
setContentView(com.godot.game.R.layout.downloading_expansion);
mPB = (ProgressBar) findViewById(com.godot.game.R.id.progressBar);
mStatusText = (TextView) findViewById(com.godot.game.R.id.statusText);
mProgressFraction = (TextView) findViewById(com.godot.game.R.id.progressAsFraction);
mProgressPercent = (TextView) findViewById(com.godot.game.R.id.progressAsPercentage);
mAverageSpeed = (TextView) findViewById(com.godot.game.R.id.progressAverageSpeed);
mTimeRemaining = (TextView) findViewById(com.godot.game.R.id.progressTimeRemaining);
mDashboard = findViewById(com.godot.game.R.id.downloaderDashboard);
mCellMessage = findViewById(com.godot.game.R.id.approveCellular);
mPauseButton = (Button) findViewById(com.godot.game.R.id.pauseButton);
mWiFiSettingsButton = (Button) findViewById(com.godot.game.R.id.wifiSettingsButton);
return;
} else{
Log.d("GODOT", "NO DOWNLOAD REQUIRED");
}
} catch (NameNotFoundException e) {
// TODO Auto-generated catch block
Log.d("GODOT", "Error downloading expansion package:" + e.getMessage());
}
}
@@ -431,6 +520,7 @@ public class Godot extends Activity implements SensorEventListener
}
@Override protected void onDestroy(){
if(mPaymentsManager != null ) mPaymentsManager.destroy();
@@ -442,8 +532,12 @@ public class Godot extends Activity implements SensorEventListener
@Override protected void onPause() {
super.onPause();
if (!godot_initialized)
if (!godot_initialized){
if (null != mDownloaderClientStub) {
mDownloaderClientStub.disconnect(this);
}
return;
}
mView.onPause();
mSensorManager.unregisterListener(this);
GodotLib.focusout();
@@ -455,8 +549,12 @@ public class Godot extends Activity implements SensorEventListener
@Override protected void onResume() {
super.onResume();
if (!godot_initialized)
if (!godot_initialized){
if (null != mDownloaderClientStub) {
mDownloaderClientStub.connect(this);
}
return;
}
mView.onResume();
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
@@ -466,6 +564,8 @@ public class Godot extends Activity implements SensorEventListener
singletons[i].onMainResume();
}
}
@@ -508,6 +608,54 @@ public class Godot extends Activity implements SensorEventListener
}
private boolean obbIsCorrupted(String f, String main_pack_md5){
try {
InputStream fis = new FileInputStream(f);
// Create MD5 Hash
byte[] buffer = new byte[16384];
MessageDigest complete = MessageDigest.getInstance("MD5");
int numRead;
do {
numRead = fis.read(buffer);
if (numRead > 0) {
complete.update(buffer, 0, numRead);
}
} while (numRead != -1);
fis.close();
byte[] messageDigest = complete.digest();
// Create Hex String
StringBuffer hexString = new StringBuffer();
for (int i=0; i<messageDigest.length; i++) {
String s = Integer.toHexString(0xFF & messageDigest[i]);
if (s.length()==1) {
s="0"+s;
}
hexString.append(s);
}
String md5str = hexString.toString();
//Log.d("GODOT","**PACK** - My MD5: "+hexString+" - APK md5: "+main_pack_md5);
if (!md5str.equals(main_pack_md5)) {
Log.d("GODOT","**PACK MD5 MISMATCH???** - MD5 Found: "+md5str+" "+Integer.toString(md5str.length())+" - MD5 Expected: "+main_pack_md5+" "+Integer.toString(main_pack_md5.length()));
return true;
}
return false;
} catch (Exception e) {
e.printStackTrace();
Log.d("GODOT","**PACK FAIL**");
return true;
}
}
//@Override public boolean dispatchTouchEvent (MotionEvent event) {
public boolean gotTouchEvent(MotionEvent event) {
@@ -602,5 +750,115 @@ public class Godot extends Activity implements SensorEventListener
// Audio
/**
* The download state should trigger changes in the UI --- it may be useful
* to show the state as being indeterminate at times. This sample can be
* considered a guideline.
*/
@Override
public void onDownloadStateChanged(int newState) {
Log.d("GODOT", "onDownloadStateChanged:" + newState);
setState(newState);
boolean showDashboard = true;
boolean showCellMessage = false;
boolean paused;
boolean indeterminate;
switch (newState) {
case IDownloaderClient.STATE_IDLE:
Log.d("GODOT", "STATE IDLE");
// STATE_IDLE means the service is listening, so it's
// safe to start making calls via mRemoteService.
paused = false;
indeterminate = true;
break;
case IDownloaderClient.STATE_CONNECTING:
case IDownloaderClient.STATE_FETCHING_URL:
Log.d("GODOT", "STATE CONNECTION / FETCHING URL");
showDashboard = true;
paused = false;
indeterminate = true;
break;
case IDownloaderClient.STATE_DOWNLOADING:
Log.d("GODOT", "STATE DOWNLOADING");
paused = false;
showDashboard = true;
indeterminate = false;
break;
case IDownloaderClient.STATE_FAILED_CANCELED:
case IDownloaderClient.STATE_FAILED:
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
case IDownloaderClient.STATE_FAILED_UNLICENSED:
Log.d("GODOT", "MANY TYPES OF FAILING");
paused = true;
showDashboard = false;
indeterminate = false;
break;
case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
Log.d("GODOT", "PAUSED FOR SOME STUPID REASON");
showDashboard = false;
paused = true;
indeterminate = false;
showCellMessage = true;
break;
case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
Log.d("GODOT", "PAUSED BY STUPID USER");
paused = true;
indeterminate = false;
break;
case IDownloaderClient.STATE_PAUSED_ROAMING:
case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
Log.d("GODOT", "PAUSED BY ROAMING WTF!?");
paused = true;
indeterminate = false;
break;
case IDownloaderClient.STATE_COMPLETED:
Log.d("GODOT", "COMPLETED");
showDashboard = false;
paused = false;
indeterminate = false;
// validateXAPKZipFiles();
initializeGodot();
return;
default:
Log.d("GODOT", "DEFAULT ????");
paused = true;
indeterminate = true;
showDashboard = true;
}
int newDashboardVisibility = showDashboard ? View.VISIBLE : View.GONE;
if (mDashboard.getVisibility() != newDashboardVisibility) {
mDashboard.setVisibility(newDashboardVisibility);
}
int cellMessageVisibility = showCellMessage ? View.VISIBLE : View.GONE;
if (mCellMessage.getVisibility() != cellMessageVisibility) {
mCellMessage.setVisibility(cellMessageVisibility);
}
mPB.setIndeterminate(indeterminate);
setButtonPausedState(paused);
}
@Override
public void onDownloadProgress(DownloadProgressInfo progress) {
mAverageSpeed.setText(getString(com.godot.game.R.string.kilobytes_per_second,
Helpers.getSpeedString(progress.mCurrentSpeed)));
mTimeRemaining.setText(getString(com.godot.game.R.string.time_remaining,
Helpers.getTimeRemaining(progress.mTimeRemaining)));
progress.mOverallTotal = progress.mOverallTotal;
mPB.setMax((int) (progress.mOverallTotal >> 8));
mPB.setProgress((int) (progress.mOverallProgress >> 8));
mProgressPercent.setText(Long.toString(progress.mOverallProgress
* 100 /
progress.mOverallTotal) + "%");
mProgressFraction.setText(Helpers.getDownloadProgressString
(progress.mOverallProgress,
progress.mOverallTotal));
}
}

View File

@@ -6,6 +6,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.util.Log;
/**
* You should start your derived downloader class when this receiver gets the message
@@ -18,10 +19,12 @@ public class GodotDownloaderAlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("GODOT", "Alarma recivida");
try {
DownloaderClientMarshaller.startDownloadServiceIfRequired(context, intent, GodotDownloaderService.class);
} catch (NameNotFoundException e) {
e.printStackTrace();
Log.d("GODOT", "Exception: " + e.getClass().getName() + ":" + e.getMessage());
}
}
}

View File

@@ -1,5 +1,9 @@
package com.android.godot;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.google.android.vending.expansion.downloader.impl.DownloaderService;
/**
@@ -21,7 +25,11 @@ public class GodotDownloaderService extends DownloaderService {
*/
@Override
public String getPublicKey() {
return BASE64_PUBLIC_KEY;
SharedPreferences prefs = getApplicationContext().getSharedPreferences("app_data_keys", Context.MODE_PRIVATE);
Log.d("GODOT", "getting public key:" + prefs.getString("store_public_key", null));
return prefs.getString("store_public_key", null);
// return BASE64_PUBLIC_KEY;
}
/**
@@ -41,7 +49,8 @@ public class GodotDownloaderService extends DownloaderService {
*/
@Override
public String getAlarmReceiverClassName() {
return GodotDownloaderAlarmReceiver.class.getName();
Log.d("GODOT", "getAlarmReceiverClassName()");
return GodotDownloaderAlarmReceiver.class.getName();
}
}

View File

@@ -1,8 +1,6 @@
package com.android.godot;
import org.json.JSONObject;
import com.android.godot.Dictionary;
import android.app.Activity;
import android.util.Log;
@@ -44,10 +42,20 @@ public class GodotPaymentV3 extends Godot.SingletonBase {
public GodotPaymentV3(Activity p_activity) {
registerClass("GodotPayments", new String[] {"purchase", "setPurchaseCallbackId", "setPurchaseValidationUrlPrefix", "setTransactionId", "getSignature"});
registerClass("GodotPayments", new String[] {"purchase", "setPurchaseCallbackId", "setPurchaseValidationUrlPrefix", "setTransactionId", "getSignature", "consumeUnconsumedPurchases"});
activity=(Godot) p_activity;
}
public void consumeUnconsumedPurchases(){
activity.getPaymentsManager().setBaseSingleton(this);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
activity.getPaymentsManager().consumeUnconsumedPurchases();
}
});
}
private String signature;
public String getSignature(){
@@ -56,9 +64,19 @@ public class GodotPaymentV3 extends Godot.SingletonBase {
public void callbackSuccess(String ticket, String signature){
Log.d(this.getClass().getName(), "PRE-Send callback to purchase success");
GodotLib.calldeferred(purchaseCallbackId, "purchase_success", new Object[]{ticket, signature});
Log.d(this.getClass().getName(), "POST-Send callback to purchase success");
Log.d(this.getClass().getName(), "PRE-Send callback to purchase success");
GodotLib.callobject(purchaseCallbackId, "purchase_success", new Object[]{ticket, signature});
Log.d(this.getClass().getName(), "POST-Send callback to purchase success");
}
public void callbackSuccessProductMassConsumed(String ticket, String signature, String sku){
Log.d(this.getClass().getName(), "PRE-Send callback to consume success");
GodotLib.calldeferred(purchaseCallbackId, "consume_success", new Object[]{ticket, signature, sku});
Log.d(this.getClass().getName(), "POST-Send callback to consume success");
}
public void callbackSuccessNoUnconsumedPurchases(){
GodotLib.calldeferred(purchaseCallbackId, "no_validation_required", new Object[]{});
}
public void callbackFail(){

View File

@@ -0,0 +1,53 @@
package com.android.godot.payments;
import com.android.vending.billing.IInAppBillingService;
import android.content.Context;
import android.os.AsyncTask;
import android.os.RemoteException;
import android.util.Log;
abstract public class GenericConsumeTask extends AsyncTask<String, String, String>{
private Context context;
private IInAppBillingService mService;
public GenericConsumeTask(Context context, IInAppBillingService mService, String sku, String receipt, String signature, String token){
this.context = context;
this.mService = mService;
this.sku = sku;
this.receipt = receipt;
this.signature = signature;
this.token = token;
}
private String sku;
private String receipt;
private String signature;
private String token;
@Override
protected String doInBackground(String... params) {
try {
Log.d("godot", "Requesting to consume an item with token ." + token);
int response = mService.consumePurchase(3, context.getPackageName(), token);
Log.d("godot", "consumePurchase response: " + response);
if(response == 0 || response == 8){
return null;
}
} catch (Exception e) {
Log.d("godot", "Error " + e.getClass().getName() + ":" + e.getMessage());
}
return null;
}
protected void onPostExecute(String sarasa){
onSuccess(sku, receipt, signature, token);
}
abstract public void onSuccess(String sku, String receipt, String signature, String token);
}

View File

@@ -64,7 +64,7 @@ abstract public class HandlePurchaseTask {
pc.setConsumableFlag("block", productId, true);
pc.setConsumableValue("token", productId, purchaseToken);
success(productId, dataSignature);
success(productId, dataSignature, purchaseData);
return;
} catch (JSONException e) {
error(e.getMessage());
@@ -74,7 +74,7 @@ abstract public class HandlePurchaseTask {
}
}
abstract protected void success(String ticket, String signature);
abstract protected void success(String sku, String signature, String ticket);
abstract protected void error(String message);
abstract protected void canceled();

View File

@@ -1,5 +1,9 @@
package com.android.godot.payments;
import java.util.ArrayList;
import java.util.List;
import android.os.RemoteException;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
@@ -7,7 +11,13 @@ import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.util.Log;
import android.os.Bundle;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONStringer;
import com.android.godot.Dictionary;
import com.android.godot.Godot;
import com.android.godot.GodotPaymentV3;
import com.android.vending.billing.IInAppBillingService;
@@ -23,7 +33,6 @@ public class PaymentsManager {
private Activity activity;
IInAppBillingService mService;
public void setActivity(Activity activity){
this.activity = activity;
}
@@ -81,18 +90,39 @@ public class PaymentsManager {
}
public void consumeUnconsumedPurchases(){
new ReleaseAllConsumablesTask(mService, activity) {
@Override
protected void success(String sku, String receipt, String signature, String token) {
godotPaymentV3.callbackSuccessProductMassConsumed(receipt, signature, sku);
}
@Override
protected void error(String message) {
godotPaymentV3.callbackFail();
}
@Override
protected void notRequired() {
godotPaymentV3.callbackSuccessNoUnconsumedPurchases();
}
}.consumeItAll();
}
public void processPurchaseResponse(int resultCode, Intent data) {
new HandlePurchaseTask(activity){
@Override
protected void success(final String sku, final String signature) {
protected void success(final String sku, final String signature, final String ticket) {
godotPaymentV3.callbackSuccess(ticket, signature);
new ConsumeTask(mService, activity) {
@Override
protected void success(String ticket) {
// godotPaymentV3.callbackSuccess("");
Log.d("XXX", "calling success:" + signature);
godotPaymentV3.callbackSuccess(ticket, signature);
}
@Override
@@ -103,7 +133,7 @@ public class PaymentsManager {
}.consume(sku);
// godotPaymentV3.callbackSuccess(new PaymentsCache(activity).getConsumableValue("ticket", sku),signature);
// godotPaymentV3.callbackSuccess(ticket);
//validatePurchase(purchaseToken, sku);
}
@@ -166,5 +196,6 @@ public class PaymentsManager {
this.godotPaymentV3 = godotPaymentV3;
}
}

View File

@@ -64,31 +64,7 @@ abstract public class PurchaseTask {
canceled();
return ;
}
if(responseCode == 7){
new ConsumeTask(mService, context) {
@Override
protected void success(String ticket) {
// Log.d("XXX", "Product was erroniously purchased!");
if(isLooping){
// Log.d("XXX", "It is looping");
error("Error while purchasing product");
return;
}
isLooping=true;
PurchaseTask.this.purchase(sku, transactionId);
}
@Override
protected void error(String message) {
PurchaseTask.this.error(message);
}
}.consume(sku);
return;
}
PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");
pc.setConsumableValue("validation_hash", sku, hash);

View File

@@ -0,0 +1,87 @@
package com.android.godot.payments;
import java.util.ArrayList;
import org.json.JSONException;
import org.json.JSONObject;
import com.android.godot.Dictionary;
import com.android.godot.Godot;
import com.android.vending.billing.IInAppBillingService;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.RemoteException;
import android.util.Log;
abstract public class ReleaseAllConsumablesTask {
private Context context;
private IInAppBillingService mService;
public ReleaseAllConsumablesTask(IInAppBillingService mService, Context context ){
this.context = context;
this.mService = mService;
}
public void consumeItAll(){
try{
Log.d("godot", "consumeItall for " + context.getPackageName());
Bundle bundle = mService.getPurchases(3, context.getPackageName(), "inapp",null);
for (String key : bundle.keySet()) {
Object value = bundle.get(key);
Log.d("godot", String.format("%s %s (%s)", key,
value.toString(), value.getClass().getName()));
}
if (bundle.getInt("RESPONSE_CODE") == 0){
final ArrayList<String> myPurchases = bundle.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
final ArrayList<String> mySignatures = bundle.getStringArrayList("INAPP_DATA_SIGNATURE_LIST");
if (myPurchases == null || myPurchases.size() == 0){
Log.d("godot", "No purchases!");
notRequired();
return;
}
Log.d("godot", "# products to be consumed:" + myPurchases.size());
for (int i=0;i<myPurchases.size();i++)
{
try{
String receipt = myPurchases.get(i);
JSONObject inappPurchaseData = new JSONObject(receipt);
String sku = inappPurchaseData.getString("productId");
String token = inappPurchaseData.getString("purchaseToken");
String signature = mySignatures.get(i);
Log.d("godot", "A punto de consumir un item con token:" + token + "\n" + receipt);
new GenericConsumeTask(context, mService, sku, receipt,signature, token) {
@Override
public void onSuccess(String sku, String receipt, String signature, String token) {
ReleaseAllConsumablesTask.this.success(sku, receipt, signature, token);
}
}.execute();
} catch (JSONException e) {
}
}
}
}catch(Exception e){
Log.d("godot", "Error releasing products:" + e.getClass().getName() + ":" + e.getMessage());
}
}
abstract protected void success(String sku, String receipt, String signature, String token);
abstract protected void error(String message);
abstract protected void notRequired();
}

View File

@@ -753,7 +753,7 @@ JNIEXPORT void JNICALL Java_com_android_godot_GodotLib_initialize(JNIEnv * env,
int cmdlen=0;
bool use_apk_expansion=false;
if (p_cmdline) {
int cmdlen = env->GetArrayLength(p_cmdline);
cmdlen = env->GetArrayLength(p_cmdline);
if (cmdlen) {
cmdline = (const char**)malloc((env->GetArrayLength(p_cmdline)+1)*sizeof(const char*));
cmdline[cmdlen]=NULL;
@@ -776,6 +776,8 @@ JNIEXPORT void JNICALL Java_com_android_godot_GodotLib_initialize(JNIEnv * env,
}
}
__android_log_print(ANDROID_LOG_INFO,"godot","CMDLINE LEN %i - APK EXPANSION %I\n",cmdlen,int(use_apk_expansion));
os_android = new OS_Android(_gfx_init_func,env,_open_uri,_get_data_dir,_get_locale, _get_model,_show_vk, _hide_vk,_set_screen_orient,_get_unique_id, _play_video, _is_video_playing, _pause_video, _stop_video,use_apk_expansion);
os_android->set_need_reload_hooks(p_need_reload_hook);
@@ -1552,7 +1554,9 @@ JNIEXPORT void JNICALL Java_com_android_godot_GodotLib_callobject(JNIEnv * env,
for (int i=0; i<count; i++) {
jobject obj = env->GetObjectArrayElement(params, i);
Variant v = _jobject_to_variant(env, obj);
Variant v;
if (obj)
v=_jobject_to_variant(env, obj);
memnew_placement(&vlist[i], Variant);
vlist[i] = v;
vptr[i] = &vlist[i];
@@ -1578,7 +1582,8 @@ JNIEXPORT void JNICALL Java_com_android_godot_GodotLib_calldeferred(JNIEnv * env
for (int i=0; i<MIN(count,VARIANT_ARG_MAX); i++) {
jobject obj = env->GetObjectArrayElement(params, i);
args[i] = _jobject_to_variant(env, obj);
if (obj)
args[i] = _jobject_to_variant(env, obj);
print_line("\targ"+itos(i)+": "+Variant::get_type_name(args[i].get_type()));
};

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

View File

@@ -0,0 +1,4 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.compliance=1.6
org.eclipse.jdt.core.compiler.source=1.6

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.vending.expansion.downloader"
android:versionCode="2"
android:versionName="1.1" >
<uses-sdk android:minSdkVersion="4" android:targetSdkVersion="15"/>
</manifest>

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="downloader_library" 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" />
<!-- if sdk.dir was not set from one of the property file, then
get it from the ANDROID_HOME env var.
This must be done before we load project.properties since
the proguard config can use sdk.dir -->
<property environment="env" />
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
<isset property="env.ANDROID_HOME" />
</condition>
<!-- 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 the ANDROID_HOME environment variable."
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>

View File

@@ -0,0 +1,6 @@
/** Automatically generated file. DO NOT MODIFY */
package com.android.vending.expansion.downloader;
public final class BuildConfig {
public final static boolean DEBUG = false;
}

View File

@@ -0,0 +1,73 @@
/* AUTO-GENERATED FILE. DO NOT MODIFY.
*
* This class was automatically generated by the
* aapt tool from the resource data it found. It
* should not be modified by hand.
*/
package com.android.vending.expansion.downloader;
public final class R {
public static final class attr {
}
public static final class drawable {
public static int notify_panel_notification_icon_bg=0x7f020000;
}
public static final class id {
public static int appIcon=0x7f060001;
public static int description=0x7f060007;
public static int notificationLayout=0x7f060000;
public static int progress_bar=0x7f060006;
public static int progress_bar_frame=0x7f060005;
public static int progress_text=0x7f060002;
public static int time_remaining=0x7f060004;
public static int title=0x7f060003;
}
public static final class layout {
public static int status_bar_ongoing_event_progress_bar=0x7f030000;
}
public static final class string {
public static int kilobytes_per_second=0x7f040014;
/** When a download completes, a notification is displayed, and this
string is used to indicate that the download successfully completed.
Note that such a download could have been initiated by a variety of
applications, including (but not limited to) the browser, an email
application, a content marketplace.
*/
public static int notification_download_complete=0x7f040000;
/** When a download completes, a notification is displayed, and this
string is used to indicate that the download failed.
Note that such a download could have been initiated by a variety of
applications, including (but not limited to) the browser, an email
application, a content marketplace.
*/
public static int notification_download_failed=0x7f040001;
public static int state_completed=0x7f040007;
public static int state_connecting=0x7f040005;
public static int state_downloading=0x7f040006;
public static int state_failed=0x7f040013;
public static int state_failed_cancelled=0x7f040012;
public static int state_failed_fetching_url=0x7f040010;
public static int state_failed_sdcard_full=0x7f040011;
public static int state_failed_unlicensed=0x7f04000f;
public static int state_fetching_url=0x7f040004;
public static int state_idle=0x7f040003;
public static int state_paused_by_request=0x7f04000a;
public static int state_paused_network_setup_failure=0x7f040009;
public static int state_paused_network_unavailable=0x7f040008;
public static int state_paused_roaming=0x7f04000d;
public static int state_paused_sdcard_unavailable=0x7f04000e;
public static int state_paused_wifi_disabled=0x7f04000c;
public static int state_paused_wifi_unavailable=0x7f04000b;
public static int state_unknown=0x7f040002;
public static int time_remaining=0x7f040015;
public static int time_remaining_notification=0x7f040016;
}
public static final class style {
public static int ButtonBackground=0x7f050003;
public static int NotificationText=0x7f050000;
public static int NotificationTextSecondary=0x7f050004;
public static int NotificationTextShadow=0x7f050001;
public static int NotificationTitle=0x7f050002;
}
}

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 *;
#}

View File

@@ -0,0 +1,13 @@
# 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 use,
# "ant.properties", and override values to adapt the script to your
# project structure.
# Project target.
target=android-15
android.library=true
android.library.reference.1=../play_licensing

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
** Copyright 2008, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
-->
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal" android:id="@+id/notificationLayout" xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:layout_width="35dp"
android:layout_height="fill_parent"
android:paddingTop="10dp"
android:paddingBottom="8dp" >
<ImageView
android:id="@+id/appIcon"
android:layout_width="fill_parent"
android:layout_height="25dp"
android:scaleType="centerInside"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:src="@android:drawable/stat_sys_download" />
<TextView
android:id="@+id/progress_text"
style="@style/NotificationText"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentBottom="true"
android:layout_gravity="center_horizontal"
android:singleLine="true"
android:gravity="center" />
</RelativeLayout>
<RelativeLayout
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1.0"
android:clickable="true"
android:focusable="true"
android:paddingTop="10dp"
android:paddingRight="8dp"
android:paddingBottom="8dp" >
<TextView
android:id="@+id/title"
style="@style/NotificationTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:singleLine="true"/>
<TextView
android:id="@+id/time_remaining"
style="@style/NotificationText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:singleLine="true"/>
<!-- Only one of progress_bar and paused_text will be visible. -->
<FrameLayout
android:id="@+id/progress_bar_frame"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" >
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingRight="25dp" />
<TextView
android:id="@+id/description"
style="@style/NotificationTextShadow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingRight="25dp"
android:singleLine="true" />
</FrameLayout>
</RelativeLayout>
</LinearLayout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NotificationTextSecondary" parent="NotificationText">
<item name="android:textSize">12sp</item>
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NotificationText" parent="android:TextAppearance.StatusBar.EventContent" />
<style name="NotificationTitle" parent="android:TextAppearance.StatusBar.EventContent.Title" />
</resources>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- When a download completes, a notification is displayed, and this
string is used to indicate that the download successfully completed.
Note that such a download could have been initiated by a variety of
applications, including (but not limited to) the browser, an email
application, a content marketplace. -->
<string name="notification_download_complete">Download complete</string>
<!-- When a download completes, a notification is displayed, and this
string is used to indicate that the download failed.
Note that such a download could have been initiated by a variety of
applications, including (but not limited to) the browser, an email
application, a content marketplace. -->
<string name="notification_download_failed">Download unsuccessful</string>
<string name="state_unknown">Starting..."</string>
<string name="state_idle">Waiting for download to start</string>
<string name="state_fetching_url">Looking for resources to download</string>
<string name="state_connecting">Connecting to the download server</string>
<string name="state_downloading">Downloading resources</string>
<string name="state_completed">Download finished</string>
<string name="state_paused_network_unavailable">Download paused because no network is available</string>
<string name="state_paused_network_setup_failure">Download paused. Test a website in browser</string>
<string name="state_paused_by_request">Download paused</string>
<string name="state_paused_wifi_unavailable">Download paused because wifi is unavailable</string>
<string name="state_paused_wifi_disabled">Download paused because wifi is disabled</string>
<string name="state_paused_roaming">Download paused because you are roaming</string>
<string name="state_paused_sdcard_unavailable">Download paused because the external storage is unavailable</string>
<string name="state_failed_unlicensed">Download failed because you may not have purchased this app</string>
<string name="state_failed_fetching_url">Download failed because the resources could not be found</string>
<string name="state_failed_sdcard_full">Download failed because the external storage is full</string>
<string name="state_failed_cancelled">Download cancelled</string>
<string name="state_failed">Download failed</string>
<string name="kilobytes_per_second">%1$s KB/s</string>
<string name="time_remaining">Time remaining: %1$s</string>
<string name="time_remaining_notification">%1$s left</string>
</resources>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NotificationText">
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
<style name="NotificationTextShadow" parent="NotificationText">
<item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="android:shadowColor">@android:color/background_dark</item>
<item name="android:shadowDx">1.0</item>
<item name="android:shadowDy">1.0</item>
<item name="android:shadowRadius">1</item>
</style>
<style name="NotificationTitle">
<item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="android:textStyle">bold</item>
</style>
<style name="ButtonBackground">
<item name="android:background">@android:color/background_dark</item>
</style>
</resources>

View File

@@ -0,0 +1,236 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader;
import java.io.File;
/**
* Contains the internal constants that are used in the download manager.
* As a general rule, modifying these constants should be done with care.
*/
public class Constants {
/** Tag used for debugging/logging */
public static final String TAG = "LVLDL";
/**
* Expansion path where we store obb files
*/
public static final String EXP_PATH = File.separator + "Android"
+ File.separator + "obb" + File.separator;
/** The intent that gets sent when the service must wake up for a retry */
public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP";
/** the intent that gets sent when clicking a successful download */
public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN";
/** the intent that gets sent when clicking an incomplete/failed download */
public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST";
/** the intent that gets sent when deleting the notification of a completed download */
public static final String ACTION_HIDE = "android.intent.action.DOWNLOAD_HIDE";
/**
* When a number has to be appended to the filename, this string is used to separate the
* base filename from the sequence number
*/
public static final String FILENAME_SEQUENCE_SEPARATOR = "-";
/** The default user agent used for downloads */
public static final String DEFAULT_USER_AGENT = "Android.LVLDM";
/** The buffer size used to stream the data */
public static final int BUFFER_SIZE = 4096;
/** The minimum amount of progress that has to be done before the progress bar gets updated */
public static final int MIN_PROGRESS_STEP = 4096;
/** The minimum amount of time that has to elapse before the progress bar gets updated, in ms */
public static final long MIN_PROGRESS_TIME = 1000;
/** The maximum number of rows in the database (FIFO) */
public static final int MAX_DOWNLOADS = 1000;
/**
* The number of times that the download manager will retry its network
* operations when no progress is happening before it gives up.
*/
public static final int MAX_RETRIES = 5;
/**
* The minimum amount of time that the download manager accepts for
* a Retry-After response header with a parameter in delta-seconds.
*/
public static final int MIN_RETRY_AFTER = 30; // 30s
/**
* The maximum amount of time that the download manager accepts for
* a Retry-After response header with a parameter in delta-seconds.
*/
public static final int MAX_RETRY_AFTER = 24 * 60 * 60; // 24h
/**
* The maximum number of redirects.
*/
public static final int MAX_REDIRECTS = 5; // can't be more than 7.
/**
* The time between a failure and the first retry after an IOException.
* Each subsequent retry grows exponentially, doubling each time.
* The time is in seconds.
*/
public static final int RETRY_FIRST_DELAY = 30;
/** Enable separate connectivity logging */
public static final boolean LOGX = true;
/** Enable verbose logging */
public static final boolean LOGV = false;
/** Enable super-verbose logging */
private static final boolean LOCAL_LOGVV = false;
public static final boolean LOGVV = LOCAL_LOGVV && LOGV;
/**
* This download has successfully completed.
* Warning: there might be other status values that indicate success
* in the future.
* Use isSucccess() to capture the entire category.
*/
public static final int STATUS_SUCCESS = 200;
/**
* This request couldn't be parsed. This is also used when processing
* requests with unknown/unsupported URI schemes.
*/
public static final int STATUS_BAD_REQUEST = 400;
/**
* This download can't be performed because the content type cannot be
* handled.
*/
public static final int STATUS_NOT_ACCEPTABLE = 406;
/**
* This download cannot be performed because the length cannot be
* determined accurately. This is the code for the HTTP error "Length
* Required", which is typically used when making requests that require
* a content length but don't have one, and it is also used in the
* client when a response is received whose length cannot be determined
* accurately (therefore making it impossible to know when a download
* completes).
*/
public static final int STATUS_LENGTH_REQUIRED = 411;
/**
* This download was interrupted and cannot be resumed.
* This is the code for the HTTP error "Precondition Failed", and it is
* also used in situations where the client doesn't have an ETag at all.
*/
public static final int STATUS_PRECONDITION_FAILED = 412;
/**
* The lowest-valued error status that is not an actual HTTP status code.
*/
public static final int MIN_ARTIFICIAL_ERROR_STATUS = 488;
/**
* The requested destination file already exists.
*/
public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488;
/**
* Some possibly transient error occurred, but we can't resume the download.
*/
public static final int STATUS_CANNOT_RESUME = 489;
/**
* This download was canceled
*/
public static final int STATUS_CANCELED = 490;
/**
* This download has completed with an error.
* Warning: there will be other status values that indicate errors in
* the future. Use isStatusError() to capture the entire category.
*/
public static final int STATUS_UNKNOWN_ERROR = 491;
/**
* This download couldn't be completed because of a storage issue.
* Typically, that's because the filesystem is missing or full.
* Use the more specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR}
* and {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate.
*/
public static final int STATUS_FILE_ERROR = 492;
/**
* This download couldn't be completed because of an HTTP
* redirect response that the download manager couldn't
* handle.
*/
public static final int STATUS_UNHANDLED_REDIRECT = 493;
/**
* This download couldn't be completed because of an
* unspecified unhandled HTTP code.
*/
public static final int STATUS_UNHANDLED_HTTP_CODE = 494;
/**
* This download couldn't be completed because of an
* error receiving or processing data at the HTTP level.
*/
public static final int STATUS_HTTP_DATA_ERROR = 495;
/**
* This download couldn't be completed because of an
* HttpException while setting up the request.
*/
public static final int STATUS_HTTP_EXCEPTION = 496;
/**
* This download couldn't be completed because there were
* too many redirects.
*/
public static final int STATUS_TOO_MANY_REDIRECTS = 497;
/**
* This download couldn't be completed due to insufficient storage
* space. Typically, this is because the SD card is full.
*/
public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498;
/**
* This download couldn't be completed because no external storage
* device was found. Typically, this is because the SD card is not
* mounted.
*/
public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499;
/**
* The wake duration to check to see if a download is possible.
*/
public static final long WATCHDOG_WAKE_TIMER = 60*1000;
/**
* The wake duration to check to see if the process was killed.
*/
public static final long ACTIVE_THREAD_WATCHDOG = 5*1000;
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader;
import android.os.Parcel;
import android.os.Parcelable;
/**
* This class contains progress information about the active download(s).
*
* When you build the Activity that initiates a download and tracks the
* progress by implementing the {@link IDownloaderClient} interface, you'll
* receive a DownloadProgressInfo object in each call to the {@link
* IDownloaderClient#onDownloadProgress} method. This allows you to update
* your activity's UI with information about the download progress, such
* as the progress so far, time remaining and current speed.
*/
public class DownloadProgressInfo implements Parcelable {
public long mOverallTotal;
public long mOverallProgress;
public long mTimeRemaining; // time remaining
public float mCurrentSpeed; // speed in KB/S
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel p, int i) {
p.writeLong(mOverallTotal);
p.writeLong(mOverallProgress);
p.writeLong(mTimeRemaining);
p.writeFloat(mCurrentSpeed);
}
public DownloadProgressInfo(Parcel p) {
mOverallTotal = p.readLong();
mOverallProgress = p.readLong();
mTimeRemaining = p.readLong();
mCurrentSpeed = p.readFloat();
}
public DownloadProgressInfo(long overallTotal, long overallProgress,
long timeRemaining,
float currentSpeed) {
this.mOverallTotal = overallTotal;
this.mOverallProgress = overallProgress;
this.mTimeRemaining = timeRemaining;
this.mCurrentSpeed = currentSpeed;
}
public static final Creator<DownloadProgressInfo> CREATOR = new Creator<DownloadProgressInfo>() {
@Override
public DownloadProgressInfo createFromParcel(Parcel parcel) {
return new DownloadProgressInfo(parcel);
}
@Override
public DownloadProgressInfo[] newArray(int i) {
return new DownloadProgressInfo[i];
}
};
}

View File

@@ -0,0 +1,277 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader;
import com.google.android.vending.expansion.downloader.impl.DownloaderService;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;
/**
* This class binds the service API to your application client. It contains the IDownloaderClient proxy,
* which is used to call functions in your client as well as the Stub, which is used to call functions
* in the client implementation of IDownloaderClient.
*
* <p>The IPC is implemented using an Android Messenger and a service Binder. The connect method
* should be called whenever the client wants to bind to the service. It opens up a service connection
* that ends up calling the onServiceConnected client API that passes the service messenger
* in. If the client wants to be notified by the service, it is responsible for then passing its
* messenger to the service in a separate call.
*
* <p>Critical methods are {@link #startDownloadServiceIfRequired} and {@link #CreateStub}.
*
* <p>When your application first starts, you should first check whether your app's expansion files are
* already on the device. If not, you should then call {@link #startDownloadServiceIfRequired}, which
* starts your {@link impl.DownloaderService} to download the expansion files if necessary. The method
* returns a value indicating whether download is required or not.
*
* <p>If a download is required, {@link #startDownloadServiceIfRequired} begins the download through
* the specified service and you should then call {@link #CreateStub} to instantiate a member {@link
* IStub} object that you need in order to receive calls through your {@link IDownloaderClient}
* interface.
*/
public class DownloaderClientMarshaller {
public static final int MSG_ONDOWNLOADSTATE_CHANGED = 10;
public static final int MSG_ONDOWNLOADPROGRESS = 11;
public static final int MSG_ONSERVICECONNECTED = 12;
public static final String PARAM_NEW_STATE = "newState";
public static final String PARAM_PROGRESS = "progress";
public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
public static final int NO_DOWNLOAD_REQUIRED = DownloaderService.NO_DOWNLOAD_REQUIRED;
public static final int LVL_CHECK_REQUIRED = DownloaderService.LVL_CHECK_REQUIRED;
public static final int DOWNLOAD_REQUIRED = DownloaderService.DOWNLOAD_REQUIRED;
private static class Proxy implements IDownloaderClient {
private Messenger mServiceMessenger;
@Override
public void onDownloadStateChanged(int newState) {
Bundle params = new Bundle(1);
params.putInt(PARAM_NEW_STATE, newState);
send(MSG_ONDOWNLOADSTATE_CHANGED, params);
}
@Override
public void onDownloadProgress(DownloadProgressInfo progress) {
Bundle params = new Bundle(1);
params.putParcelable(PARAM_PROGRESS, progress);
send(MSG_ONDOWNLOADPROGRESS, params);
}
private void send(int method, Bundle params) {
Message m = Message.obtain(null, method);
m.setData(params);
try {
mServiceMessenger.send(m);
} catch (RemoteException e) {
e.printStackTrace();
}
}
public Proxy(Messenger msg) {
mServiceMessenger = msg;
}
@Override
public void onServiceConnected(Messenger m) {
/**
* This is never called through the proxy.
*/
}
}
private static class Stub implements IStub {
private IDownloaderClient mItf = null;
private Class<?> mDownloaderServiceClass;
private boolean mBound;
private Messenger mServiceMessenger;
private Context mContext;
/**
* Target we publish for clients to send messages to IncomingHandler.
*/
final Messenger mMessenger = new Messenger(new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_ONDOWNLOADPROGRESS:
Bundle bun = msg.getData();
if ( null != mContext ) {
bun.setClassLoader(mContext.getClassLoader());
DownloadProgressInfo dpi = (DownloadProgressInfo) msg.getData()
.getParcelable(PARAM_PROGRESS);
mItf.onDownloadProgress(dpi);
}
break;
case MSG_ONDOWNLOADSTATE_CHANGED:
mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE));
break;
case MSG_ONSERVICECONNECTED:
mItf.onServiceConnected(
(Messenger) msg.getData().getParcelable(PARAM_MESSENGER));
break;
}
}
});
public Stub(IDownloaderClient itf, Class<?> downloaderService) {
mItf = itf;
mDownloaderServiceClass = downloaderService;
}
/**
* Class for interacting with the main interface of the service.
*/
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
// This is called when the connection with the service has been
// established, giving us the object we can use to
// interact with the service. We are communicating with the
// service using a Messenger, so here we get a client-side
// representation of that from the raw IBinder object.
mServiceMessenger = new Messenger(service);
mItf.onServiceConnected(
mServiceMessenger);
}
public void onServiceDisconnected(ComponentName className) {
// This is called when the connection with the service has been
// unexpectedly disconnected -- that is, its process crashed.
mServiceMessenger = null;
}
};
@Override
public void connect(Context c) {
mContext = c;
Intent bindIntent = new Intent(c, mDownloaderServiceClass);
bindIntent.putExtra(PARAM_MESSENGER, mMessenger);
if ( !c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND) ) {
if ( Constants.LOGVV ) {
Log.d(Constants.TAG, "Service Unbound");
}
} else {
mBound = true;
}
}
@Override
public void disconnect(Context c) {
if (mBound) {
c.unbindService(mConnection);
mBound = false;
}
mContext = null;
}
@Override
public Messenger getMessenger() {
return mMessenger;
}
}
/**
* Returns a proxy that will marshal calls to IDownloaderClient methods
*
* @param msg
* @return
*/
public static IDownloaderClient CreateProxy(Messenger msg) {
return new Proxy(msg);
}
/**
* Returns a stub object that, when connected, will listen for marshaled
* {@link IDownloaderClient} methods and translate them into calls to the supplied
* interface.
*
* @param itf An implementation of IDownloaderClient that will be called
* when remote method calls are unmarshaled.
* @param downloaderService The class for your implementation of {@link
* impl.DownloaderService}.
* @return The {@link IStub} that allows you to connect to the service such that
* your {@link IDownloaderClient} receives status updates.
*/
public static IStub CreateStub(IDownloaderClient itf, Class<?> downloaderService) {
return new Stub(itf, downloaderService);
}
/**
* Starts the download if necessary. This function starts a flow that does `
* many things. 1) Checks to see if the APK version has been checked and
* the metadata database updated 2) If the APK version does not match,
* checks the new LVL status to see if a new download is required 3) If the
* APK version does match, then checks to see if the download(s) have been
* completed 4) If the downloads have been completed, returns
* NO_DOWNLOAD_REQUIRED The idea is that this can be called during the
* startup of an application to quickly ascertain if the application needs
* to wait to hear about any updated APK expansion files. Note that this does
* mean that the application MUST be run for the first time with a network
* connection, even if Market delivers all of the files.
*
* @param context Your application Context.
* @param notificationClient A PendingIntent to start the Activity in your application
* that shows the download progress and which will also start the application when download
* completes.
* @param serviceClass the class of your {@link imp.DownloaderService} implementation
* @return whether the service was started and the reason for starting the service.
* Either {@link #NO_DOWNLOAD_REQUIRED}, {@link #LVL_CHECK_REQUIRED}, or {@link
* #DOWNLOAD_REQUIRED}.
* @throws NameNotFoundException
*/
public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient,
Class<?> serviceClass)
throws NameNotFoundException {
return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
serviceClass);
}
/**
* This version assumes that the intent contains the pending intent as a parameter. This
* is used for responding to alarms.
* <p>The pending intent must be in an extra with the key {@link
* impl.DownloaderService#EXTRA_PENDING_INTENT}.
*
* @param context
* @param notificationClient
* @param serviceClass the class of the service to start
* @return
* @throws NameNotFoundException
*/
public static int startDownloadServiceIfRequired(Context context, Intent notificationClient,
Class<?> serviceClass)
throws NameNotFoundException {
return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
serviceClass);
}
}

View File

@@ -0,0 +1,181 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader;
import com.google.android.vending.expansion.downloader.impl.DownloaderService;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
/**
* This class is used by the client activity to proxy requests to the Downloader
* Service.
*
* Most importantly, you must call {@link #CreateProxy} during the {@link
* IDownloaderClient#onServiceConnected} callback in your activity in order to instantiate
* an {@link IDownloaderService} object that you can then use to issue commands to the {@link
* DownloaderService} (such as to pause and resume downloads).
*/
public class DownloaderServiceMarshaller {
public static final int MSG_REQUEST_ABORT_DOWNLOAD =
1;
public static final int MSG_REQUEST_PAUSE_DOWNLOAD =
2;
public static final int MSG_SET_DOWNLOAD_FLAGS =
3;
public static final int MSG_REQUEST_CONTINUE_DOWNLOAD =
4;
public static final int MSG_REQUEST_DOWNLOAD_STATE =
5;
public static final int MSG_REQUEST_CLIENT_UPDATE =
6;
public static final String PARAMS_FLAGS = "flags";
public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
private static class Proxy implements IDownloaderService {
private Messenger mMsg;
private void send(int method, Bundle params) {
Message m = Message.obtain(null, method);
m.setData(params);
try {
mMsg.send(m);
} catch (RemoteException e) {
e.printStackTrace();
}
}
public Proxy(Messenger msg) {
mMsg = msg;
}
@Override
public void requestAbortDownload() {
send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle());
}
@Override
public void requestPauseDownload() {
send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle());
}
@Override
public void setDownloadFlags(int flags) {
Bundle params = new Bundle();
params.putInt(PARAMS_FLAGS, flags);
send(MSG_SET_DOWNLOAD_FLAGS, params);
}
@Override
public void requestContinueDownload() {
send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle());
}
@Override
public void requestDownloadStatus() {
send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle());
}
@Override
public void onClientUpdated(Messenger clientMessenger) {
Bundle bundle = new Bundle(1);
bundle.putParcelable(PARAM_MESSENGER, clientMessenger);
send(MSG_REQUEST_CLIENT_UPDATE, bundle);
}
}
private static class Stub implements IStub {
private IDownloaderService mItf = null;
final Messenger mMessenger = new Messenger(new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_REQUEST_ABORT_DOWNLOAD:
mItf.requestAbortDownload();
break;
case MSG_REQUEST_CONTINUE_DOWNLOAD:
mItf.requestContinueDownload();
break;
case MSG_REQUEST_PAUSE_DOWNLOAD:
mItf.requestPauseDownload();
break;
case MSG_SET_DOWNLOAD_FLAGS:
mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS));
break;
case MSG_REQUEST_DOWNLOAD_STATE:
mItf.requestDownloadStatus();
break;
case MSG_REQUEST_CLIENT_UPDATE:
mItf.onClientUpdated((Messenger) msg.getData().getParcelable(
PARAM_MESSENGER));
break;
}
}
});
public Stub(IDownloaderService itf) {
mItf = itf;
}
@Override
public Messenger getMessenger() {
return mMessenger;
}
@Override
public void connect(Context c) {
}
@Override
public void disconnect(Context c) {
}
}
/**
* Returns a proxy that will marshall calls to IDownloaderService methods
*
* @param ctx
* @return
*/
public static IDownloaderService CreateProxy(Messenger msg) {
return new Proxy(msg);
}
/**
* Returns a stub object that, when connected, will listen for marshalled
* IDownloaderService methods and translate them into calls to the supplied
* interface.
*
* @param itf An implementation of IDownloaderService that will be called
* when remote method calls are unmarshalled.
* @return
*/
public static IStub CreateStub(IDownloaderService itf) {
return new Stub(itf);
}
}

View File

@@ -0,0 +1,306 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader;
import com.android.vending.expansion.downloader.R;
import android.content.Context;
import android.os.Environment;
import android.os.StatFs;
import android.os.SystemClock;
import android.util.Log;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Random;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Some helper functions for the download manager
*/
public class Helpers {
public static Random sRandom = new Random(SystemClock.uptimeMillis());
/** Regex used to parse content-disposition headers */
private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern
.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
private Helpers() {
}
/*
* Parse the Content-Disposition HTTP Header. The format of the header is
* defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This
* header provides a filename for content that is going to be downloaded to
* the file system. We only support the attachment type.
*/
static String parseContentDisposition(String contentDisposition) {
try {
Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
if (m.find()) {
return m.group(1);
}
} catch (IllegalStateException ex) {
// This function is defined as returning null when it can't parse
// the header
}
return null;
}
/**
* @return the root of the filesystem containing the given path
*/
public static File getFilesystemRoot(String path) {
File cache = Environment.getDownloadCacheDirectory();
if (path.startsWith(cache.getPath())) {
return cache;
}
File external = Environment.getExternalStorageDirectory();
if (path.startsWith(external.getPath())) {
return external;
}
throw new IllegalArgumentException(
"Cannot determine filesystem root for " + path);
}
public static boolean isExternalMediaMounted() {
if (!Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
// No SD card found.
if ( Constants.LOGVV ) {
Log.d(Constants.TAG, "no external storage");
}
return false;
}
return true;
}
/**
* @return the number of bytes available on the filesystem rooted at the
* given File
*/
public static long getAvailableBytes(File root) {
StatFs stat = new StatFs(root.getPath());
// put a bit of margin (in case creating the file grows the system by a
// few blocks)
long availableBlocks = (long) stat.getAvailableBlocks() - 4;
return stat.getBlockSize() * availableBlocks;
}
/**
* Checks whether the filename looks legitimate
*/
public static boolean isFilenameValid(String filename) {
filename = filename.replaceFirst("/+", "/"); // normalize leading
// slashes
return filename.startsWith(Environment.getDownloadCacheDirectory().toString())
|| filename.startsWith(Environment.getExternalStorageDirectory().toString());
}
/*
* Delete the given file from device
*/
/* package */static void deleteFile(String path) {
try {
File file = new File(path);
file.delete();
} catch (Exception e) {
Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
}
}
/**
* Showing progress in MB here. It would be nice to choose the unit (KB, MB,
* GB) based on total file size, but given what we know about the expected
* ranges of file sizes for APK expansion files, it's probably not necessary.
*
* @param overallProgress
* @param overallTotal
* @return
*/
static public String getDownloadProgressString(long overallProgress, long overallTotal) {
if (overallTotal == 0) {
if ( Constants.LOGVV ) {
Log.e(Constants.TAG, "Notification called when total is zero");
}
return "";
}
return String.format("%.2f",
(float) overallProgress / (1024.0f * 1024.0f))
+ "MB /" +
String.format("%.2f", (float) overallTotal /
(1024.0f * 1024.0f)) + "MB";
}
/**
* Adds a percentile to getDownloadProgressString.
*
* @param overallProgress
* @param overallTotal
* @return
*/
static public String getDownloadProgressStringNotification(long overallProgress,
long overallTotal) {
if (overallTotal == 0) {
if ( Constants.LOGVV ) {
Log.e(Constants.TAG, "Notification called when total is zero");
}
return "";
}
return getDownloadProgressString(overallProgress, overallTotal) + " (" +
getDownloadProgressPercent(overallProgress, overallTotal) + ")";
}
public static String getDownloadProgressPercent(long overallProgress, long overallTotal) {
if (overallTotal == 0) {
if ( Constants.LOGVV ) {
Log.e(Constants.TAG, "Notification called when total is zero");
}
return "";
}
return Long.toString(overallProgress * 100 / overallTotal) + "%";
}
public static String getSpeedString(float bytesPerMillisecond) {
return String.format("%.2f", bytesPerMillisecond * 1000 / 1024);
}
public static String getTimeRemaining(long durationInMilliseconds) {
SimpleDateFormat sdf;
if (durationInMilliseconds > 1000 * 60 * 60) {
sdf = new SimpleDateFormat("HH:mm", Locale.getDefault());
} else {
sdf = new SimpleDateFormat("mm:ss", Locale.getDefault());
}
return sdf.format(new Date(durationInMilliseconds - TimeZone.getDefault().getRawOffset()));
}
/**
* Returns the file name (without full path) for an Expansion APK file from
* the given context.
*
* @param c the context
* @param mainFile true for main file, false for patch file
* @param versionCode the version of the file
* @return String the file name of the expansion file
*/
public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) {
return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb";
}
/**
* Returns the filename (where the file should be saved) from info about a
* download
*/
static public String generateSaveFileName(Context c, String fileName) {
String path = getSaveFilePath(c)
+ File.separator + fileName;
return path;
}
static public String getSaveFilePath(Context c) {
File root = Environment.getExternalStorageDirectory();
String path = root.toString() + Constants.EXP_PATH + c.getPackageName();
return path;
}
/**
* Helper function to ascertain the existence of a file and return
* true/false appropriately
*
* @param c the app/activity/service context
* @param fileName the name (sans path) of the file to query
* @param fileSize the size that the file must match
* @param deleteFileOnMismatch if the file sizes do not match, delete the
* file
* @return true if it does exist, false otherwise
*/
static public boolean doesFileExist(Context c, String fileName, long fileSize,
boolean deleteFileOnMismatch) {
// the file may have been delivered by Market --- let's make sure
// it's the size we expect
File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName));
if (fileForNewFile.exists()) {
if (fileForNewFile.length() == fileSize) {
return true;
}
if (deleteFileOnMismatch) {
// delete the file --- we won't be able to resume
// because we cannot confirm the integrity of the file
fileForNewFile.delete();
}
}
return false;
}
/**
* Converts download states that are returned by the {@link
* IDownloaderClient#onDownloadStateChanged} callback into usable strings.
* This is useful if using the state strings built into the library to display user messages.
* @param state One of the STATE_* constants from {@link IDownloaderClient}.
* @return string resource ID for the corresponding string.
*/
static public int getDownloaderStringResourceIDFromState(int state) {
switch (state) {
case IDownloaderClient.STATE_IDLE:
return R.string.state_idle;
case IDownloaderClient.STATE_FETCHING_URL:
return R.string.state_fetching_url;
case IDownloaderClient.STATE_CONNECTING:
return R.string.state_connecting;
case IDownloaderClient.STATE_DOWNLOADING:
return R.string.state_downloading;
case IDownloaderClient.STATE_COMPLETED:
return R.string.state_completed;
case IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE:
return R.string.state_paused_network_unavailable;
case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
return R.string.state_paused_by_request;
case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
return R.string.state_paused_wifi_disabled;
case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
return R.string.state_paused_wifi_unavailable;
case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED:
return R.string.state_paused_wifi_disabled;
case IDownloaderClient.STATE_PAUSED_NEED_WIFI:
return R.string.state_paused_wifi_unavailable;
case IDownloaderClient.STATE_PAUSED_ROAMING:
return R.string.state_paused_roaming;
case IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE:
return R.string.state_paused_network_setup_failure;
case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
return R.string.state_paused_sdcard_unavailable;
case IDownloaderClient.STATE_FAILED_UNLICENSED:
return R.string.state_failed_unlicensed;
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
return R.string.state_failed_fetching_url;
case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
return R.string.state_failed_sdcard_full;
case IDownloaderClient.STATE_FAILED_CANCELED:
return R.string.state_failed_cancelled;
default:
return R.string.state_unknown;
}
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader;
import android.os.Messenger;
/**
* This interface should be implemented by the client activity for the
* downloader. It is used to pass status from the service to the client.
*/
public interface IDownloaderClient {
static final int STATE_IDLE = 1;
static final int STATE_FETCHING_URL = 2;
static final int STATE_CONNECTING = 3;
static final int STATE_DOWNLOADING = 4;
static final int STATE_COMPLETED = 5;
static final int STATE_PAUSED_NETWORK_UNAVAILABLE = 6;
static final int STATE_PAUSED_BY_REQUEST = 7;
/**
* Both STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION and
* STATE_PAUSED_NEED_CELLULAR_PERMISSION imply that Wi-Fi is unavailable and
* cellular permission will restart the service. Wi-Fi disabled means that
* the Wi-Fi manager is returning that Wi-Fi is not enabled, while in the
* other case Wi-Fi is enabled but not available.
*/
static final int STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION = 8;
static final int STATE_PAUSED_NEED_CELLULAR_PERMISSION = 9;
/**
* Both STATE_PAUSED_WIFI_DISABLED and STATE_PAUSED_NEED_WIFI imply that
* Wi-Fi is unavailable and cellular permission will NOT restart the
* service. Wi-Fi disabled means that the Wi-Fi manager is returning that
* Wi-Fi is not enabled, while in the other case Wi-Fi is enabled but not
* available.
* <p>
* The service does not return these values. We recommend that app
* developers with very large payloads do not allow these payloads to be
* downloaded over cellular connections.
*/
static final int STATE_PAUSED_WIFI_DISABLED = 10;
static final int STATE_PAUSED_NEED_WIFI = 11;
static final int STATE_PAUSED_ROAMING = 12;
/**
* Scary case. We were on a network that redirected us to another website
* that delivered us the wrong file.
*/
static final int STATE_PAUSED_NETWORK_SETUP_FAILURE = 13;
static final int STATE_PAUSED_SDCARD_UNAVAILABLE = 14;
static final int STATE_FAILED_UNLICENSED = 15;
static final int STATE_FAILED_FETCHING_URL = 16;
static final int STATE_FAILED_SDCARD_FULL = 17;
static final int STATE_FAILED_CANCELED = 18;
static final int STATE_FAILED = 19;
/**
* Called internally by the stub when the service is bound to the client.
* <p>
* Critical implementation detail. In onServiceConnected we create the
* remote service and marshaler. This is how we pass the client information
* back to the service so the client can be properly notified of changes. We
* must do this every time we reconnect to the service.
* <p>
* That is, when you receive this callback, you should call
* {@link DownloaderServiceMarshaller#CreateProxy} to instantiate a member
* instance of {@link IDownloaderService}, then call
* {@link IDownloaderService#onClientUpdated} with the Messenger retrieved
* from your {@link IStub} proxy object.
*
* @param m the service Messenger. This Messenger is used to call the
* service API from the client.
*/
void onServiceConnected(Messenger m);
/**
* Called when the download state changes. Depending on the state, there may
* be user requests. The service is free to change the download state in the
* middle of a user request, so the client should be able to handle this.
* <p>
* The Downloader Library includes a collection of string resources that
* correspond to each of the states, which you can use to provide users a
* useful message based on the state provided in this callback. To fetch the
* appropriate string for a state, call
* {@link Helpers#getDownloaderStringResourceIDFromState}.
* <p>
* What this means to the developer: The application has gotten a message
* that the download has paused due to lack of WiFi. The developer should
* then show UI asking the user if they want to enable downloading over
* cellular connections with appropriate warnings. If the application
* suddenly starts downloading, the application should revert to showing the
* progress again, rather than leaving up the download over cellular UI up.
*
* @param newState one of the STATE_* values defined in IDownloaderClient
*/
void onDownloadStateChanged(int newState);
/**
* Shows the download progress. This is intended to be used to fill out a
* client UI. This progress should only be shown in a few states such as
* STATE_DOWNLOADING.
*
* @param progress the DownloadProgressInfo object containing the current
* progress of all downloads.
*/
void onDownloadProgress(DownloadProgressInfo progress);
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader;
import com.google.android.vending.expansion.downloader.impl.DownloaderService;
import android.os.Messenger;
/**
* This interface is implemented by the DownloaderService and by the
* DownloaderServiceMarshaller. It contains functions to control the service.
* When a client binds to the service, it must call the onClientUpdated
* function.
* <p>
* You can acquire a proxy that implements this interface for your service by
* calling {@link DownloaderServiceMarshaller#CreateProxy} during the
* {@link IDownloaderClient#onServiceConnected} callback. At which point, you
* should immediately call {@link #onClientUpdated}.
*/
public interface IDownloaderService {
/**
* Set this flag in response to the
* IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION state and then
* call RequestContinueDownload to resume a download
*/
public static final int FLAGS_DOWNLOAD_OVER_CELLULAR = 1;
/**
* Request that the service abort the current download. The service should
* respond by changing the state to {@link IDownloaderClient.STATE_ABORTED}.
*/
void requestAbortDownload();
/**
* Request that the service pause the current download. The service should
* respond by changing the state to
* {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}.
*/
void requestPauseDownload();
/**
* Request that the service continue a paused download, when in any paused
* or failed state, including
* {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}.
*/
void requestContinueDownload();
/**
* Set the flags for this download (e.g.
* {@link DownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR}).
*
* @param flags
*/
void setDownloadFlags(int flags);
/**
* Requests that the download status be sent to the client.
*/
void requestDownloadStatus();
/**
* Call this when you get {@link
* IDownloaderClient.onServiceConnected(Messenger m)} from the
* DownloaderClient to register the client with the service. It will
* automatically send the current status to the client.
*
* @param clientMessenger
*/
void onClientUpdated(Messenger clientMessenger);
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader;
import android.content.Context;
import android.os.Messenger;
/**
* This is the interface that is used to connect/disconnect from the downloader
* service.
* <p>
* You should get a proxy object that implements this interface by calling
* {@link DownloaderClientMarshaller#CreateStub} in your activity when the
* downloader service starts. Then, call {@link #connect} during your activity's
* onResume() and call {@link #disconnect} during onStop().
* <p>
* Then during the {@link IDownloaderClient#onServiceConnected} callback, you
* should call {@link #getMessenger} to pass the stub's Messenger object to
* {@link IDownloaderService#onClientUpdated}.
*/
public interface IStub {
Messenger getMessenger();
void connect(Context c);
void disconnect(Context c);
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.telephony.TelephonyManager;
import android.util.Log;
/**
* Contains useful helper functions, typically tied to the application context.
*/
class SystemFacade {
private Context mContext;
private NotificationManager mNotificationManager;
public SystemFacade(Context context) {
mContext = context;
mNotificationManager = (NotificationManager)
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
}
public long currentTimeMillis() {
return System.currentTimeMillis();
}
public Integer getActiveNetworkType() {
ConnectivityManager connectivity =
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivity == null) {
Log.w(Constants.TAG, "couldn't get connectivity manager");
return null;
}
NetworkInfo activeInfo = connectivity.getActiveNetworkInfo();
if (activeInfo == null) {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "network is not available");
}
return null;
}
return activeInfo.getType();
}
public boolean isNetworkRoaming() {
ConnectivityManager connectivity =
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivity == null) {
Log.w(Constants.TAG, "couldn't get connectivity manager");
return false;
}
NetworkInfo info = connectivity.getActiveNetworkInfo();
boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE);
TelephonyManager tm = (TelephonyManager) mContext
.getSystemService(Context.TELEPHONY_SERVICE);
if (null == tm) {
Log.w(Constants.TAG, "couldn't get telephony manager");
return false;
}
boolean isRoaming = isMobile && tm.isNetworkRoaming();
if (Constants.LOGVV && isRoaming) {
Log.v(Constants.TAG, "network is roaming");
}
return isRoaming;
}
public Long getMaxBytesOverMobile() {
return (long) Integer.MAX_VALUE;
}
public Long getRecommendedMaxBytesOverMobile() {
return 2097152L;
}
public void sendBroadcast(Intent intent) {
mContext.sendBroadcast(intent);
}
public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException {
return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid;
}
public void postNotification(long id, Notification notification) {
/**
* TODO: The system notification manager takes ints, not longs, as IDs,
* but the download manager uses IDs take straight from the database,
* which are longs. This will have to be dealt with at some point.
*/
mNotificationManager.notify((int) id, notification);
}
public void cancelNotification(long id) {
mNotificationManager.cancel((int) id);
}
public void cancelAllNotifications() {
mNotificationManager.cancelAll();
}
public void startThread(Thread thread) {
thread.start();
}
}

View File

@@ -0,0 +1,536 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* This is a port of AndroidHttpClient to pre-Froyo devices, that takes advantage of
* the SSLSessionCache added Froyo devices using reflection.
*/
package com.google.android.vending.expansion.downloader.impl;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.scheme.SocketFactory;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.RequestWrapper;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.BasicHttpProcessor;
import org.apache.http.protocol.HttpContext;
import android.content.ContentResolver;
import android.content.Context;
import android.net.SSLCertificateSocketFactory;
import android.os.Looper;
import android.util.Log;
/**
* Subclass of the Apache {@link DefaultHttpClient} that is configured with
* reasonable default settings and registered schemes for Android, and
* also lets the user add {@link HttpRequestInterceptor} classes.
* Don't create this directly, use the {@link #newInstance} factory method.
*
* <p>This client processes cookies but does not retain them by default.
* To retain cookies, simply add a cookie store to the HttpContext:</p>
*
* <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre>
*/
public final class AndroidHttpClient implements HttpClient {
static Class<?> sSslSessionCacheClass;
static {
// if we are on Froyo+ devices, we can take advantage of the SSLSessionCache
try {
sSslSessionCacheClass = Class.forName("android.net.SSLSessionCache");
} catch (Exception e) {
}
}
// Gzip of data shorter than this probably won't be worthwhile
public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256;
// Default connection and socket timeout of 60 seconds. Tweak to taste.
private static final int SOCKET_OPERATION_TIMEOUT = 60 * 1000;
private static final String TAG = "AndroidHttpClient";
/** Interceptor throws an exception if the executing thread is blocked */
private static final HttpRequestInterceptor sThreadCheckInterceptor =
new HttpRequestInterceptor() {
public void process(HttpRequest request, HttpContext context) {
// Prevent the HttpRequest from being sent on the main thread
if (Looper.myLooper() != null && Looper.myLooper() == Looper.getMainLooper() ) {
throw new RuntimeException("This thread forbids HTTP requests");
}
}
};
/**
* Create a new HttpClient with reasonable defaults (which you can update).
*
* @param userAgent to report in your HTTP requests
* @param context to use for caching SSL sessions (may be null for no caching)
* @return AndroidHttpClient for you to use for all your requests.
*/
public static AndroidHttpClient newInstance(String userAgent, Context context) {
HttpParams params = new BasicHttpParams();
// Turn off stale checking. Our connections break all the time anyway,
// and it's not worth it to pay the penalty of checking every time.
HttpConnectionParams.setStaleCheckingEnabled(params, false);
HttpConnectionParams.setConnectionTimeout(params, SOCKET_OPERATION_TIMEOUT);
HttpConnectionParams.setSoTimeout(params, SOCKET_OPERATION_TIMEOUT);
HttpConnectionParams.setSocketBufferSize(params, 8192);
// Don't handle redirects -- return them to the caller. Our code
// often wants to re-POST after a redirect, which we must do ourselves.
HttpClientParams.setRedirecting(params, false);
Object sessionCache = null;
// Use a session cache for SSL sockets -- Froyo only
if ( null != context && null != sSslSessionCacheClass ) {
Constructor<?> ct;
try {
ct = sSslSessionCacheClass.getConstructor(Context.class);
sessionCache = ct.newInstance(context);
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// Set the specified user agent and register standard protocols.
HttpProtocolParams.setUserAgent(params, userAgent);
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http",
PlainSocketFactory.getSocketFactory(), 80));
SocketFactory sslCertificateSocketFactory = null;
if ( null != sessionCache ) {
Method getHttpSocketFactoryMethod;
try {
getHttpSocketFactoryMethod = SSLCertificateSocketFactory.class.getDeclaredMethod("getHttpSocketFactory",Integer.TYPE, sSslSessionCacheClass);
sslCertificateSocketFactory = (SocketFactory)getHttpSocketFactoryMethod.invoke(null, SOCKET_OPERATION_TIMEOUT, sessionCache);
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if ( null == sslCertificateSocketFactory ) {
sslCertificateSocketFactory = SSLSocketFactory.getSocketFactory();
}
schemeRegistry.register(new Scheme("https",
sslCertificateSocketFactory, 443));
ClientConnectionManager manager =
new ThreadSafeClientConnManager(params, schemeRegistry);
// We use a factory method to modify superclass initialization
// parameters without the funny call-a-static-method dance.
return new AndroidHttpClient(manager, params);
}
/**
* Create a new HttpClient with reasonable defaults (which you can update).
* @param userAgent to report in your HTTP requests.
* @return AndroidHttpClient for you to use for all your requests.
*/
public static AndroidHttpClient newInstance(String userAgent) {
return newInstance(userAgent, null /* session cache */);
}
private final HttpClient delegate;
private RuntimeException mLeakedException = new IllegalStateException(
"AndroidHttpClient created and never closed");
private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) {
this.delegate = new DefaultHttpClient(ccm, params) {
@Override
protected BasicHttpProcessor createHttpProcessor() {
// Add interceptor to prevent making requests from main thread.
BasicHttpProcessor processor = super.createHttpProcessor();
processor.addRequestInterceptor(sThreadCheckInterceptor);
processor.addRequestInterceptor(new CurlLogger());
return processor;
}
@Override
protected HttpContext createHttpContext() {
// Same as DefaultHttpClient.createHttpContext() minus the
// cookie store.
HttpContext context = new BasicHttpContext();
context.setAttribute(
ClientContext.AUTHSCHEME_REGISTRY,
getAuthSchemes());
context.setAttribute(
ClientContext.COOKIESPEC_REGISTRY,
getCookieSpecs());
context.setAttribute(
ClientContext.CREDS_PROVIDER,
getCredentialsProvider());
return context;
}
};
}
@Override
protected void finalize() throws Throwable {
super.finalize();
if (mLeakedException != null) {
Log.e(TAG, "Leak found", mLeakedException);
mLeakedException = null;
}
}
/**
* Modifies a request to indicate to the server that we would like a
* gzipped response. (Uses the "Accept-Encoding" HTTP header.)
* @param request the request to modify
* @see #getUngzippedContent
*/
public static void modifyRequestToAcceptGzipResponse(HttpRequest request) {
request.addHeader("Accept-Encoding", "gzip");
}
/**
* Gets the input stream from a response entity. If the entity is gzipped
* then this will get a stream over the uncompressed data.
*
* @param entity the entity whose content should be read
* @return the input stream to read from
* @throws IOException
*/
public static InputStream getUngzippedContent(HttpEntity entity)
throws IOException {
InputStream responseStream = entity.getContent();
if (responseStream == null) return responseStream;
Header header = entity.getContentEncoding();
if (header == null) return responseStream;
String contentEncoding = header.getValue();
if (contentEncoding == null) return responseStream;
if (contentEncoding.contains("gzip")) responseStream
= new GZIPInputStream(responseStream);
return responseStream;
}
/**
* Release resources associated with this client. You must call this,
* or significant resources (sockets and memory) may be leaked.
*/
public void close() {
if (mLeakedException != null) {
getConnectionManager().shutdown();
mLeakedException = null;
}
}
public HttpParams getParams() {
return delegate.getParams();
}
public ClientConnectionManager getConnectionManager() {
return delegate.getConnectionManager();
}
public HttpResponse execute(HttpUriRequest request) throws IOException {
return delegate.execute(request);
}
public HttpResponse execute(HttpUriRequest request, HttpContext context)
throws IOException {
return delegate.execute(request, context);
}
public HttpResponse execute(HttpHost target, HttpRequest request)
throws IOException {
return delegate.execute(target, request);
}
public HttpResponse execute(HttpHost target, HttpRequest request,
HttpContext context) throws IOException {
return delegate.execute(target, request, context);
}
public <T> T execute(HttpUriRequest request,
ResponseHandler<? extends T> responseHandler)
throws IOException, ClientProtocolException {
return delegate.execute(request, responseHandler);
}
public <T> T execute(HttpUriRequest request,
ResponseHandler<? extends T> responseHandler, HttpContext context)
throws IOException, ClientProtocolException {
return delegate.execute(request, responseHandler, context);
}
public <T> T execute(HttpHost target, HttpRequest request,
ResponseHandler<? extends T> responseHandler) throws IOException,
ClientProtocolException {
return delegate.execute(target, request, responseHandler);
}
public <T> T execute(HttpHost target, HttpRequest request,
ResponseHandler<? extends T> responseHandler, HttpContext context)
throws IOException, ClientProtocolException {
return delegate.execute(target, request, responseHandler, context);
}
/**
* Compress data to send to server.
* Creates a Http Entity holding the gzipped data.
* The data will not be compressed if it is too short.
* @param data The bytes to compress
* @return Entity holding the data
*/
public static AbstractHttpEntity getCompressedEntity(byte data[], ContentResolver resolver)
throws IOException {
AbstractHttpEntity entity;
if (data.length < getMinGzipSize(resolver)) {
entity = new ByteArrayEntity(data);
} else {
ByteArrayOutputStream arr = new ByteArrayOutputStream();
OutputStream zipper = new GZIPOutputStream(arr);
zipper.write(data);
zipper.close();
entity = new ByteArrayEntity(arr.toByteArray());
entity.setContentEncoding("gzip");
}
return entity;
}
/**
* Retrieves the minimum size for compressing data.
* Shorter data will not be compressed.
*/
public static long getMinGzipSize(ContentResolver resolver) {
return DEFAULT_SYNC_MIN_GZIP_BYTES; // For now, this is just a constant.
}
/* cURL logging support. */
/**
* Logging tag and level.
*/
private static class LoggingConfiguration {
private final String tag;
private final int level;
private LoggingConfiguration(String tag, int level) {
this.tag = tag;
this.level = level;
}
/**
* Returns true if logging is turned on for this configuration.
*/
private boolean isLoggable() {
return Log.isLoggable(tag, level);
}
/**
* Prints a message using this configuration.
*/
private void println(String message) {
Log.println(level, tag, message);
}
}
/** cURL logging configuration. */
private volatile LoggingConfiguration curlConfiguration;
/**
* Enables cURL request logging for this client.
*
* @param name to log messages with
* @param level at which to log messages (see {@link android.util.Log})
*/
public void enableCurlLogging(String name, int level) {
if (name == null) {
throw new NullPointerException("name");
}
if (level < Log.VERBOSE || level > Log.ASSERT) {
throw new IllegalArgumentException("Level is out of range ["
+ Log.VERBOSE + ".." + Log.ASSERT + "]");
}
curlConfiguration = new LoggingConfiguration(name, level);
}
/**
* Disables cURL logging for this client.
*/
public void disableCurlLogging() {
curlConfiguration = null;
}
/**
* Logs cURL commands equivalent to requests.
*/
private class CurlLogger implements HttpRequestInterceptor {
public void process(HttpRequest request, HttpContext context)
throws HttpException, IOException {
LoggingConfiguration configuration = curlConfiguration;
if (configuration != null
&& configuration.isLoggable()
&& request instanceof HttpUriRequest) {
// Never print auth token -- we used to check ro.secure=0 to
// enable that, but can't do that in unbundled code.
configuration.println(toCurl((HttpUriRequest) request, false));
}
}
}
/**
* Generates a cURL command equivalent to the given request.
*/
private static String toCurl(HttpUriRequest request, boolean logAuthToken) throws IOException {
StringBuilder builder = new StringBuilder();
builder.append("curl ");
for (Header header: request.getAllHeaders()) {
if (!logAuthToken
&& (header.getName().equals("Authorization") ||
header.getName().equals("Cookie"))) {
continue;
}
builder.append("--header \"");
builder.append(header.toString().trim());
builder.append("\" ");
}
URI uri = request.getURI();
// If this is a wrapped request, use the URI from the original
// request instead. getURI() on the wrapper seems to return a
// relative URI. We want an absolute URI.
if (request instanceof RequestWrapper) {
HttpRequest original = ((RequestWrapper) request).getOriginal();
if (original instanceof HttpUriRequest) {
uri = ((HttpUriRequest) original).getURI();
}
}
builder.append("\"");
builder.append(uri);
builder.append("\"");
if (request instanceof HttpEntityEnclosingRequest) {
HttpEntityEnclosingRequest entityRequest =
(HttpEntityEnclosingRequest) request;
HttpEntity entity = entityRequest.getEntity();
if (entity != null && entity.isRepeatable()) {
if (entity.getContentLength() < 1024) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
entity.writeTo(stream);
String entityString = stream.toString();
// TODO: Check the content type, too.
builder.append(" --data-ascii \"")
.append(entityString)
.append("\"");
} else {
builder.append(" [TOO MUCH DATA TO INCLUDE]");
}
}
}
return builder.toString();
}
/**
* Returns the date of the given HTTP date string. This method can identify
* and parse the date formats emitted by common HTTP servers, such as
* <a href="http://www.ietf.org/rfc/rfc0822.txt">RFC 822</a>,
* <a href="http://www.ietf.org/rfc/rfc0850.txt">RFC 850</a>,
* <a href="http://www.ietf.org/rfc/rfc1036.txt">RFC 1036</a>,
* <a href="http://www.ietf.org/rfc/rfc1123.txt">RFC 1123</a> and
* <a href="http://www.opengroup.org/onlinepubs/007908799/xsh/asctime.html">ANSI
* C's asctime()</a>.
*
* @return the number of milliseconds since Jan. 1, 1970, midnight GMT.
* @throws IllegalArgumentException if {@code dateString} is not a date or
* of an unsupported format.
*/
public static long parseDate(String dateString) {
return HttpDateTime.parse(dateString);
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader.impl;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
/**
* This service differs from IntentService in a few minor ways/ It will not
* auto-stop itself after the intent is handled unless the target returns "true"
* in should stop. Since the goal of this service is to handle a single kind of
* intent, it does not queue up batches of intents of the same type.
*/
public abstract class CustomIntentService extends Service {
private String mName;
private boolean mRedelivery;
private volatile ServiceHandler mServiceHandler;
private volatile Looper mServiceLooper;
private static final String LOG_TAG = "CancellableIntentService";
private static final int WHAT_MESSAGE = -10;
public CustomIntentService(String paramString) {
this.mName = paramString;
}
@Override
public IBinder onBind(Intent paramIntent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
HandlerThread localHandlerThread = new HandlerThread("IntentService["
+ this.mName + "]");
localHandlerThread.start();
this.mServiceLooper = localHandlerThread.getLooper();
this.mServiceHandler = new ServiceHandler(this.mServiceLooper);
}
@Override
public void onDestroy() {
Thread localThread = this.mServiceLooper.getThread();
if ((localThread != null) && (localThread.isAlive())) {
localThread.interrupt();
}
this.mServiceLooper.quit();
Log.d(LOG_TAG, "onDestroy");
}
protected abstract void onHandleIntent(Intent paramIntent);
protected abstract boolean shouldStop();
@Override
public void onStart(Intent paramIntent, int startId) {
if (!this.mServiceHandler.hasMessages(WHAT_MESSAGE)) {
Message localMessage = this.mServiceHandler.obtainMessage();
localMessage.arg1 = startId;
localMessage.obj = paramIntent;
localMessage.what = WHAT_MESSAGE;
this.mServiceHandler.sendMessage(localMessage);
}
}
@Override
public int onStartCommand(Intent paramIntent, int flags, int startId) {
onStart(paramIntent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}
public void setIntentRedelivery(boolean enabled) {
this.mRedelivery = enabled;
}
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message paramMessage) {
CustomIntentService.this
.onHandleIntent((Intent) paramMessage.obj);
if (shouldStop()) {
Log.d(LOG_TAG, "stopSelf");
CustomIntentService.this.stopSelf(paramMessage.arg1);
Log.d(LOG_TAG, "afterStopSelf");
}
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader.impl;
/**
* Uses the class-loader model to utilize the updated notification builders in
* Honeycomb while maintaining a compatible version for older devices.
*/
public class CustomNotificationFactory {
static public DownloadNotification.ICustomNotification createCustomNotification() {
if (android.os.Build.VERSION.SDK_INT > 13)
return new V14CustomNotification();
else
return new V3CustomNotification();
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader.impl;
import com.google.android.vending.expansion.downloader.Constants;
import com.google.android.vending.expansion.downloader.Helpers;
import android.util.Log;
/**
* Representation of information about an individual download from the database.
*/
public class DownloadInfo {
public String mUri;
public final int mIndex;
public final String mFileName;
public String mETag;
public long mTotalBytes;
public long mCurrentBytes;
public long mLastMod;
public int mStatus;
public int mControl;
public int mNumFailed;
public int mRetryAfter;
public int mRedirectCount;
boolean mInitialized;
public int mFuzz;
public DownloadInfo(int index, String fileName, String pkg) {
mFuzz = Helpers.sRandom.nextInt(1001);
mFileName = fileName;
mIndex = index;
}
public void resetDownload() {
mCurrentBytes = 0;
mETag = "";
mLastMod = 0;
mStatus = 0;
mControl = 0;
mNumFailed = 0;
mRetryAfter = 0;
mRedirectCount = 0;
}
/**
* Returns the time when a download should be restarted.
*/
public long restartTime(long now) {
if (mNumFailed == 0) {
return now;
}
if (mRetryAfter > 0) {
return mLastMod + mRetryAfter;
}
return mLastMod +
Constants.RETRY_FIRST_DELAY *
(1000 + mFuzz) * (1 << (mNumFailed - 1));
}
public void logVerboseInfo() {
Log.v(Constants.TAG, "Service adding new entry");
Log.v(Constants.TAG, "FILENAME: " + mFileName);
Log.v(Constants.TAG, "URI : " + mUri);
Log.v(Constants.TAG, "FILENAME: " + mFileName);
Log.v(Constants.TAG, "CONTROL : " + mControl);
Log.v(Constants.TAG, "STATUS : " + mStatus);
Log.v(Constants.TAG, "FAILED_C: " + mNumFailed);
Log.v(Constants.TAG, "RETRY_AF: " + mRetryAfter);
Log.v(Constants.TAG, "REDIRECT: " + mRedirectCount);
Log.v(Constants.TAG, "LAST_MOD: " + mLastMod);
Log.v(Constants.TAG, "TOTAL : " + mTotalBytes);
Log.v(Constants.TAG, "CURRENT : " + mCurrentBytes);
Log.v(Constants.TAG, "ETAG : " + mETag);
}
}

View File

@@ -0,0 +1,231 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader.impl;
import com.android.vending.expansion.downloader.R;
import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
import com.google.android.vending.expansion.downloader.Helpers;
import com.google.android.vending.expansion.downloader.IDownloaderClient;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.os.Messenger;
/**
* This class handles displaying the notification associated with the download
* queue going on in the download manager. It handles multiple status types;
* Some require user interaction and some do not. Some of the user interactions
* may be transient. (for example: the user is queried to continue the download
* on 3G when it started on WiFi, but then the phone locks onto WiFi again so
* the prompt automatically goes away)
* <p/>
* The application interface for the downloader also needs to understand and
* handle these transient states.
*/
public class DownloadNotification implements IDownloaderClient {
private int mState;
private final Context mContext;
private final NotificationManager mNotificationManager;
private String mCurrentTitle;
private IDownloaderClient mClientProxy;
final ICustomNotification mCustomNotification;
private Notification mNotification;
private Notification mCurrentNotification;
private CharSequence mLabel;
private String mCurrentText;
private PendingIntent mContentIntent;
private DownloadProgressInfo mProgressInfo;
static final String LOGTAG = "DownloadNotification";
static final int NOTIFICATION_ID = LOGTAG.hashCode();
public PendingIntent getClientIntent() {
return mContentIntent;
}
public void setClientIntent(PendingIntent mClientIntent) {
this.mContentIntent = mClientIntent;
}
public void resendState() {
if (null != mClientProxy) {
mClientProxy.onDownloadStateChanged(mState);
}
}
@Override
public void onDownloadStateChanged(int newState) {
if (null != mClientProxy) {
mClientProxy.onDownloadStateChanged(newState);
}
if (newState != mState) {
mState = newState;
if (newState == IDownloaderClient.STATE_IDLE || null == mContentIntent) {
return;
}
int stringDownloadID;
int iconResource;
boolean ongoingEvent;
// get the new title string and paused text
switch (newState) {
case 0:
iconResource = android.R.drawable.stat_sys_warning;
stringDownloadID = R.string.state_unknown;
ongoingEvent = false;
break;
case IDownloaderClient.STATE_DOWNLOADING:
iconResource = android.R.drawable.stat_sys_download;
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
ongoingEvent = true;
break;
case IDownloaderClient.STATE_FETCHING_URL:
case IDownloaderClient.STATE_CONNECTING:
iconResource = android.R.drawable.stat_sys_download_done;
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
ongoingEvent = true;
break;
case IDownloaderClient.STATE_COMPLETED:
case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
iconResource = android.R.drawable.stat_sys_download_done;
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
ongoingEvent = false;
break;
case IDownloaderClient.STATE_FAILED:
case IDownloaderClient.STATE_FAILED_CANCELED:
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
case IDownloaderClient.STATE_FAILED_UNLICENSED:
iconResource = android.R.drawable.stat_sys_warning;
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
ongoingEvent = false;
break;
default:
iconResource = android.R.drawable.stat_sys_warning;
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
ongoingEvent = true;
break;
}
mCurrentText = mContext.getString(stringDownloadID);
mCurrentTitle = mLabel.toString();
mCurrentNotification.tickerText = mLabel + ": " + mCurrentText;
mCurrentNotification.icon = iconResource;
mCurrentNotification.setLatestEventInfo(mContext, mCurrentTitle, mCurrentText,
mContentIntent);
if (ongoingEvent) {
mCurrentNotification.flags |= Notification.FLAG_ONGOING_EVENT;
} else {
mCurrentNotification.flags &= ~Notification.FLAG_ONGOING_EVENT;
mCurrentNotification.flags |= Notification.FLAG_AUTO_CANCEL;
}
mNotificationManager.notify(NOTIFICATION_ID, mCurrentNotification);
}
}
@Override
public void onDownloadProgress(DownloadProgressInfo progress) {
mProgressInfo = progress;
if (null != mClientProxy) {
mClientProxy.onDownloadProgress(progress);
}
if (progress.mOverallTotal <= 0) {
// we just show the text
mNotification.tickerText = mCurrentTitle;
mNotification.icon = android.R.drawable.stat_sys_download;
mNotification.setLatestEventInfo(mContext, mLabel, mCurrentText, mContentIntent);
mCurrentNotification = mNotification;
} else {
mCustomNotification.setCurrentBytes(progress.mOverallProgress);
mCustomNotification.setTotalBytes(progress.mOverallTotal);
mCustomNotification.setIcon(android.R.drawable.stat_sys_download);
mCustomNotification.setPendingIntent(mContentIntent);
mCustomNotification.setTicker(mLabel + ": " + mCurrentText);
mCustomNotification.setTitle(mLabel);
mCustomNotification.setTimeRemaining(progress.mTimeRemaining);
mCurrentNotification = mCustomNotification.updateNotification(mContext);
}
mNotificationManager.notify(NOTIFICATION_ID, mCurrentNotification);
}
public interface ICustomNotification {
void setTitle(CharSequence title);
void setTicker(CharSequence ticker);
void setPendingIntent(PendingIntent mContentIntent);
void setTotalBytes(long totalBytes);
void setCurrentBytes(long currentBytes);
void setIcon(int iconResource);
void setTimeRemaining(long timeRemaining);
Notification updateNotification(Context c);
}
/**
* Called in response to onClientUpdated. Creates a new proxy and notifies
* it of the current state.
*
* @param msg the client Messenger to notify
*/
public void setMessenger(Messenger msg) {
mClientProxy = DownloaderClientMarshaller.CreateProxy(msg);
if (null != mProgressInfo) {
mClientProxy.onDownloadProgress(mProgressInfo);
}
if (mState != -1) {
mClientProxy.onDownloadStateChanged(mState);
}
}
/**
* Constructor
*
* @param ctx The context to use to obtain access to the Notification
* Service
*/
DownloadNotification(Context ctx, CharSequence applicationLabel) {
mState = -1;
mContext = ctx;
mLabel = applicationLabel;
mNotificationManager = (NotificationManager)
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
mCustomNotification = CustomNotificationFactory
.createCustomNotification();
mNotification = new Notification();
mCurrentNotification = mNotification;
}
@Override
public void onServiceConnected(Messenger m) {
}
}

View File

@@ -0,0 +1,963 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader.impl;
import com.google.android.vending.expansion.downloader.Constants;
import com.google.android.vending.expansion.downloader.Helpers;
import com.google.android.vending.expansion.downloader.IDownloaderClient;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.params.ConnRouteParams;
import android.content.Context;
import android.net.Proxy;
import android.os.PowerManager;
import android.os.Process;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SyncFailedException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Locale;
/**
* Runs an actual download
*/
public class DownloadThread {
private Context mContext;
private DownloadInfo mInfo;
private DownloaderService mService;
private final DownloadsDB mDB;
private final DownloadNotification mNotification;
private String mUserAgent;
public DownloadThread(DownloadInfo info, DownloaderService service,
DownloadNotification notification) {
mContext = service;
mInfo = info;
mService = service;
mNotification = notification;
mDB = DownloadsDB.getDB(service);
mUserAgent = "APKXDL (Linux; U; Android " + android.os.Build.VERSION.RELEASE + ";"
+ Locale.getDefault().toString() + "; " + android.os.Build.DEVICE + "/"
+ android.os.Build.ID + ")" +
service.getPackageName();
}
/**
* Returns the default user agent
*/
private String userAgent() {
return mUserAgent;
}
/**
* State for the entire run() method.
*/
private static class State {
public String mFilename;
public FileOutputStream mStream;
public boolean mCountRetry = false;
public int mRetryAfter = 0;
public int mRedirectCount = 0;
public String mNewUri;
public boolean mGotData = false;
public String mRequestUri;
public State(DownloadInfo info, DownloaderService service) {
mRedirectCount = info.mRedirectCount;
mRequestUri = info.mUri;
mFilename = service.generateTempSaveFileName(info.mFileName);
}
}
/**
* State within executeDownload()
*/
private static class InnerState {
public int mBytesSoFar = 0;
public int mBytesThisSession = 0;
public String mHeaderETag;
public boolean mContinuingDownload = false;
public String mHeaderContentLength;
public String mHeaderContentDisposition;
public String mHeaderContentLocation;
public int mBytesNotified = 0;
public long mTimeLastNotification = 0;
}
/**
* Raised from methods called by run() to indicate that the current request
* should be stopped immediately. Note the message passed to this exception
* will be logged and therefore must be guaranteed not to contain any PII,
* meaning it generally can't include any information about the request URI,
* headers, or destination filename.
*/
private class StopRequest extends Throwable {
/**
*
*/
private static final long serialVersionUID = 6338592678988347973L;
public int mFinalStatus;
public StopRequest(int finalStatus, String message) {
super(message);
mFinalStatus = finalStatus;
}
public StopRequest(int finalStatus, String message, Throwable throwable) {
super(message, throwable);
mFinalStatus = finalStatus;
}
}
/**
* Raised from methods called by executeDownload() to indicate that the
* download should be retried immediately.
*/
private class RetryDownload extends Throwable {
/**
*
*/
private static final long serialVersionUID = 6196036036517540229L;
}
/**
* Returns the preferred proxy to be used by clients. This is a wrapper
* around {@link android.net.Proxy#getHost()}. Currently no proxy will be
* returned for localhost or if the active network is Wi-Fi.
*
* @param context the context which will be passed to
* {@link android.net.Proxy#getHost()}
* @param url the target URL for the request
* @note Calling this method requires permission
* android.permission.ACCESS_NETWORK_STATE
* @return The preferred proxy to be used by clients, or null if there is no
* proxy.
*/
public HttpHost getPreferredHttpHost(Context context,
String url) {
if (!isLocalHost(url) && !mService.isWiFi()) {
final String proxyHost = Proxy.getHost(context);
if (proxyHost != null) {
return new HttpHost(proxyHost, Proxy.getPort(context), "http");
}
}
return null;
}
static final private boolean isLocalHost(String url) {
if (url == null) {
return false;
}
try {
final URI uri = URI.create(url);
final String host = uri.getHost();
if (host != null) {
// TODO: InetAddress.isLoopbackAddress should be used to check
// for localhost. However no public factory methods exist which
// can be used without triggering DNS lookup if host is not
// localhost.
if (host.equalsIgnoreCase("localhost") ||
host.equals("127.0.0.1") ||
host.equals("[::1]")) {
return true;
}
}
} catch (IllegalArgumentException iex) {
// Ignore (URI.create)
}
return false;
}
/**
* Executes the download in a separate thread
*/
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
State state = new State(mInfo, mService);
AndroidHttpClient client = null;
PowerManager.WakeLock wakeLock = null;
int finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR;
try {
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
wakeLock.acquire();
if (Constants.LOGV) {
Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
Log.v(Constants.TAG, " at " + mInfo.mUri);
}
client = AndroidHttpClient.newInstance(userAgent(), mContext);
boolean finished = false;
while (!finished) {
if (Constants.LOGV) {
Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
Log.v(Constants.TAG, " at " + mInfo.mUri);
}
// Set or unset proxy, which may have changed since last GET
// request.
// setDefaultProxy() supports null as proxy parameter.
ConnRouteParams.setDefaultProxy(client.getParams(),
getPreferredHttpHost(mContext, state.mRequestUri));
HttpGet request = new HttpGet(state.mRequestUri);
try {
executeDownload(state, client, request);
finished = true;
} catch (RetryDownload exc) {
// fall through
} finally {
request.abort();
request = null;
}
}
if (Constants.LOGV) {
Log.v(Constants.TAG, "download completed for " + mInfo.mFileName);
Log.v(Constants.TAG, " at " + mInfo.mUri);
}
finalizeDestinationFile(state);
finalStatus = DownloaderService.STATUS_SUCCESS;
} catch (StopRequest error) {
// remove the cause before printing, in case it contains PII
Log.w(Constants.TAG,
"Aborting request for download " + mInfo.mFileName + ": " + error.getMessage());
error.printStackTrace();
finalStatus = error.mFinalStatus;
// fall through to finally block
} catch (Throwable ex) { // sometimes the socket code throws unchecked
// exceptions
Log.w(Constants.TAG, "Exception for " + mInfo.mFileName + ": " + ex);
finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR;
// falls through to the code that reports an error
} finally {
if (wakeLock != null) {
wakeLock.release();
wakeLock = null;
}
if (client != null) {
client.close();
client = null;
}
cleanupDestination(state, finalStatus);
notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
state.mRedirectCount, state.mGotData, state.mFilename);
}
}
/**
* Fully execute a single download request - setup and send the request,
* handle the response, and transfer the data to the destination file.
*/
private void executeDownload(State state, AndroidHttpClient client, HttpGet request)
throws StopRequest, RetryDownload {
InnerState innerState = new InnerState();
byte data[] = new byte[Constants.BUFFER_SIZE];
checkPausedOrCanceled(state);
setupDestinationFile(state, innerState);
addRequestHeaders(innerState, request);
// check just before sending the request to avoid using an invalid
// connection at all
checkConnectivity(state);
mNotification.onDownloadStateChanged(IDownloaderClient.STATE_CONNECTING);
HttpResponse response = sendRequest(state, client, request);
handleExceptionalStatus(state, innerState, response);
if (Constants.LOGV) {
Log.v(Constants.TAG, "received response for " + mInfo.mUri);
}
processResponseHeaders(state, innerState, response);
InputStream entityStream = openResponseEntity(state, response);
mNotification.onDownloadStateChanged(IDownloaderClient.STATE_DOWNLOADING);
transferData(state, innerState, data, entityStream);
}
/**
* Check if current connectivity is valid for this request.
*/
private void checkConnectivity(State state) throws StopRequest {
switch (mService.getNetworkAvailabilityState(mDB)) {
case DownloaderService.NETWORK_OK:
return;
case DownloaderService.NETWORK_NO_CONNECTION:
throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK,
"waiting for network to return");
case DownloaderService.NETWORK_TYPE_DISALLOWED_BY_REQUESTOR:
throw new StopRequest(
DownloaderService.STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION,
"waiting for wifi or for download over cellular to be authorized");
case DownloaderService.NETWORK_CANNOT_USE_ROAMING:
throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK,
"roaming is not allowed");
case DownloaderService.NETWORK_UNUSABLE_DUE_TO_SIZE:
throw new StopRequest(DownloaderService.STATUS_QUEUED_FOR_WIFI, "waiting for wifi");
}
}
/**
* Transfer as much data as possible from the HTTP response to the
* destination file.
*
* @param data buffer to use to read data
* @param entityStream stream for reading the HTTP response entity
*/
private void transferData(State state, InnerState innerState, byte[] data,
InputStream entityStream) throws StopRequest {
for (;;) {
int bytesRead = readFromResponse(state, innerState, data, entityStream);
if (bytesRead == -1) { // success, end of stream already reached
handleEndOfStream(state, innerState);
return;
}
state.mGotData = true;
writeDataToDestination(state, data, bytesRead);
innerState.mBytesSoFar += bytesRead;
innerState.mBytesThisSession += bytesRead;
reportProgress(state, innerState);
checkPausedOrCanceled(state);
}
}
/**
* Called after a successful completion to take any necessary action on the
* downloaded file.
*/
private void finalizeDestinationFile(State state) throws StopRequest {
syncDestination(state);
String tempFilename = state.mFilename;
String finalFilename = Helpers.generateSaveFileName(mService, mInfo.mFileName);
if (!state.mFilename.equals(finalFilename)) {
File startFile = new File(tempFilename);
File destFile = new File(finalFilename);
if (mInfo.mTotalBytes != -1 && mInfo.mCurrentBytes == mInfo.mTotalBytes) {
if (!startFile.renameTo(destFile)) {
throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
"unable to finalize destination file");
}
} else {
throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY,
"file delivered with incorrect size. probably due to network not browser configured");
}
}
}
/**
* Called just before the thread finishes, regardless of status, to take any
* necessary action on the downloaded file.
*/
private void cleanupDestination(State state, int finalStatus) {
closeDestination(state);
if (state.mFilename != null && DownloaderService.isStatusError(finalStatus)) {
new File(state.mFilename).delete();
state.mFilename = null;
}
}
/**
* Sync the destination file to storage.
*/
private void syncDestination(State state) {
FileOutputStream downloadedFileStream = null;
try {
downloadedFileStream = new FileOutputStream(state.mFilename, true);
downloadedFileStream.getFD().sync();
} catch (FileNotFoundException ex) {
Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
} catch (SyncFailedException ex) {
Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
} catch (IOException ex) {
Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
} catch (RuntimeException ex) {
Log.w(Constants.TAG, "exception while syncing file: ", ex);
} finally {
if (downloadedFileStream != null) {
try {
downloadedFileStream.close();
} catch (IOException ex) {
Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
} catch (RuntimeException ex) {
Log.w(Constants.TAG, "exception while closing file: ", ex);
}
}
}
}
/**
* Close the destination output stream.
*/
private void closeDestination(State state) {
try {
// close the file
if (state.mStream != null) {
state.mStream.close();
state.mStream = null;
}
} catch (IOException ex) {
if (Constants.LOGV) {
Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
}
// nothing can really be done if the file can't be closed
}
}
/**
* Check if the download has been paused or canceled, stopping the request
* appropriately if it has been.
*/
private void checkPausedOrCanceled(State state) throws StopRequest {
if (mService.getControl() == DownloaderService.CONTROL_PAUSED) {
int status = mService.getStatus();
switch (status) {
case DownloaderService.STATUS_PAUSED_BY_APP:
throw new StopRequest(mService.getStatus(),
"download paused");
}
}
}
/**
* Report download progress through the database if necessary.
*/
private void reportProgress(State state, InnerState innerState) {
long now = System.currentTimeMillis();
if (innerState.mBytesSoFar - innerState.mBytesNotified
> Constants.MIN_PROGRESS_STEP
&& now - innerState.mTimeLastNotification
> Constants.MIN_PROGRESS_TIME) {
// we store progress updates to the database here
mInfo.mCurrentBytes = innerState.mBytesSoFar;
mDB.updateDownloadCurrentBytes(mInfo);
innerState.mBytesNotified = innerState.mBytesSoFar;
innerState.mTimeLastNotification = now;
long totalBytesSoFar = innerState.mBytesThisSession + mService.mBytesSoFar;
if (Constants.LOGVV) {
Log.v(Constants.TAG, "downloaded " + mInfo.mCurrentBytes + " out of "
+ mInfo.mTotalBytes);
Log.v(Constants.TAG, " total " + totalBytesSoFar + " out of "
+ mService.mTotalLength);
}
mService.notifyUpdateBytes(totalBytesSoFar);
}
}
/**
* Write a data buffer to the destination file.
*
* @param data buffer containing the data to write
* @param bytesRead how many bytes to write from the buffer
*/
private void writeDataToDestination(State state, byte[] data, int bytesRead)
throws StopRequest {
for (;;) {
try {
if (state.mStream == null) {
state.mStream = new FileOutputStream(state.mFilename, true);
}
state.mStream.write(data, 0, bytesRead);
// we close after every write --- this may be too inefficient
closeDestination(state);
return;
} catch (IOException ex) {
if (!Helpers.isExternalMediaMounted()) {
throw new StopRequest(DownloaderService.STATUS_DEVICE_NOT_FOUND_ERROR,
"external media not mounted while writing destination file");
}
long availableBytes =
Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename));
if (availableBytes < bytesRead) {
throw new StopRequest(DownloaderService.STATUS_INSUFFICIENT_SPACE_ERROR,
"insufficient space while writing destination file", ex);
}
throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
"while writing destination file: " + ex.toString(), ex);
}
}
}
/**
* Called when we've reached the end of the HTTP response stream, to update
* the database and check for consistency.
*/
private void handleEndOfStream(State state, InnerState innerState) throws StopRequest {
mInfo.mCurrentBytes = innerState.mBytesSoFar;
// this should always be set from the market
// if ( innerState.mHeaderContentLength == null ) {
// mInfo.mTotalBytes = innerState.mBytesSoFar;
// }
mDB.updateDownload(mInfo);
boolean lengthMismatched = (innerState.mHeaderContentLength != null)
&& (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
if (lengthMismatched) {
if (cannotResume(innerState)) {
throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
"mismatched content length");
} else {
throw new StopRequest(getFinalStatusForHttpError(state),
"closed socket before end of file");
}
}
}
private boolean cannotResume(InnerState innerState) {
return innerState.mBytesSoFar > 0 && innerState.mHeaderETag == null;
}
/**
* Read some data from the HTTP response stream, handling I/O errors.
*
* @param data buffer to use to read data
* @param entityStream stream for reading the HTTP response entity
* @return the number of bytes actually read or -1 if the end of the stream
* has been reached
*/
private int readFromResponse(State state, InnerState innerState, byte[] data,
InputStream entityStream) throws StopRequest {
try {
return entityStream.read(data);
} catch (IOException ex) {
logNetworkState();
mInfo.mCurrentBytes = innerState.mBytesSoFar;
mDB.updateDownload(mInfo);
if (cannotResume(innerState)) {
String message = "while reading response: " + ex.toString()
+ ", can't resume interrupted download with no ETag";
throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
message, ex);
} else {
throw new StopRequest(getFinalStatusForHttpError(state),
"while reading response: " + ex.toString(), ex);
}
}
}
/**
* Open a stream for the HTTP response entity, handling I/O errors.
*
* @return an InputStream to read the response entity
*/
private InputStream openResponseEntity(State state, HttpResponse response)
throws StopRequest {
try {
return response.getEntity().getContent();
} catch (IOException ex) {
logNetworkState();
throw new StopRequest(getFinalStatusForHttpError(state),
"while getting entity: " + ex.toString(), ex);
}
}
private void logNetworkState() {
if (Constants.LOGX) {
Log.i(Constants.TAG,
"Net "
+ (mService.getNetworkAvailabilityState(mDB) == DownloaderService.NETWORK_OK ? "Up"
: "Down"));
}
}
/**
* Read HTTP response headers and take appropriate action, including setting
* up the destination file and updating the database.
*/
private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
throws StopRequest {
if (innerState.mContinuingDownload) {
// ignore response headers on resume requests
return;
}
readResponseHeaders(state, innerState, response);
try {
state.mFilename = mService.generateSaveFile(mInfo.mFileName, mInfo.mTotalBytes);
} catch (DownloaderService.GenerateSaveFileError exc) {
throw new StopRequest(exc.mStatus, exc.mMessage);
}
try {
state.mStream = new FileOutputStream(state.mFilename);
} catch (FileNotFoundException exc) {
// make sure the directory exists
File pathFile = new File(Helpers.getSaveFilePath(mService));
try {
if (pathFile.mkdirs()) {
state.mStream = new FileOutputStream(state.mFilename);
}
} catch (Exception ex) {
throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
"while opening destination file: " + exc.toString(), exc);
}
}
if (Constants.LOGV) {
Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
}
updateDatabaseFromHeaders(state, innerState);
// check connectivity again now that we know the total size
checkConnectivity(state);
}
/**
* Update necessary database fields based on values of HTTP response headers
* that have been read.
*/
private void updateDatabaseFromHeaders(State state, InnerState innerState) {
mInfo.mETag = innerState.mHeaderETag;
mDB.updateDownload(mInfo);
}
/**
* Read headers from the HTTP response and store them into local state.
*/
private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
throws StopRequest {
Header header = response.getFirstHeader("Content-Disposition");
if (header != null) {
innerState.mHeaderContentDisposition = header.getValue();
}
header = response.getFirstHeader("Content-Location");
if (header != null) {
innerState.mHeaderContentLocation = header.getValue();
}
header = response.getFirstHeader("ETag");
if (header != null) {
innerState.mHeaderETag = header.getValue();
}
String headerTransferEncoding = null;
header = response.getFirstHeader("Transfer-Encoding");
if (header != null) {
headerTransferEncoding = header.getValue();
}
String headerContentType = null;
header = response.getFirstHeader("Content-Type");
if (header != null) {
headerContentType = header.getValue();
if (!headerContentType.equals("application/vnd.android.obb")) {
throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY,
"file delivered with incorrect Mime type");
}
}
if (headerTransferEncoding == null) {
header = response.getFirstHeader("Content-Length");
if (header != null) {
innerState.mHeaderContentLength = header.getValue();
// this is always set from Market
long contentLength = Long.parseLong(innerState.mHeaderContentLength);
if (contentLength != -1 && contentLength != mInfo.mTotalBytes) {
// we're most likely on a bad wifi connection -- we should
// probably
// also look at the mime type --- but the size mismatch is
// enough
// to tell us that something is wrong here
Log.e(Constants.TAG, "Incorrect file size delivered.");
}
}
} else {
// Ignore content-length with transfer-encoding - 2616 4.4 3
if (Constants.LOGVV) {
Log.v(Constants.TAG,
"ignoring content-length because of xfer-encoding");
}
}
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Content-Disposition: " +
innerState.mHeaderContentDisposition);
Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag);
Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
}
boolean noSizeInfo = innerState.mHeaderContentLength == null
&& (headerTransferEncoding == null
|| !headerTransferEncoding.equalsIgnoreCase("chunked"));
if (noSizeInfo) {
throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
"can't know size of download, giving up");
}
}
/**
* Check the HTTP response status and handle anything unusual (e.g. not
* 200/206).
*/
private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
throws StopRequest, RetryDownload {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
handleServiceUnavailable(state, response);
}
if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) {
handleRedirect(state, response, statusCode);
}
int expectedStatus = innerState.mContinuingDownload ? 206
: DownloaderService.STATUS_SUCCESS;
if (statusCode != expectedStatus) {
handleOtherStatus(state, innerState, statusCode);
} else {
// no longer redirected
state.mRedirectCount = 0;
}
}
/**
* Handle a status that we don't know how to deal with properly.
*/
private void handleOtherStatus(State state, InnerState innerState, int statusCode)
throws StopRequest {
int finalStatus;
if (DownloaderService.isStatusError(statusCode)) {
finalStatus = statusCode;
} else if (statusCode >= 300 && statusCode < 400) {
finalStatus = DownloaderService.STATUS_UNHANDLED_REDIRECT;
} else if (innerState.mContinuingDownload && statusCode == DownloaderService.STATUS_SUCCESS) {
finalStatus = DownloaderService.STATUS_CANNOT_RESUME;
} else {
finalStatus = DownloaderService.STATUS_UNHANDLED_HTTP_CODE;
}
throw new StopRequest(finalStatus, "http error " + statusCode);
}
/**
* Handle a 3xx redirect status.
*/
private void handleRedirect(State state, HttpResponse response, int statusCode)
throws StopRequest, RetryDownload {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
}
if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
throw new StopRequest(DownloaderService.STATUS_TOO_MANY_REDIRECTS, "too many redirects");
}
Header header = response.getFirstHeader("Location");
if (header == null) {
return;
}
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Location :" + header.getValue());
}
String newUri;
try {
newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString();
} catch (URISyntaxException ex) {
if (Constants.LOGV) {
Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue()
+ " for " + mInfo.mUri);
}
throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
"Couldn't resolve redirect URI");
}
++state.mRedirectCount;
state.mRequestUri = newUri;
if (statusCode == 301 || statusCode == 303) {
// use the new URI for all future requests (should a retry/resume be
// necessary)
state.mNewUri = newUri;
}
throw new RetryDownload();
}
/**
* Add headers for this download to the HTTP request to allow for resume.
*/
private void addRequestHeaders(InnerState innerState, HttpGet request) {
if (innerState.mContinuingDownload) {
if (innerState.mHeaderETag != null) {
request.addHeader("If-Match", innerState.mHeaderETag);
}
request.addHeader("Range", "bytes=" + innerState.mBytesSoFar + "-");
}
}
/**
* Handle a 503 Service Unavailable status by processing the Retry-After
* header.
*/
private void handleServiceUnavailable(State state, HttpResponse response) throws StopRequest {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "got HTTP response code 503");
}
state.mCountRetry = true;
Header header = response.getFirstHeader("Retry-After");
if (header != null) {
try {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Retry-After :" + header.getValue());
}
state.mRetryAfter = Integer.parseInt(header.getValue());
if (state.mRetryAfter < 0) {
state.mRetryAfter = 0;
} else {
if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
state.mRetryAfter = Constants.MIN_RETRY_AFTER;
} else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
state.mRetryAfter = Constants.MAX_RETRY_AFTER;
}
state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
state.mRetryAfter *= 1000;
}
} catch (NumberFormatException ex) {
// ignored - retryAfter stays 0 in this case.
}
}
throw new StopRequest(DownloaderService.STATUS_WAITING_TO_RETRY,
"got 503 Service Unavailable, will retry later");
}
/**
* Send the request to the server, handling any I/O exceptions.
*/
private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
throws StopRequest {
try {
return client.execute(request);
} catch (IllegalArgumentException ex) {
throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
"while trying to execute request: " + ex.toString(), ex);
} catch (IOException ex) {
logNetworkState();
throw new StopRequest(getFinalStatusForHttpError(state),
"while trying to execute request: " + ex.toString(), ex);
}
}
private int getFinalStatusForHttpError(State state) {
if (mService.getNetworkAvailabilityState(mDB) != DownloaderService.NETWORK_OK) {
return DownloaderService.STATUS_WAITING_FOR_NETWORK;
} else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
state.mCountRetry = true;
return DownloaderService.STATUS_WAITING_TO_RETRY;
} else {
Log.w(Constants.TAG, "reached max retries for " + mInfo.mNumFailed);
return DownloaderService.STATUS_HTTP_DATA_ERROR;
}
}
/**
* Prepare the destination file to receive data. If the file already exists,
* we'll set up appropriately for resumption.
*/
private void setupDestinationFile(State state, InnerState innerState)
throws StopRequest {
if (state.mFilename != null) { // only true if we've already run a
// thread for this download
if (!Helpers.isFilenameValid(state.mFilename)) {
// this should never happen
throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
"found invalid internal destination filename");
}
// We're resuming a download that got interrupted
File f = new File(state.mFilename);
if (f.exists()) {
long fileLength = f.length();
if (fileLength == 0) {
// The download hadn't actually started, we can restart from
// scratch
f.delete();
state.mFilename = null;
} else if (mInfo.mETag == null) {
// This should've been caught upon failure
f.delete();
throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
"Trying to resume a download that can't be resumed");
} else {
// All right, we'll be able to resume this download
try {
state.mStream = new FileOutputStream(state.mFilename, true);
} catch (FileNotFoundException exc) {
throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
"while opening destination for resuming: " + exc.toString(), exc);
}
innerState.mBytesSoFar = (int) fileLength;
if (mInfo.mTotalBytes != -1) {
innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
}
innerState.mHeaderETag = mInfo.mETag;
innerState.mContinuingDownload = true;
}
}
}
if (state.mStream != null) {
closeDestination(state);
}
}
/**
* Stores information about the completed download, and notifies the
* initiating application.
*/
private void notifyDownloadCompleted(
int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
String filename) {
updateDownloadDatabase(
status, countRetry, retryAfter, redirectCount, gotData, filename);
if (DownloaderService.isStatusCompleted(status)) {
// TBD: send status update?
}
}
private void updateDownloadDatabase(
int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
String filename) {
mInfo.mStatus = status;
mInfo.mRetryAfter = retryAfter;
mInfo.mRedirectCount = redirectCount;
mInfo.mLastMod = System.currentTimeMillis();
if (!countRetry) {
mInfo.mNumFailed = 0;
} else if (gotData) {
mInfo.mNumFailed = 1;
} else {
mInfo.mNumFailed++;
}
mDB.updateDownload(mInfo);
}
}

View File

@@ -0,0 +1,510 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader.impl;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDoneException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteStatement;
import android.provider.BaseColumns;
import android.util.Log;
public class DownloadsDB {
private static final String DATABASE_NAME = "DownloadsDB";
private static final int DATABASE_VERSION = 7;
public static final String LOG_TAG = DownloadsDB.class.getName();
final SQLiteOpenHelper mHelper;
SQLiteStatement mGetDownloadByIndex;
SQLiteStatement mUpdateCurrentBytes;
private static DownloadsDB mDownloadsDB;
long mMetadataRowID = -1;
int mVersionCode = -1;
int mStatus = -1;
int mFlags;
static public synchronized DownloadsDB getDB(Context paramContext) {
if (null == mDownloadsDB) {
return new DownloadsDB(paramContext);
}
return mDownloadsDB;
}
private SQLiteStatement getDownloadByIndexStatement() {
if (null == mGetDownloadByIndex) {
mGetDownloadByIndex = mHelper.getReadableDatabase().compileStatement(
"SELECT " + BaseColumns._ID + " FROM "
+ DownloadColumns.TABLE_NAME + " WHERE "
+ DownloadColumns.INDEX + " = ?");
}
return mGetDownloadByIndex;
}
private SQLiteStatement getUpdateCurrentBytesStatement() {
if (null == mUpdateCurrentBytes) {
mUpdateCurrentBytes = mHelper.getReadableDatabase().compileStatement(
"UPDATE " + DownloadColumns.TABLE_NAME + " SET " + DownloadColumns.CURRENTBYTES
+ " = ?" +
" WHERE " + DownloadColumns.INDEX + " = ?");
}
return mUpdateCurrentBytes;
}
private DownloadsDB(Context paramContext) {
this.mHelper = new DownloadsContentDBHelper(paramContext);
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
// Query for the version code, the row ID of the metadata (for future
// updating) the status and the flags
Cursor cur = sqldb.rawQuery("SELECT " +
MetadataColumns.APKVERSION + "," +
BaseColumns._ID + "," +
MetadataColumns.DOWNLOAD_STATUS + "," +
MetadataColumns.FLAGS +
" FROM "
+ MetadataColumns.TABLE_NAME + " LIMIT 1", null);
if (null != cur && cur.moveToFirst()) {
mVersionCode = cur.getInt(0);
mMetadataRowID = cur.getLong(1);
mStatus = cur.getInt(2);
mFlags = cur.getInt(3);
cur.close();
}
mDownloadsDB = this;
}
protected DownloadInfo getDownloadInfoByFileName(String fileName) {
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
Cursor itemcur = null;
try {
itemcur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
DownloadColumns.FILENAME + " = ?",
new String[] {
fileName
}, null, null, null);
if (null != itemcur && itemcur.moveToFirst()) {
return getDownloadInfoFromCursor(itemcur);
}
} finally {
if (null != itemcur)
itemcur.close();
}
return null;
}
public long getIDForDownloadInfo(final DownloadInfo di) {
return getIDByIndex(di.mIndex);
}
public long getIDByIndex(int index) {
SQLiteStatement downloadByIndex = getDownloadByIndexStatement();
downloadByIndex.clearBindings();
downloadByIndex.bindLong(1, index);
try {
return downloadByIndex.simpleQueryForLong();
} catch (SQLiteDoneException e) {
return -1;
}
}
public void updateDownloadCurrentBytes(final DownloadInfo di) {
SQLiteStatement downloadCurrentBytes = getUpdateCurrentBytesStatement();
downloadCurrentBytes.clearBindings();
downloadCurrentBytes.bindLong(1, di.mCurrentBytes);
downloadCurrentBytes.bindLong(2, di.mIndex);
downloadCurrentBytes.execute();
}
public void close() {
this.mHelper.close();
}
protected static class DownloadsContentDBHelper extends SQLiteOpenHelper {
DownloadsContentDBHelper(Context paramContext) {
super(paramContext, DATABASE_NAME, null, DATABASE_VERSION);
}
private String createTableQueryFromArray(String paramString,
String[][] paramArrayOfString) {
StringBuilder localStringBuilder = new StringBuilder();
localStringBuilder.append("CREATE TABLE ");
localStringBuilder.append(paramString);
localStringBuilder.append(" (");
int i = paramArrayOfString.length;
for (int j = 0;; j++) {
if (j >= i) {
localStringBuilder
.setLength(localStringBuilder.length() - 1);
localStringBuilder.append(");");
return localStringBuilder.toString();
}
String[] arrayOfString = paramArrayOfString[j];
localStringBuilder.append(' ');
localStringBuilder.append(arrayOfString[0]);
localStringBuilder.append(' ');
localStringBuilder.append(arrayOfString[1]);
localStringBuilder.append(',');
}
}
/**
* These two arrays must match and have the same order. For every Schema
* there must be a corresponding table name.
*/
static final private String[][][] sSchemas = {
DownloadColumns.SCHEMA, MetadataColumns.SCHEMA
};
static final private String[] sTables = {
DownloadColumns.TABLE_NAME, MetadataColumns.TABLE_NAME
};
/**
* Goes through all of the tables in sTables and drops each table if it
* exists. Altered to no longer make use of reflection.
*/
private void dropTables(SQLiteDatabase paramSQLiteDatabase) {
for (String table : sTables) {
try {
paramSQLiteDatabase.execSQL("DROP TABLE IF EXISTS " + table);
} catch (Exception localException) {
localException.printStackTrace();
}
}
}
/**
* Goes through all of the tables in sTables and creates a database with
* the corresponding schema described in sSchemas. Altered to no longer
* make use of reflection.
*/
public void onCreate(SQLiteDatabase paramSQLiteDatabase) {
int numSchemas = sSchemas.length;
for (int i = 0; i < numSchemas; i++) {
try {
String[][] schema = (String[][]) sSchemas[i];
paramSQLiteDatabase.execSQL(createTableQueryFromArray(
sTables[i], schema));
} catch (Exception localException) {
while (true)
localException.printStackTrace();
}
}
}
public void onUpgrade(SQLiteDatabase paramSQLiteDatabase,
int paramInt1, int paramInt2) {
Log.w(DownloadsContentDBHelper.class.getName(),
"Upgrading database from version " + paramInt1 + " to "
+ paramInt2 + ", which will destroy all old data");
dropTables(paramSQLiteDatabase);
onCreate(paramSQLiteDatabase);
}
}
public static class MetadataColumns implements BaseColumns {
public static final String APKVERSION = "APKVERSION";
public static final String DOWNLOAD_STATUS = "DOWNLOADSTATUS";
public static final String FLAGS = "DOWNLOADFLAGS";
public static final String[][] SCHEMA = {
{
BaseColumns._ID, "INTEGER PRIMARY KEY"
},
{
APKVERSION, "INTEGER"
}, {
DOWNLOAD_STATUS, "INTEGER"
},
{
FLAGS, "INTEGER"
}
};
public static final String TABLE_NAME = "MetadataColumns";
public static final String _ID = "MetadataColumns._id";
}
public static class DownloadColumns implements BaseColumns {
public static final String INDEX = "FILEIDX";
public static final String URI = "URI";
public static final String FILENAME = "FN";
public static final String ETAG = "ETAG";
public static final String TOTALBYTES = "TOTALBYTES";
public static final String CURRENTBYTES = "CURRENTBYTES";
public static final String LASTMOD = "LASTMOD";
public static final String STATUS = "STATUS";
public static final String CONTROL = "CONTROL";
public static final String NUM_FAILED = "FAILCOUNT";
public static final String RETRY_AFTER = "RETRYAFTER";
public static final String REDIRECT_COUNT = "REDIRECTCOUNT";
public static final String[][] SCHEMA = {
{
BaseColumns._ID, "INTEGER PRIMARY KEY"
},
{
INDEX, "INTEGER UNIQUE"
}, {
URI, "TEXT"
},
{
FILENAME, "TEXT UNIQUE"
}, {
ETAG, "TEXT"
},
{
TOTALBYTES, "INTEGER"
}, {
CURRENTBYTES, "INTEGER"
},
{
LASTMOD, "INTEGER"
}, {
STATUS, "INTEGER"
},
{
CONTROL, "INTEGER"
}, {
NUM_FAILED, "INTEGER"
},
{
RETRY_AFTER, "INTEGER"
}, {
REDIRECT_COUNT, "INTEGER"
}
};
public static final String TABLE_NAME = "DownloadColumns";
public static final String _ID = "DownloadColumns._id";
}
private static final String[] DC_PROJECTION = {
DownloadColumns.FILENAME,
DownloadColumns.URI, DownloadColumns.ETAG,
DownloadColumns.TOTALBYTES, DownloadColumns.CURRENTBYTES,
DownloadColumns.LASTMOD, DownloadColumns.STATUS,
DownloadColumns.CONTROL, DownloadColumns.NUM_FAILED,
DownloadColumns.RETRY_AFTER, DownloadColumns.REDIRECT_COUNT,
DownloadColumns.INDEX
};
private static final int FILENAME_IDX = 0;
private static final int URI_IDX = 1;
private static final int ETAG_IDX = 2;
private static final int TOTALBYTES_IDX = 3;
private static final int CURRENTBYTES_IDX = 4;
private static final int LASTMOD_IDX = 5;
private static final int STATUS_IDX = 6;
private static final int CONTROL_IDX = 7;
private static final int NUM_FAILED_IDX = 8;
private static final int RETRY_AFTER_IDX = 9;
private static final int REDIRECT_COUNT_IDX = 10;
private static final int INDEX_IDX = 11;
/**
* This function will add a new file to the database if it does not exist.
*
* @param di DownloadInfo that we wish to store
* @return the row id of the record to be updated/inserted, or -1
*/
public boolean updateDownload(DownloadInfo di) {
ContentValues cv = new ContentValues();
cv.put(DownloadColumns.INDEX, di.mIndex);
cv.put(DownloadColumns.FILENAME, di.mFileName);
cv.put(DownloadColumns.URI, di.mUri);
cv.put(DownloadColumns.ETAG, di.mETag);
cv.put(DownloadColumns.TOTALBYTES, di.mTotalBytes);
cv.put(DownloadColumns.CURRENTBYTES, di.mCurrentBytes);
cv.put(DownloadColumns.LASTMOD, di.mLastMod);
cv.put(DownloadColumns.STATUS, di.mStatus);
cv.put(DownloadColumns.CONTROL, di.mControl);
cv.put(DownloadColumns.NUM_FAILED, di.mNumFailed);
cv.put(DownloadColumns.RETRY_AFTER, di.mRetryAfter);
cv.put(DownloadColumns.REDIRECT_COUNT, di.mRedirectCount);
return updateDownload(di, cv);
}
public boolean updateDownload(DownloadInfo di, ContentValues cv) {
long id = di == null ? -1 : getIDForDownloadInfo(di);
try {
final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
if (id != -1) {
if (1 != sqldb.update(DownloadColumns.TABLE_NAME,
cv, DownloadColumns._ID + " = " + id, null)) {
return false;
}
} else {
return -1 != sqldb.insert(DownloadColumns.TABLE_NAME,
DownloadColumns.URI, cv);
}
} catch (android.database.sqlite.SQLiteException ex) {
ex.printStackTrace();
}
return false;
}
public int getLastCheckedVersionCode() {
return mVersionCode;
}
public boolean isDownloadRequired() {
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
Cursor cur = sqldb.rawQuery("SELECT Count(*) FROM "
+ DownloadColumns.TABLE_NAME + " WHERE "
+ DownloadColumns.STATUS + " <> 0", null);
try {
if (null != cur && cur.moveToFirst()) {
return 0 == cur.getInt(0);
}
} finally {
if (null != cur)
cur.close();
}
return true;
}
public int getFlags() {
return mFlags;
}
public boolean updateFlags(int flags) {
if (mFlags != flags) {
ContentValues cv = new ContentValues();
cv.put(MetadataColumns.FLAGS, flags);
if (updateMetadata(cv)) {
mFlags = flags;
return true;
} else {
return false;
}
} else {
return true;
}
};
public boolean updateStatus(int status) {
if (mStatus != status) {
ContentValues cv = new ContentValues();
cv.put(MetadataColumns.DOWNLOAD_STATUS, status);
if (updateMetadata(cv)) {
mStatus = status;
return true;
} else {
return false;
}
} else {
return true;
}
};
public boolean updateMetadata(ContentValues cv) {
final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
if (-1 == this.mMetadataRowID) {
long newID = sqldb.insert(MetadataColumns.TABLE_NAME,
MetadataColumns.APKVERSION, cv);
if (-1 == newID)
return false;
mMetadataRowID = newID;
} else {
if (0 == sqldb.update(MetadataColumns.TABLE_NAME, cv,
BaseColumns._ID + " = " + mMetadataRowID, null))
return false;
}
return true;
}
public boolean updateMetadata(int apkVersion, int downloadStatus) {
ContentValues cv = new ContentValues();
cv.put(MetadataColumns.APKVERSION, apkVersion);
cv.put(MetadataColumns.DOWNLOAD_STATUS, downloadStatus);
if (updateMetadata(cv)) {
mVersionCode = apkVersion;
mStatus = downloadStatus;
return true;
} else {
return false;
}
};
public boolean updateFromDb(DownloadInfo di) {
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
Cursor cur = null;
try {
cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
DownloadColumns.FILENAME + "= ?",
new String[] {
di.mFileName
}, null, null, null);
if (null != cur && cur.moveToFirst()) {
setDownloadInfoFromCursor(di, cur);
return true;
}
return false;
} finally {
if (null != cur) {
cur.close();
}
}
}
public void setDownloadInfoFromCursor(DownloadInfo di, Cursor cur) {
di.mUri = cur.getString(URI_IDX);
di.mETag = cur.getString(ETAG_IDX);
di.mTotalBytes = cur.getLong(TOTALBYTES_IDX);
di.mCurrentBytes = cur.getLong(CURRENTBYTES_IDX);
di.mLastMod = cur.getLong(LASTMOD_IDX);
di.mStatus = cur.getInt(STATUS_IDX);
di.mControl = cur.getInt(CONTROL_IDX);
di.mNumFailed = cur.getInt(NUM_FAILED_IDX);
di.mRetryAfter = cur.getInt(RETRY_AFTER_IDX);
di.mRedirectCount = cur.getInt(REDIRECT_COUNT_IDX);
}
public DownloadInfo getDownloadInfoFromCursor(Cursor cur) {
DownloadInfo di = new DownloadInfo(cur.getInt(INDEX_IDX),
cur.getString(FILENAME_IDX), this.getClass().getPackage()
.getName());
setDownloadInfoFromCursor(di, cur);
return di;
}
public DownloadInfo[] getDownloads() {
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
Cursor cur = null;
try {
cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, null,
null, null, null, null);
if (null != cur && cur.moveToFirst()) {
DownloadInfo[] retInfos = new DownloadInfo[cur.getCount()];
int idx = 0;
do {
DownloadInfo di = getDownloadInfoFromCursor(cur);
retInfos[idx++] = di;
} while (cur.moveToNext());
return retInfos;
}
return null;
} finally {
if (null != cur) {
cur.close();
}
}
}
}

View File

@@ -0,0 +1,200 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader.impl;
import android.text.format.Time;
import java.util.Calendar;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Helper for parsing an HTTP date.
*/
public final class HttpDateTime {
/*
* Regular expression for parsing HTTP-date. Wdy, DD Mon YYYY HH:MM:SS GMT
* RFC 822, updated by RFC 1123 Weekday, DD-Mon-YY HH:MM:SS GMT RFC 850,
* obsoleted by RFC 1036 Wdy Mon DD HH:MM:SS YYYY ANSI C's asctime() format
* with following variations Wdy, DD-Mon-YYYY HH:MM:SS GMT Wdy, (SP)D Mon
* YYYY HH:MM:SS GMT Wdy,DD Mon YYYY HH:MM:SS GMT Wdy, DD-Mon-YY HH:MM:SS
* GMT Wdy, DD Mon YYYY HH:MM:SS -HHMM Wdy, DD Mon YYYY HH:MM:SS Wdy Mon
* (SP)D HH:MM:SS YYYY Wdy Mon DD HH:MM:SS YYYY GMT HH can be H if the first
* digit is zero. Mon can be the full name of the month.
*/
private static final String HTTP_DATE_RFC_REGEXP =
"([0-9]{1,2})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]"
+ "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])";
private static final String HTTP_DATE_ANSIC_REGEXP =
"[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]"
+ "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})";
/**
* The compiled version of the HTTP-date regular expressions.
*/
private static final Pattern HTTP_DATE_RFC_PATTERN =
Pattern.compile(HTTP_DATE_RFC_REGEXP);
private static final Pattern HTTP_DATE_ANSIC_PATTERN =
Pattern.compile(HTTP_DATE_ANSIC_REGEXP);
private static class TimeOfDay {
TimeOfDay(int h, int m, int s) {
this.hour = h;
this.minute = m;
this.second = s;
}
int hour;
int minute;
int second;
}
public static long parse(String timeString)
throws IllegalArgumentException {
int date = 1;
int month = Calendar.JANUARY;
int year = 1970;
TimeOfDay timeOfDay;
Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString);
if (rfcMatcher.find()) {
date = getDate(rfcMatcher.group(1));
month = getMonth(rfcMatcher.group(2));
year = getYear(rfcMatcher.group(3));
timeOfDay = getTime(rfcMatcher.group(4));
} else {
Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString);
if (ansicMatcher.find()) {
month = getMonth(ansicMatcher.group(1));
date = getDate(ansicMatcher.group(2));
timeOfDay = getTime(ansicMatcher.group(3));
year = getYear(ansicMatcher.group(4));
} else {
throw new IllegalArgumentException();
}
}
// FIXME: Y2038 BUG!
if (year >= 2038) {
year = 2038;
month = Calendar.JANUARY;
date = 1;
}
Time time = new Time(Time.TIMEZONE_UTC);
time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date,
month, year);
return time.toMillis(false /* use isDst */);
}
private static int getDate(String dateString) {
if (dateString.length() == 2) {
return (dateString.charAt(0) - '0') * 10
+ (dateString.charAt(1) - '0');
} else {
return (dateString.charAt(0) - '0');
}
}
/*
* jan = 9 + 0 + 13 = 22 feb = 5 + 4 + 1 = 10 mar = 12 + 0 + 17 = 29 apr = 0
* + 15 + 17 = 32 may = 12 + 0 + 24 = 36 jun = 9 + 20 + 13 = 42 jul = 9 + 20
* + 11 = 40 aug = 0 + 20 + 6 = 26 sep = 18 + 4 + 15 = 37 oct = 14 + 2 + 19
* = 35 nov = 13 + 14 + 21 = 48 dec = 3 + 4 + 2 = 9
*/
private static int getMonth(String monthString) {
int hash = Character.toLowerCase(monthString.charAt(0)) +
Character.toLowerCase(monthString.charAt(1)) +
Character.toLowerCase(monthString.charAt(2)) - 3 * 'a';
switch (hash) {
case 22:
return Calendar.JANUARY;
case 10:
return Calendar.FEBRUARY;
case 29:
return Calendar.MARCH;
case 32:
return Calendar.APRIL;
case 36:
return Calendar.MAY;
case 42:
return Calendar.JUNE;
case 40:
return Calendar.JULY;
case 26:
return Calendar.AUGUST;
case 37:
return Calendar.SEPTEMBER;
case 35:
return Calendar.OCTOBER;
case 48:
return Calendar.NOVEMBER;
case 9:
return Calendar.DECEMBER;
default:
throw new IllegalArgumentException();
}
}
private static int getYear(String yearString) {
if (yearString.length() == 2) {
int year = (yearString.charAt(0) - '0') * 10
+ (yearString.charAt(1) - '0');
if (year >= 70) {
return year + 1900;
} else {
return year + 2000;
}
} else if (yearString.length() == 3) {
// According to RFC 2822, three digit years should be added to 1900.
int year = (yearString.charAt(0) - '0') * 100
+ (yearString.charAt(1) - '0') * 10
+ (yearString.charAt(2) - '0');
return year + 1900;
} else if (yearString.length() == 4) {
return (yearString.charAt(0) - '0') * 1000
+ (yearString.charAt(1) - '0') * 100
+ (yearString.charAt(2) - '0') * 10
+ (yearString.charAt(3) - '0');
} else {
return 1970;
}
}
private static TimeOfDay getTime(String timeString) {
// HH might be H
int i = 0;
int hour = timeString.charAt(i++) - '0';
if (timeString.charAt(i) != ':')
hour = hour * 10 + (timeString.charAt(i++) - '0');
// Skip ':'
i++;
int minute = (timeString.charAt(i++) - '0') * 10
+ (timeString.charAt(i++) - '0');
// Skip ':'
i++;
int second = (timeString.charAt(i++) - '0') * 10
+ (timeString.charAt(i++) - '0');
return new TimeOfDay(hour, minute, second);
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader.impl;
import com.android.vending.expansion.downloader.R;
import com.google.android.vending.expansion.downloader.Helpers;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
public class V14CustomNotification implements DownloadNotification.ICustomNotification {
CharSequence mTitle;
CharSequence mTicker;
int mIcon;
long mTotalKB = -1;
long mCurrentKB = -1;
long mTimeRemaining;
PendingIntent mPendingIntent;
@Override
public void setIcon(int icon) {
mIcon = icon;
}
@Override
public void setTitle(CharSequence title) {
mTitle = title;
}
@Override
public void setTotalBytes(long totalBytes) {
mTotalKB = totalBytes;
}
@Override
public void setCurrentBytes(long currentBytes) {
mCurrentKB = currentBytes;
}
void setProgress(Notification.Builder builder) {
}
@Override
public Notification updateNotification(Context c) {
Notification.Builder builder = new Notification.Builder(c);
builder.setContentTitle(mTitle);
if (mTotalKB > 0 && -1 != mCurrentKB) {
builder.setProgress((int) (mTotalKB >> 8), (int) (mCurrentKB >> 8), false);
} else {
builder.setProgress(0, 0, true);
}
builder.setContentText(Helpers.getDownloadProgressString(mCurrentKB, mTotalKB));
builder.setContentInfo(c.getString(R.string.time_remaining_notification,
Helpers.getTimeRemaining(mTimeRemaining)));
if (mIcon != 0) {
builder.setSmallIcon(mIcon);
} else {
int iconResource = android.R.drawable.stat_sys_download;
builder.setSmallIcon(iconResource);
}
builder.setOngoing(true);
builder.setTicker(mTicker);
builder.setContentIntent(mPendingIntent);
builder.setOnlyAlertOnce(true);
return builder.getNotification();
}
@Override
public void setPendingIntent(PendingIntent contentIntent) {
mPendingIntent = contentIntent;
}
@Override
public void setTicker(CharSequence ticker) {
mTicker = ticker;
}
@Override
public void setTimeRemaining(long timeRemaining) {
mTimeRemaining = timeRemaining;
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader.impl;
import com.android.vending.expansion.downloader.R;
import com.google.android.vending.expansion.downloader.Helpers;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.view.View;
import android.widget.RemoteViews;
public class V3CustomNotification implements DownloadNotification.ICustomNotification {
CharSequence mTitle;
CharSequence mTicker;
int mIcon;
long mTotalBytes = -1;
long mCurrentBytes = -1;
long mTimeRemaining;
PendingIntent mPendingIntent;
Notification mNotification = new Notification();
@Override
public void setIcon(int icon) {
mIcon = icon;
}
@Override
public void setTitle(CharSequence title) {
mTitle = title;
}
@Override
public void setTotalBytes(long totalBytes) {
mTotalBytes = totalBytes;
}
@Override
public void setCurrentBytes(long currentBytes) {
mCurrentBytes = currentBytes;
}
@Override
public Notification updateNotification(Context c) {
Notification n = mNotification;
n.icon = mIcon;
n.flags |= Notification.FLAG_ONGOING_EVENT;
if (android.os.Build.VERSION.SDK_INT > 10) {
n.flags |= Notification.FLAG_ONLY_ALERT_ONCE; // only matters for
// Honeycomb
}
// Build the RemoteView object
RemoteViews expandedView = new RemoteViews(
c.getPackageName(),
R.layout.status_bar_ongoing_event_progress_bar);
expandedView.setTextViewText(R.id.title, mTitle);
// look at strings
expandedView.setViewVisibility(R.id.description, View.VISIBLE);
expandedView.setTextViewText(R.id.description,
Helpers.getDownloadProgressString(mCurrentBytes, mTotalBytes));
expandedView.setViewVisibility(R.id.progress_bar_frame, View.VISIBLE);
expandedView.setProgressBar(R.id.progress_bar,
(int) (mTotalBytes >> 8),
(int) (mCurrentBytes >> 8),
mTotalBytes <= 0);
expandedView.setViewVisibility(R.id.time_remaining, View.VISIBLE);
expandedView.setTextViewText(
R.id.time_remaining,
c.getString(R.string.time_remaining_notification,
Helpers.getTimeRemaining(mTimeRemaining)));
expandedView.setTextViewText(R.id.progress_text,
Helpers.getDownloadProgressPercent(mCurrentBytes, mTotalBytes));
expandedView.setImageViewResource(R.id.appIcon, mIcon);
n.contentView = expandedView;
n.contentIntent = mPendingIntent;
return n;
}
@Override
public void setPendingIntent(PendingIntent contentIntent) {
mPendingIntent = contentIntent;
}
@Override
public void setTicker(CharSequence ticker) {
mTicker = ticker;
}
@Override
public void setTimeRemaining(long timeRemaining) {
mTimeRemaining = timeRemaining;
}
}

View File

@@ -4,5 +4,6 @@
<classpathentry kind="src" path="gen"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

View File

@@ -63,7 +63,7 @@ public class LicenseChecker implements ServiceConnection {
private static final int TIMEOUT_MS = 10 * 1000;
private static final SecureRandom RANDOM = new SecureRandom();
private static final boolean DEBUG_LICENSE_ERROR = false;
private static final boolean DEBUG_LICENSE_ERROR = true;
private ILicensingService mService;

View File

@@ -35,7 +35,7 @@ if env['ios_appirater'] == "yes":
obj = env_ios.Object('godot_iphone.cpp')
prog = None
if env["target"]=="release":
if env["target"]=="release" || env["target"] == "release_debug":
prog = env_ios.Program('#bin/godot_opt', [obj] + iphone_lib)
#action = "dsymutil "+File(prog)[0].path+" -o ../build/script_exec/build/Debug-iphoneos/script_exec.app.dSYM"
#env.AddPostAction(prog, action)

View File

@@ -99,7 +99,16 @@ OSStatus AudioDriverIphone::output_callback(void *inRefCon,
AudioBuffer *abuf;
AudioDriverIphone* ad = (AudioDriverIphone*)inRefCon;
if (!ad->active) {
bool mix = true;
if (!ad_active)
mix = false;
else {
mix = mutex->try_lock() == OK;
};
if (!mix) {
for (unsigned int i = 0; i < ioData->mNumberBuffers; i++) {
abuf = &ioData->mBuffers[i];
zeromem(abuf->mData, abuf->mDataByteSize);
@@ -147,11 +156,28 @@ AudioDriverSW::OutputFormat AudioDriverIphone::get_output_format() const {
return OUTPUT_STEREO;
};
void AudioDriverIphone::lock() {};
void AudioDriverIphone::unlock() {};
void AudioDriverIphone::lock() {
if (active && mutex)
mutex->lock();
};
void AudioDriverIphone::unlock() {
if (active && mutex)
mutex->unlock();
};
void AudioDriverIphone::finish() {
memdelete_arr(samples_in);
};
AudioDriverIphone::AudioDriverIphone() {
mutex=Mutex::create();//NULL;
};
AudioDriverIphone::~AudioDriverIphone() {
};

View File

@@ -34,6 +34,7 @@ class AudioDriverIphone : public AudioDriverSW {
AudioComponentInstance audio_unit;
bool active;
Mutex *mutex;
int channels;
int32_t* samples_in;
@@ -59,5 +60,8 @@ public:
virtual void lock();
virtual void unlock();
virtual void finish();
AudioDriverIphone();
~AudioDriverIphone();
};

View File

@@ -104,6 +104,12 @@ def configure(env):
env['OBJSUFFIX'] = "_opt"+env['OBJSUFFIX']
env['LIBSUFFIX'] = "_opt"+env['LIBSUFFIX']
elif env["target"] == "release_debug":
env.Append(CCFLAGS=['-Os', '-ffast-math', '-DNS_BLOCK_ASSERTIONS=1','-Wall','-DDEBUG_ENABLED'])
env.Append(LINKFLAGS=['-Os', '-ffast-math'])
env['OBJSUFFIX'] = "_opt"+env['OBJSUFFIX']
env['LIBSUFFIX'] = "_opt"+env['LIBSUFFIX']
elif (env["target"]=="debug"):
env.Append(CCFLAGS=['-D_DEBUG', '-DDEBUG=1', '-gdwarf-2', '-Wall', '-O0', '-DDEBUG_ENABLED'])

View File

@@ -574,6 +574,8 @@ LRESULT CALLBACK WndProc(HWND hWnd,UINT uMsg, WPARAM wParam, LPARAM lParam) {
void OS_Windows::probe_joysticks() {
static uint32_t last_attached = 0;
int device_count = joyGetNumDevs();
JOYINFOEX jinfo;
@@ -582,21 +584,40 @@ void OS_Windows::probe_joysticks() {
for (int i=0; i<JOYSTICKS_MAX; i++) {
joysticks[i].attached = (device_count > 0) && (joyGetPosEx(JOYSTICKID1 + i, &jinfo) == JOYERR_NOERROR);
if (!joysticks[i].attached) {
Joystick joy;
joy.id = i;
joy.attached = (device_count > 0) && (joyGetPosEx(JOYSTICKID1 + i, &jinfo) == JOYERR_NOERROR);
if (joy.attached == (last_attached & (1 << i) != 0)) {
continue;
};
joysticks[i].last_buttons = jinfo.dwButtons;
// there's been a change since last call
joysticks[i].last_axis[0] = jinfo.dwXpos;
joysticks[i].last_axis[1] = jinfo.dwYpos;
joysticks[i].last_axis[2] = jinfo.dwZpos;
joysticks[i].last_axis[3] = jinfo.dwRpos;
joysticks[i].last_axis[4] = jinfo.dwUpos;
joysticks[i].last_axis[5] = jinfo.dwVpos;
if (joy.attached)
last_attached = last_attached | (1 << i);
else
last_attached &= ~(1 << i);
if (joy.attached) {
joy.last_buttons = jinfo.dwButtons;
joy.last_axis[0] = jinfo.dwXpos;
joy.last_axis[1] = jinfo.dwYpos;
joy.last_axis[2] = jinfo.dwZpos;
joy.last_axis[3] = jinfo.dwRpos;
joy.last_axis[4] = jinfo.dwUpos;
joy.last_axis[5] = jinfo.dwVpos;
JOYCAPS jcaps;
MMRESULT res = joyGetDevCaps(JOYSTICKID1 + i, &jcaps, sizeof(jcaps));
if (res == JOYERR_NOERROR) {
joy.name = jcaps.szPname;
};
};
joystick_change_queue.push_back(joy);
};
};
@@ -998,7 +1019,13 @@ void OS_Windows::initialize(const VideoMode& p_desired,int p_video_driver,int p_
spatial_sound_2d_server = memnew( SpatialSound2DServerSW );
spatial_sound_2d_server->init();
probe_joysticks();
probe_joysticks(); // todo: move this to a thread
while (joystick_change_queue.size() > 0) {
Joystick joy = joystick_change_queue.front()->get();
joystick_change_queue.pop_front();
joysticks[joy.id] = joy;
input->joy_connection_changed(joy.id, joy.attached, joy.name);
};
TRACKMOUSEEVENT tme;
tme.cbSize=sizeof(TRACKMOUSEEVENT);

View File

@@ -106,13 +106,16 @@ class OS_Windows : public OS {
struct Joystick {
int id;
bool attached;
DWORD last_axis[JOY_AXIS_COUNT];
DWORD last_buttons;
DWORD last_pov;
String name;
Joystick() {
id = -1;
attached = false;
for (int i=0; i<JOY_AXIS_COUNT; i++) {
@@ -123,6 +126,7 @@ class OS_Windows : public OS {
};
};
List<Joystick> joystick_change_queue;
int joystick_count;
Joystick joysticks[JOYSTICKS_MAX];

View File

@@ -1046,10 +1046,12 @@ void OS_X11::close_joystick(int p_id) {
close(joysticks[p_id].fd);
joysticks[p_id].fd = -1;
};
input->joy_connection_changed(p_id, false, "");
};
void OS_X11::probe_joystick(int p_id) {
#ifndef __FreeBSD__
if (p_id == -1) {
for (int i=0; i<JOYSTICKS_MAX; i++) {
@@ -1059,7 +1061,8 @@ void OS_X11::probe_joystick(int p_id) {
return;
};
close_joystick(p_id);
if (joysticks[p_id].fd != -1)
close_joystick(p_id);
const char *joy_names[] = {
"/dev/input/js%d",
@@ -1072,12 +1075,22 @@ void OS_X11::probe_joystick(int p_id) {
char fname[64];
sprintf(fname, joy_names[i], p_id);
int fd = open(fname, O_RDONLY);
int fd = open(fname, O_RDONLY|O_NONBLOCK);
if (fd != -1) {
fcntl( fd, F_SETFL, O_NONBLOCK );
//fcntl( fd, F_SETFL, O_NONBLOCK );
joysticks[p_id] = Joystick(); // this will reset the axis array
joysticks[p_id].fd = fd;
String name;
char namebuf[255] = {0};
if (ioctl(fd, JSIOCGNAME(sizeof(namebuf)), namebuf) >= 0) {
name = namebuf;
} else {
name = "error";
};
input->joy_connection_changed(p_id, true, name);
break; // don't try the next name
};
@@ -1098,8 +1111,11 @@ void OS_X11::process_joysticks() {
InputEvent ievent;
for (int i=0; i<JOYSTICKS_MAX; i++) {
if (joysticks[i].fd == -1)
continue;
if (joysticks[i].fd == -1) {
probe_joystick(i);
if (joysticks[i].fd == -1)
continue;
};
ievent.device = i;
while ( (bytes = read(joysticks[i].fd, &events, sizeof(events))) > 0) {
@@ -1117,8 +1133,9 @@ void OS_X11::process_joysticks() {
case JS_EVENT_AXIS:
if (joysticks[i].last_axis[event.number] != event.value) {
//if (joysticks[i].last_axis[event.number] != event.value) {
/*
if (event.number==5 || event.number==6) {
int axis=event.number-5;
@@ -1165,17 +1182,19 @@ void OS_X11::process_joysticks() {
dpad_last[axis]=val;
}
*/
//print_line("ev: "+itos(event.number)+" val: "+ rtos((float)event.value / (float)MAX_JOY_AXIS));
if (event.number >= JOY_AXIS_MAX)
break;
//if (event.number >= JOY_AXIS_MAX)
// break;
//ERR_FAIL_COND(event.number >= JOY_AXIS_MAX);
ievent.type = InputEvent::JOYSTICK_MOTION;
ievent.ID = ++event_id;
ievent.joy_motion.axis = _pc_joystick_get_native_axis(event.number);
ievent.joy_motion.axis = event.number; //_pc_joystick_get_native_axis(event.number);
ievent.joy_motion.axis_value = (float)event.value / (float)MAX_JOY_AXIS;
joysticks[i].last_axis[event.number] = event.value;
if (event.number < JOY_AXIS_MAX)
joysticks[i].last_axis[event.number] = event.value;
input->parse_input_event( ievent );
};
//};
break;
case JS_EVENT_BUTTON:
@@ -1183,13 +1202,16 @@ void OS_X11::process_joysticks() {
ievent.type = InputEvent::JOYSTICK_BUTTON;
ievent.ID = ++event_id;
ievent.joy_button.button_index = _pc_joystick_get_native_button(event.number);
ievent.joy_button.button_index = event.number; // _pc_joystick_get_native_button(event.number);
ievent.joy_button.pressed = event.value;
input->parse_input_event( ievent );
break;
};
};
};
if (bytes == 0 || (bytes < 0 && errno != EAGAIN)) {
close_joystick(i);
};
};
#endif
};